@@ -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