| // 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. |
| |
| // @dart = 2.9 |
| |
| import "package:convert/convert.dart"; |
| import "package:crypto/crypto.dart"; |
| import "package:expect/expect.dart"; |
| import 'dart:async'; |
| import 'dart:io'; |
| import 'dart:isolate'; |
| |
| class Server { |
| 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}) { |
| 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) { |
| 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(); |
| } |
| |
| 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); |
| Completer 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(); |
| }); |
| }); |
| } |
| |
| // 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() { |
| 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(); |
| // These teste are not normally run. They can be used for locally |
| // testing with another web server (e.g. Apache). |
| //testLocalServerDigest(); |
| } |