├── .github └── workflows │ └── test-package.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── example.dart ├── lib └── shelf_proxy.dart ├── pubspec.yaml └── test └── shelf_proxy_test.dart /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart dev. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [2.14.0, dev] 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: dart-lang/setup-dart@v1.0 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | run: dart pub get 31 | - run: dart format --output=none --set-exit-if-changed . 32 | if: matrix.sdk == 'dev' && steps.install.outcome == 'success' 33 | - run: dart analyze --fatal-infos 34 | if: matrix.sdk == 'dev' && steps.install.outcome == 'success' 35 | - run: dart analyze 36 | if: matrix.sdk != 'dev' && steps.install.output == 'success' 37 | 38 | # Run tests on a matrix consisting of two dimensions: 39 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 40 | # 2. release channel: dev 41 | test: 42 | needs: analyze 43 | runs-on: ${{ matrix.os }} 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | # Add macos-latest and/or windows-latest if relevant for this package. 48 | os: [ubuntu-latest] 49 | sdk: [2.14.0, dev] 50 | steps: 51 | - uses: actions/checkout@v2 52 | - uses: dart-lang/setup-dart@v1.0 53 | with: 54 | sdk: ${{ matrix.sdk }} 55 | - id: install 56 | run: dart pub get 57 | - run: dart test 58 | if: always() && steps.install.outcome == 'success' 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.1 2 | 3 | - Drop dependency on `package:pedantic`. 4 | - Require Dart `2.14`. 5 | 6 | ## 1.0.0 7 | 8 | - Require Dart `2.12`. 9 | - Enable null safety. 10 | - Removed deprecated `createProxyHandler`. 11 | 12 | ## 0.1.0+7 13 | 14 | * Added example. 15 | * Fixed links in README. 16 | 17 | ## 0.1.0+6 18 | 19 | * Support the latest version of `package:http`. 20 | 21 | ## 0.1.0+5 22 | 23 | * Support Dart 2. 24 | 25 | ## 0.1.0+4 26 | 27 | * Internal changes only. 28 | 29 | ## 0.1.0+3 30 | 31 | * Support version `0.7.0` of `shelf`. 32 | 33 | ## 0.1.0+2 34 | 35 | * Support version `0.6.0` of `shelf`. 36 | 37 | ## 0.1.0+1 38 | 39 | * Added `drone.io` badge to `README.md`. 40 | 41 | ## 0.1.0 42 | 43 | * `createProxyHandler` (new deprecated) is replaced with `proxyHandler`. 44 | 45 | * Updated to be compatible with RFC 2616 Proxy specification. 46 | 47 | ## 0.0.2 48 | 49 | * Updated `README.md` and doc comments on `createProxyHandler`. 50 | 51 | * Added an example. 52 | 53 | ## 0.0.1 54 | 55 | * First release. 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This package has moved to https://github.com/dart-lang/shelf/tree/master/pkgs/shelf_proxy 2 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | strong-mode: 5 | implicit-casts: false 6 | 7 | linter: 8 | rules: 9 | - avoid_bool_literals_in_conditional_expressions 10 | - avoid_catching_errors 11 | - avoid_classes_with_only_static_members 12 | - avoid_dynamic_calls 13 | - avoid_empty_else 14 | - avoid_function_literals_in_foreach_calls 15 | - avoid_private_typedef_functions 16 | - avoid_redundant_argument_values 17 | - avoid_renaming_method_parameters 18 | - avoid_returning_null 19 | - avoid_returning_null_for_future 20 | - avoid_returning_null_for_void 21 | - avoid_returning_this 22 | - avoid_unused_constructor_parameters 23 | - avoid_void_async 24 | - camel_case_types 25 | - cancel_subscriptions 26 | - cascade_invocations 27 | - comment_references 28 | - constant_identifier_names 29 | - control_flow_in_finally 30 | - directives_ordering 31 | - empty_statements 32 | - file_names 33 | - hash_and_equals 34 | - implementation_imports 35 | - invariant_booleans 36 | - iterable_contains_unrelated_type 37 | - join_return_with_assignment 38 | - lines_longer_than_80_chars 39 | - list_remove_unrelated_type 40 | - literal_only_boolean_expressions 41 | - missing_whitespace_between_adjacent_strings 42 | - no_adjacent_strings_in_list 43 | - no_runtimeType_toString 44 | - non_constant_identifier_names 45 | - only_throw_errors 46 | - overridden_fields 47 | - package_api_docs 48 | - package_names 49 | - package_prefixed_library_names 50 | - prefer_asserts_in_initializer_lists 51 | - prefer_const_constructors 52 | - prefer_const_declarations 53 | - prefer_expression_function_bodies 54 | - prefer_final_locals 55 | - prefer_function_declarations_over_variables 56 | - prefer_initializing_formals 57 | - prefer_interpolation_to_compose_strings 58 | - prefer_is_not_operator 59 | - prefer_null_aware_operators 60 | - prefer_relative_imports 61 | - prefer_typing_uninitialized_variables 62 | - prefer_void_to_null 63 | - provide_deprecation_message 64 | - sort_pub_dependencies 65 | - test_types_in_equals 66 | - throw_in_finally 67 | - unnecessary_await_in_return 68 | - unnecessary_lambdas 69 | - unnecessary_null_aware_assignments 70 | - unnecessary_overrides 71 | - unnecessary_parenthesis 72 | - unnecessary_statements 73 | - unnecessary_string_interpolations 74 | - use_string_buffers 75 | - void_checks 76 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:shelf/shelf_io.dart' as shelf_io; 6 | import 'package:shelf_proxy/shelf_proxy.dart'; 7 | 8 | Future main() async { 9 | final server = await shelf_io.serve( 10 | proxyHandler('https://dart.dev'), 11 | 'localhost', 12 | 8080, 13 | ); 14 | 15 | print('Proxying at http://${server.address.host}:${server.port}'); 16 | } 17 | -------------------------------------------------------------------------------- /lib/shelf_proxy.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:http/http.dart' as http; 8 | import 'package:path/path.dart' as p; 9 | import 'package:shelf/shelf.dart'; 10 | 11 | /// A handler that proxies requests to [url]. 12 | /// 13 | /// To generate the proxy request, this concatenates [url] and [Request.url]. 14 | /// This means that if the handler mounted under `/documentation` and [url] is 15 | /// `http://example.com/docs`, a request to `/documentation/tutorials` 16 | /// will be proxied to `http://example.com/docs/tutorials`. 17 | /// 18 | /// [url] must be a [String] or [Uri]. 19 | /// 20 | /// [client] is used internally to make HTTP requests. It defaults to a 21 | /// `dart:io`-based client. 22 | /// 23 | /// [proxyName] is used in headers to identify this proxy. It should be a valid 24 | /// HTTP token or a hostname. It defaults to `shelf_proxy`. 25 | Handler proxyHandler(url, {http.Client? client, String? proxyName}) { 26 | Uri uri; 27 | if (url is String) { 28 | uri = Uri.parse(url); 29 | } else if (url is Uri) { 30 | uri = url; 31 | } else { 32 | throw ArgumentError.value(url, 'url', 'url must be a String or Uri.'); 33 | } 34 | final nonNullClient = client ?? http.Client(); 35 | proxyName ??= 'shelf_proxy'; 36 | 37 | return (serverRequest) async { 38 | // TODO(nweiz): Support WebSocket requests. 39 | 40 | // TODO(nweiz): Handle TRACE requests correctly. See 41 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8 42 | final requestUrl = uri.resolve(serverRequest.url.toString()); 43 | final clientRequest = http.StreamedRequest(serverRequest.method, requestUrl) 44 | ..followRedirects = false 45 | ..headers.addAll(serverRequest.headers) 46 | ..headers['Host'] = uri.authority; 47 | 48 | // Add a Via header. See 49 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45 50 | _addHeader(clientRequest.headers, 'via', 51 | '${serverRequest.protocolVersion} $proxyName'); 52 | 53 | serverRequest 54 | .read() 55 | .forEach(clientRequest.sink.add) 56 | .catchError(clientRequest.sink.addError) 57 | .whenComplete(clientRequest.sink.close) 58 | .ignore(); 59 | final clientResponse = await nonNullClient.send(clientRequest); 60 | // Add a Via header. See 61 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45 62 | _addHeader(clientResponse.headers, 'via', '1.1 $proxyName'); 63 | 64 | // Remove the transfer-encoding since the body has already been decoded by 65 | // [client]. 66 | clientResponse.headers.remove('transfer-encoding'); 67 | 68 | // If the original response was gzipped, it will be decoded by [client] 69 | // and we'll have no way of knowing its actual content-length. 70 | if (clientResponse.headers['content-encoding'] == 'gzip') { 71 | clientResponse.headers.remove('content-encoding'); 72 | clientResponse.headers.remove('content-length'); 73 | 74 | // Add a Warning header. See 75 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2 76 | _addHeader( 77 | clientResponse.headers, 'warning', '214 $proxyName "GZIP decoded"'); 78 | } 79 | 80 | // Make sure the Location header is pointing to the proxy server rather 81 | // than the destination server, if possible. 82 | if (clientResponse.isRedirect && 83 | clientResponse.headers.containsKey('location')) { 84 | final location = 85 | requestUrl.resolve(clientResponse.headers['location']!).toString(); 86 | if (p.url.isWithin(uri.toString(), location)) { 87 | clientResponse.headers['location'] = 88 | '/${p.url.relative(location, from: uri.toString())}'; 89 | } else { 90 | clientResponse.headers['location'] = location; 91 | } 92 | } 93 | 94 | return Response(clientResponse.statusCode, 95 | body: clientResponse.stream, headers: clientResponse.headers); 96 | }; 97 | } 98 | 99 | // TODO(nweiz): use built-in methods for this when http and shelf support them. 100 | /// Add a header with [name] and [value] to [headers], handling existing headers 101 | /// gracefully. 102 | void _addHeader(Map headers, String name, String value) { 103 | final existing = headers[name]; 104 | headers[name] = existing == null ? value : '$existing, $value'; 105 | } 106 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: shelf_proxy 2 | version: 1.0.1 3 | description: A shelf handler for proxying HTTP requests to another server. 4 | repository: https://github.com/dart-lang/shelf_proxy 5 | 6 | environment: 7 | sdk: '>=2.14.0 <3.0.0' 8 | 9 | dependencies: 10 | http: ^0.13.0 11 | path: ^1.8.0 12 | shelf: ^1.0.0 13 | 14 | dev_dependencies: 15 | lints: ^1.0.0 16 | test: ^1.6.0 17 | -------------------------------------------------------------------------------- /test/shelf_proxy_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:http/http.dart' as http; 8 | import 'package:http/testing.dart'; 9 | import 'package:shelf/shelf.dart' as shelf; 10 | import 'package:shelf/shelf_io.dart' as shelf_io; 11 | import 'package:shelf_proxy/shelf_proxy.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | /// The URI of the server the current proxy server is proxying to. 15 | late Uri targetUri; 16 | 17 | /// The URI of the current proxy server. 18 | late Uri proxyUri; 19 | 20 | void main() { 21 | group('forwarding', () { 22 | test('forwards request method', () async { 23 | await createProxy((request) { 24 | expect(request.method, equals('DELETE')); 25 | return shelf.Response.ok(':)'); 26 | }); 27 | 28 | await http.delete(proxyUri); 29 | }); 30 | 31 | test('forwards request headers', () async { 32 | await createProxy((request) { 33 | expect(request.headers, containsPair('foo', 'bar')); 34 | expect(request.headers, containsPair('accept', '*/*')); 35 | return shelf.Response.ok(':)'); 36 | }); 37 | 38 | await get(headers: {'foo': 'bar', 'accept': '*/*'}); 39 | }); 40 | 41 | test('forwards request body', () async { 42 | await createProxy((request) { 43 | expect(request.readAsString(), completion(equals('hello, server'))); 44 | return shelf.Response.ok(':)'); 45 | }); 46 | 47 | await http.post(proxyUri, body: 'hello, server'); 48 | }); 49 | 50 | test('forwards response status', () async { 51 | await createProxy((request) => shelf.Response(567)); 52 | 53 | final response = await get(); 54 | expect(response.statusCode, equals(567)); 55 | }); 56 | 57 | test('forwards response headers', () async { 58 | await createProxy((request) => 59 | shelf.Response.ok(':)', headers: {'foo': 'bar', 'accept': '*/*'})); 60 | 61 | final response = await get(); 62 | 63 | expect(response.headers, containsPair('foo', 'bar')); 64 | expect(response.headers, containsPair('accept', '*/*')); 65 | }); 66 | 67 | test('forwards response body', () async { 68 | await createProxy((request) => shelf.Response.ok('hello, client')); 69 | 70 | expect(await http.read(proxyUri), equals('hello, client')); 71 | }); 72 | 73 | test('adjusts the Host header for the target server', () async { 74 | await createProxy((request) { 75 | expect(request.headers, containsPair('host', targetUri.authority)); 76 | return shelf.Response.ok(':)'); 77 | }); 78 | 79 | await get(); 80 | }); 81 | }); 82 | 83 | group('via', () { 84 | test('adds a Via header to the request', () async { 85 | await createProxy((request) { 86 | expect(request.headers, containsPair('via', '1.1 shelf_proxy')); 87 | return shelf.Response.ok(':)'); 88 | }); 89 | 90 | await get(); 91 | }); 92 | 93 | test("adds to a request's existing Via header", () async { 94 | await createProxy((request) { 95 | expect(request.headers, 96 | containsPair('via', '1.0 something, 1.1 shelf_proxy')); 97 | return shelf.Response.ok(':)'); 98 | }); 99 | 100 | await get(headers: {'via': '1.0 something'}); 101 | }); 102 | 103 | test('adds a Via header to the response', () async { 104 | await createProxy((request) => shelf.Response.ok(':)')); 105 | 106 | final response = await get(); 107 | expect(response.headers, containsPair('via', '1.1 shelf_proxy')); 108 | }); 109 | 110 | test("adds to a response's existing Via header", () async { 111 | await createProxy((request) => 112 | shelf.Response.ok(':)', headers: {'via': '1.0 something'})); 113 | 114 | final response = await get(); 115 | expect(response.headers, 116 | containsPair('via', '1.0 something, 1.1 shelf_proxy')); 117 | }); 118 | }); 119 | 120 | group('redirects', () { 121 | test("doesn't modify a Location for a foreign server", () async { 122 | await createProxy( 123 | (request) => shelf.Response.found('http://dartlang.org')); 124 | 125 | final response = await get(); 126 | expect(response.headers, containsPair('location', 'http://dartlang.org')); 127 | }); 128 | 129 | test('relativizes a reachable root-relative Location', () async { 130 | await createProxy((request) => shelf.Response.found('/foo/bar'), 131 | targetPath: '/foo'); 132 | 133 | final response = await get(); 134 | expect(response.headers, containsPair('location', '/bar')); 135 | }); 136 | 137 | test('absolutizes an unreachable root-relative Location', () async { 138 | await createProxy((request) => shelf.Response.found('/baz'), 139 | targetPath: '/foo'); 140 | 141 | final response = await get(); 142 | expect(response.headers, 143 | containsPair('location', targetUri.resolve('/baz').toString())); 144 | }); 145 | }); 146 | 147 | test('removes a transfer-encoding header', () async { 148 | final handler = mockHandler((request) => 149 | http.Response('', 200, headers: {'transfer-encoding': 'chunked'})); 150 | 151 | final response = 152 | await handler(shelf.Request('GET', Uri.parse('http://localhost/'))); 153 | 154 | expect(response.headers, isNot(contains('transfer-encoding'))); 155 | }); 156 | 157 | test('removes content-length and content-encoding for a gzipped response', 158 | () async { 159 | final handler = mockHandler((request) => http.Response('', 200, 160 | headers: {'content-encoding': 'gzip', 'content-length': '1234'})); 161 | 162 | final response = 163 | await handler(shelf.Request('GET', Uri.parse('http://localhost/'))); 164 | 165 | expect(response.headers, isNot(contains('content-encoding'))); 166 | expect(response.headers, isNot(contains('content-length'))); 167 | expect(response.headers, 168 | containsPair('warning', '214 shelf_proxy "GZIP decoded"')); 169 | }); 170 | } 171 | 172 | /// Creates a proxy server proxying to a server running [handler]. 173 | /// 174 | /// [targetPath] is the root-relative path on the target server to proxy to. It 175 | /// defaults to `/`. 176 | Future createProxy(shelf.Handler handler, {String? targetPath}) async { 177 | handler = expectAsync1(handler, reason: 'target server handler'); 178 | final targetServer = await shelf_io.serve(handler, 'localhost', 0); 179 | targetUri = Uri.parse('http://localhost:${targetServer.port}'); 180 | if (targetPath != null) targetUri = targetUri.resolve(targetPath); 181 | final proxyServerHandler = 182 | expectAsync1(proxyHandler(targetUri), reason: 'proxy server handler'); 183 | 184 | final proxyServer = await shelf_io.serve(proxyServerHandler, 'localhost', 0); 185 | proxyUri = Uri.parse('http://localhost:${proxyServer.port}'); 186 | 187 | addTearDown(() { 188 | proxyServer.close(force: true); 189 | targetServer.close(force: true); 190 | }); 191 | } 192 | 193 | /// Creates a [shelf.Handler] that's backed by a [MockClient] running 194 | /// [callback]. 195 | shelf.Handler mockHandler( 196 | FutureOr Function(http.Request) callback) { 197 | final client = MockClient((request) async => await callback(request)); 198 | return proxyHandler('http://dartlang.org', client: client); 199 | } 200 | 201 | /// Schedules a GET request with [headers] to the proxy server. 202 | Future get({Map? headers}) { 203 | final uri = proxyUri; 204 | final request = http.Request('GET', uri); 205 | if (headers != null) request.headers.addAll(headers); 206 | request.followRedirects = false; 207 | return request.send().then(http.Response.fromStream); 208 | } 209 | --------------------------------------------------------------------------------