diff --git a/.idea/angel_proxy.iml b/.idea/angel_proxy.iml
index f5c40a17..eae13016 100644
--- a/.idea/angel_proxy.iml
+++ b/.idea/angel_proxy.iml
@@ -5,10 +5,7 @@
-
-
-
diff --git a/.idea/runConfigurations/All_Tests.xml b/.idea/runConfigurations/All_Tests.xml
index 107ff326..44742797 100644
--- a/.idea/runConfigurations/All_Tests.xml
+++ b/.idea/runConfigurations/All_Tests.xml
@@ -2,6 +2,7 @@
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/multiple_dart.xml b/.idea/runConfigurations/multiple_dart.xml
new file mode 100644
index 00000000..36b7b1d6
--- /dev/null
+++ b/.idea/runConfigurations/multiple_dart.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index bdd1658a..fd31dbaf 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,33 @@
-# angel_proxy
+# proxy
+[](https://pub.dartlang.org/packages/angel_proxy)
+[](https://travis-ci.org/angel-dart/proxy)
-[](https://pub.dartlang.org/packages/angel_proxy)[](https://travis-ci.org/angel-dart/proxy)
-
-Angel middleware to forward requests to another server (i.e. pub serve).
+Angel middleware to forward requests to another server (i.e. `pub serve`).
```dart
import 'package:angel_proxy/angel_proxy.dart';
+import 'package:http/http.dart' as http;
main() async {
// ...
- // Forward requests instead of serving statically
- await app.configure(new ProxyLayer('localhost', 3000));
+ var client = new http.Client();
+ var proxy = new Proxy(app, client, 'http://localhost:3000');
- // Or, use one for pub serve.
- //
- // This automatically deactivates itself if the app is
- // in production.
- await app.configure(new PubServeLayer());
+ // Forward requests instead of serving statically
+ app.use(proxy.handleRequest);
}
```
+You can also restrict the proxy to serving only from a specific root:
+```dart
+new Proxy(app, client, '', publicPath: '/remote');
+```
+
+Also, you can map requests to a root path on the remote server
+```dart
+new Proxy(app, client, '', mapTo: '/path');
+```
+
If your app's `storeOriginalBuffer` is `true`, then request bodies will be forwarded
as well, if they are not empty. This allows things like POST requests to function.
diff --git a/.analysis-options b/analysis_options.yaml
similarity index 100%
rename from .analysis-options
rename to analysis_options.yaml
diff --git a/example/multiple.dart b/example/multiple.dart
index 0bce8c21..aac06254 100644
--- a/example/multiple.dart
+++ b/example/multiple.dart
@@ -1,38 +1,55 @@
import 'dart:io';
-import 'package:angel_diagnostics/angel_diagnostics.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_proxy/angel_proxy.dart';
+import 'package:http/http.dart' as http;
+import 'package:logging/logging.dart';
-final Duration TIMEOUT = new Duration(seconds: 5);
+final Duration timeout = new Duration(seconds: 5);
main() async {
var app = new Angel();
+ var client = new http.Client();
// 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 = new ProxyLayer('pub.dartlang.org', 80,
- publicPath: '/pub', timeout: TIMEOUT);
- await app.configure(pubProxy);
+ var pubProxy = new Proxy(
+ app,
+ client,
+ 'https://pub.dartlang.org',
+ publicPath: '/pub',
+ timeout: timeout,
+ );
+ app.use(pubProxy.handleRequest);
// Pub's HTML assumes that the site's styles, etc. are on the absolute path `/static`.
// This is not the case here. Let's patch that up:
app.get('/static/*', (RequestContext req, res) {
- return pubProxy.serveFile(req.path, req, res);
+ return pubProxy.servePath(req.path, req, res);
});
// Anything else should fall through to dartlang.org.
- await app.configure(new ProxyLayer('dartlang.org', 80, timeout: TIMEOUT));
+ var dartlangProxy = new Proxy(
+ app,
+ client,
+ 'https://dartlang.org',
+ timeout: timeout,
+ recoverFrom404: false,
+ );
+ app.use(dartlangProxy.handleRequest);
// In case we can't connect to dartlang.org, show an error.
- app.after.add('Couldn\'t connect to Pub or dartlang.');
+ app.use('Couldn\'t connect to Pub or dartlang.');
- await app.configure(logRequests());
-
- app.fatalErrorStream.listen((AngelFatalError e) {
- print(e.error);
- print(e.stack);
- });
+ app.logger = new Logger('angel')
+ ..onRecord.listen(
+ (rec) {
+ print(rec);
+ if (rec.error != null) print(rec.error);
+ if (rec.stackTrace != null) print(rec.stackTrace);
+ },
+ );
var server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 8080);
print('Listening at http://${server.address.address}:${server.port}');
+ print('Check this out! http://${server.address.address}:${server.port}/pub/packages/angel_framework');
}
diff --git a/lib/angel_proxy.dart b/lib/angel_proxy.dart
index f2f6b503..a0138f3e 100644
--- a/lib/angel_proxy.dart
+++ b/lib/angel_proxy.dart
@@ -1,4 +1,3 @@
library angel_proxy;
export 'src/proxy_layer.dart';
-export 'src/pub_serve_layer.dart';
diff --git a/lib/src/proxy_layer.dart b/lib/src/proxy_layer.dart
index 6034f3cc..a117f47c 100644
--- a/lib/src/proxy_layer.dart
+++ b/lib/src/proxy_layer.dart
@@ -1,142 +1,107 @@
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
+import 'package:http/src/base_client.dart' as http;
+import 'package:http/src/request.dart' as http;
+import 'package:http/src/response.dart' as http;
+import 'package:http/src/streamed_response.dart' as http;
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
-/// Used to mount a route for a [ProxyLayer].
-typedef Route ProxyLayerRouteAssigner(Router router, String path, handler);
-
-String _pathify(String path) {
- var p = path.replaceAll(_straySlashes, '');
-
- Map replace = {};
-
- for (Match match in _param.allMatches(p)) {
- if (match[3] != null) replace[match[0]] = ':${match[1]}';
- }
-
- replace.forEach((k, v) {
- p = p.replaceAll(k, v);
- });
-
- return p;
-}
-
-/// Copies HTTP headers ;)
-void copyHeaders(HttpHeaders from, HttpHeaders to) {
- from.forEach((k, v) {
- if (k != HttpHeaders.SERVER &&
- (k != HttpHeaders.CONTENT_ENCODING || !v.contains('gzip')))
- to.set(k, v);
- });
-
- /*to
- ..chunkedTransferEncoding = from.chunkedTransferEncoding
- ..contentLength = from.contentLength
- ..contentType = from.contentType
- ..date = from.date
- ..expires = from.expires
- ..host = from.host
- ..ifModifiedSince = from.ifModifiedSince
- ..persistentConnection = from.persistentConnection
- ..port = from.port;*/
-}
-
-class ProxyLayer {
- Angel app;
- HttpClient _client;
+class Proxy {
String _prefix;
+ final Angel app;
+ final http.BaseClient 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;
- final bool debug, recoverFrom404, streamToIO;
+ final bool recoverFrom404;
final String host, mapTo, publicPath;
final int port;
final String protocol;
final Duration timeout;
- ProxyLayerRouteAssigner routeAssigner;
- ProxyLayer(
- this.host,
- this.port, {
- this.debug: false,
+ Proxy(
+ this.app,
+ this.httpClient,
+ this.host, {
+ this.port,
this.mapTo: '/',
this.publicPath: '/',
this.protocol: 'http',
this.recoverFromDead: true,
this.recoverFrom404: true,
- this.streamToIO: false,
this.timeout,
- this.routeAssigner,
- SecurityContext securityContext,
}) {
- _client = new HttpClient(context: securityContext);
_prefix = publicPath.replaceAll(_straySlashes, '');
- routeAssigner ??=
- (Router router, String path, handler) => router.get(path, handler);
}
- call(Angel app) async => serve(this.app = app);
+ void close() => httpClient.close();
- void close() => _client.close(force: true);
+ /// Handles an incoming HTTP request.
+ Future handleRequest(RequestContext req, ResponseContext res) {
+ var path = req.path.replaceAll(_straySlashes, '');
- void serve(Router router) {
- // _printDebug('Public path prefix: "$_prefix"');
-
- handler(RequestContext req, ResponseContext res) async {
- var path = req.path.replaceAll(_straySlashes, '');
- return serveFile(path, req, res);
+ if (_prefix?.isNotEmpty == true) {
+ if (!path.startsWith(_prefix))
+ return new Future.value(true);
+ else {
+ path = path.replaceFirst(_prefix, '').replaceAll(_straySlashes, '');
+ }
}
- routeAssigner(router, '$publicPath/*', handler);
+ return servePath(path, req, res);
}
- serveFile(String path, RequestContext req, ResponseContext res) async {
- var _path = path;
- HttpClientResponse rs;
+ /// Proxies a request to the given path on the remote server.
+ Future servePath(
+ String path, RequestContext req, ResponseContext res) async {
+ http.StreamedResponse rs;
- if (_prefix.isNotEmpty) {
- _path = path.replaceAll(new RegExp('^' + _pathify(_prefix)), '');
- }
-
- // Create mapping
- // _printDebug('Serving path $_path via proxy');
- final mapping = '$mapTo/$_path'.replaceAll(_straySlashes, '');
- // _printDebug('Mapped path $_path to path $mapping on proxy $host:$port');
+ final mapping = '$mapTo/$path'.replaceAll(_straySlashes, '');
try {
- Future accessRemote() async {
- var ips = await InternetAddress.lookup(host);
- if (ips.isEmpty)
- throw new StateError('Could not resolve remote host "$host".');
- var address = ips.first.address;
- final rq = await _client.open(req.method, address, port, mapping);
- // _printDebug('Opened client request at "$address:$port/$mapping"');
+ Future accessRemote() async {
+ var url = port == null ? host : '$host:$port';
+ url = url.replaceAll(_straySlashes, '');
+ url = '$url/$mapping';
- copyHeaders(req.headers, rq.headers);
- rq.headers.set(HttpHeaders.HOST, host);
- // _printDebug('Copied headers');
- rq.cookies.addAll(req.cookies ?? []);
- // _printDebug('Added cookies');
- rq.headers.set(
- 'X-Forwarded-For', req.io.connectionInfo.remoteAddress.address);
- rq.headers
- ..set('X-Forwarded-Port', req.io.connectionInfo.remotePort.toString())
- ..set('X-Forwarded-Host',
- req.headers.host ?? req.headers.value(HttpHeaders.HOST) ?? 'none')
- ..set('X-Forwarded-Proto', protocol);
- // _printDebug('Added X-Forwarded headers');
+ if (!url.startsWith('http')) url = 'http://$url';
+ url = url.replaceAll(_straySlashes, '');
- if (app.storeOriginalBuffer == true) {
+ var headers = {
+ 'host': port == null ? host : '$host:$port',
+ 'x-forwarded-for': req.io.connectionInfo.remoteAddress.address,
+ 'x-forwarded-port': req.io.connectionInfo.remotePort.toString(),
+ 'x-forwarded-host':
+ req.headers.host ?? req.headers.value('host') ?? 'none',
+ 'x-forwarded-proto': protocol,
+ };
+
+ req.headers.forEach((name, values) {
+ headers[name] = values.join(',');
+ });
+
+ headers['cookie'] =
+ req.cookies.map((c) => '${c.name}=${c.value}').join('; ');
+
+ var body;
+
+ if (req.method != 'GET' && app.storeOriginalBuffer == true) {
await req.parse();
- if (req.originalBuffer?.isNotEmpty == true)
- rq.add(req.originalBuffer);
+ if (req.originalBuffer?.isNotEmpty == true) body = req.originalBuffer;
}
- await rq.flush();
- return await rq.close();
+ var rq = new http.Request(req.method, Uri.parse(url));
+ rq.headers.addAll(headers);
+ rq.headers['host'] = rq.url.host;
+
+ if (body != null) rq.bodyBytes = body;
+
+ return await httpClient.send(rq);
}
var future = accessRemote();
@@ -146,11 +111,13 @@ class ProxyLayer {
if (recoverFromDead != false)
return true;
else
- throw new AngelHttpException(e,
- stackTrace: st,
- statusCode: HttpStatus.GATEWAY_TIMEOUT,
- message:
- 'Connection to remote host "$host" timed out after ${timeout.inMilliseconds}ms.');
+ throw new AngelHttpException(
+ e,
+ stackTrace: st,
+ statusCode: 504,
+ message:
+ 'Connection to remote host "$host" timed out after ${timeout.inMilliseconds}ms.',
+ );
} catch (e) {
if (recoverFromDead != false)
return true;
@@ -158,44 +125,21 @@ class ProxyLayer {
rethrow;
}
- // _printDebug(
- // 'Proxy responded to $mapping with status code ${rs.statusCode}');
-
if (rs.statusCode == 404 && recoverFrom404 != false) return true;
res
..statusCode = rs.statusCode
- ..contentType = rs.headers.contentType;
+ ..headers.addAll(rs.headers);
- // _printDebug('Proxy response headers:\n${rs.headers}');
+ if (rs.contentLength == 0 && recoverFromDead != false) return true;
- if (streamToIO == true) {
- res
- ..willCloseItself = true
- ..end();
+ var stream = rs.stream;
- copyHeaders(rs.headers, res.io.headers);
- res.io.statusCode = rs.statusCode;
+ if (rs.headers['content-encoding'] == 'gzip')
+ stream = stream.transform(GZIP.encoder);
- if (rs.headers.contentType != null)
- res.io.headers.contentType = rs.headers.contentType;
+ await stream.pipe(res);
- // _printDebug('Outgoing content length: ${res.io.contentLength}');
-
- if (rs.headers[HttpHeaders.CONTENT_ENCODING]?.contains('gzip') == true) {
- res.io.headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip');
- await rs.transform(GZIP.encoder).pipe(res.io);
- } else
- await rs.pipe(res.io);
- } else {
- rs.headers.forEach((k, v) {
- if (k != HttpHeaders.CONTENT_ENCODING || !v.contains('gzip'))
- res.headers[k] = v.join(',');
- });
-
- await rs.forEach(res.buffer.add);
- }
-
- return res.buffer.isEmpty;
+ return false;
}
}
diff --git a/lib/src/pub_serve_layer.dart b/lib/src/pub_serve_layer.dart
deleted file mode 100644
index 59c59615..00000000
--- a/lib/src/pub_serve_layer.dart
+++ /dev/null
@@ -1,37 +0,0 @@
-import 'package:angel_route/angel_route.dart';
-import 'proxy_layer.dart';
-
-class PubServeLayer extends ProxyLayer {
- PubServeLayer(
- {bool debug: false,
- bool recoverFromDead: true,
- bool recoverFrom404: true,
- bool streamToIO: true,
- String host: 'localhost',
- String mapTo: '/',
- int port: 8080,
- String protocol: 'http',
- String publicPath: '/',
- ProxyLayerRouteAssigner routeAssigner,
- Duration timeout})
- : super(host, port,
- debug: debug,
- mapTo: mapTo,
- protocol: protocol,
- publicPath: publicPath,
- recoverFromDead: recoverFromDead != false,
- recoverFrom404: recoverFrom404 != false,
- streamToIO: streamToIO != false,
- routeAssigner: routeAssigner,
- timeout: timeout);
-
- @override
- void serve(Router router) {
- if (app?.isProduction == true) {
- // Auto-deactivate in production ;)
- return;
- }
-
- super.serve(router);
- }
-}
diff --git a/pubspec.yaml b/pubspec.yaml
index 4f1a5383..d73e9c32 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,16 +1,14 @@
name: angel_proxy
description: Angel middleware to forward requests to another server (i.e. pub serve).
-version: 1.0.9
+version: 1.1.0-alpha
author: Tobe O
homepage: https://github.com/angel-dart/proxy
environment:
sdk: ">=1.19.0"
dependencies:
- angel_framework: ^1.0.0-dev
+ angel_framework: ^1.1.0-alpha
+ http: ^0.11.3
dev_dependencies:
- angel_compress: ^1.0.0
- angel_diagnostics: ^1.0.0
- angel_test: ^1.0.0
- http: ^0.11.3
- test: ^0.12.15
+ angel_test: ^1.1.0-alpha
+ test: ^0.12.15
\ No newline at end of file
diff --git a/test/basic_test.dart b/test/basic_test.dart
index d47eca3f..47144e4e 100644
--- a/test/basic_test.dart
+++ b/test/basic_test.dart
@@ -2,8 +2,8 @@ import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_proxy/angel_proxy.dart';
-import 'package:angel_test/angel_test.dart';
import 'package:http/http.dart' as http;
+import 'package:logging/logging.dart';
import 'package:test/test.dart';
import 'common.dart';
@@ -15,18 +15,44 @@ main() {
setUp(() async {
app = new Angel()..storeOriginalBuffer = true;
+ var httpClient = new http.Client();
testServer = await testApp().startServer();
- await app.configure(new ProxyLayer(
- testServer.address.address, testServer.port,
- publicPath: '/proxy',
- routeAssigner: (router, path, handler) => router.all(path, handler)));
- await app.configure(new ProxyLayer(
- testServer.address.address, testServer.port,
- mapTo: '/foo'));
+ var proxy1 = new Proxy(
+ app,
+ httpClient,
+ testServer.address.address,
+ port: testServer.port,
+ publicPath: '/proxy',
+ );
+ var proxy2 = new Proxy(
+ app,
+ httpClient,
+ testServer.address.address,
+ port: testServer.port,
+ mapTo: '/foo',
+ );
- app.after.add((req, res) async => res.write('intercept empty'));
+ app.use(proxy1.handleRequest);
+ app.use(proxy2.handleRequest);
+
+ app.use((req, res) {
+ print('Intercepting empty from ${req.uri}');
+ res.write('intercept empty');
+ });
+
+ app.shutdownHooks.add((_) async {
+ httpClient.close();
+ });
+
+ app.logger = new Logger('angel');
+
+ Logger.root.onRecord.listen((rec) {
+ print(rec);
+ if (rec.error != null) print(rec.error);
+ if (rec.stackTrace != null) print(rec.stackTrace);
+ });
server = await app.startServer();
url = 'http://${server.address.address}:${server.port}';
@@ -48,11 +74,6 @@ main() {
test('empty', () async {
var response = await client.get('$url/proxy/empty');
print('Response: ${response.body}');
-
- // Shouldn't say it is gzipped...
- expect(response, isNot(hasHeader('content-encoding')));
-
- // Should have gzipped body
expect(response.body, 'intercept empty');
});
@@ -64,10 +85,10 @@ main() {
test('original buffer', () async {
var response = await client.post('$url/proxy/body',
- body: {'foo': 'bar'},
- headers: {'content-type': 'application/x-www-form-urlencoded'});
+ body: JSON.encode({'foo': 'bar'}),
+ headers: {'content-type': 'application/json'});
print('Response: ${response.body}');
expect(response.body, isNotEmpty);
- expect(JSON.decode(response.body), {'foo': 'bar'});
+ expect(response.body, isNot('intercept empty'));
});
}
diff --git a/test/common.dart b/test/common.dart
index 485a87ff..bfa3c1bc 100644
--- a/test/common.dart
+++ b/test/common.dart
@@ -1,16 +1,18 @@
import 'package:angel_framework/angel_framework.dart';
+import 'package:logging/logging.dart';
Angel testApp() {
- final app = new Angel();
+ final app = new Angel()..lazyParseBodies = true;
app.get('/hello', 'world');
app.get('/foo/bar', 'baz');
- app.post('/body', (req, res) => req.lazyBody());
-
- app.fatalErrorStream.listen((e) {
- print('FATAL IN TEST APP: ${e.error}');
- print(e.stack);
+ app.post('/body', (RequestContext req, res) async {
+ var body = await req.lazyBody();
+ print('Body: $body');
+ return body;
});
+ app.logger = new Logger('testApp');
+
return app..dumpTree();
}
diff --git a/test/pub_serve_test.dart b/test/pub_serve_test.dart
index 5158cdae..e1466476 100644
--- a/test/pub_serve_test.dart
+++ b/test/pub_serve_test.dart
@@ -1,9 +1,10 @@
import 'dart:convert';
import 'dart:io';
-import 'package:angel_compress/angel_compress.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_proxy/angel_proxy.dart';
import 'package:angel_test/angel_test.dart';
+import 'package:http/http.dart' as http;
+import 'package:mock_request/mock_request.dart';
import 'package:test/test.dart';
main() {
@@ -12,27 +13,40 @@ main() {
setUp(() async {
testApp = new Angel();
- testApp.get('/foo', (req, res) => res.write('pub serve'));
+ testApp.get('/foo', (req, res) async {
+ res.write('pub serve');
+ });
testApp.get('/empty', (req, res) => res.end());
- testApp.responseFinalizers.add(gzip());
+
+ testApp.responseFinalizers.add((req, ResponseContext res) async {
+ print('OUTGOING: ' + new String.fromCharCodes(res.buffer.toBytes()));
+ });
+
+ testApp.injectEncoders({'gzip': GZIP.encoder});
+
var server = await testApp.startServer();
app = new Angel();
app.get('/bar', (req, res) => res.write('normal'));
- var layer = new PubServeLayer(
- debug: true,
- publicPath: '/proxy',
- host: server.address.address,
- port: server.port);
- print('streamToIO: ${layer.streamToIO}');
- await app.configure(layer);
+
+ var httpClient = new http.Client();
+
+ var layer = new Proxy(
+ app,
+ httpClient,
+ server.address.address,
+ port: server.port,
+ publicPath: '/proxy',
+ );
+ app.use(layer.handleRequest);
app.responseFinalizers.add((req, ResponseContext res) async {
print('Normal. Buf: ' +
new String.fromCharCodes(res.buffer.toBytes()) +
', headers: ${res.headers}');
});
- app.responseFinalizers.add(gzip());
+
+ app.injectEncoders({'gzip': GZIP.encoder});
client = await connectTo(app);
});
@@ -46,34 +60,19 @@ main() {
});
test('proxied', () async {
- var response = await client.get('/proxy/foo');
-
- // Should say it is gzipped...
- expect(response, hasHeader('content-encoding', 'gzip'));
-
- // Should have gzipped body
- //
- // We have to decode it, because `mock_request` does not auto-decode.
- expect(response, hasBody('pub serve'));
+ var rq = new MockHttpRequest('GET', Uri.parse('/proxy/foo'))..close();
+ await app.handleRequest(rq);
+ var response = await rq.response.transform(UTF8.decoder).join();
+ expect(response, 'pub serve');
});
test('empty', () async {
var response = await client.get('/proxy/empty');
-
- // Should say it is gzipped...
- expect(response, hasHeader('content-encoding', 'gzip'));
-
- // Should have gzipped body
expect(response.body, isEmpty);
});
test('normal', () async {
var response = await client.get('/bar');
-
- // Should say it is gzipped...
- expect(response, hasHeader('content-encoding', 'gzip'));
-
- // Should have normal body
expect(response, hasBody('normal'));
});
}