Migrated http_proxy_test and some test certificates from SDK. (#44)
* Migrated http_proxy_test and some test certificates from SDK.
* Fixed Platform path resolution issues.
diff --git a/test/certificates/README b/test/certificates/README
new file mode 100644
index 0000000..6ecd518
--- /dev/null
+++ b/test/certificates/README
@@ -0,0 +1,70 @@
+ This directory, tests/standalone/io/certificates, contains the
+X509 TLS certificates and private keys needed to run tests of Dart's
+secure networking code. The SecureSocket and SecureServer classes
+are tested by making TLS (formerly called SSL) connections, secured
+by certificates from a self-signed test root authority.
+
+The certificates are created by running ../create_sample_certificates.sh
+in a bash or sh shell, with the openssl tools installed. Run the script
+twice to create the untrusted_* files.
+
+PEM files:
+
+server_chain.pem:
+ Contains the chain of certificates, from the self-signed
+test certificate authority, through the intermediate CA, to the server
+certificate, used on the server side of a test connection.
+
+server_key.pem:
+ Contains the private key for the server certificate
+
+trusted_certs.pem:
+ Contains the self-signed certificate of the test certificate authority.
+This certificate is set as "trusted" by the client side of the connection
+in its SecurityContext object, so that a verified TLS connection to the
+server can be made.
+
+untrusted_server_chain.pem:
+ Contains a chain of certificates, from a different self-signed
+test certificate authority, through an intermediate CA, to a server
+certificate, used on the server side of a test connection that is intended
+to fail because the client does not accept this certificate authority
+
+untrusted_server_key.pem:
+ Contains the private key for the untrusted server certificate
+in untrusted_server_chain.pem
+
+*_malformed.pem:
+ Truncated PEM formatted certificates used to test error handling.
+
+PKCS12 files:
+
+server_key.12:
+ Created with:
+ $ openssl pkcs12 -export -inkey server_key.pem -out server_key.p12 -nocerts
+ with password 'dartdart'
+
+server_chain.p12:
+ Created with:
+ $ openssl pkcs12 -export -in server_chain.pem -out server_chain.p12 -nokeys
+ with password 'dartdart'
+
+client1_key.p12:
+ Created with:
+ $ openssl pkcs12 -export -inkey client1_key.pem -out client1_key.p12 -nocerts
+ with password 'dartdart'
+
+client1.p12:
+ Created with:
+ $ openssl pkcs12 -export -in client1.pem -out client1.p12 -nokeys
+ with password 'dartdart'
+
+trusted_certs.p12:
+ Created with:
+ $ openssl pkcs12 -export -in trusted_certs.pem -out trusted_certs.p12 -nokeys
+ with password 'dartdart'
+
+client_authority.p12:
+ Created with:
+ $ openssl pkcs12 -export -in client_authority.pem -out client_authority.p12 -nokeys
+ with password 'dartdart'
diff --git a/test/certificates/server_chain.pem b/test/certificates/server_chain.pem
new file mode 100644
index 0000000..341a86f
--- /dev/null
+++ b/test/certificates/server_chain.pem
@@ -0,0 +1,59 @@
+-----BEGIN CERTIFICATE-----
+MIIDZDCCAkygAwIBAgIBATANBgkqhkiG9w0BAQsFADAgMR4wHAYDVQQDDBVpbnRl
+cm1lZGlhdGVhdXRob3JpdHkwHhcNMTUxMDI3MTAyNjM1WhcNMjUxMDI0MTAyNjM1
+WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQCkg/Qr8RQeLTOSgCkyiEX2ztgkgscX8hKGHEHdvlkmVK3JVEIIwkvu
+/Y9LtHZUia3nPAgqEEbexzTENZjSCcC0V6I2XW/e5tIE3rO0KLZyhtZhN/2SfJ6p
+KbOh0HLr1VtkKJGp1tzUmHW/aZI32pK60ZJ/N917NLPCJpCaL8+wHo3+w3oNqln6
+oJsfgxy9SUM8Bsc9WMYKMUdqLO1QKs1A5YwqZuO7Mwj+4LY2QDixC7Ua7V9YAPo2
+1SBeLvMCHbYxSPCuxcZ/kDkgax/DF9u7aZnGhMImkwBka0OQFvpfjKtTIuoobTpe
+PAG7MQYXk4RjnjdyEX/9XAQzvNo1CDObAgMBAAGjgbQwgbEwPAYDVR0RBDUwM4IJ
+bG9jYWxob3N0ggkxMjcuMC4wLjGCAzo6MYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAA
+ATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSvhJo6taTggJQBukEvMo/PDk8tKTAf
+BgNVHSMEGDAWgBS98L4T5RaIToE3DkBRsoeWPil0eDAOBgNVHQ8BAf8EBAMCA6gw
+EwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAHLOt0mL2S4A
+B7vN7KsfQeGlVgZUVlEjem6kqBh4fIzl4CsQuOO8oJ0FlO1z5JAIo98hZinymJx1
+phBVpyGIKakT/etMH0op5evLe9dD36VA3IM/FEv5ibk35iGnPokiJXIAcdHd1zam
+YaTHRAnZET5S03+7BgRTKoRuszhbvuFz/vKXaIAnVNOF4Gf2NUJ/Ax7ssJtRkN+5
+UVxe8TZVxzgiRv1uF6NTr+J8PDepkHCbJ6zEQNudcFKAuC56DN1vUe06gRDrNbVq
+2JHEh4pRfMpdsPCrS5YHBjVq/XHtFHgwDR6g0WTwSUJvDeM4OPQY5f61FB0JbFza
+PkLkXmoIod8=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDLjCCAhagAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1yb290
+YXV0aG9yaXR5MB4XDTE1MTAyNzEwMjYzNVoXDTI1MTAyNDEwMjYzNVowIDEeMBwG
+A1UEAwwVaW50ZXJtZWRpYXRlYXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEA6GndRFiXk+2q+Ig7ZOWKKGta+is8137qyXz+eVFs5sA0ajMN
+ZBAMWS0TIXw/Yks+y6fEcV/tfv91k1eUN4YXPcoxTdDF97d2hO9wxumeYOMnQeDy
+VZVDKQBZ+jFMeI+VkNpMEdmsLErpZDGob/1dC8tLEuR6RuRR8X6IDGMPOCMw1jLK
+V1bQjPtzqKadTscfjLuKxuLgspJdTrzsu6hdcl1mm8K6CjTY2HNXWxs1yYmwfuQ2
+Z4/8sOMNqFqLjN+ChD7pksTMq7IosqGiJzi2bpd5f44ek/k822Y0ATncJHk4h1Z+
+kZBnW6kgcLna1gDri9heRwSZ+M8T8nlHgIMZIQIDAQABo3sweTASBgNVHRMBAf8E
+CDAGAQH/AgEAMB0GA1UdDgQWBBS98L4T5RaIToE3DkBRsoeWPil0eDAfBgNVHSME
+GDAWgBRxD5DQHTmtpDFKDOiMf5FAi6vfbzAOBgNVHQ8BAf8EBAMCAgQwEwYDVR0l
+BAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAD+4KpUeV5mUPw5IG/7w
+eOXnUpeS96XFGuS1JuFo/TbgntPWSPyo+rD4GrPIkUXyoHaMCDd2UBEjyGbBIKlB
+NZA3RJOAEp7DTkLNK4RFn/OEcLwG0J5brL7kaLRO4vwvItVIdZ2XIqzypRQTc0MG
+MmF08zycnSlaN01ryM67AsMhwdHqVa+uXQPo8R8sdFGnZ33yywTYD73FeImXilQ2
+rDnFUVqmrW1fjl0Fi4rV5XI0EQiPrzKvRtmF8ZqjGATPOsRd64cwQX6V+P5hNeIR
+9pba6td7AbNGausHfacRYMyoGJWWWkFPd+7jWOCPqW7Fk1tmBgdB8GzXa3inWIRM
+RUE=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIC+zCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1yb290
+YXV0aG9yaXR5MB4XDTE1MTAyNzEwMjYzNFoXDTI1MTAyNDEwMjYzNFowGDEWMBQG
+A1UEAwwNcm9vdGF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAMl+dcraUM/E7E6zl7+7hK9oUJYXJLnfiMtP/TRFVbH4+2aEN8vXzPbzKdR3
+FfaHczXQTwnTCaYA4u4uSDvSOsFFEfxEwYORsdKmQEM8nGpVX2NVvKsMcGIhh8kh
+ZwJfkMIOcAxmGIHGdMhF8VghonJ8uGiuqktxdfpARq0g3fqIjDHsF9/LpfshUfk9
+wsRyTF0yr90U/dsfnE+u8l7GvVl8j2Zegp0sagAGtLaNv7tP17AibqEGg2yDBrBN
+9r9ihe4CqMjx+Q2kQ2S9Gz2V2ReO/n6vm2VQxsPRB/lV/9jh7cUcS0/9mggLYrDy
+cq1v7rLLQrWuxMz1E3gOhyCYJ38CAwEAAaNQME4wHQYDVR0OBBYEFHEPkNAdOa2k
+MUoM6Ix/kUCLq99vMB8GA1UdIwQYMBaAFHEPkNAdOa2kMUoM6Ix/kUCLq99vMAwG
+A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABrhjnWC6b+z9Kw73C/niOwo
+9sPdufjS6tb0sCwDjt3mjvE4NdNWt+/+ZOugW6dqtvqhtqZM1q0u9pJkNwIrqgFD
+ZHcfNaf31G6Z2YE+Io7woTVw6fFobg/EFo+a/qwbvWL26McmiRL5yiSBjVjpX4a5
+kdZ+aPQUCBaLrTWwlCDqzSVIULWUQvveRWbToMFKPNID58NtEpymAx3Pgir7YjV9
+UnlU2l5vZrh1PTCqZxvC/IdRESUfW80LdHaeyizRUP+6vKxGgSz2MRuYINjbd6GO
+hGiCpWlwziW2xLV1l2qSRLko2kIafLZP18N0ThM9zKbU5ps9NgFOf//wqSGtLaE=
+-----END CERTIFICATE-----
diff --git a/test/certificates/server_key.pem b/test/certificates/server_key.pem
new file mode 100644
index 0000000..895b7d2
--- /dev/null
+++ b/test/certificates/server_key.pem
@@ -0,0 +1,29 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIE4zAcBgoqhkiG9w0BDAEBMA4ECBMCjlg8JYZ4AgIIAASCBMFd9cBoZ5xcTock
+AVQcg/HzYJtMceKn1gtMDdC7mmXuyN0shoxhG4BpQInHkFARL+nenesXFxEm4X5e
+L603Pcgw72/ratxVpTW7hPMjiLTEBqza0GjQm7Sarbdy+Vzdp/6XFrAcPfFl1juY
+oyYzbozPsvFHz3Re44y1KmI4HAzU/qkjJUbNTTiPPVI2cDP6iYN2XXxBb1wwp8jR
+iqdZqFG7lU/wvPEbD7BVPpmJBHWNG681zb4ea5Zn4hW8UaxpiIBiaH0/IWc2SVZd
+RliAFo3NEsGxCcsnBo/n00oudGbOJxdOp7FbH5hJpeqX2WhCyJRxIeHOWmeuMAet
+03HFriiEmJ99m2nEJN1x0A3QUUM7ji6vZAb4qb1dyq7LlX4M2aaqixRnaTcQkapf
+DOxX35DEBXSKrDpyWp6Rx4wNpUyi1TKyhaVnYgD3Gn0VfC/2w86gSFlrf9PMYGM0
+PvFxTDzTyjOuPBRa728gZOGXgDOL7qvdInU/opVew7kFeRQHXxHzFCLK5dD+Vrig
+5fS3m0++f55ODkxqHXB8gbXbd3GMmsW6MrGpU7VsCNtbVPdSMW0FalovEB0M+2lj
+1VfuvL+0F5huTe+BgZAt6xgET/CIcZXdNMRPVhraqUjqWtI9Rdk4STPCpU1rDkjG
+YDl/fo4W2T6qQWFUpiC9IvVVGkVxaqfZZ4Qu+V5xPUi6vk95QiTNkN1t+m+sCCgS
+Llkea8Um0aHMy33Lj3NsfL0LMrnpniqcAks8BvcgIZwk1VRqcj7BQVCygJSYrmAR
+DBhMpjWlXuSggnyVPuduZDtnTN+8lCHLOKL3a3bDb6ySaKX49Km6GutDLfpDtEA0
+3mQvmEG4XVm7zy+AlN72qFbtSLDRi/D/uQh2q/ZrFQLOBQBQB56TvEbKouLimUDM
+ascQA3aUyhOE7e+d02NOFIFTozwc/C//CIFeA+ZEwxyfha/3Bor6Jez7PC/eHNxZ
+w7YMXzPW9NhcCcerhYGebuCJxLwzqJ+IGdukjKsGV2ytWDoB2xZiJNu096j4RKcq
+YSJoen0R7IH8N4eDujXR8m9kAl724Uqs1OoAs4VNICvzTutbsgVZ6Z+NMOcfnPw9
+jZkFhot16w8znD+OmhBR7/bzOLpaeUhk7EhNq5M6U0NNWx3WwkDlvU/jx+6/EQe3
+iLEHptH2HYBF1xscaKGbtKNtuQsfdzgWpOX0qK2YbK3yCKvL/xIm1DQmDZDKkWdW
+VNh8oGV1H96CivWlvxhAgXKz9F/83CjMw8YXRk7RJvWR4vtNvXFAvGkFIYCN9Jv9
+p+1ukaYoxSLGBik907I6gWSHqumJiCprUyAX/bVfZfNiYh4hzeA3lhwxZSax3JG4
+7QFPvyepOmF/3AAzS/Pusx6jOZnuCMCkfQi6Wpem1o3s4x+fP7kz00Xuj01ErucM
+S10ixfIh84kXBN3dTRDtDdeCyoMsBKO0W5jDBBlWL02YfdF6Opo1Q4cPh2DYgXMh
+XEszNZSK5LB0y+f3A6Kdx/hkZzHVvMONA70OyrkoZzGyWENhcB0c7ntTJyPPD2qM
+s0HRA2VwF/0ypU3OKERM1Ua5NSkTgvnnVTlV9GO90Tkn5v4fxdl8NzIuJLyGguTP
+Xc0tRM34Lg==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/test/certificates/trusted_certs.pem b/test/certificates/trusted_certs.pem
new file mode 100644
index 0000000..8b5bf3e
--- /dev/null
+++ b/test/certificates/trusted_certs.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC+zCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1yb290
+YXV0aG9yaXR5MB4XDTE1MTAyNzEwMjYzNFoXDTI1MTAyNDEwMjYzNFowGDEWMBQG
+A1UEAwwNcm9vdGF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAMl+dcraUM/E7E6zl7+7hK9oUJYXJLnfiMtP/TRFVbH4+2aEN8vXzPbzKdR3
+FfaHczXQTwnTCaYA4u4uSDvSOsFFEfxEwYORsdKmQEM8nGpVX2NVvKsMcGIhh8kh
+ZwJfkMIOcAxmGIHGdMhF8VghonJ8uGiuqktxdfpARq0g3fqIjDHsF9/LpfshUfk9
+wsRyTF0yr90U/dsfnE+u8l7GvVl8j2Zegp0sagAGtLaNv7tP17AibqEGg2yDBrBN
+9r9ihe4CqMjx+Q2kQ2S9Gz2V2ReO/n6vm2VQxsPRB/lV/9jh7cUcS0/9mggLYrDy
+cq1v7rLLQrWuxMz1E3gOhyCYJ38CAwEAAaNQME4wHQYDVR0OBBYEFHEPkNAdOa2k
+MUoM6Ix/kUCLq99vMB8GA1UdIwQYMBaAFHEPkNAdOa2kMUoM6Ix/kUCLq99vMAwG
+A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABrhjnWC6b+z9Kw73C/niOwo
+9sPdufjS6tb0sCwDjt3mjvE4NdNWt+/+ZOugW6dqtvqhtqZM1q0u9pJkNwIrqgFD
+ZHcfNaf31G6Z2YE+Io7woTVw6fFobg/EFo+a/qwbvWL26McmiRL5yiSBjVjpX4a5
+kdZ+aPQUCBaLrTWwlCDqzSVIULWUQvveRWbToMFKPNID58NtEpymAx3Pgir7YjV9
+UnlU2l5vZrh1PTCqZxvC/IdRESUfW80LdHaeyizRUP+6vKxGgSz2MRuYINjbd6GO
+hGiCpWlwziW2xLV1l2qSRLko2kIafLZP18N0ThM9zKbU5ps9NgFOf//wqSGtLaE=
+-----END CERTIFICATE-----
diff --git a/test/http_proxy_test.dart b/test/http_proxy_test.dart
new file mode 100644
index 0000000..4df58be
--- /dev/null
+++ b/test/http_proxy_test.dart
@@ -0,0 +1,491 @@
+// 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,
+ equals(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";
+ }
+
+ 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, isNull);
+ expect(nc, isNull);
+ }
+ 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();
+}
+
+testInvalidProxy() {
+ HttpClient client = new HttpClient(context: clientContext);
+
+ client.findProxy = (Uri uri) => "";
+ client
+ .getUrl(Uri.parse("http://www.google.com/test"))
+ .catchError((error) {}, test: (e) => e is HttpException);
+
+ client.findProxy = (Uri uri) => "XXX";
+ client
+ .getUrl(Uri.parse("http://www.google.com/test"))
+ .catchError((error) {}, test: (e) => e is HttpException);
+
+ client.findProxy = (Uri uri) => "PROXY www.google.com";
+ client
+ .getUrl(Uri.parse("http://www.google.com/test"))
+ .catchError((error) {}, test: (e) => e is HttpException);
+
+ client.findProxy = (Uri uri) => "PROXY www.google.com:http";
+ client
+ .getUrl(Uri.parse("http://www.google.com/test"))
+ .catchError((error) {}, test: (e) => e is HttpException);
+}
+
+int testDirectDoneCount = 0;
+Future<Null> testDirectProxy() {
+ final completer = new Completer<Null>();
+ setupServer(0).then((server) {
+ HttpClient client = new HttpClient(context: clientContext);
+ List<String> proxy = [
+ "DIRECT",
+ " DIRECT ",
+ "DIRECT ;",
+ " DIRECT ; ",
+ ";DIRECT",
+ " ; DIRECT ",
+ ";;DIRECT;;"
+ ];
+
+ client.findProxy = (Uri uri) {
+ 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: () {
+ testDirectDoneCount++;
+ if (testDirectDoneCount == proxy.length) {
+ expect(proxy.length, equals(server.requestCount));
+ server.shutdown();
+ client.close();
+ completer.complete();
+ }
+ });
+ });
+ }
+ });
+ return completer.future;
+}
+
+int testProxyDoneCount = 0;
+Future<Null> testProxy() {
+ final completer = new Completer<Null>();
+ setupProxyServer().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;
+ if (Platform.operatingSystem == "windows") {
+ proxy = [
+ "PROXY localhost:${proxyServer.port}",
+ "PROXY localhost:${proxyServer.port}; PROXY hede.hule.hest:8080",
+ "PROXY localhost:${proxyServer.port}",
+ ""
+ " PROXY localhost:${proxyServer.port}",
+ "DIRECT",
+ "PROXY localhost:${proxyServer.port}; DIRECT"
+ ];
+ } else {
+ proxy = [
+ "PROXY localhost:${proxyServer.port}",
+ "PROXY localhost:${proxyServer.port}; PROXY hede.hule.hest:8080",
+ "PROXY hede.hule.hest:8080; PROXY localhost:${proxyServer.port}",
+ "PROXY hede.hule.hest:8080; PROXY hede.hule.hest:8181;"
+ " PROXY localhost:${proxyServer.port}",
+ "PROXY hede.hule.hest:8080; PROXY hede.hule.hest:8181; DIRECT",
+ "PROXY localhost:${proxyServer.port}; 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++) {
+ 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: () {
+ testProxyDoneCount++;
+ if (testProxyDoneCount == 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 testProxyChainDoneCount = 0;
+Future<Null> testProxyChain() {
+ final completer = new Completer<Null>();
+ // Setup two proxy servers having the first using the second as its proxy.
+ setupProxyServer().then((proxyServer1) {
+ setupProxyServer().then((proxyServer2) {
+ proxyServer1.client.findProxy =
+ (_) => "PROXY localhost:${proxyServer2.port}";
+
+ setupServer(2, directRequestPaths: ["/4"]).then((server) {
+ HttpClient client = new HttpClient(context: clientContext);
+
+ List<String> proxy;
+ if (Platform.operatingSystem == "windows") {
+ proxy = [
+ "PROXY localhost:${proxyServer1.port}",
+ "PROXY localhost:${proxyServer1.port}; PROXY hede.hule.hest:8080",
+ "PROXY localhost:${proxyServer1.port}",
+ "PROXY localhost:${proxyServer1.port}",
+ "DIRECT",
+ "PROXY localhost:${proxyServer1.port}; DIRECT"
+ ];
+ } else {
+ proxy = [
+ "PROXY localhost:${proxyServer1.port}",
+ "PROXY localhost:${proxyServer1.port}; PROXY hede.hule.hest:8080",
+ "PROXY hede.hule.hest:8080; PROXY localhost:${proxyServer1.port}",
+ "PROXY hede.hule.hest:8080; PROXY hede.hule.hest:8181;"
+ " PROXY localhost:${proxyServer1.port}",
+ "PROXY hede.hule.hest:8080; PROXY hede.hule.hest:8181; DIRECT",
+ "PROXY localhost:${proxyServer1.port}; 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: () {
+ testProxyChainDoneCount++;
+ if (testProxyChainDoneCount == proxy.length) {
+ expect(proxy.length, equals(server.requestCount));
+ proxyServer1.shutdown();
+ proxyServer2.shutdown();
+ server.shutdown();
+ client.close();
+ completer.complete();
+ }
+ });
+ });
+ }
+ });
+ });
+ });
+ return completer.future;
+}
+
+main() {
+ test('invalidProxy', testInvalidProxy);
+ test('directProxy', testDirectProxy);
+ test('proxy', testProxy);
+ test('proxyChain', testProxyChain);
+}