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}/";