diff --git a/CHANGELOG.md b/CHANGELOG.md index 05759311..94651d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.2.0 +* Use `http.Client` instead of `http.BaseClient`, and make it an +optional parameter. +* Allow `baseUrl` to accept `Uri` or `String`. +* Add `Proxy.pushState`. + # 2.1.2 * Apply lints. diff --git a/README.md b/README.md index d1ece3b4..7e4400c9 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,9 @@ import 'package:angel_proxy/angel_proxy.dart'; import 'package:http/http.dart' as http; main() async { - // ... - - var client = http.Client(); - - // Forward requests instead of serving statically - var proxy1 = Proxy(client, Uri.parse('http://localhost:3000')); + // Forward requests instead of serving statically. + // You can also pass a URI, instead of a string. + var proxy1 = Proxy('http://localhost:3000'); // handle all methods (GET, POST, ...) app.fallback(proxy.handleRequest); @@ -24,12 +21,12 @@ main() async { You can also restrict the proxy to serving only from a specific root: ```dart -Proxy(client, baseUrl, publicPath: '/remote'); +Proxy(baseUrl, publicPath: '/remote'); ``` Also, you can map requests to a root path on the remote server: ```dart -Proxy(client, baseUrl.replace(path: '/path')); +Proxy(baseUrl.replace(path: '/path')); ``` Request bodies will be forwarded as well, if they are not empty. This allows things like POST requests to function. diff --git a/example/main.dart b/example/main.dart index 2d850aa3..f4998ef6 100644 --- a/example/main.dart +++ b/example/main.dart @@ -2,20 +2,17 @@ import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/http.dart'; import 'package:angel_proxy/angel_proxy.dart'; -import 'package:http/io_client.dart' as http; import 'package:logging/logging.dart'; final Duration timeout = Duration(seconds: 5); main() async { var app = Angel(); - var client = http.IOClient(); // Forward any /api requests to pub. // By default, if the host throws a 404, the request will fall through to the next handler. var pubProxy = Proxy( - client, - Uri.parse('https://pub.dartlang.org'), + 'https://pub.dartlang.org', publicPath: '/pub', timeout: timeout, ); @@ -25,8 +22,7 @@ main() async { // // Play around with this at http://www.websocket.org/echo.html. var echoProxy = Proxy( - client, - Uri.parse('http://echo.websocket.org'), + 'http://echo.websocket.org', publicPath: '/echo', timeout: timeout, ); @@ -40,8 +36,7 @@ main() async { // Anything else should fall through to dartlang.org. var dartlangProxy = Proxy( - client, - Uri.parse('https://dartlang.org'), + 'https://dartlang.org', timeout: timeout, recoverFrom404: false, ); diff --git a/lib/src/proxy_layer.dart b/lib/src/proxy_layer.dart index ca672c6d..dc1e87b3 100644 --- a/lib/src/proxy_layer.dart +++ b/lib/src/proxy_layer.dart @@ -10,10 +10,14 @@ import 'package:path/path.dart' as p; final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)'); final MediaType _fallbackMediaType = MediaType('application', 'octet-stream'); +/// A middleware class that forwards requests (reverse proxies) to an upstream server. +/// +/// Supports WebSockets, in addition to regular HTTP requests. class Proxy { String _prefix; - final http.BaseClient httpClient; + /// The underlying [Client] to use. + final http.Client httpClient; /// If `true` (default), then the plug-in will ignore failures to connect to the proxy, and allow other handlers to run. final bool recoverFromDead; @@ -25,26 +29,53 @@ class Proxy { final Duration timeout; Proxy( - this.httpClient, - this.baseUrl, { + baseUrl, { + http.Client httpClient, this.publicPath = '/', this.recoverFromDead = true, this.recoverFrom404 = true, this.timeout, - }) { - if (!baseUrl.hasScheme || !baseUrl.hasAuthority) + }) : this.baseUrl = baseUrl is Uri ? baseUrl : Uri.parse(baseUrl.toString()), + this.httpClient = httpClient ?? http.Client() { + if (!this.baseUrl.hasScheme || !this.baseUrl.hasAuthority) { throw ArgumentError( 'Invalid `baseUrl`. URI must have both a scheme and authority.'); - if (this.recoverFromDead == null) + } + if (this.recoverFromDead == null) { throw ArgumentError.notNull("recoverFromDead"); - if (this.recoverFrom404 == null) + } + if (this.recoverFrom404 == null) { throw ArgumentError.notNull("recoverFrom404"); + } _prefix = publicPath?.replaceAll(_straySlashes, '') ?? ''; } void close() => httpClient.close(); + /// A handler that serves the file at the given path, unless the user has requested that path. + /// + /// You can also limit this functionality to specific values of the `Accept` header, ex. `text/html`. + /// If [accepts] is `null`, OR at least one of the content types in [accepts] is present, + /// the view will be served. + RequestHandler pushState(String path, {Iterable accepts}) { + var vPath = path.replaceAll(_straySlashes, ''); + if (_prefix?.isNotEmpty == true) vPath = '$_prefix/$vPath'; + + return (RequestContext req, ResponseContext res) { + var path = req.path.replaceAll(_straySlashes, ''); + if (path == vPath) return Future.value(true); + + if (accepts?.isNotEmpty == true) { + if (!accepts.any((x) => req.accepts(x, strict: true))) { + return Future.value(true); + } + } + + return servePath(vPath, req, res); + }; + } + /// Handles an incoming HTTP request. Future handleRequest(RequestContext req, ResponseContext res) { var path = req.path.replaceAll(_straySlashes, ''); diff --git a/pubspec.yaml b/pubspec.yaml index f439dd04..a319e2a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_proxy description: Angel middleware to forward requests to another server (i.e. pub serve). -version: 2.1.2 +version: 2.2.0 author: Tobe O homepage: https://github.com/angel-dart/proxy environment: diff --git a/test/basic_test.dart b/test/basic_test.dart index cb93c3aa..6d62d82d 100644 --- a/test/basic_test.dart +++ b/test/basic_test.dart @@ -17,12 +17,10 @@ main() { setUp(() async { app = Angel(); var appHttp = AngelHttp(app); - var httpClient = http.IOClient(); testServer = await startTestServer(); var proxy1 = Proxy( - httpClient, Uri( scheme: 'http', host: testServer.address.address, @@ -30,7 +28,7 @@ main() { publicPath: '/proxy', ); - var proxy2 = Proxy(httpClient, proxy1.baseUrl.replace(path: '/foo')); + var proxy2 = Proxy(proxy1.baseUrl.replace(path: '/foo')); print('Proxy 1 on: ${proxy1.baseUrl}'); print('Proxy 2 on: ${proxy2.baseUrl}'); @@ -42,10 +40,6 @@ main() { res.write('intercept empty'); }); - app.shutdownHooks.add((_) async { - httpClient.close(); - }); - app.logger = Logger('angel'); Logger.root.onRecord.listen((rec) { diff --git a/test/pub_serve_test.dart b/test/pub_serve_test.dart index b0a23940..48cefa4e 100644 --- a/test/pub_serve_test.dart +++ b/test/pub_serve_test.dart @@ -4,7 +4,6 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/http.dart'; import 'package:angel_proxy/angel_proxy.dart'; import 'package:angel_test/angel_test.dart'; -import 'package:http/io_client.dart' as http; import 'package:logging/logging.dart'; import 'package:mock_request/mock_request.dart'; import 'package:test/test.dart'; @@ -37,10 +36,7 @@ main() { }); app.get('/bar', (req, res) => res.write('normal')); - var httpClient = http.IOClient(); - layer = Proxy( - httpClient, Uri(scheme: 'http', host: server.address.address, port: server.port), publicPath: '/proxy', );