Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 1d4f432

Browse files
author
Lakshmi Priya Sekar
committed
Add test infra for auth testing.
1 parent 3c24c32 commit 1d4f432

File tree

4 files changed

+403
-15
lines changed

4 files changed

+403
-15
lines changed

src/Common/tests/System/Net/Http/LoopbackServer.cs

Lines changed: 259 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ public class LoopbackServer
2222
{
2323
public static Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> AllowAllCertificates = (_, __, ___, ____) => true;
2424

25+
private enum AuthenticationProtocols
26+
{
27+
Basic,
28+
Digest,
29+
None
30+
}
31+
2532
public class Options
2633
{
2734
public IPAddress Address { get; set; } = IPAddress.Loopback;
@@ -30,6 +37,9 @@ public class Options
3037
public SslProtocols SslProtocols { get; set; } = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
3138
public bool WebSocketEndpoint { get; set; } = false;
3239
public Func<Stream, Stream> ResponseStreamWrapper { get; set; }
40+
public string Domain { get; set; }
41+
public string Username { get; set; }
42+
public string Password { get; set; }
3343
}
3444

3545
public static Task CreateServerAsync(Func<Socket, Uri, Task> funcAsync, Options options = null)
@@ -49,7 +59,7 @@ public static Task CreateServerAsync(Func<Socket, Uri, Task> funcAsync, out IPEn
4959
server.Listen(options.ListenBacklog);
5060

5161
localEndPoint = (IPEndPoint)server.LocalEndPoint;
52-
string host = options.Address.AddressFamily == AddressFamily.InterNetworkV6 ?
62+
string host = options.Address.AddressFamily == AddressFamily.InterNetworkV6 ?
5363
$"[{localEndPoint.Address}]" :
5464
localEndPoint.Address.ToString();
5565

@@ -89,6 +99,11 @@ public static Task<List<string>> ReadRequestAndSendResponseAsync(Socket server,
8999
return AcceptSocketAsync(server, (s, stream, reader, writer) => ReadWriteAcceptedAsync(s, reader, writer, response), options);
90100
}
91101

102+
public static Task<List<string>> ReadRequestAndAuthenticateAsync(Socket server, string response, Options options)
103+
{
104+
return AcceptSocketAsync(server, (s, stream, reader, writer) => ValidateAuthenticationAsync(s, reader, writer, response, options), options);
105+
}
106+
92107
public static async Task<List<string>> ReadWriteAcceptedAsync(Socket s, StreamReader reader, StreamWriter writer, string response = null)
93108
{
94109
// Read request line and headers. Skip any request body.
@@ -104,6 +119,247 @@ public static async Task<List<string>> ReadWriteAcceptedAsync(Socket s, StreamRe
104119
return lines;
105120
}
106121

122+
public static async Task<List<string>> ValidateAuthenticationAsync(Socket s, StreamReader reader, StreamWriter writer, string response, Options options)
123+
{
124+
// Send unauthorized response from server.
125+
await ReadWriteAcceptedAsync(s, reader, writer, response);
126+
127+
// Read the request method.
128+
string line = await reader.ReadLineAsync().ConfigureAwait(false);
129+
int index = line != null ? line.IndexOf(' ') : -1;
130+
string requestMethod = null;
131+
if (index != -1)
132+
{
133+
requestMethod = line.Substring(0, index);
134+
}
135+
136+
// Read the authorization header from client.
137+
AuthenticationProtocols protocol = AuthenticationProtocols.None;
138+
string clientResponse = null;
139+
while (!string.IsNullOrEmpty(line = await reader.ReadLineAsync().ConfigureAwait(false)))
140+
{
141+
if (line.StartsWith("Authorization", StringComparison.OrdinalIgnoreCase))
142+
{
143+
clientResponse = line;
144+
if (line.Contains(nameof(AuthenticationProtocols.Basic), StringComparison.OrdinalIgnoreCase))
145+
{
146+
protocol = AuthenticationProtocols.Basic;
147+
break;
148+
}
149+
else if (line.Contains(nameof(AuthenticationProtocols.Digest), StringComparison.OrdinalIgnoreCase))
150+
{
151+
protocol = AuthenticationProtocols.Digest;
152+
break;
153+
}
154+
}
155+
}
156+
157+
bool success = false;
158+
switch (protocol)
159+
{
160+
case AuthenticationProtocols.Basic:
161+
success = IsBasicAuthTokenValid(line, options);
162+
break;
163+
164+
case AuthenticationProtocols.Digest:
165+
// Read the request content.
166+
string requestContent = null;
167+
while (!string.IsNullOrEmpty(line = await reader.ReadLineAsync().ConfigureAwait(false)))
168+
{
169+
if (line.Contains("Content-Length", StringComparison.OrdinalIgnoreCase))
170+
{
171+
line = await reader.ReadLineAsync().ConfigureAwait(false);
172+
while (!string.IsNullOrEmpty(line = await reader.ReadLineAsync().ConfigureAwait(false)))
173+
{
174+
requestContent += line;
175+
}
176+
}
177+
}
178+
179+
success = IsDigestAuthTokenValid(clientResponse, requestContent, requestMethod, options);
180+
break;
181+
}
182+
183+
if (success)
184+
{
185+
await writer.WriteAsync(DefaultHttpResponse).ConfigureAwait(false);
186+
}
187+
else
188+
{
189+
await writer.WriteAsync(response).ConfigureAwait(false);
190+
}
191+
192+
return null;
193+
}
194+
195+
private static bool IsBasicAuthTokenValid(string clientResponse, Options options)
196+
{
197+
string clientHash = clientResponse.Substring(clientResponse.IndexOf(nameof(AuthenticationProtocols.Basic), StringComparison.OrdinalIgnoreCase) +
198+
nameof(AuthenticationProtocols.Basic).Length).Trim();
199+
string userPass = string.IsNullOrEmpty(options.Domain) ? options.Username + ":" + options.Password : options.Domain + "\\" + options.Username + ":" + options.Password;
200+
return clientHash == Convert.ToBase64String(Encoding.UTF8.GetBytes(userPass));
201+
}
202+
203+
private static bool IsDigestAuthTokenValid(string clientResponse, string requestContent, string requestMethod, Options options)
204+
{
205+
string clientHash = clientResponse.Substring(clientResponse.IndexOf(nameof(AuthenticationProtocols.Digest), StringComparison.OrdinalIgnoreCase) +
206+
nameof(AuthenticationProtocols.Digest).Length).Trim();
207+
string[] values = clientHash.Split(',');
208+
209+
string username = null, uri = null, realm = null, nonce = null, response = null, algorithm = null, cnonce = null, opaque = null, qop = null, nc = null;
210+
bool userhash = false;
211+
for (int i = 0; i < values.Length; i++)
212+
{
213+
string trimmedValue = values[i].Trim();
214+
if (trimmedValue.Contains(nameof(username), StringComparison.OrdinalIgnoreCase))
215+
{
216+
// Username is a quoted string.
217+
int startIndex = trimmedValue.IndexOf('"') + 1;
218+
219+
if (startIndex != -1)
220+
username = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
221+
222+
// Username is mandatory.
223+
if (string.IsNullOrEmpty(username))
224+
return false;
225+
}
226+
if (trimmedValue.Contains(nameof(userhash), StringComparison.OrdinalIgnoreCase) && trimmedValue.Contains("true", StringComparison.OrdinalIgnoreCase))
227+
{
228+
userhash = true;
229+
}
230+
else if (trimmedValue.Contains(nameof(uri), StringComparison.OrdinalIgnoreCase))
231+
{
232+
int startIndex = trimmedValue.IndexOf('"') + 1;
233+
if (startIndex != -1)
234+
uri = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
235+
236+
// Request uri is mandatory.
237+
if (string.IsNullOrEmpty(uri))
238+
return false;
239+
}
240+
else if (trimmedValue.Contains(nameof(realm), StringComparison.OrdinalIgnoreCase))
241+
{
242+
// Realm is a quoted string.
243+
int startIndex = trimmedValue.IndexOf('"') + 1;
244+
if (startIndex != -1)
245+
realm = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
246+
247+
// Realm is mandatory.
248+
if (string.IsNullOrEmpty(realm))
249+
return false;
250+
}
251+
else if (trimmedValue.Contains(nameof(cnonce), StringComparison.OrdinalIgnoreCase))
252+
{
253+
// CNonce is a quoted string.
254+
int startIndex = trimmedValue.IndexOf('"') + 1;
255+
if (startIndex != -1)
256+
cnonce = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
257+
}
258+
else if (trimmedValue.Contains(nameof(nonce), StringComparison.OrdinalIgnoreCase))
259+
{
260+
// Nonce is a quoted string.
261+
int startIndex = trimmedValue.IndexOf('"') + 1;
262+
if (startIndex != -1)
263+
nonce = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
264+
265+
// Nonce is mandatory.
266+
if (string.IsNullOrEmpty(nonce))
267+
return false;
268+
}
269+
else if (trimmedValue.Contains(nameof(response), StringComparison.OrdinalIgnoreCase))
270+
{
271+
// response is a quoted string.
272+
int startIndex = trimmedValue.IndexOf('"') + 1;
273+
if (startIndex != -1)
274+
response = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
275+
276+
// Response is mandatory.
277+
if (string.IsNullOrEmpty(response))
278+
return false;
279+
}
280+
else if (trimmedValue.Contains(nameof(algorithm), StringComparison.OrdinalIgnoreCase))
281+
{
282+
int startIndex = trimmedValue.IndexOf('=') + 1;
283+
if (startIndex != -1)
284+
algorithm = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex).Trim();
285+
286+
if (string.IsNullOrEmpty(algorithm))
287+
algorithm = "sha-256";
288+
}
289+
else if (trimmedValue.Contains(nameof(opaque), StringComparison.OrdinalIgnoreCase))
290+
{
291+
// Opaque is a quoted string.
292+
int startIndex = trimmedValue.IndexOf('"') + 1;
293+
if (startIndex != -1)
294+
opaque = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex - 1);
295+
}
296+
else if (trimmedValue.Contains(nameof(qop), StringComparison.OrdinalIgnoreCase))
297+
{
298+
int startIndex = trimmedValue.IndexOf('=') + 1;
299+
if (startIndex != -1)
300+
qop = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex).Trim();
301+
}
302+
else if (trimmedValue.Contains(nameof(nc), StringComparison.OrdinalIgnoreCase))
303+
{
304+
int startIndex = trimmedValue.IndexOf('=') + 1;
305+
if (startIndex != -1)
306+
nc = trimmedValue.Substring(startIndex, trimmedValue.Length - startIndex).Trim();
307+
}
308+
}
309+
310+
// Verify username.
311+
if (userhash && ComputeHash(options.Username + ":" + realm, algorithm) != username)
312+
{
313+
return false;
314+
}
315+
316+
if (!userhash && options.Username != username)
317+
{
318+
return false;
319+
}
320+
321+
// Calculate response and compare with the client response hash.
322+
string a1 = options.Username + ":" + realm + ":" + options.Password;
323+
if (algorithm.Contains("sess", StringComparison.OrdinalIgnoreCase))
324+
{
325+
a1 = ComputeHash(a1, algorithm) + ":" + nonce + ":" + cnonce ?? string.Empty;
326+
}
327+
328+
string a2 = requestMethod + ":" + uri;
329+
if (qop.Equals("auth-int", StringComparison.OrdinalIgnoreCase))
330+
{
331+
string content = requestContent ?? string.Empty;
332+
a2 = a2 + ":" + ComputeHash(content, algorithm);
333+
}
334+
335+
string serverResponseHash = ComputeHash(ComputeHash(a1, algorithm) + ":" +
336+
nonce + ":" +
337+
nc + ":" +
338+
cnonce + ":" +
339+
qop + ":" +
340+
ComputeHash(a2, algorithm), algorithm);
341+
342+
return response == serverResponseHash;
343+
}
344+
345+
private static string ComputeHash(string data, string algorithm)
346+
{
347+
// Disable MD5 insecure warning.
348+
#pragma warning disable CA5351
349+
using (HashAlgorithm hash = algorithm.Contains("sha-256", StringComparison.OrdinalIgnoreCase) ? SHA256.Create() : (HashAlgorithm)MD5.Create())
350+
#pragma warning restore CA5351
351+
{
352+
Encoding enc = Encoding.UTF8;
353+
byte[] result = hash.ComputeHash(enc.GetBytes(data));
354+
355+
StringBuilder sb = new StringBuilder(result.Length * 2);
356+
foreach (byte b in result)
357+
sb.Append(b.ToString("x2"));
358+
359+
return sb.ToString();
360+
}
361+
}
362+
107363
public static async Task<bool> WebSocketHandshakeAsync(Socket s, StreamReader reader, StreamWriter writer)
108364
{
109365
string serverResponse = null;
@@ -215,7 +471,7 @@ public static Task StartTransferTypeAndErrorServer(
215471
{
216472
// Read past request headers.
217473
string line;
218-
while (!string.IsNullOrEmpty(line = reader.ReadLine())) ;
474+
while (!string.IsNullOrEmpty(line = reader.ReadLine()));
219475

220476
// Determine response transfer headers.
221477
string transferHeader = null;
@@ -261,6 +517,6 @@ public static Task StartTransferTypeAndErrorServer(
261517

262518
return null;
263519
}), out localEndPoint);
264-
}
520+
}
265521
}
266522
}

0 commit comments

Comments
 (0)