| // 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 |
| |
| // 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 { |
| 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.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<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.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<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.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<String> 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<List<int>>().pipe(detached); |
| detached.cast<List<int>>().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.viaHeader]; |
| String viaPrefix = via == null ? "" : "${via[0]}, "; |
| clientRequest.headers |
| .add(HttpHeaders.viaHeader, "${viaPrefix}1.1 localhost:$port"); |
| // Copy all content. |
| return request.cast<List<int>>().pipe(clientRequest); |
| }).then((clientResponse) { |
| (clientResponse as HttpClientResponse) |
| .cast<List<int>>() |
| .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; |
| 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<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.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<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.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<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.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(); |
| } |