Merge pull request #1416 from dart-lang/merge-http_multi_server-package
Merge `package:http_multi_server`
diff --git a/.github/ISSUE_TEMPLATE/http_multi_server.md b/.github/ISSUE_TEMPLATE/http_multi_server.md
new file mode 100644
index 0000000..10d75c5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/http_multi_server.md
@@ -0,0 +1,5 @@
+---
+name: "package:http_multi_server"
+about: "Create a bug or file a feature request against package:http_multi_server."
+labels: "package:http_multi_server"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 3add1c1..1ab6973 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -2,28 +2,32 @@
'type-infra':
- changed-files:
- - any-glob-to-any-file: '.github/**'
+ - any-glob-to-any-file: '.github/**'
'package:cronet_http':
- changed-files:
- - any-glob-to-any-file: 'pkgs/cronet_http/**'
+ - any-glob-to-any-file: 'pkgs/cronet_http/**'
'package:cupertino_http':
- changed-files:
- - any-glob-to-any-file: 'pkgs/cupertino_http/**'
+ - any-glob-to-any-file: 'pkgs/cupertino_http/**'
'package:http':
- changed-files:
- - any-glob-to-any-file: 'pkgs/http/**'
+ - any-glob-to-any-file: 'pkgs/http/**'
'package:http2':
- changed-files:
- - any-glob-to-any-file: 'pkgs/http2/**'
-
-'package:http_parser':
- - changed-files:
- - any-glob-to-any-file: 'pkgs/http_parser/**'
+ - any-glob-to-any-file: 'pkgs/http2/**'
'package:http_client_conformance_tests':
- changed-files:
- - any-glob-to-any-file: 'pkgs/http_client_conformance_tests/**'
+ - any-glob-to-any-file: 'pkgs/http_client_conformance_tests/**'
+
+'package:http_multi_server':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/http_multi_server/**'
+
+'package:http_parser':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/http_parser/**'
diff --git a/.github/workflows/http_multi_server.yaml b/.github/workflows/http_multi_server.yaml
new file mode 100644
index 0000000..71f6062
--- /dev/null
+++ b/.github/workflows/http_multi_server.yaml
@@ -0,0 +1,70 @@
+name: package:http_multi_server
+
+on:
+ push:
+ branches:
+ - master
+ paths:
+ - '.github/workflows/http_multi_server.yaml'
+ - 'pkgs/http_multi_server/**'
+ pull_request:
+ paths:
+ - '.github/workflows/http_multi_server.yaml'
+ - 'pkgs/http_multi_server/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+defaults:
+ run:
+ working-directory: pkgs/http_multi_server/
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.2, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index 31fc650..fac1ade 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@
| [http](pkgs/http/) | A composable, multi-platform, Future-based API for HTTP requests. | [](https://pub.dev/packages/http) |
| [http2](pkgs/http2/) | A HTTP/2 implementation in Dart. | [](https://pub.dev/packages/http2) |
| [http_client_conformance_tests](pkgs/http_client_conformance_tests/) | A library that tests whether implementations of package:http's `Client` class behave as expected. | |
+| [http_multi_server](pkgs/http_multi_server/) | A `dart:io` `HttpServer` wrapper that handles requests from multiple servers. | [](https://pub.dev/packages/http_multi_server) |
| [http_parser](pkgs/http_parser/) | A platform-independent package for parsing and serializing HTTP formats. | [](https://pub.dev/packages/http_parser) |
| [http_profile](pkgs/http_profile/) | A library used by HTTP client authors to integrate with the DevTools Network View. | [](https://pub.dev/packages/http_profile) |
| [ok_http](pkgs/ok_http/) | An Android Flutter plugin that provides access to the [OkHttp](https://square.github.io/okhttp/) HTTP client and the OkHttp [WebSocket](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-web-socket/index.html) API. | [](https://pub.dev/packages/ok_http) |
diff --git a/pkgs/http_multi_server/.gitignore b/pkgs/http_multi_server/.gitignore
new file mode 100644
index 0000000..e98dd14
--- /dev/null
+++ b/pkgs/http_multi_server/.gitignore
@@ -0,0 +1,14 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.dart_tool/
+.packages
+build/
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pkgs/http_multi_server/.test_config b/pkgs/http_multi_server/.test_config
new file mode 100644
index 0000000..531426a
--- /dev/null
+++ b/pkgs/http_multi_server/.test_config
@@ -0,0 +1,5 @@
+{
+ "test_package": {
+ "platforms": ["vm"]
+ }
+}
\ No newline at end of file
diff --git a/pkgs/http_multi_server/CHANGELOG.md b/pkgs/http_multi_server/CHANGELOG.md
new file mode 100644
index 0000000..2af6d1d
--- /dev/null
+++ b/pkgs/http_multi_server/CHANGELOG.md
@@ -0,0 +1,113 @@
+## 3.2.2
+
+* Require Dart 3.2
+* Move to `dart-lang/http` monorepo.
+
+## 3.2.1
+
+* Populate the pubspec `repository` field.
+
+## 3.2.0
+
+* Honor the `preserveHeaderCase` argument to `MultiHeaders.set` and `.add`.
+
+## 3.1.0
+
+* Add `HttpMultiServer.bindSecure` to match `HttpMultiServer.bind`.
+
+## 3.0.1
+
+* Fix an issue where `bind` would bind to the `anyIPv6` address in unsupported
+ environments.
+
+## 3.0.0
+
+* Migrate to null safety.
+
+## 2.2.0
+
+* Preparation for [HttpHeaders change]. Update signature of `MultiHeaders.add()`
+ and `MultiHeaders.set()` to match new signature of `HttpHeaders`. The
+ parameter is not yet forwarded and will not behave as expected.
+
+ [HttpHeaders change]: https://github.com/dart-lang/sdk/issues/39657
+
+## 2.1.0
+
+* Add `HttpMultiServer.bind` static which centralizes logic around common local
+ serving scenarios - handling a more flexible 'localhost' and listening on
+ 'any' hostname.
+* Update SDK constraints to `>=2.1.0 <3.0.0`.
+
+## 2.0.6
+
+* If there is a problem starting a loopback Ipv6 server, don't keep the Ipv4
+ server open when throwing the exception.
+
+## 2.0.5
+
+* Update SDK constraints to `>=2.0.0-dev <3.0.0`.
+
+## 2.0.4
+
+* Declare support for `async` 2.0.0.
+
+## 2.0.3
+
+* Fix `HttpMultiServer.loopback()` and `.loopbackSecure()` for environments that
+ don't support IPv4.
+
+## 2.0.2
+
+* Fix a dependency that was incorrectly marked as dev-only.
+
+## 2.0.1
+
+* Fix most strong mode errors and warnings.
+
+## 2.0.0
+
+* **Breaking:** Change the signature of `HttpMultiServer.loopbackSecure()` to
+ match the new Dart 1.13 `HttpServer.bindSecure()` signature. This removes the
+ `certificateName` named parameter and adds the required `context` parameter
+ and the named `v6Only` and `shared` parameters.
+
+* Added `v6Only` and `shared` parameters to `HttpMultiServer.loopback()` to
+ match `HttpServer.bind()`.
+
+## 1.3.2
+
+* Eventually stop retrying port allocation if it fails repeatedly.
+
+* Properly detect socket errors caused by already-in-use addresses.
+
+## 1.3.1
+
+* `loopback()` and `loopbackSecure()` recover gracefully if an ephemeral port is
+ requested and the located port isn't available on both IPv4 and IPv6.
+
+## 1.3.0
+
+* Add support for `HttpServer.autoCompress`.
+
+## 1.2.0
+
+* Add support for `HttpServer.defaultResponseHeaders.clear`.
+
+* Fix `HttpServer.defaultResponseHeaders.remove` and `.removeAll`.
+
+## 1.1.0
+
+* Add support for `HttpServer.defaultResponseHeaders`.
+
+## 1.0.2
+
+* Remove the workaround for [issue 19815][].
+
+## 1.0.1
+
+* Ignore errors from one of the servers if others are still bound. In
+ particular, this works around [issue 19815][] on some Windows machines where
+ IPv6 failure isn't discovered until we try to connect to the socket.
+
+[issue 19815]: https://code.google.com/p/dart/issues/detail?id=19815
diff --git a/pkgs/http_multi_server/LICENSE b/pkgs/http_multi_server/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/pkgs/http_multi_server/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/http_multi_server/README.md b/pkgs/http_multi_server/README.md
new file mode 100644
index 0000000..7d1d6bf
--- /dev/null
+++ b/pkgs/http_multi_server/README.md
@@ -0,0 +1,28 @@
+[](https://github.com/dart-lang/http/actions/workflows/http_multi_server.yaml)
+[](https://pub.dev/packages/http_multi_server)
+[](https://pub.dev/packages/http_multi_server/publisher)
+
+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() async {
+ // Both http://127.0.0.1:8080 and http://[::1]:8080 will be bound to the same
+ // server.
+ var server = await HttpMultiServer.loopback(8080);
+ shelf_io.serveRequests(server, (request) {
+ return 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/pkgs/http_multi_server/analysis_options.yaml b/pkgs/http_multi_server/analysis_options.yaml
new file mode 100644
index 0000000..99d2063
--- /dev/null
+++ b/pkgs/http_multi_server/analysis_options.yaml
@@ -0,0 +1,25 @@
+# https://dart.dev/tools/analysis#the-analysis-options-file
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+
+linter:
+ rules:
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_classes_with_only_static_members
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_returning_this
+ - avoid_unused_constructor_parameters
+ - cancel_subscriptions
+ - cascade_invocations
+ - join_return_with_assignment
+ - literal_only_boolean_expressions
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - prefer_expression_function_bodies
+ - prefer_final_locals
+ - use_string_buffers
diff --git a/pkgs/http_multi_server/example/main.dart b/pkgs/http_multi_server/example/main.dart
new file mode 100644
index 0000000..6e90c8b
--- /dev/null
+++ b/pkgs/http_multi_server/example/main.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2024, 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 '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() async {
+ // Both http://127.0.0.1:8080 and http://[::1]:8080 will be bound to the same
+ // server.
+ final server = await HttpMultiServer.loopback(8080);
+ shelf_io.serveRequests(
+ server,
+ (request) => shelf.Response.ok('Hello, world!'),
+ );
+}
diff --git a/pkgs/http_multi_server/lib/http_multi_server.dart b/pkgs/http_multi_server/lib/http_multi_server.dart
new file mode 100644
index 0000000..18fba33
--- /dev/null
+++ b/pkgs/http_multi_server/lib/http_multi_server.dart
@@ -0,0 +1,250 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+
+import 'src/multi_headers.dart';
+import 'src/utils.dart';
+
+/// The error code for an error caused by a port already being in use.
+final _addressInUseErrno = _computeAddressInUseErrno();
+int _computeAddressInUseErrno() {
+ if (Platform.isWindows) return 10048;
+ if (Platform.isMacOS) return 48;
+ assert(Platform.isLinux);
+ return 98;
+}
+
+/// 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;
+
+ /// Returns the default value of the `Server` header for all responses
+ /// generated by each server.
+ ///
+ /// If the wrapped servers have different default values, it's not defined
+ /// which value is returned.
+ @override
+ String? get serverHeader => _servers.first.serverHeader;
+
+ @override
+ set serverHeader(String? value) {
+ for (var server in _servers) {
+ server.serverHeader = value;
+ }
+ }
+
+ /// Returns the default set of headers added to all response objects.
+ ///
+ /// If the wrapped servers have different default headers, it's not defined
+ /// which header is returned for accessor methods.
+ @override
+ final HttpHeaders defaultResponseHeaders;
+
+ @override
+ Duration? get idleTimeout => _servers.first.idleTimeout;
+ @override
+ set idleTimeout(Duration? value) {
+ for (var server in _servers) {
+ server.idleTimeout = value;
+ }
+ }
+
+ @override
+ bool get autoCompress => _servers.first.autoCompress;
+ @override
+ set autoCompress(bool value) {
+ for (var server in _servers) {
+ server.autoCompress = 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.
+ @override
+ 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.
+ @override
+ InternetAddress get address => _servers.first.address;
+
+ @override
+ 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(),
+ defaultResponseHeaders = MultiHeaders(
+ servers.map((server) => server.defaultResponseHeaders)),
+ super(StreamGroup.merge(servers));
+
+ /// Creates an [HttpServer] listening on all available loopback addresses for
+ /// this computer.
+ ///
+ /// See [HttpServer.bind].
+ static Future<HttpServer> loopback(int port,
+ {int backlog = 0, bool v6Only = false, bool shared = false}) =>
+ _loopback(
+ port,
+ (address, port) => HttpServer.bind(address, port,
+ backlog: backlog, v6Only: v6Only, shared: shared));
+
+ /// Like [loopback], but supports HTTPS requests.
+ ///
+ /// See [HttpServer.bindSecure].
+ static Future<HttpServer> loopbackSecure(int port, SecurityContext context,
+ {int backlog = 0,
+ bool v6Only = false,
+ bool requestClientCertificate = false,
+ bool shared = false}) =>
+ _loopback(
+ port,
+ (address, port) => HttpServer.bindSecure(address, port, context,
+ backlog: backlog,
+ v6Only: v6Only,
+ shared: shared,
+ requestClientCertificate: requestClientCertificate));
+
+ /// Bind an [HttpServer] with handling for special addresses 'localhost' and
+ /// 'any'.
+ ///
+ /// For address 'localhost' behaves like [loopback].
+ ///
+ /// For 'any' listens on [InternetAddress.anyIPv6] if the system supports IPv6
+ /// otherwise [InternetAddress.anyIPv4]. Note [InternetAddress.anyIPv6]
+ /// listens on all hostnames for both IPv4 and IPv6.
+ ///
+ /// For any other address forwards directly to `HttpServer.bind` where
+ /// the IPvX support may vary.
+ ///
+ /// See [HttpServer.bind].
+ static Future<HttpServer> bind(dynamic address, int port,
+ {int backlog = 0, bool v6Only = false, bool shared = false}) async {
+ if (address == 'localhost') {
+ return HttpMultiServer.loopback(port,
+ backlog: backlog, v6Only: v6Only, shared: shared);
+ }
+ if (address == 'any') {
+ return HttpServer.bind(
+ await supportsIPv6
+ ? InternetAddress.anyIPv6
+ : InternetAddress.anyIPv4,
+ port,
+ backlog: backlog,
+ v6Only: v6Only,
+ shared: shared);
+ }
+ return HttpServer.bind(address, port,
+ backlog: backlog, v6Only: v6Only, shared: shared);
+ }
+
+ /// Bind a secure [HttpServer] with handling for special addresses 'localhost'
+ /// and 'any'.
+ ///
+ /// For address 'localhost' behaves like [loopback].
+ ///
+ /// For 'any' listens on [InternetAddress.anyIPv6] if the system supports IPv6
+ /// otherwise [InternetAddress.anyIPv4]. Note [InternetAddress.anyIPv6]
+ /// listens on all hostnames for both IPv4 and IPv6.
+ ///
+ /// See [HttpServer.bindSecure].
+ static Future<HttpServer> bindSecure(
+ dynamic address, int port, SecurityContext context,
+ {int backlog = 0, bool v6Only = false, bool shared = false}) async {
+ if (address == 'localhost') {
+ return await HttpMultiServer.loopbackSecure(port, context,
+ backlog: backlog, v6Only: v6Only, shared: shared);
+ }
+ if (address == 'any') {
+ return await HttpServer.bindSecure(
+ await supportsIPv6
+ ? InternetAddress.anyIPv6
+ : InternetAddress.anyIPv4,
+ port,
+ context,
+ backlog: backlog,
+ v6Only: v6Only,
+ shared: shared);
+ }
+ return await HttpServer.bindSecure(address, port, context,
+ backlog: backlog, v6Only: v6Only, shared: shared);
+ }
+
+ /// 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> Function(InternetAddress, int port) bind,
+ [int remainingRetries = 5]) async {
+ if (!await supportsIPv4) {
+ return await bind(InternetAddress.loopbackIPv6, port);
+ }
+
+ final v4Server = await bind(InternetAddress.loopbackIPv4, port);
+ if (!await supportsIPv6) return v4Server;
+
+ try {
+ // Reuse the IPv4 server's port so that if [port] is 0, both servers use
+ // the same ephemeral port.
+ final v6Server = await bind(InternetAddress.loopbackIPv6, v4Server.port);
+ return HttpMultiServer([v4Server, v6Server]);
+ } on SocketException catch (error) {
+ // If there is already a server listening we'll lose the reference on a
+ // rethrow.
+ await v4Server.close();
+
+ if (error.osError?.errorCode != _addressInUseErrno) rethrow;
+ if (port != 0) rethrow;
+ if (remainingRetries == 0) rethrow;
+
+ // A port being available on IPv4 doesn't necessarily mean that the same
+ // port is available on IPv6. If it's not (which is rare in practice),
+ // we try again until we find one that's available on both.
+ return await _loopback(port, bind, remainingRetries - 1);
+ }
+ }
+
+ @override
+ 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.
+ @override
+ HttpConnectionsInfo connectionsInfo() {
+ final info = HttpConnectionsInfo();
+ for (var server in _servers) {
+ final subInfo = server.connectionsInfo();
+ info
+ ..total += subInfo.total
+ ..active += subInfo.active
+ ..idle += subInfo.idle
+ ..closing += subInfo.closing;
+ }
+ return info;
+ }
+}
diff --git a/pkgs/http_multi_server/lib/src/multi_headers.dart b/pkgs/http_multi_server/lib/src/multi_headers.dart
new file mode 100644
index 0000000..c5ee3d2
--- /dev/null
+++ b/pkgs/http_multi_server/lib/src/multi_headers.dart
@@ -0,0 +1,147 @@
+// 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.
+
+import 'dart:io';
+
+/// A class that delegates header access and setting to many [HttpHeaders]
+/// instances.
+class MultiHeaders implements HttpHeaders {
+ /// The wrapped headers.
+ final Set<HttpHeaders> _headers;
+
+ @override
+ bool get chunkedTransferEncoding => _headers.first.chunkedTransferEncoding;
+ @override
+ set chunkedTransferEncoding(bool value) {
+ for (var headers in _headers) {
+ headers.chunkedTransferEncoding = value;
+ }
+ }
+
+ @override
+ int get contentLength => _headers.first.contentLength;
+ @override
+ set contentLength(int value) {
+ for (var headers in _headers) {
+ headers.contentLength = value;
+ }
+ }
+
+ @override
+ ContentType? get contentType => _headers.first.contentType;
+ @override
+ set contentType(ContentType? value) {
+ for (var headers in _headers) {
+ headers.contentType = value;
+ }
+ }
+
+ @override
+ DateTime? get date => _headers.first.date;
+ @override
+ set date(DateTime? value) {
+ for (var headers in _headers) {
+ headers.date = value;
+ }
+ }
+
+ @override
+ DateTime? get expires => _headers.first.expires;
+ @override
+ set expires(DateTime? value) {
+ for (var headers in _headers) {
+ headers.expires = value;
+ }
+ }
+
+ @override
+ String? get host => _headers.first.host;
+ @override
+ set host(String? value) {
+ for (var headers in _headers) {
+ headers.host = value;
+ }
+ }
+
+ @override
+ DateTime? get ifModifiedSince => _headers.first.ifModifiedSince;
+ @override
+ set ifModifiedSince(DateTime? value) {
+ for (var headers in _headers) {
+ headers.ifModifiedSince = value;
+ }
+ }
+
+ @override
+ bool get persistentConnection => _headers.first.persistentConnection;
+ @override
+ set persistentConnection(bool value) {
+ for (var headers in _headers) {
+ headers.persistentConnection = value;
+ }
+ }
+
+ @override
+ int? get port => _headers.first.port;
+ @override
+ set port(int? value) {
+ for (var headers in _headers) {
+ headers.port = value;
+ }
+ }
+
+ MultiHeaders(Iterable<HttpHeaders> headers) : _headers = headers.toSet();
+
+ @override
+ void add(String name, Object value, {bool preserveHeaderCase = false}) {
+ for (var headers in _headers) {
+ headers.add(name, value, preserveHeaderCase: preserveHeaderCase);
+ }
+ }
+
+ @override
+ void forEach(void Function(String name, List<String> values) f) =>
+ _headers.first.forEach(f);
+
+ @override
+ void noFolding(String name) {
+ for (var headers in _headers) {
+ headers.noFolding(name);
+ }
+ }
+
+ @override
+ void remove(String name, Object value) {
+ for (var headers in _headers) {
+ headers.remove(name, value);
+ }
+ }
+
+ @override
+ void removeAll(String name) {
+ for (var headers in _headers) {
+ headers.removeAll(name);
+ }
+ }
+
+ @override
+ void set(String name, Object value, {bool preserveHeaderCase = false}) {
+ for (var headers in _headers) {
+ headers.set(name, value, preserveHeaderCase: preserveHeaderCase);
+ }
+ }
+
+ @override
+ String? value(String name) => _headers.first.value(name);
+
+ @override
+ List<String>? operator [](String name) => _headers.first[name];
+
+ @override
+ void clear() {
+ for (var headers in _headers) {
+ headers.clear();
+ }
+ }
+}
diff --git a/pkgs/http_multi_server/lib/src/utils.dart b/pkgs/http_multi_server/lib/src/utils.dart
new file mode 100644
index 0000000..87520fb
--- /dev/null
+++ b/pkgs/http_multi_server/lib/src/utils.dart
@@ -0,0 +1,27 @@
+// 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.
+
+import 'dart:io';
+
+/// Returns whether this computer supports binding to IPv6 addresses.
+final Future<bool> supportsIPv6 = () async {
+ try {
+ final socket = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0);
+ await socket.close();
+ return true;
+ } on SocketException catch (_) {
+ return false;
+ }
+}();
+
+/// Returns whether this computer supports binding to IPv4 addresses.
+final Future<bool> supportsIPv4 = () async {
+ try {
+ final socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
+ await socket.close();
+ return true;
+ } on SocketException catch (_) {
+ return false;
+ }
+}();
diff --git a/pkgs/http_multi_server/pubspec.yaml b/pkgs/http_multi_server/pubspec.yaml
new file mode 100644
index 0000000..c359aec
--- /dev/null
+++ b/pkgs/http_multi_server/pubspec.yaml
@@ -0,0 +1,17 @@
+name: http_multi_server
+version: 3.2.2
+description: >-
+ A dart:io HttpServer wrapper that handles requests from multiple servers.
+repository: https://github.com/dart-lang/http/tree/master/pkgs/http_multi_server
+
+environment:
+ sdk: ^3.2.0
+
+dependencies:
+ async: ^2.5.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^2.0.0
+ http: ^1.0.0
+ shelf: ^1.4.0
+ test: ^1.16.0
diff --git a/pkgs/http_multi_server/test/http_multi_server_test.dart b/pkgs/http_multi_server/test/http_multi_server_test.dart
new file mode 100644
index 0000000..a644348
--- /dev/null
+++ b/pkgs/http_multi_server/test/http_multi_server_test.dart
@@ -0,0 +1,418 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:http/http.dart' as http;
+import 'package:http/io_client.dart' as http;
+import 'package:http_multi_server/http_multi_server.dart';
+import 'package:http_multi_server/src/utils.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('with multiple HttpServers', () {
+ late HttpMultiServer multiServer;
+ late HttpServer subServer1;
+ late HttpServer subServer2;
+ late HttpServer subServer3;
+
+ setUp(() => Future.wait([
+ HttpServer.bind('localhost', 0).then((server) => subServer1 = server),
+ HttpServer.bind('localhost', 0).then((server) => subServer2 = server),
+ HttpServer.bind('localhost', 0).then((server) => subServer3 = server)
+ ]).then((servers) => multiServer = 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'
+ ..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('autoCompress= sets the value for all servers', () {
+ multiServer
+ ..autoCompress = true
+ ..listen((request) {
+ request.response.write('got request');
+ request.response.close();
+ });
+
+ expect(
+ _get(subServer1).then((response) {
+ expect(response.headers['content-encoding'], equals('gzip'));
+ }),
+ completes);
+
+ expect(
+ _get(subServer2).then((response) {
+ expect(response.headers['content-encoding'], equals('gzip'));
+ }),
+ completes);
+
+ expect(
+ _get(subServer3).then((response) {
+ expect(response.headers['content-encoding'], equals('gzip'));
+ }),
+ completes);
+ });
+
+ test('headers.set sets the value for all servers', () {
+ multiServer.defaultResponseHeaders
+ .set('server', '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;
+ final awaitingResponseCompleter = Completer<void>();
+ final sendResponseCompleter = Completer<void>();
+ 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((_) {
+ final 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', () {
+ late HttpServer server;
+
+ setUp(() => HttpMultiServer.loopback(0).then((s) => server = s));
+
+ tearDown(() => server.close());
+
+ test('listens on all localhost interfaces', () async {
+ server.listen((request) {
+ request.response.write('got request');
+ request.response.close();
+ });
+
+ if (await supportsIPv4) {
+ expect(http.read(Uri.http('127.0.0.1:${server.port}', '/')),
+ completion(equals('got request')));
+ }
+
+ if (await supportsIPv6) {
+ expect(http.read(Uri.http('[::1]:${server.port}', '/')),
+ completion(equals('got request')));
+ }
+ });
+ });
+
+ group('HttpMultiServer.bind', () {
+ test("listens on all localhost interfaces for 'localhost'", () async {
+ final server = await HttpMultiServer.bind('localhost', 0);
+ server.listen((request) {
+ request.response.write('got request');
+ request.response.close();
+ });
+
+ if (await supportsIPv4) {
+ expect(http.read(Uri.http('127.0.0.1:${server.port}', '/')),
+ completion(equals('got request')));
+ }
+
+ if (await supportsIPv6) {
+ expect(http.read(Uri.http('[::1]:${server.port}', '/')),
+ completion(equals('got request')));
+ }
+ });
+
+ test("listens on all localhost interfaces for 'any'", () async {
+ final server = await HttpMultiServer.bind('any', 0);
+ server.listen((request) {
+ request.response.write('got request');
+ request.response.close();
+ });
+
+ if (await supportsIPv4) {
+ expect(http.read(Uri.http('127.0.0.1:${server.port}', '/')),
+ completion(equals('got request')));
+ }
+
+ if (await supportsIPv6) {
+ expect(http.read(Uri.http('[::1]:${server.port}', '/')),
+ completion(equals('got request')));
+ }
+ });
+
+ test("uses the correct server address for 'any'", () async {
+ final server = await HttpMultiServer.bind('any', 0);
+
+ if (!await supportsIPv6) {
+ expect(server.address, InternetAddress.anyIPv4);
+ } else {
+ expect(server.address, InternetAddress.anyIPv6);
+ }
+ });
+
+ test('listens on specified hostname', () async {
+ if (!await supportsIPv4) return;
+ final server = await HttpMultiServer.bind(InternetAddress.anyIPv4, 0);
+ server.listen((request) {
+ request.response.write('got request');
+ request.response.close();
+ });
+
+ expect(http.read(Uri.http('127.0.0.1:${server.port}', '/')),
+ completion(equals('got request')));
+
+ if (await supportsIPv6) {
+ expect(http.read(Uri.http('[::1]:${server.port}', '/')),
+ throwsA(isA<SocketException>()));
+ }
+ });
+ });
+
+ group('HttpMultiServer.bindSecure', () {
+ late http.Client client;
+ late SecurityContext context;
+ setUp(() async {
+ context = SecurityContext()
+ ..setTrustedCertificatesBytes(_sslCert)
+ ..useCertificateChainBytes(_sslCert)
+ ..usePrivateKeyBytes(_sslKey, password: 'dartdart');
+ client = http.IOClient(HttpClient(context: context));
+ });
+ test('listens on all localhost interfaces for "localhost"', () async {
+ final server = await HttpMultiServer.bindSecure('localhost', 0, context);
+ server.listen((request) {
+ request.response.write('got request');
+ request.response.close();
+ });
+
+ if (await supportsIPv4) {
+ expect(client.read(Uri.https('127.0.0.1:${server.port}')),
+ completion(equals('got request')));
+ }
+
+ if (await supportsIPv6) {
+ expect(client.read(Uri.https('[::1]:${server.port}')),
+ completion(equals('got request')));
+ }
+ });
+
+ test('listens on all localhost interfaces for "any"', () async {
+ final server = await HttpMultiServer.bindSecure('any', 0, context);
+ server.listen((request) {
+ request.response.write('got request');
+ request.response.close();
+ });
+
+ if (await supportsIPv4) {
+ expect(client.read(Uri.https('127.0.0.1:${server.port}')),
+ completion(equals('got request')));
+ }
+
+ if (await supportsIPv6) {
+ expect(client.read(Uri.https('[::1]:${server.port}')),
+ completion(equals('got request')));
+ }
+ });
+
+ test('listens on specified hostname', () async {
+ if (!await supportsIPv4) return;
+ final server =
+ await HttpMultiServer.bindSecure(InternetAddress.anyIPv4, 0, context);
+ server.listen((request) {
+ request.response.write('got request');
+ request.response.close();
+ });
+
+ expect(client.read(Uri.https('127.0.0.1:${server.port}')),
+ completion(equals('got request')));
+
+ if (await supportsIPv6) {
+ expect(client.read(Uri.https('[::1]:${server.port}')),
+ throwsA(isA<SocketException>()));
+ }
+ });
+ });
+}
+
+/// 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].
+Uri _urlFor(HttpServer server) =>
+ Uri.http('${server.address.host}:${server.port}', '/');
+
+final _sslCert = utf8.encode('''
+-----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-----
+''');
+
+List<int> _sslKey = utf8.encode('''
+-----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-----
+''');