| // 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 'package:http/http.dart' as http; |
| import 'package:path/path.dart' as p; |
| import 'package:pedantic/pedantic.dart'; |
| import 'package:shelf/shelf.dart'; |
| |
| import 'src/utils.dart'; |
| |
| /// A handler that proxies requests to [url]. |
| /// |
| /// To generate the proxy request, this concatenates [url] and [Request.url]. |
| /// This means that if the handler mounted under `/documentation` and [url] is |
| /// `http://example.com/docs`, a request to `/documentation/tutorials` |
| /// will be proxied to `http://example.com/docs/tutorials`. |
| /// |
| /// [url] must be a [String] or [Uri]. |
| /// |
| /// [client] is used internally to make HTTP requests. It defaults to a |
| /// `dart:io`-based client. |
| /// |
| /// [proxyName] is used in headers to identify this proxy. It should be a valid |
| /// HTTP token or a hostname. It defaults to `shelf_proxy`. |
| Handler proxyHandler(url, {http.Client client, String proxyName}) { |
| Uri uri; |
| if (url is String) { |
| uri = Uri.parse(url); |
| } else if (url is Uri) { |
| uri = url; |
| } else { |
| throw ArgumentError.value(url, 'url', 'url must be a String or Uri.'); |
| } |
| client ??= http.Client(); |
| proxyName ??= 'shelf_proxy'; |
| |
| return (serverRequest) async { |
| // TODO(nweiz): Support WebSocket requests. |
| |
| // TODO(nweiz): Handle TRACE requests correctly. See |
| // http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8 |
| var requestUrl = uri.resolve(serverRequest.url.toString()); |
| var clientRequest = http.StreamedRequest(serverRequest.method, requestUrl); |
| clientRequest.followRedirects = false; |
| clientRequest.headers.addAll(serverRequest.headers); |
| clientRequest.headers['Host'] = uri.authority; |
| |
| // Add a Via header. See |
| // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45 |
| _addHeader(clientRequest.headers, 'via', |
| '${serverRequest.protocolVersion} $proxyName'); |
| |
| unawaited(store(serverRequest.read(), clientRequest.sink)); |
| var clientResponse = await client.send(clientRequest); |
| // Add a Via header. See |
| // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45 |
| _addHeader(clientResponse.headers, 'via', '1.1 $proxyName'); |
| |
| // Remove the transfer-encoding since the body has already been decoded by |
| // [client]. |
| clientResponse.headers.remove('transfer-encoding'); |
| |
| // If the original response was gzipped, it will be decoded by [client] |
| // and we'll have no way of knowing its actual content-length. |
| if (clientResponse.headers['content-encoding'] == 'gzip') { |
| clientResponse.headers.remove('content-encoding'); |
| clientResponse.headers.remove('content-length'); |
| |
| // Add a Warning header. See |
| // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2 |
| _addHeader( |
| clientResponse.headers, 'warning', '214 $proxyName "GZIP decoded"'); |
| } |
| |
| // Make sure the Location header is pointing to the proxy server rather |
| // than the destination server, if possible. |
| if (clientResponse.isRedirect && |
| clientResponse.headers.containsKey('location')) { |
| var location = |
| requestUrl.resolve(clientResponse.headers['location']).toString(); |
| if (p.url.isWithin(uri.toString(), location)) { |
| clientResponse.headers['location'] = |
| '/' + p.url.relative(location, from: uri.toString()); |
| } else { |
| clientResponse.headers['location'] = location; |
| } |
| } |
| |
| return Response(clientResponse.statusCode, |
| body: clientResponse.stream, headers: clientResponse.headers); |
| }; |
| } |
| |
| /// Use [proxyHandler] instead. |
| @deprecated |
| Handler createProxyHandler(Uri rootUri) => proxyHandler(rootUri); |
| |
| // TODO(nweiz): use built-in methods for this when http and shelf support them. |
| /// Add a header with [name] and [value] to [headers], handling existing headers |
| /// gracefully. |
| void _addHeader(Map<String, String> headers, String name, String value) { |
| if (headers.containsKey(name)) { |
| headers[name] += ', $value'; |
| } else { |
| headers[name] = value; |
| } |
| } |