fa28d5b912
Change-Id: I80cff0d2f864828386f1b8711810e960bbe00f3d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/105729 Reviewed-by: Ryan Macnak <rmacnak@google.com> Commit-Queue: Alexander Markov <alexmarkov@google.com>
382 lines
13 KiB
Dart
382 lines
13 KiB
Dart
// 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 "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 = (Uri url, String scheme, String 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 = (Uri url, String scheme, String 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();
|
|
}
|