Add an http_multi_server package.

This package supports using the same server class for multiple bound interfaces.
In particular, it's useful for binding both IPv4 and IPv6 loopback interfaces.

R=rnystrom@google.com
BUG=19147

Review URL: https://codereview.chromium.org//311233004

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@37023 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e12c8d1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,25 @@
+An implementation of `dart:io`'s [HttpServer][] that wraps multiple servers and
+forwards methods to all of them. It's useful for serving the same application on
+multiple network interfaces while still having a unified way of controlling the
+servers. In particular, it supports serving on both the IPv4 and IPv6 loopback
+addresses using [HttpMultiServer.loopback][].
+
+```dart
+import 'package:http_multi_server/http_multi_server.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+
+void main() {
+  // Both http://127.0.0.1:8080 and http://[::1]:8080 will be bound to the same
+  // server.
+  HttpMultiServer.loopback(8080).then((server) {
+    shelf_io.serveRequests(server, (request) {
+      return new shelf.Response.ok("Hello, world!");
+    });
+  });
+}
+```
+
+[HttpServer]: https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/dart-io.HttpServer
+
+[HttpMultiServer.loopback]: https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/http_multi_server/http_multi_server.HttpMultiServer#id_loopback
diff --git a/lib/http_multi_server.dart b/lib/http_multi_server.dart
new file mode 100644
index 0000000..c055367
--- /dev/null
+++ b/lib/http_multi_server.dart
@@ -0,0 +1,136 @@
+// Copyright (c) 2014, 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.
+
+library http_multi_server;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'src/utils.dart';
+
+/// An implementation of `dart:io`'s [HttpServer] that wraps multiple servers
+/// and forwards methods to all of them.
+///
+/// This is useful for serving the same application on multiple network
+/// interfaces while still having a unified way of controlling the servers. In
+/// particular, it supports serving on both the IPv4 and IPv6 loopback addresses
+/// using [HttpMultiServer.loopback].
+class HttpMultiServer extends StreamView<HttpRequest> implements HttpServer {
+  /// The wrapped servers.
+  final Set<HttpServer> _servers;
+
+  String get serverHeader => _servers.first.serverHeader;
+  set serverHeader(String value) {
+    for (var server in _servers) {
+      server.serverHeader = value;
+    }
+  }
+
+  Duration get idleTimeout => _servers.first.idleTimeout;
+  set idleTimeout(Duration value) {
+    for (var server in _servers) {
+      server.idleTimeout = value;
+    }
+  }
+
+  /// Returns the port that one of the wrapped servers is listening on.
+  ///
+  /// If the wrapped servers are listening on different ports, it's not defined
+  /// which port is returned.
+  int get port => _servers.first.port;
+
+  /// Returns the address that one of the wrapped servers is listening on.
+  ///
+  /// If the wrapped servers are listening on different addresses, it's not
+  /// defined which address is returned.
+  InternetAddress get address => _servers.first.address;
+
+  set sessionTimeout(int value) {
+    for (var server in _servers) {
+      server.sessionTimeout = value;
+    }
+  }
+
+  /// Creates an [HttpMultiServer] wrapping [servers].
+  ///
+  /// All [servers] should have the same configuration and none should be
+  /// listened to when this is called.
+  HttpMultiServer(Iterable<HttpServer> servers)
+      : _servers = servers.toSet(),
+        super(mergeStreams(servers));
+
+  /// Creates an [HttpServer] listening on all available loopback addresses for
+  /// this computer.
+  ///
+  /// If this computer supports both IPv4 and IPv6, this returns an
+  /// [HttpMultiServer] listening to [port] on both loopback addresses.
+  /// Otherwise, it returns a normal [HttpServer] listening only on the IPv4
+  /// address.
+  ///
+  /// If [port] is 0, the same ephemeral port is used for both the IPv4 and IPv6
+  /// addresses.
+  static Future<HttpServer> loopback(int port, {int backlog}) {
+    if (backlog == null) backlog = 0;
+
+    return _loopback(port, (address, port) =>
+        HttpServer.bind(address, port, backlog: backlog));
+  }
+
+  /// Like [loopback], but supports HTTPS requests.
+  ///
+  /// The certificate with nickname or distinguished name (DN) [certificateName]
+  /// is looked up in the certificate database, and is used as the server
+  /// certificate. If [requestClientCertificate] is true, the server will
+  /// request clients to authenticate with a client certificate.
+  static Future<HttpServer> loopbackSecure(int port, {int backlog,
+      String certificateName, bool requestClientCertificate: false}) {
+    if (backlog == null) backlog = 0;
+
+    return _loopback(port, (address, port) =>
+        HttpServer.bindSecure(address, port, backlog: backlog,
+            certificateName: certificateName,
+            requestClientCertificate: requestClientCertificate));
+  }
+
+  /// A helper method for initializing loopback servers.
+  ///
+  /// [bind] should forward to either [HttpServer.bind] or
+  /// [HttpServer.bindSecure].
+  static Future<HttpServer> _loopback(int port,
+      Future<HttpServer> bind(InternetAddress address, int port)) {
+    return Future.wait([
+      supportsIpV6,
+      bind(InternetAddress.LOOPBACK_IP_V4, port)
+    ]).then((results) {
+      var supportsIpV6 = results[0];
+      var v4Server = results[1];
+
+      if (!supportsIpV6) return v4Server;
+
+      // Reuse the IPv4 server's port so that if [port] is 0, both servers use
+      // the same ephemeral port.
+      return bind(InternetAddress.LOOPBACK_IP_V6, v4Server.port)
+          .then((v6Server) {
+        return new HttpMultiServer([v4Server, v6Server]);
+      });
+    });
+  }
+
+  Future close({bool force: false}) =>
+      Future.wait(_servers.map((server) => server.close(force: force)));
+
+  /// Returns an HttpConnectionsInfo object summarizing the total number of
+  /// current connections handled by all the servers.
+  HttpConnectionsInfo connectionsInfo() {
+    var info = new HttpConnectionsInfo();
+    for (var server in _servers) {
+      var subInfo = server.connectionsInfo();
+      info.total += subInfo.total;
+      info.active += subInfo.active;
+      info.idle += subInfo.idle;
+      info.closing += subInfo.closing;
+    }
+    return info;
+  }
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..9e95aba
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,62 @@
+// Copyright (c) 2014, 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.
+
+library http_multi_server.utils;
+
+import 'dart:async';
+import 'dart:io';
+
+/// Merges all streams in [streams] into a single stream that emits all of their
+/// values.
+///
+/// The returned stream will be closed only when every stream in [streams] is
+/// closed.
+Stream mergeStreams(Iterable<Stream> streams) {
+  var subscriptions = new Set();
+  var controller;
+  controller = new StreamController(onListen: () {
+    for (var stream in streams) {
+      var subscription;
+      subscription = stream.listen(controller.add,
+          onError: controller.addError,
+          onDone: () {
+        subscriptions.remove(subscription);
+        if (subscriptions.isEmpty) controller.close();
+      });
+      subscriptions.add(subscription);
+    }
+  }, onCancel: () {
+    for (var subscription in subscriptions) {
+      subscription.cancel();
+    }
+  }, onPause: () {
+    for (var subscription in subscriptions) {
+      subscription.pause();
+    }
+  }, onResume: () {
+    for (var subscription in subscriptions) {
+      subscription.resume();
+    }
+  }, sync: true);
+
+  return controller.stream;
+}
+
+/// A cache for [supportsIpV6].
+bool _supportsIpV6;
+
+/// Returns whether this computer supports binding to IPv6 addresses.
+Future<bool> get supportsIpV6 {
+  if (_supportsIpV6 != null) return new Future.value(_supportsIpV6);
+
+  return ServerSocket.bind(InternetAddress.LOOPBACK_IP_V6, 0).then((socket) {
+    _supportsIpV6 = true;
+    socket.close();
+    return true;
+  }).catchError((error) {
+    if (error is! SocketException) throw error;
+    _supportsIpV6 = false;
+    return false;
+  });
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..4199c58
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,11 @@
+name: http_multi_server
+version: 1.0.0
+author: "Dart Team <misc@dartlang.org>"
+homepage: http://www.dartlang.org
+description:
+  A dart:io HttpServer wrapper that handles requests from multiple servers.
+dev_dependencies:
+  unittest: ">=0.11.0 <0.12.0"
+  http: ">=0.11.0 <0.12.0"
+environment:
+  sdk: ">=1.4.0 <2.0.0"
diff --git a/test/http_multi_server_test.dart b/test/http_multi_server_test.dart
new file mode 100644
index 0000000..a3d9062
--- /dev/null
+++ b/test/http_multi_server_test.dart
@@ -0,0 +1,128 @@
+// Copyright (c) 2014, 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.
+
+library http_multi_server.test;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:http/http.dart' as http;
+import 'package:http_multi_server/http_multi_server.dart';
+import 'package:http_multi_server/src/utils.dart';
+import 'package:unittest/unittest.dart';
+
+void main() {
+  group("with multiple HttpServers", () {
+    var multiServer;
+    var subServer1;
+    var subServer2;
+    var subServer3;
+    setUp(() {
+      return Future.wait([
+        HttpServer.bind("127.0.0.1", 0).then((server) => subServer1 = server),
+        HttpServer.bind("127.0.0.1", 0).then((server) => subServer2 = server),
+        HttpServer.bind("127.0.0.1", 0).then((server) => subServer3 = server)
+      ]).then((servers) => multiServer = new HttpMultiServer(servers));
+    });
+
+    tearDown(() => multiServer.close());
+
+    test("listen listens to all servers", () {
+      multiServer.listen((request) {
+        request.response.write("got request");
+        request.response.close();
+      });
+
+      expect(_read(subServer1), completion(equals("got request")));
+      expect(_read(subServer2), completion(equals("got request")));
+      expect(_read(subServer3), completion(equals("got request")));
+    });
+
+    test("serverHeader= sets the value for all servers", () {
+      multiServer.serverHeader = "http_multi_server test";
+
+      multiServer.listen((request) {
+        request.response.write("got request");
+        request.response.close();
+      });
+
+      expect(_get(subServer1).then((response) {
+        expect(response.headers['server'], equals("http_multi_server test"));
+      }), completes);
+
+      expect(_get(subServer2).then((response) {
+        expect(response.headers['server'], equals("http_multi_server test"));
+      }), completes);
+
+      expect(_get(subServer3).then((response) {
+        expect(response.headers['server'], equals("http_multi_server test"));
+      }), completes);
+    });
+
+    test("connectionsInfo sums the values for all servers", () {
+      var pendingRequests = 0;
+      var awaitingResponseCompleter = new Completer();
+      var sendResponseCompleter = new Completer();
+      multiServer.listen((request) {
+        sendResponseCompleter.future.then((_) {
+          request.response.write("got request");
+          request.response.close();
+        });
+
+        pendingRequests++;
+        if (pendingRequests == 2) awaitingResponseCompleter.complete();
+      });
+
+      // Queue up some requests, then wait on [awaitingResponseCompleter] to
+      // make sure they're in-flight before we check [connectionsInfo].
+      expect(_get(subServer1), completes);
+      expect(_get(subServer2), completes);
+
+      return awaitingResponseCompleter.future.then((_) {
+        var info = multiServer.connectionsInfo();
+        expect(info.total, equals(2));
+        expect(info.active, equals(2));
+        expect(info.idle, equals(0));
+        expect(info.closing, equals(0));
+
+        sendResponseCompleter.complete();
+      });
+    });
+  });
+
+  group("HttpMultiServer.loopback", () {
+    var server;
+    setUp(() {
+      return HttpMultiServer.loopback(0).then((server_) => server = server_);
+    });
+
+    tearDown(() => server.close());
+
+    test("listens on all localhost interfaces", () {
+      server.listen((request) {
+        request.response.write("got request");
+        request.response.close();
+      });
+
+      expect(http.read("http://127.0.0.1:${server.port}/"),
+          completion(equals("got request")));
+
+      return supportsIpV6.then((supportsIpV6) {
+        if (!supportsIpV6) return;
+        expect(http.read("http://[::1]:${server.port}/"),
+            completion(equals("got request")));
+      });
+    });
+  });
+}
+
+/// Makes a GET request to the root of [server] and returns the response.
+Future<http.Response> _get(HttpServer server) => http.get(_urlFor(server));
+
+/// Makes a GET request to the root of [server] and returns the response body.
+Future<String> _read(HttpServer server) => http.read(_urlFor(server));
+
+/// Returns the URL for the root of [server].
+String _urlFor(HttpServer server) =>
+    "http://${server.address.host}:${server.port}/";