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();
+}