// 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. // OtherResources=certificates/server_chain.pem // OtherResources=certificates/server_key.pem // OtherResources=certificates/trusted_certs.pem import "dart:async"; import "dart:io"; import 'dart:convert'; import "package:convert/convert.dart"; import "package:crypto/crypto.dart"; import "package:expect/expect.dart"; import "package:path/path.dart"; String localFile(path) => Platform.script.resolve(path).toFilePath(); 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 { late HttpServer server; bool secure; int proxyHops; List directRequestPaths; int requestCount = 0; Server(this.proxyHops, this.directRequestPaths, this.secure); Future 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.isNotNull(request.headers[HttpHeaders.viaHeader]); Expect.equals(1, request.headers[HttpHeaders.viaHeader]!.length); Expect.equals(proxyHops, request.headers[HttpHeaders.viaHeader]![0].split(",").length); } else { Expect.isNull(request.headers[HttpHeaders.viaHeader]); } var body = new StringBuffer(); onRequestComplete() { String path = request.uri.path.substring(1); if (path != "A") { String content = "$path$path$path"; Expect.equals(content, 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 setupServer(int proxyHops, {List directRequestPaths: const [], secure: false}) { Server server = new Server(proxyHops, directRequestPaths, secure); return server.start(); } class ProxyServer { final bool ipV6; late HttpServer server; final client = new HttpClient(); 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}); 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.proxyAuthenticateHeader, "Basic, realm=$realm"); response.statusCode = HttpStatus.proxyAuthenticationRequired; response.close(); }); } digestAuthenticationRequired(request, {stale: false}) { request.fold(null, (x, y) {}).then((_) { var response = request.response; response.statusCode = HttpStatus.proxyAuthenticationRequired; 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.proxyAuthenticateHeader, authHeader); response.close(); }); } Future start() { var x = new Completer(); 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.proxyAuthorizationHeader] == null) { if (authScheme == "Digest") { digestAuthenticationRequired(request); } else { basicAuthenticationRequired(request); } return; } else { Expect.equals(1, request.headers[HttpHeaders.proxyAuthorizationHeader]!.length); String authorization = request.headers[HttpHeaders.proxyAuthorizationHeader]![0]; if (authScheme == "Basic") { List tokens = authorization.split(" "); Expect.equals("Basic", 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.equals("Digest", header.value); var uri = header.parameters["uri"]; var qop = header.parameters["qop"]; var cnonce = header.parameters["cnonce"]; var nc = header.parameters["nc"]; Expect.equals(username, 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 digest = md5.convert("${request.method}:${uri}".codeUnits); var ha2 = hex.encode(digest.bytes); var x; 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"]); // 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.cast>().pipe(detached); detached.cast>().pipe(socket); }); }); } else { client .openUrl(request.method, request.uri) .then((HttpClientRequest clientRequest) { // Forward all headers. request.headers.forEach((String name, List 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? via = request.headers[HttpHeaders.viaHeader]; String viaPrefix = via == null ? "" : "${via[0]}, "; clientRequest.headers .add(HttpHeaders.viaHeader, "${viaPrefix}1.1 localhost:$port"); // Copy all content. return request.cast>().pipe(clientRequest); }).then((clientResponse) { (clientResponse as HttpClientResponse) .cast>() .pipe(request.response); }); } }); }); return x.future; } void shutdown() { server.close(); client.close(); } int get port => server.port; } Future setupProxyServer({ipV6: false}) { ProxyServer proxyServer = new ProxyServer(ipV6: ipV6); return proxyServer.start(); } int testProxyIPV6DoneCount = 0; void testProxyIPV6() { 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 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.equals(proxy.length, server.requestCount); Expect.equals(proxy.length, secureServer.requestCount); proxyServer.shutdown(); server.shutdown(); secureServer.shutdown(); client.close(); } }); }); } test(false); test(true); } }); }); }); } int testProxyFromEnviromentDoneCount = 0; void testProxyFromEnviroment() { 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.equals(loopCount, server.requestCount); Expect.equals(loopCount, secureServer.requestCount); proxyServer.shutdown(); server.shutdown(); secureServer.shutdown(); client.close(); } }); }); } test(false); test(true); } }); }); }); } 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) { Expect.fail("No response expected"); }).catchError((e) { testProxyAuthenticateCount++; if (testProxyAuthenticateCount == loopCount * 2) { Expect.equals(0, server.requestCount); Expect.equals(0, 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.equals(HttpStatus.ok, response.statusCode); if (testProxyAuthenticateCount == loopCount * 2) { Expect.equals(loopCount, server.requestCount); Expect.equals(loopCount, 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.equals(HttpStatus.ok, response.statusCode); if (testProxyAuthenticateCount == loopCount * 2) { Expect.equals(loopCount * 2, server.requestCount); Expect.equals(loopCount * 2, 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 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.equals(proxy.length, server.requestCount); server.shutdown(); client.close(); } }); }); } }); } int testRealProxyAuthDoneCount = 0; void testRealProxyAuth() { setupServer(1).then((server) { HttpClient client = new HttpClient(context: clientContext); List 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.equals(proxy.length, server.requestCount); server.shutdown(); client.close(); } }); }); } }); } main() { testProxyIPV6(); testProxyFromEnviroment(); // The two invocations use the same global variable for state - // run one after the other. 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(); }