/
MultiProxy.cs
271 lines (228 loc) · 10.3 KB
/
MultiProxy.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace System.Net.Http
{
/// <summary>
/// A collection of proxies.
/// </summary>
internal struct MultiProxy
{
private readonly FailedProxyCache? _failedProxyCache;
private readonly Uri[]? _uris;
private readonly string? _proxyConfig;
private readonly bool _secure;
private int _currentIndex;
private Uri? _currentUri;
private MultiProxy(FailedProxyCache? failedProxyCache, Uri[] uris)
{
_failedProxyCache = failedProxyCache;
_uris = uris;
_proxyConfig = null;
_secure = default;
_currentIndex = 0;
_currentUri = null;
}
private MultiProxy(FailedProxyCache failedProxyCache, string proxyConfig, bool secure)
{
_failedProxyCache = failedProxyCache;
_uris = null;
_proxyConfig = proxyConfig;
_secure = secure;
_currentIndex = 0;
_currentUri = null;
}
public static MultiProxy Empty => new MultiProxy(null, Array.Empty<Uri>());
/// <summary>
/// Parses a WinHTTP proxy config into a MultiProxy instance.
/// </summary>
/// <param name="failedProxyCache">The cache of failed proxy requests to employ.</param>
/// <param name="proxyConfig">The WinHTTP proxy config to parse.</param>
/// <param name="secure">If true, return proxies suitable for use with a secure connection. If false, return proxies suitable for an insecure connection.</param>
public static MultiProxy Parse(FailedProxyCache failedProxyCache, string? proxyConfig, bool secure)
{
Debug.Assert(failedProxyCache != null);
Uri[] uris = Array.Empty<Uri>();
ReadOnlySpan<char> span = proxyConfig;
while (TryParseProxyConfigPart(span, secure, out Uri? uri, out int charactersConsumed))
{
int idx = uris.Length;
// Assume that we will typically not have more than 1...3 proxies, so just
// grow by 1. This method is currently only used once per process, so the
// case of an abnormally large config will not be much of a concern anyway.
Array.Resize(ref uris, idx + 1);
uris[idx] = uri;
span = span.Slice(charactersConsumed);
}
return new MultiProxy(failedProxyCache, uris);
}
/// <summary>
/// Initializes a MultiProxy instance that lazily parses a given WinHTTP configuration string.
/// </summary>
/// <param name="failedProxyCache">The cache of failed proxy requests to employ.</param>
/// <param name="proxyConfig">The WinHTTP proxy config to parse.</param>
/// <param name="secure">If true, return proxies suitable for use with a secure connection. If false, return proxies suitable for an insecure connection.</param>
public static MultiProxy CreateLazy(FailedProxyCache failedProxyCache, string proxyConfig, bool secure)
{
Debug.Assert(failedProxyCache != null);
return string.IsNullOrEmpty(proxyConfig) == false ?
new MultiProxy(failedProxyCache, proxyConfig, secure) :
MultiProxy.Empty;
}
/// <summary>
/// Reads the next proxy URI from the MultiProxy.
/// </summary>
/// <param name="uri">The next proxy to use for the request.</param>
/// <param name="isFinalProxy">If true, indicates there are no further proxies to read from the config.</param>
/// <returns>If there is a proxy available, true. Otherwise, false.</returns>
public bool ReadNext([NotNullWhen(true)] out Uri? uri, out bool isFinalProxy)
{
// Enumerating indicates the previous proxy has failed; mark it as such.
if (_currentUri != null)
{
Debug.Assert(_failedProxyCache != null);
_failedProxyCache.SetProxyFailed(_currentUri);
}
// If no more proxies to read, return out quickly.
if (!ReadNextHelper(out uri, out isFinalProxy))
{
_currentUri = null;
return false;
}
// If this is the first ReadNext() and all proxies are marked as failed, return the proxy that is closest to renewal.
Uri? oldestFailedProxyUri = null;
long oldestFailedProxyTicks = long.MaxValue;
do
{
Debug.Assert(_failedProxyCache != null);
long renewTicks = _failedProxyCache.GetProxyRenewTicks(uri);
// Proxy hasn't failed recently, return for use.
if (renewTicks == FailedProxyCache.Immediate)
{
_currentUri = uri;
return true;
}
if (renewTicks < oldestFailedProxyTicks)
{
oldestFailedProxyUri = uri;
oldestFailedProxyTicks = renewTicks;
}
}
while (ReadNextHelper(out uri, out isFinalProxy));
// All the proxies in the config have failed; in this case, return the proxy that is closest to renewal.
if (_currentUri == null)
{
uri = oldestFailedProxyUri;
_currentUri = oldestFailedProxyUri;
if (oldestFailedProxyUri != null)
{
Debug.Assert(uri != null);
_failedProxyCache.TryRenewProxy(uri, oldestFailedProxyTicks);
return true;
}
}
return false;
}
/// <summary>
/// Reads the next proxy URI from the MultiProxy, either via parsing a config string or from an array.
/// </summary>
private bool ReadNextHelper([NotNullWhen(true)] out Uri? uri, out bool isFinalProxy)
{
Debug.Assert(_uris != null || _proxyConfig != null, $"{nameof(ReadNext)} must not be called on a default-initialized {nameof(MultiProxy)}.");
if (_uris != null)
{
if (_currentIndex == _uris.Length)
{
uri = default;
isFinalProxy = default;
return false;
}
uri = _uris[_currentIndex++];
isFinalProxy = _currentIndex == _uris.Length;
return true;
}
Debug.Assert(_proxyConfig != null);
if (_currentIndex < _proxyConfig.Length)
{
bool hasProxy = TryParseProxyConfigPart(_proxyConfig.AsSpan(_currentIndex), _secure, out uri!, out int charactersConsumed);
_currentIndex += charactersConsumed;
Debug.Assert(_currentIndex <= _proxyConfig.Length);
isFinalProxy = _currentIndex == _proxyConfig.Length;
return hasProxy;
}
uri = default;
isFinalProxy = default;
return false;
}
/// <summary>
/// This method is used to parse WinINet Proxy strings, a single proxy at a time.
/// </summary>
/// <remarks>
/// The strings are a semicolon or whitespace separated list, with each entry in the following format:
/// ([<scheme>=][<scheme>"://"]<server>[":"<port>])
/// </remarks>
private static bool TryParseProxyConfigPart(ReadOnlySpan<char> proxyString, bool secure, [NotNullWhen(true)] out Uri? uri, out int charactersConsumed)
{
const int SECURE_FLAG = 1;
const int INSECURE_FLAG = 2;
const string ProxyDelimiters = "; \n\r\t";
int wantedFlag = secure ? SECURE_FLAG : INSECURE_FLAG;
int originalLength = proxyString.Length;
while (true)
{
// Skip any delimiters.
int iter = 0;
while (iter < proxyString.Length && ProxyDelimiters.Contains(proxyString[iter]))
{
++iter;
}
if (iter == proxyString.Length)
{
break;
}
proxyString = proxyString.Slice(iter);
// Determine which scheme this part is for.
// If no schema is defined, use both.
int proxyType = SECURE_FLAG | INSECURE_FLAG;
if (proxyString.StartsWith("http="))
{
proxyType = INSECURE_FLAG;
proxyString = proxyString.Slice("http=".Length);
}
else if (proxyString.StartsWith("https="))
{
proxyType = SECURE_FLAG;
proxyString = proxyString.Slice("https=".Length);
}
if (proxyString.StartsWith("http://"))
{
proxyType = INSECURE_FLAG;
proxyString = proxyString.Slice("http://".Length);
}
else if (proxyString.StartsWith("https://"))
{
proxyType = SECURE_FLAG;
proxyString = proxyString.Slice("https://".Length);
}
// Find the next delimiter, or end of string.
iter = proxyString.IndexOfAny(ProxyDelimiters);
if (iter < 0)
{
iter = proxyString.Length;
}
// Return URI if it's a match to what we want.
if ((proxyType & wantedFlag) != 0 && Uri.TryCreate(string.Concat("http://", proxyString.Slice(0, iter)), UriKind.Absolute, out uri))
{
charactersConsumed = originalLength - proxyString.Length + iter;
Debug.Assert(charactersConsumed > 0);
return true;
}
proxyString = proxyString.Slice(iter);
}
uri = null;
charactersConsumed = originalLength;
return false;
}
}
}