diff --git a/.github/ISSUE_TEMPLATE/http_multi_server.md b/.github/ISSUE_TEMPLATE/http_multi_server.md new file mode 100644 index 0000000000..10d75c5ef8 --- /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 3add1c1717..1ab6973fd8 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/**' + - any-glob-to-any-file: 'pkgs/http2/**' -'package:http_parser': +'package:http_client_conformance_tests': - changed-files: - - any-glob-to-any-file: 'pkgs/http_parser/**' + - any-glob-to-any-file: 'pkgs/http_client_conformance_tests/**' -'package: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_client_conformance_tests/**' + - 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 0000000000..71f6062ead --- /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 31fc6500b1..fac1ade842 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ and the browser. | [http](pkgs/http/) | A composable, multi-platform, Future-based API for HTTP requests. | [![pub package](https://img.shields.io/pub/v/http.svg)](https://pub.dev/packages/http) | | [http2](pkgs/http2/) | A HTTP/2 implementation in Dart. | [![pub package](https://img.shields.io/pub/v/http2.svg)](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. | [![pub package](https://img.shields.io/pub/v/http_multi_server.svg)](https://pub.dev/packages/http_multi_server) | | [http_parser](pkgs/http_parser/) | A platform-independent package for parsing and serializing HTTP formats. | [![pub package](https://img.shields.io/pub/v/http_parser.svg)](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. | [![pub package](https://img.shields.io/pub/v/http_profile.svg)](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. | [![pub package](https://img.shields.io/pub/v/ok_http.svg)](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 0000000000..e98dd1481e --- /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 0000000000..531426abb3 --- /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 0000000000..2af6d1dc7e --- /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 0000000000..162572a442 --- /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 0000000000..7d1d6bff4b --- /dev/null +++ b/pkgs/http_multi_server/README.md @@ -0,0 +1,28 @@ +[![Dart CI](https://github.com/dart-lang/http/actions/workflows/http_multi_server.yaml/badge.svg)](https://github.com/dart-lang/http/actions/workflows/http_multi_server.yaml) +[![pub package](https://img.shields.io/pub/v/http_multi_server.svg)](https://pub.dev/packages/http_multi_server) +[![package publisher](https://img.shields.io/pub/publisher/http_multi_server.svg)](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 0000000000..99d2063d7d --- /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 0000000000..6e90c8b890 --- /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 0000000000..18fba335e4 --- /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 implements HttpServer { + /// The wrapped servers. + final Set _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 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 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 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 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 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 _loopback( + int port, Future 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 0000000000..c5ee3d2ae8 --- /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 _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 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 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? 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 0000000000..87520fb9cd --- /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 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 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 0000000000..c359aec851 --- /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 0000000000..a644348cbb --- /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(); + final sendResponseCompleter = 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((_) { + 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())); + } + }); + }); + + 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())); + } + }); + }); +} + +/// Makes a GET request to the root of [server] and returns the response. +Future _get(HttpServer server) => http.get(_urlFor(server)); + +/// Makes a GET request to the root of [server] and returns the response body. +Future _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 _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----- +''');