Migrated http_proxy_advanced_test from SDK. (#46)
* Migrated http_proxy_advanced_test from SDK.
diff --git a/test/http_proxy_advanced_test.dart b/test/http_proxy_advanced_test.dart
new file mode 100644
index 0000000..5276acc
--- /dev/null
+++ b/test/http_proxy_advanced_test.dart
@@ -0,0 +1,632 @@
+// Copyright (c) 2018, 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:convert';
+import 'dart:io' show Directory, File, Platform, SecurityContext, Socket;
+
+import 'package:convert/convert.dart';
+import 'package:crypto/crypto.dart';
+import 'package:http_io/http_io.dart';
+import 'package:test/test.dart';
+
+String localFile(path) {
+ final localPath = '${Directory.current.path}/test/$path';
+ if (!(new File(localPath).existsSync())) {
+ return Platform.script.resolve(path).toFilePath();
+ }
+ return localPath;
+}
+
+final SecurityContext serverContext = new SecurityContext()
+ ..useCertificateChain(localFile('certificates/server_chain.pem'))
+ ..usePrivateKey(localFile('certificates/server_key.pem'),
+ password: 'dartdart');
+
+final SecurityContext clientContext = new SecurityContext()
+ ..setTrustedCertificates(localFile('certificates/trusted_certs.pem'));
+
+class Server {
+ HttpServer server;
+ bool secure;
+ int proxyHops;
+ List<String> directRequestPaths;
+ int requestCount = 0;
+
+ Server(this.proxyHops, this.directRequestPaths, this.secure);
+
+ Future<Server> start() {
+ return (secure
+ ? HttpServer.bindSecure("localhost", 0, serverContext)
+ : HttpServer.bind("localhost", 0)).then((s) {
+ server = s;
+ server.listen(requestHandler);
+ return this;
+ });
+ }
+
+ void requestHandler(HttpRequest request) {
+ var response = request.response;
+ requestCount++;
+ // Check whether a proxy or direct connection is expected.
+ bool direct = directRequestPaths.fold(
+ false, (prev, path) => prev ? prev : path == request.uri.path);
+ if (!secure && !direct && proxyHops > 0) {
+ expect(request.headers[HttpHeaders.VIA], isNotNull);
+ expect(1, equals(request.headers[HttpHeaders.VIA].length));
+ expect(proxyHops, request.headers[HttpHeaders.VIA][0].split(",").length);
+ } else {
+ expect(request.headers[HttpHeaders.VIA], isNull);
+ }
+ var body = new StringBuffer();
+ onRequestComplete() {
+ String path = request.uri.path.substring(1);
+ if (path != "A") {
+ String content = "$path$path$path";
+ expect(content, equals(body.toString()));
+ }
+ response.write(request.uri.path);
+ response.close();
+ }
+
+ request.listen((data) {
+ body.write(new String.fromCharCodes(data));
+ }, onDone: onRequestComplete);
+ }
+
+ void shutdown() {
+ server.close();
+ }
+
+ int get port => server.port;
+}
+
+Future<Server> setupServer(int proxyHops,
+ {List<String> directRequestPaths: const <String>[], secure: false}) {
+ Server server = new Server(proxyHops, directRequestPaths, secure);
+ return server.start();
+}
+
+class ProxyServer {
+ final bool ipV6;
+ HttpServer server;
+ HttpClient client;
+ int requestCount = 0;
+ String authScheme;
+ String realm = "test";
+ String username;
+ String password;
+
+ var ha1;
+ String serverAlgorithm = "MD5";
+ String serverQop = "auth";
+ Set ncs = new Set();
+
+ var nonce = "12345678"; // No need for random nonce in test.
+
+ ProxyServer({this.ipV6: false}) : client = new HttpClient();
+
+ void useBasicAuthentication(String username, String password) {
+ this.username = username;
+ this.password = password;
+ authScheme = "Basic";
+ }
+
+ void useDigestAuthentication(String username, String password) {
+ this.username = username;
+ this.password = password;
+ authScheme = "Digest";
+
+ // Calculate ha1.
+ var digest = md5.convert("${username}:${realm}:${password}".codeUnits);
+ ha1 = hex.encode(digest.bytes);
+ }
+
+ basicAuthenticationRequired(request) {
+ request.fold(null, (x, y) {}).then((_) {
+ var response = request.response;
+ response.headers
+ .set(HttpHeaders.PROXY_AUTHENTICATE, "Basic, realm=$realm");
+ response.statusCode = HttpStatus.PROXY_AUTHENTICATION_REQUIRED;
+ response.close();
+ });
+ }
+
+ digestAuthenticationRequired(request, {stale: false}) {
+ request.fold(null, (x, y) {}).then((_) {
+ var response = request.response;
+ response.statusCode = HttpStatus.PROXY_AUTHENTICATION_REQUIRED;
+ 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');
+ }
+ if (serverQop != null) authHeader.write(', qop="$serverQop"');
+ response.headers.set(HttpHeaders.PROXY_AUTHENTICATE, authHeader);
+ response.close();
+ });
+ }
+
+ Future<ProxyServer> start() {
+ var x = new Completer<ProxyServer>();
+ var host = ipV6 ? "::1" : "localhost";
+ HttpServer.bind(host, 0).then((s) {
+ server = s;
+ x.complete(this);
+ server.listen((HttpRequest request) {
+ requestCount++;
+ if (username != null && password != null) {
+ if (request.headers[HttpHeaders.PROXY_AUTHORIZATION] == null) {
+ if (authScheme == "Digest") {
+ digestAuthenticationRequired(request);
+ } else {
+ basicAuthenticationRequired(request);
+ }
+ return;
+ } else {
+ expect(
+ 1,
+ equals(
+ request.headers[HttpHeaders.PROXY_AUTHORIZATION].length));
+ String authorization =
+ request.headers[HttpHeaders.PROXY_AUTHORIZATION][0];
+ if (authScheme == "Basic") {
+ List<String> tokens = authorization.split(" ");
+ expect("Basic", equals(tokens[0]));
+ String auth = base64.encode(utf8.encode("$username:$password"));
+ if (auth != tokens[1]) {
+ basicAuthenticationRequired(request);
+ return;
+ }
+ } else {
+ HeaderValue header =
+ HeaderValue.parse(authorization, parameterSeparator: ",");
+ expect("Digest", equals(header.value));
+ var uri = header.parameters["uri"];
+ var qop = header.parameters["qop"];
+ var cnonce = header.parameters["cnonce"];
+ var nc = header.parameters["nc"];
+ expect(username, equals(header.parameters["username"]));
+ expect(realm, equals(header.parameters["realm"]));
+ expect("MD5", equals(header.parameters["algorithm"]));
+ expect(nonce, equals(header.parameters["nonce"]));
+ expect(request.uri.toString(), equals(uri));
+ if (qop != null) {
+ // A server qop of auth-int is downgraded to none by the client.
+ expect("auth", equals(serverQop));
+ expect("auth", equals(header.parameters["qop"]));
+ expect(cnonce, isNotNull);
+ expect(nc, isNotNull);
+ expect(ncs.contains(nc), isFalse);
+ ncs.add(nc);
+ } else {
+ expect(cnonce, isNotNull);
+ expect(nc, isNotNull);
+ }
+ expect(header.parameters["response"], isNotNull);
+
+ var digest = md5.convert("${request.method}:${uri}".codeUnits);
+ var ha2 = hex.encode(digest.bytes);
+
+ 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(hex.encode(digest.bytes),
+ equals(header.parameters["response"]));
+
+ // Add a bogus Proxy-Authentication-Info for testing.
+ var info = 'rspauth="77180d1ab3d6c9de084766977790f482", '
+ 'cnonce="8f971178", '
+ 'nc=000002c74, '
+ 'qop=auth';
+ request.response.headers.set("Proxy-Authentication-Info", info);
+ }
+ }
+ }
+ // Open the connection from the proxy.
+ if (request.method == "CONNECT") {
+ var tmp = request.uri.toString().split(":");
+ Socket.connect(tmp[0], int.parse(tmp[1])).then((socket) {
+ request.response.reasonPhrase = "Connection established";
+ request.response.detachSocket().then((detached) {
+ socket.pipe(detached);
+ detached.pipe(socket);
+ });
+ });
+ } else {
+ client
+ .openUrl(request.method, request.uri)
+ .then((HttpClientRequest clientRequest) {
+ // Forward all headers.
+ request.headers.forEach((String name, List<String> values) {
+ values.forEach((String value) {
+ if (name != "content-length" && name != "via") {
+ clientRequest.headers.add(name, value);
+ }
+ });
+ });
+ // Special handling of Content-Length and Via.
+ clientRequest.contentLength = request.contentLength;
+ List<String> via = request.headers[HttpHeaders.VIA];
+ String viaPrefix = via == null ? "" : "${via[0]}, ";
+ clientRequest.headers
+ .add(HttpHeaders.VIA, "${viaPrefix}1.1 localhost:$port");
+ // Copy all content.
+ return request.pipe(clientRequest);
+ }).then((clientResponse) {
+ (clientResponse as HttpClientResponse).pipe(request.response);
+ });
+ }
+ });
+ });
+ return x.future;
+ }
+
+ void shutdown() {
+ server.close();
+ client.close();
+ }
+
+ int get port => server.port;
+}
+
+Future<ProxyServer> setupProxyServer({ipV6: false}) {
+ ProxyServer proxyServer = new ProxyServer(ipV6: ipV6);
+ return proxyServer.start();
+}
+
+int testProxyIPV6DoneCount = 0;
+Future<Null> testProxyIPV6() {
+ final completer = new Completer<Null>();
+ setupProxyServer(ipV6: true).then((proxyServer) {
+ setupServer(1, directRequestPaths: ["/4"]).then((server) {
+ setupServer(1, directRequestPaths: ["/4"], secure: true)
+ .then((secureServer) {
+ HttpClient client = new HttpClient(context: clientContext);
+
+ List<String> proxy = ["PROXY [::1]:${proxyServer.port}"];
+ client.findProxy = (Uri uri) {
+ // Pick the proxy configuration based on the request path.
+ int index = int.parse(uri.path.substring(1));
+ return proxy[index];
+ };
+
+ for (int i = 0; i < proxy.length; i++) {
+ test(bool secure) {
+ String url = secure
+ ? "https://localhost:${secureServer.port}/$i"
+ : "http://localhost:${server.port}/$i";
+
+ client
+ .postUrl(Uri.parse(url))
+ .then((HttpClientRequest clientRequest) {
+ String content = "$i$i$i";
+ clientRequest.write(content);
+ return clientRequest.close();
+ }).then((HttpClientResponse response) {
+ response.listen((_) {}, onDone: () {
+ testProxyIPV6DoneCount++;
+ if (testProxyIPV6DoneCount == proxy.length * 2) {
+ expect(proxy.length, equals(server.requestCount));
+ expect(proxy.length, equals(secureServer.requestCount));
+ proxyServer.shutdown();
+ server.shutdown();
+ secureServer.shutdown();
+ client.close();
+ completer.complete();
+ }
+ });
+ });
+ }
+
+ test(false);
+ test(true);
+ }
+ });
+ });
+ });
+ return completer.future;
+}
+
+int testProxyFromEnviromentDoneCount = 0;
+Future<Null> testProxyFromEnviroment() {
+ final completer = new Completer<Null>();
+ setupProxyServer().then((proxyServer) {
+ setupServer(1).then((server) {
+ setupServer(1, secure: true).then((secureServer) {
+ HttpClient client = new HttpClient(context: clientContext);
+
+ client.findProxy = (Uri uri) {
+ return HttpClient.findProxyFromEnvironment(uri, environment: {
+ "http_proxy": "localhost:${proxyServer.port}",
+ "https_proxy": "localhost:${proxyServer.port}"
+ });
+ };
+
+ const int loopCount = 5;
+ for (int i = 0; i < loopCount; i++) {
+ test(bool secure) {
+ String url = secure
+ ? "https://localhost:${secureServer.port}/$i"
+ : "http://localhost:${server.port}/$i";
+
+ client
+ .postUrl(Uri.parse(url))
+ .then((HttpClientRequest clientRequest) {
+ String content = "$i$i$i";
+ clientRequest.write(content);
+ return clientRequest.close();
+ }).then((HttpClientResponse response) {
+ response.listen((_) {}, onDone: () {
+ testProxyFromEnviromentDoneCount++;
+ if (testProxyFromEnviromentDoneCount == loopCount * 2) {
+ expect(loopCount, equals(server.requestCount));
+ expect(loopCount, equals(secureServer.requestCount));
+ proxyServer.shutdown();
+ server.shutdown();
+ secureServer.shutdown();
+ client.close();
+ completer.complete();
+ }
+ });
+ });
+ }
+
+ test(false);
+ test(true);
+ }
+ });
+ });
+ });
+ return completer.future;
+}
+
+int testProxyAuthenticateCount = 0;
+Future testProxyAuthenticate(bool useDigestAuthentication) {
+ testProxyAuthenticateCount = 0;
+ var completer = new Completer();
+
+ setupProxyServer().then((proxyServer) {
+ setupServer(1).then((server) {
+ setupServer(1, secure: true).then((secureServer) {
+ HttpClient client = new HttpClient(context: clientContext);
+
+ Completer step1 = new Completer();
+ Completer step2 = new Completer();
+
+ if (useDigestAuthentication) {
+ proxyServer.useDigestAuthentication("dart", "password");
+ } else {
+ proxyServer.useBasicAuthentication("dart", "password");
+ }
+
+ // Test with no authentication.
+ client.findProxy = (Uri uri) {
+ return "PROXY localhost:${proxyServer.port}";
+ };
+
+ const int loopCount = 2;
+ for (int i = 0; i < loopCount; i++) {
+ test(bool secure) {
+ String url = secure
+ ? "https://localhost:${secureServer.port}/$i"
+ : "http://localhost:${server.port}/$i";
+
+ client
+ .postUrl(Uri.parse(url))
+ .then((HttpClientRequest clientRequest) {
+ String content = "$i$i$i";
+ clientRequest.write(content);
+ return clientRequest.close();
+ }).then((HttpClientResponse response) {
+ fail("No response expected");
+ }).catchError((e) {
+ testProxyAuthenticateCount++;
+ if (testProxyAuthenticateCount == loopCount * 2) {
+ expect(0, equals(server.requestCount));
+ expect(0, equals(secureServer.requestCount));
+ step1.complete(null);
+ }
+ });
+ }
+
+ test(false);
+ test(true);
+ }
+ step1.future.then((_) {
+ testProxyAuthenticateCount = 0;
+ if (useDigestAuthentication) {
+ client.findProxy =
+ (Uri uri) => "PROXY localhost:${proxyServer.port}";
+ client.addProxyCredentials("localhost", proxyServer.port, "test",
+ new HttpClientDigestCredentials("dart", "password"));
+ } else {
+ client.findProxy = (Uri uri) {
+ return "PROXY dart:password@localhost:${proxyServer.port}";
+ };
+ }
+
+ for (int i = 0; i < loopCount; i++) {
+ test(bool secure) {
+ var path = useDigestAuthentication ? "A" : "$i";
+ String url = secure
+ ? "https://localhost:${secureServer.port}/$path"
+ : "http://localhost:${server.port}/$path";
+
+ client
+ .postUrl(Uri.parse(url))
+ .then((HttpClientRequest clientRequest) {
+ String content = "$i$i$i";
+ clientRequest.write(content);
+ return clientRequest.close();
+ }).then((HttpClientResponse response) {
+ response.listen((_) {}, onDone: () {
+ testProxyAuthenticateCount++;
+ expect(HttpStatus.OK, equals(response.statusCode));
+ if (testProxyAuthenticateCount == loopCount * 2) {
+ expect(loopCount, equals(server.requestCount));
+ expect(loopCount, equals(secureServer.requestCount));
+ step2.complete(null);
+ }
+ });
+ });
+ }
+
+ test(false);
+ test(true);
+ }
+ });
+
+ step2.future.then((_) {
+ testProxyAuthenticateCount = 0;
+ client.findProxy = (Uri uri) {
+ return "PROXY localhost:${proxyServer.port}";
+ };
+
+ client.authenticateProxy = (host, port, scheme, realm) {
+ client.addProxyCredentials("localhost", proxyServer.port, "realm",
+ new HttpClientBasicCredentials("dart", "password"));
+ return new Future.value(true);
+ };
+
+ for (int i = 0; i < loopCount; i++) {
+ test(bool secure) {
+ String url = secure
+ ? "https://localhost:${secureServer.port}/A"
+ : "http://localhost:${server.port}/A";
+
+ client
+ .postUrl(Uri.parse(url))
+ .then((HttpClientRequest clientRequest) {
+ String content = "$i$i$i";
+ clientRequest.write(content);
+ return clientRequest.close();
+ }).then((HttpClientResponse response) {
+ response.listen((_) {}, onDone: () {
+ testProxyAuthenticateCount++;
+ expect(HttpStatus.OK, equals(response.statusCode));
+ if (testProxyAuthenticateCount == loopCount * 2) {
+ expect(loopCount * 2, equals(server.requestCount));
+ expect(loopCount * 2, equals(secureServer.requestCount));
+ proxyServer.shutdown();
+ server.shutdown();
+ secureServer.shutdown();
+ client.close();
+ completer.complete(null);
+ }
+ });
+ });
+ }
+
+ test(false);
+ test(true);
+ }
+ });
+ });
+ });
+ });
+
+ return completer.future;
+}
+
+int testRealProxyDoneCount = 0;
+void testRealProxy() {
+ setupServer(1).then((server) {
+ HttpClient client = new HttpClient(context: clientContext);
+ client.addProxyCredentials("localhost", 8080, "test",
+ new HttpClientBasicCredentials("dart", "password"));
+
+ List<String> proxy = [
+ "PROXY localhost:8080",
+ "PROXY localhost:8080; PROXY hede.hule.hest:8080",
+ "PROXY hede.hule.hest:8080; PROXY localhost:8080",
+ "PROXY localhost:8080; DIRECT"
+ ];
+
+ client.findProxy = (Uri uri) {
+ // Pick the proxy configuration based on the request path.
+ int index = int.parse(uri.path.substring(1));
+ return proxy[index];
+ };
+
+ for (int i = 0; i < proxy.length; i++) {
+ client
+ .getUrl(Uri.parse("http://localhost:${server.port}/$i"))
+ .then((HttpClientRequest clientRequest) {
+ String content = "$i$i$i";
+ clientRequest.contentLength = content.length;
+ clientRequest.write(content);
+ return clientRequest.close();
+ }).then((HttpClientResponse response) {
+ response.listen((_) {}, onDone: () {
+ if (++testRealProxyDoneCount == proxy.length) {
+ expect(proxy.length, equals(server.requestCount));
+ server.shutdown();
+ client.close();
+ }
+ });
+ });
+ }
+ });
+}
+
+int testRealProxyAuthDoneCount = 0;
+void testRealProxyAuth() {
+ setupServer(1).then((server) {
+ HttpClient client = new HttpClient(context: clientContext);
+
+ List<String> proxy = [
+ "PROXY dart:password@localhost:8080",
+ "PROXY dart:password@localhost:8080; PROXY hede.hule.hest:8080",
+ "PROXY hede.hule.hest:8080; PROXY dart:password@localhost:8080",
+ "PROXY dart:password@localhost:8080; DIRECT"
+ ];
+
+ client.findProxy = (Uri uri) {
+ // Pick the proxy configuration based on the request path.
+ int index = int.parse(uri.path.substring(1));
+ return proxy[index];
+ };
+
+ for (int i = 0; i < proxy.length; i++) {
+ client
+ .getUrl(Uri.parse("http://localhost:${server.port}/$i"))
+ .then((HttpClientRequest clientRequest) {
+ String content = "$i$i$i";
+ clientRequest.contentLength = content.length;
+ clientRequest.write(content);
+ return clientRequest.close();
+ }).then((HttpClientResponse response) {
+ response.listen((_) {}, onDone: () {
+ if (++testRealProxyAuthDoneCount == proxy.length) {
+ expect(proxy.length, equals(server.requestCount));
+ server.shutdown();
+ client.close();
+ }
+ });
+ });
+ }
+ });
+}
+
+main() {
+ test('proxyIPV6', testProxyIPV6);
+ test('proxyFromEnvironment', testProxyFromEnviroment);
+ // The two invocations use the same global variable for state -
+ // run one after the other.
+ test('proxyAuth', () {
+ testProxyAuthenticate(false).then((_) => testProxyAuthenticate(true));
+ });
+
+ // This test is not normally run. It can be used for locally testing
+ // with a real proxy server (e.g. Apache).
+ // testRealProxy();
+ // testRealProxyAuth();
+}