|  | // Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file | 
|  | // for details. All rights reserved. Use of this source code is governed by a | 
|  | // BSD-style license that can be found in the LICENSE file. | 
|  |  | 
|  | import 'dart:async'; | 
|  | import 'dart:io'; | 
|  |  | 
|  | import "package:convert/convert.dart"; | 
|  | import "package:crypto/crypto.dart"; | 
|  | import "package:expect/async_helper.dart"; | 
|  | import "package:expect/expect.dart"; | 
|  |  | 
|  | class Server { | 
|  | late HttpServer server; | 
|  | int unauthCount = 0; // Counter of the 401 responses. | 
|  | int successCount = 0; // Counter of the successful responses. | 
|  | int nonceCount = 0; // Counter of use of current nonce. | 
|  | var ha1; | 
|  |  | 
|  | static Future<Server> start( | 
|  | String? algorithm, | 
|  | String? qop, { | 
|  | int? nonceStaleAfter, | 
|  | bool useNextNonce = false, | 
|  | }) { | 
|  | asyncStart(); | 
|  | return new Server()._start(algorithm, qop, nonceStaleAfter, useNextNonce); | 
|  | } | 
|  |  | 
|  | Future<Server> _start( | 
|  | String? serverAlgorithm, | 
|  | String? serverQop, | 
|  | int? nonceStaleAfter, | 
|  | bool useNextNonce, | 
|  | ) { | 
|  | Set ncs = new Set(); | 
|  | // Calculate ha1. | 
|  | String realm = "test"; | 
|  | String username = "dart"; | 
|  | String password = "password"; | 
|  | var hasher = md5.convert("${username}:${realm}:${password}".codeUnits); | 
|  | ha1 = hex.encode(hasher.bytes); | 
|  |  | 
|  | var nonce = "12345678"; // No need for random nonce in test. | 
|  |  | 
|  | var completer = new Completer<Server>(); | 
|  | HttpServer.bind("127.0.0.1", 0).then((s) { | 
|  | server = s; | 
|  | server.listen((HttpRequest request) { | 
|  | if (request.uri.path == "/malformedAuthenticate") { | 
|  | request.response.statusCode = HttpStatus.unauthorized; | 
|  | // This authenticate header is malformed because of missing commas | 
|  | request.response.headers.set( | 
|  | HttpHeaders.wwwAuthenticateHeader, | 
|  | 'Digest realm="$realm" nonce="$nonce" domain="/digest/"', | 
|  | ); | 
|  | request.response.close(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | sendUnauthorizedResponse(HttpResponse response, {stale = false}) { | 
|  | response.statusCode = HttpStatus.unauthorized; | 
|  | StringBuffer authHeader = new StringBuffer(); | 
|  | authHeader.write('Digest'); | 
|  | authHeader.write(', realm="$realm"'); | 
|  | authHeader.write(', nonce="$nonce"'); | 
|  | if (stale) authHeader.write(', stale="true"'); | 
|  | if (serverAlgorithm != null) { | 
|  | authHeader.write(', algorithm=$serverAlgorithm'); | 
|  | } | 
|  | authHeader.write(', domain="/digest/"'); | 
|  | if (serverQop != null) authHeader.write(', qop="$serverQop"'); | 
|  | response.headers.set(HttpHeaders.wwwAuthenticateHeader, authHeader); | 
|  | unauthCount++; | 
|  | } | 
|  |  | 
|  | var response = request.response; | 
|  | if (request.headers[HttpHeaders.authorizationHeader] != null) { | 
|  | Expect.equals( | 
|  | 1, | 
|  | request.headers[HttpHeaders.authorizationHeader]!.length, | 
|  | ); | 
|  | String authorization = | 
|  | request.headers[HttpHeaders.authorizationHeader]![0]; | 
|  | HeaderValue header = HeaderValue.parse( | 
|  | authorization, | 
|  | parameterSeparator: ",", | 
|  | ); | 
|  | if (header.value.toLowerCase() == "basic") { | 
|  | sendUnauthorizedResponse(response); | 
|  | } else if (!useNextNonce && nonceCount == nonceStaleAfter) { | 
|  | nonce = "87654321"; | 
|  | nonceCount = 0; | 
|  | sendUnauthorizedResponse(response, stale: true); | 
|  | } else { | 
|  | var uri = header.parameters["uri"]; | 
|  | var qop = header.parameters["qop"]; | 
|  | var cnonce = header.parameters["cnonce"]; | 
|  | var nc = header.parameters["nc"]; | 
|  | Expect.equals("digest", header.value.toLowerCase()); | 
|  | Expect.equals("dart", header.parameters["username"]); | 
|  | Expect.equals(realm, header.parameters["realm"]); | 
|  | Expect.equals("MD5", header.parameters["algorithm"]); | 
|  | Expect.equals(nonce, header.parameters["nonce"]); | 
|  | Expect.equals(request.uri.toString(), uri); | 
|  | if (qop != null) { | 
|  | // A server qop of auth-int is downgraded to none by the client. | 
|  | Expect.equals("auth", serverQop); | 
|  | Expect.equals("auth", header.parameters["qop"]); | 
|  | Expect.isNotNull(cnonce); | 
|  | Expect.isNotNull(nc); | 
|  | Expect.isFalse(ncs.contains(nc)); | 
|  | ncs.add(nc); | 
|  | } else { | 
|  | Expect.isNull(cnonce); | 
|  | Expect.isNull(nc); | 
|  | } | 
|  | Expect.isNotNull(header.parameters["response"]); | 
|  |  | 
|  | var hasher = md5.convert("${request.method}:${uri}".codeUnits); | 
|  | var ha2 = hex.encode(hasher.bytes); | 
|  |  | 
|  | var x; | 
|  | Digest digest; | 
|  | if (qop == null || qop == "" || qop == "none") { | 
|  | digest = md5.convert("$ha1:${nonce}:$ha2".codeUnits); | 
|  | } else { | 
|  | digest = md5.convert( | 
|  | "$ha1:${nonce}:${nc}:${cnonce}:${qop}:$ha2".codeUnits, | 
|  | ); | 
|  | } | 
|  | Expect.equals( | 
|  | hex.encode(digest.bytes), | 
|  | header.parameters["response"], | 
|  | ); | 
|  |  | 
|  | successCount++; | 
|  | nonceCount++; | 
|  |  | 
|  | // Add a bogus Authentication-Info for testing. | 
|  | var info = | 
|  | 'rspauth="77180d1ab3d6c9de084766977790f482", ' | 
|  | 'cnonce="8f971178", ' | 
|  | 'nc=000002c74, ' | 
|  | 'qop=auth'; | 
|  | if (useNextNonce && nonceCount == nonceStaleAfter) { | 
|  | nonce = "abcdef01"; | 
|  | info += ', nextnonce="$nonce"'; | 
|  | } | 
|  | response.headers.set("Authentication-Info", info); | 
|  | } | 
|  | } else { | 
|  | sendUnauthorizedResponse(response); | 
|  | } | 
|  | response.close(); | 
|  | }); | 
|  | completer.complete(this); | 
|  | }); | 
|  | return completer.future; | 
|  | } | 
|  |  | 
|  | void shutdown() { | 
|  | server.close(); | 
|  | asyncEnd(); | 
|  | } | 
|  |  | 
|  | int get port => server.port; | 
|  | } | 
|  |  | 
|  | void testNoCredentials(String? algorithm, String? qop) { | 
|  | Server.start(algorithm, qop).then((server) { | 
|  | HttpClient client = new HttpClient(); | 
|  |  | 
|  | // Add digest credentials which does not match the path requested. | 
|  | client.addCredentials( | 
|  | Uri.parse("http://127.0.0.1:${server.port}/xxx"), | 
|  | "test", | 
|  | new HttpClientDigestCredentials("dart", "password"), | 
|  | ); | 
|  |  | 
|  | // Add basic credentials for the path requested. | 
|  | client.addCredentials( | 
|  | Uri.parse("http://127.0.0.1:${server.port}/digest"), | 
|  | "test", | 
|  | new HttpClientBasicCredentials("dart", "password"), | 
|  | ); | 
|  |  | 
|  | Future makeRequest(Uri url) { | 
|  | return client | 
|  | .getUrl(url) | 
|  | .then((HttpClientRequest request) => request.close()) | 
|  | .then((HttpClientResponse response) { | 
|  | Expect.equals(HttpStatus.unauthorized, response.statusCode); | 
|  | return response.fold(null, (x, y) {}); | 
|  | }); | 
|  | } | 
|  |  | 
|  | var futures = <Future>[]; | 
|  | for (int i = 0; i < 5; i++) { | 
|  | futures.add( | 
|  | makeRequest(Uri.parse("http://127.0.0.1:${server.port}/digest")), | 
|  | ); | 
|  | } | 
|  | Future.wait(futures).then((_) { | 
|  | server.shutdown(); | 
|  | client.close(); | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void testCredentials(String? algorithm, String? qop) { | 
|  | Server.start(algorithm, qop).then((server) { | 
|  | HttpClient client = new HttpClient(); | 
|  |  | 
|  | Future makeRequest(Uri url) { | 
|  | return client | 
|  | .getUrl(url) | 
|  | .then((HttpClientRequest request) => request.close()) | 
|  | .then((HttpClientResponse response) { | 
|  | Expect.equals(HttpStatus.ok, response.statusCode); | 
|  | Expect.equals(1, response.headers["Authentication-Info"]?.length); | 
|  | return response.fold(null, (x, y) {}); | 
|  | }); | 
|  | } | 
|  |  | 
|  | client.addCredentials( | 
|  | Uri.parse("http://127.0.0.1:${server.port}/digest"), | 
|  | "test", | 
|  | new HttpClientDigestCredentials("dart", "password"), | 
|  | ); | 
|  |  | 
|  | var futures = <Future>[]; | 
|  | for (int i = 0; i < 2; i++) { | 
|  | String uriBase = "http://127.0.0.1:${server.port}/digest"; | 
|  | futures.add(makeRequest(Uri.parse(uriBase))); | 
|  | futures.add(makeRequest(Uri.parse("$uriBase?querystring"))); | 
|  | futures.add(makeRequest(Uri.parse("$uriBase?querystring#fragment"))); | 
|  | } | 
|  | Future.wait(futures).then((_) { | 
|  | server.shutdown(); | 
|  | client.close(); | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void testAuthenticateCallback(String? algorithm, String? qop) { | 
|  | Server.start(algorithm, qop).then((server) { | 
|  | HttpClient client = new HttpClient(); | 
|  |  | 
|  | client.authenticate = (url, scheme, realm) { | 
|  | Expect.equals("Digest", scheme); | 
|  | Expect.equals("test", realm); | 
|  | final completer = new Completer<bool>(); | 
|  | new Timer(const Duration(milliseconds: 10), () { | 
|  | client.addCredentials( | 
|  | Uri.parse("http://127.0.0.1:${server.port}/digest"), | 
|  | "test", | 
|  | new HttpClientDigestCredentials("dart", "password"), | 
|  | ); | 
|  | completer.complete(true); | 
|  | }); | 
|  | return completer.future; | 
|  | }; | 
|  |  | 
|  | Future makeRequest(Uri url) { | 
|  | return client | 
|  | .getUrl(url) | 
|  | .then((HttpClientRequest request) => request.close()) | 
|  | .then((HttpClientResponse response) { | 
|  | Expect.equals(HttpStatus.ok, response.statusCode); | 
|  | Expect.equals(1, response.headers["Authentication-Info"]?.length); | 
|  | return response.fold(null, (x, y) {}); | 
|  | }); | 
|  | } | 
|  |  | 
|  | var futures = <Future>[]; | 
|  | for (int i = 0; i < 5; i++) { | 
|  | futures.add( | 
|  | makeRequest(Uri.parse("http://127.0.0.1:${server.port}/digest")), | 
|  | ); | 
|  | } | 
|  | Future.wait(futures).then((_) { | 
|  | server.shutdown(); | 
|  | client.close(); | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void testStaleNonce() { | 
|  | Server.start("MD5", "auth", nonceStaleAfter: 2).then((server) { | 
|  | HttpClient client = new HttpClient(); | 
|  |  | 
|  | Future makeRequest(Uri url) { | 
|  | return client | 
|  | .getUrl(url) | 
|  | .then((HttpClientRequest request) => request.close()) | 
|  | .then((HttpClientResponse response) { | 
|  | Expect.equals(HttpStatus.ok, response.statusCode); | 
|  | Expect.equals(1, response.headers["Authentication-Info"]?.length); | 
|  | return response.fold(null, (x, y) {}); | 
|  | }); | 
|  | } | 
|  |  | 
|  | Uri uri = Uri.parse("http://127.0.0.1:${server.port}/digest"); | 
|  | var credentials = new HttpClientDigestCredentials("dart", "password"); | 
|  | client.addCredentials(uri, "test", credentials); | 
|  |  | 
|  | makeRequest(uri) | 
|  | .then((_) => makeRequest(uri)) | 
|  | .then((_) => makeRequest(uri)) | 
|  | .then((_) => makeRequest(uri)) | 
|  | .then((_) { | 
|  | Expect.equals(2, server.unauthCount); | 
|  | Expect.equals(4, server.successCount); | 
|  | server.shutdown(); | 
|  | client.close(); | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void testNextNonce() { | 
|  | Server.start("MD5", "auth", nonceStaleAfter: 2, useNextNonce: true).then(( | 
|  | server, | 
|  | ) { | 
|  | HttpClient client = new HttpClient(); | 
|  |  | 
|  | Future makeRequest(Uri url) { | 
|  | return client | 
|  | .getUrl(url) | 
|  | .then((HttpClientRequest request) => request.close()) | 
|  | .then((HttpClientResponse response) { | 
|  | Expect.equals(HttpStatus.ok, response.statusCode); | 
|  | Expect.equals(1, response.headers["Authentication-Info"]?.length); | 
|  | return response.fold(null, (x, y) {}); | 
|  | }); | 
|  | } | 
|  |  | 
|  | Uri uri = Uri.parse("http://127.0.0.1:${server.port}/digest"); | 
|  | var credentials = new HttpClientDigestCredentials("dart", "password"); | 
|  | client.addCredentials(uri, "test", credentials); | 
|  |  | 
|  | makeRequest(uri) | 
|  | .then((_) => makeRequest(uri)) | 
|  | .then((_) => makeRequest(uri)) | 
|  | .then((_) => makeRequest(uri)) | 
|  | .then((_) { | 
|  | Expect.equals(1, server.unauthCount); | 
|  | Expect.equals(4, server.successCount); | 
|  | server.shutdown(); | 
|  | client.close(); | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void testMalformedAuthenticateHeaderNoAuthHandler() { | 
|  | Server.start('MD5', 'auth').then((server) async { | 
|  | HttpClient client = new HttpClient(); | 
|  | final uri = Uri.parse( | 
|  | 'http://${InternetAddress.loopbackIPv4.address}:${server.port}/malformedAuthenticate', | 
|  | ); | 
|  |  | 
|  | // Request should resolve normally if no authentication is configured | 
|  | await client.getUrl(uri).then((request) => request.close()); | 
|  |  | 
|  | server.shutdown(); | 
|  | client.close(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void testMalformedAuthenticateHeaderWithAuthHandler() { | 
|  | Server.start('MD5', 'auth').then((server) async { | 
|  | HttpClient client = new HttpClient(); | 
|  | final uri = Uri.parse( | 
|  | 'http://${InternetAddress.loopbackIPv4.address}:${server.port}/malformedAuthenticate', | 
|  | ); | 
|  |  | 
|  | // Request should throw an exception if the authenticate handler is set | 
|  | client.authenticate = (Uri url, String scheme, String? realm) async => | 
|  | false; | 
|  | await asyncExpectThrows<HttpException>( | 
|  | client.getUrl(uri).then((request) => request.close()), | 
|  | ); | 
|  |  | 
|  | server.shutdown(); | 
|  | client.close(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void testMalformedAuthenticateHeaderWithCredentials() { | 
|  | Server.start('MD5', 'auth').then((server) async { | 
|  | HttpClient client = new HttpClient(); | 
|  | final uri = Uri.parse( | 
|  | 'http://${InternetAddress.loopbackIPv4.address}:${server.port}/malformedAuthenticate', | 
|  | ); | 
|  |  | 
|  | // Request should throw an exception if credentials have been added | 
|  | client.addCredentials( | 
|  | uri, | 
|  | 'realm', | 
|  | HttpClientDigestCredentials('dart', 'password'), | 
|  | ); | 
|  | await asyncExpectThrows<HttpException>( | 
|  | client.getUrl(uri).then((request) => request.close()), | 
|  | ); | 
|  |  | 
|  | server.shutdown(); | 
|  | client.close(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | // An Apache virtual directory configuration like this can be used for | 
|  | // running the local server tests. | 
|  | // | 
|  | //  <Directory "/usr/local/prj/website/digest/"> | 
|  | //    AllowOverride None | 
|  | //    Order deny,allow | 
|  | //    Deny from all | 
|  | //    Allow from 127.0.0.0/255.0.0.0 ::1/128 | 
|  | //    AuthType Digest | 
|  | //    AuthName "test" | 
|  | //    AuthDigestDomain /digest/ | 
|  | //    AuthDigestAlgorithm MD5 | 
|  | //    AuthDigestQop auth | 
|  | //    AuthDigestNonceLifetime 10 | 
|  | //    AuthDigestProvider file | 
|  | //    AuthUserFile /usr/local/prj/apache/passwd/digest-passwd | 
|  | //    Require valid-user | 
|  | //  </Directory> | 
|  | // | 
|  |  | 
|  | void testLocalServerDigest() { | 
|  | int count = 0; | 
|  | HttpClient client = new HttpClient(); | 
|  |  | 
|  | Future makeRequest() { | 
|  | return client | 
|  | .getUrl(Uri.parse("http://127.0.0.1/digest/test")) | 
|  | .then((HttpClientRequest request) => request.close()) | 
|  | .then((HttpClientResponse response) { | 
|  | count++; | 
|  | if (count % 100 == 0) print(count); | 
|  | Expect.equals(HttpStatus.ok, response.statusCode); | 
|  | return response.fold(null, (x, y) {}); | 
|  | }); | 
|  | } | 
|  |  | 
|  | client.addCredentials( | 
|  | Uri.parse("http://127.0.0.1/digest"), | 
|  | "test", | 
|  | new HttpClientDigestCredentials("dart", "password"), | 
|  | ); | 
|  |  | 
|  | client.authenticate = (url, scheme, realm) { | 
|  | client.addCredentials( | 
|  | Uri.parse("http://127.0.0.1/digest"), | 
|  | "test", | 
|  | new HttpClientDigestCredentials("dart", "password"), | 
|  | ); | 
|  | return new Future.value(true); | 
|  | }; | 
|  |  | 
|  | next() { | 
|  | makeRequest().then((_) => next()); | 
|  | } | 
|  |  | 
|  | next(); | 
|  | } | 
|  |  | 
|  | main() { | 
|  | asyncStart(); | 
|  |  | 
|  | testNoCredentials(null, null); | 
|  | testNoCredentials("MD5", null); | 
|  | testNoCredentials("MD5", "auth"); | 
|  | testCredentials(null, null); | 
|  | testCredentials("MD5", null); | 
|  | testCredentials("MD5", "auth"); | 
|  | testCredentials("MD5", "auth-int"); | 
|  | testAuthenticateCallback(null, null); | 
|  | testAuthenticateCallback("MD5", null); | 
|  | testAuthenticateCallback("MD5", "auth"); | 
|  | testAuthenticateCallback("MD5", "auth-int"); | 
|  | testStaleNonce(); | 
|  | testNextNonce(); | 
|  | testMalformedAuthenticateHeaderNoAuthHandler(); | 
|  | testMalformedAuthenticateHeaderWithAuthHandler(); | 
|  | testMalformedAuthenticateHeaderWithCredentials(); | 
|  | // These teste are not normally run. They can be used for locally | 
|  | // testing with another web server (e.g. Apache). | 
|  | //testLocalServerDigest(); | 
|  |  | 
|  | asyncEnd(); | 
|  | } |