diff --git a/example/main.dart b/example/main.dart index aac06254..9deaefce 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,5 +1,6 @@ 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/http.dart' as http; import 'package:logging/logging.dart'; @@ -19,7 +20,7 @@ main() async { publicPath: '/pub', timeout: timeout, ); - app.use(pubProxy.handleRequest); + app.all("/pub/*", 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: @@ -35,10 +36,10 @@ main() async { timeout: timeout, recoverFrom404: false, ); - app.use(dartlangProxy.handleRequest); + app.all('*', dartlangProxy.handleRequest); // In case we can't connect to dartlang.org, show an error. - app.use('Couldn\'t connect to Pub or dartlang.'); + app.fallback((req, res) => res.write('Couldn\'t connect to Pub or dartlang.')); app.logger = new Logger('angel') ..onRecord.listen( @@ -49,7 +50,7 @@ main() async { }, ); - var server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 8080); + var server = await AngelHttp(app).startServer(InternetAddress.loopbackIPv4, 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/src/proxy_layer.dart b/lib/src/proxy_layer.dart index 1a509684..aa2922e7 100644 --- a/lib/src/proxy_layer.dart +++ b/lib/src/proxy_layer.dart @@ -1,18 +1,18 @@ import 'dart:async'; import 'dart:io'; +import 'dart:convert'; 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; +import 'package:http_parser/http_parser.dart'; +import 'package:http/http.dart' as http; final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); +final MediaType _fallbackMediaType = MediaType('application', 'octet-stream'); class Proxy { String _prefix; final Angel app; - final http.BaseClient httpClient; + 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; @@ -20,6 +20,8 @@ class Proxy { final String host, mapTo, publicPath; final int port; final String protocol; + + /// If `null` then no timout is added for requests final Duration timeout; Proxy( @@ -34,7 +36,10 @@ class Proxy { this.recoverFrom404: true, this.timeout, }) { - _prefix = publicPath.replaceAll(_straySlashes, ''); + if (this.recoverFromDead == null) throw ArgumentError.notNull("recoverFromDead"); + if (this.recoverFrom404 == null) throw ArgumentError.notNull("recoverFrom404"); + + _prefix = publicPath?.replaceAll(_straySlashes, '') ?? ''; } void close() => httpClient.close(); @@ -43,20 +48,17 @@ class Proxy { Future handleRequest(RequestContext req, ResponseContext res) { var path = req.path.replaceAll(_straySlashes, ''); - if (_prefix?.isNotEmpty == true) { - if (!path.startsWith(_prefix)) - return new Future.value(true); - else { - path = path.replaceFirst(_prefix, '').replaceAll(_straySlashes, ''); - } + if (_prefix.isNotEmpty) { + if (!path.startsWith(_prefix)) return new Future.value(true); + + path = path.replaceFirst(_prefix, '').replaceAll(_straySlashes, ''); } return servePath(path, req, res); } /// Proxies a request to the given path on the remote server. - Future servePath( - String path, RequestContext req, ResponseContext res) async { + Future servePath(String path, RequestContext req, ResponseContext res) async { http.StreamedResponse rs; final mapping = '$mapTo/$path'.replaceAll(_straySlashes, ''); @@ -74,8 +76,7 @@ class Proxy { 'host': port == null ? host : '$host:$port', 'x-forwarded-for': req.remoteAddress.address, 'x-forwarded-port': req.uri.port.toString(), - 'x-forwarded-host': - req.headers.host ?? req.headers.value('host') ?? 'none', + 'x-forwarded-host': req.headers.host ?? req.headers.value('host') ?? 'none', 'x-forwarded-proto': protocol, }; @@ -83,19 +84,18 @@ class Proxy { headers[name] = values.join(','); }); - headers['cookie'] = - req.cookies.map((c) => '${c.name}=${c.value}').join('; '); + headers[HttpHeaders.cookieHeader] = 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) body = req.originalBuffer; + if (req.method != 'GET' && app.keepRawRequestBuffers == true) { + body = (await req.parse()).originalBuffer; } var rq = new http.Request(req.method, Uri.parse(url)); rq.headers.addAll(headers); rq.headers['host'] = rq.url.host; + rq.encoding = Utf8Codec(allowMalformed: true); if (body != null) rq.bodyBytes = body; @@ -106,37 +106,51 @@ class Proxy { if (timeout != null) future = future.timeout(timeout); rs = await future; } on TimeoutException catch (e, st) { - if (recoverFromDead != false) - return true; - else + if (recoverFromDead) return true; + + throw new AngelHttpException( + e, + stackTrace: st, + statusCode: 504, + message: 'Connection to remote host "$host" timed out after ${timeout.inMilliseconds}ms.', + ); + } catch (e) { + if (recoverFromDead) return true; + rethrow; + } + + if (rs.statusCode == 404 && recoverFrom404) return true; + if (rs.contentLength == 0 && recoverFromDead) return true; + + MediaType mediaType; + if (rs.headers.containsKey(HttpHeaders.contentTypeHeader)) { + try { + mediaType = MediaType.parse(rs.headers[HttpHeaders.contentTypeHeader]); + } on FormatException catch (e, st) { + if (recoverFromDead) return true; + throw new AngelHttpException( e, stackTrace: st, statusCode: 504, - message: - 'Connection to remote host "$host" timed out after ${timeout.inMilliseconds}ms.', + message: 'Host "$host" returned a malformed content-type', ); - } catch (e) { - if (recoverFromDead != false) - return true; - else - rethrow; + } + } else { + mediaType = _fallbackMediaType; } - if (rs.statusCode == 404 && recoverFrom404 != false) return true; + var proxiedHeaders = new Map.from(rs.headers); - // http/2 client implementations usually get confused by transfer-encoding res + ..contentType = mediaType ..statusCode = rs.statusCode - ..headers.addAll(new Map.from(rs.headers) - ..remove(HttpHeaders.TRANSFER_ENCODING)); - - if (rs.contentLength == 0 && recoverFromDead != false) return true; + ..headers.addAll(proxiedHeaders); var stream = rs.stream; - if (rs.headers['content-encoding'] == 'gzip') - stream = stream.transform(GZIP.encoder); + // [upgrading to dart2] Keeping this workaround as a reference. It's not properly typed for dart2 + //if (rs.headers[HttpHeaders.contentEncodingHeader] == 'gzip') stream = stream.transform(gzip.encoder); await stream.pipe(res); diff --git a/pubspec.yaml b/pubspec.yaml index fc146e9d..2f68faec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,11 +4,12 @@ version: 1.1.1 author: Tobe O homepage: https://github.com/angel-dart/proxy environment: - sdk: ">=1.19.0 <3.0.0" + sdk: ">=2.0.0 <3.0.0" dependencies: - angel_framework: ^1.1.0-alpha + angel_framework: ^2.0.0-alpha # The core server library. http: ^0.11.3 + dev_dependencies: - angel_test: ^1.1.0-alpha - test: ^0.12.15 + angel_test: 2.0.0-alpha + test: ^1.0.0 diff --git a/test/basic_test.dart b/test/basic_test.dart index 47144e4e..943e1368 100644 --- a/test/basic_test.dart +++ b/test/basic_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; 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/http.dart' as http; import 'package:logging/logging.dart'; @@ -14,10 +15,11 @@ main() { String url; setUp(() async { - app = new Angel()..storeOriginalBuffer = true; + app = new Angel()..keepRawRequestBuffers = true; + var appHttp = AngelHttp(app); var httpClient = new http.Client(); - testServer = await testApp().startServer(); + testServer = await startTestServer(); var proxy1 = new Proxy( app, @@ -34,10 +36,10 @@ main() { mapTo: '/foo', ); - app.use(proxy1.handleRequest); - app.use(proxy2.handleRequest); + app.all("/proxy/*", proxy1.handleRequest); + app.all("*", proxy2.handleRequest); - app.use((req, res) { + app.fallback((req, res) { print('Intercepting empty from ${req.uri}'); res.write('intercept empty'); }); @@ -54,7 +56,9 @@ main() { if (rec.stackTrace != null) print(rec.stackTrace); }); - server = await app.startServer(); + await appHttp.startServer(); + + server = appHttp.httpServer; url = 'http://${server.address.address}:${server.port}'; }); @@ -68,7 +72,7 @@ main() { test('publicPath', () async { final response = await client.get('$url/proxy/hello'); print('Response: ${response.body}'); - expect(response.body, equals('"world"')); + expect(response.body, equals('world')); }); test('empty', () async { @@ -80,13 +84,12 @@ main() { test('mapTo', () async { final response = await client.get('$url/bar'); print('Response: ${response.body}'); - expect(response.body, equals('"baz"')); + expect(response.body, equals('baz')); }); test('original buffer', () async { - var response = await client.post('$url/proxy/body', - body: JSON.encode({'foo': 'bar'}), - headers: {'content-type': 'application/json'}); + var response = await client + .post('$url/proxy/body', body: json.encode({'foo': 'bar'}), headers: {'content-type': 'application/json'}); print('Response: ${response.body}'); expect(response.body, isNotEmpty); expect(response.body, isNot('intercept empty')); diff --git a/test/common.dart b/test/common.dart index bfa3c1bc..0b775011 100644 --- a/test/common.dart +++ b/test/common.dart @@ -1,18 +1,26 @@ +import 'dart:async'; +import 'dart:io'; + import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/http.dart'; import 'package:logging/logging.dart'; -Angel testApp() { - final app = new Angel()..lazyParseBodies = true; +Future startTestServer() { + final app = new Angel() + ..eagerParseRequestBodies = false + ..keepRawRequestBuffers = true; - app.get('/hello', 'world'); - app.get('/foo/bar', 'baz'); + app.get('/hello', (req, res) => res.write('world')); + app.get('/foo/bar', (req, res) => res.write('baz')); app.post('/body', (RequestContext req, res) async { - var body = await req.lazyBody(); - print('Body: $body'); + var body = await req.parseBody(); + app.logger.info('Body: $body'); return body; }); app.logger = new Logger('testApp'); + var server = AngelHttp(app); + app.dumpTree(); - return app..dumpTree(); + return server.startServer(); } diff --git a/test/pub_serve_test.dart b/test/pub_serve_test.dart index 571337f4..ee0b87c6 100644 --- a/test/pub_serve_test.dart +++ b/test/pub_serve_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; 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:angel_test/angel_test.dart'; import 'package:http/http.dart' as http; @@ -10,43 +11,42 @@ import 'package:test/test.dart'; main() { Angel app, testApp; TestClient client; + Proxy layer; setUp(() async { testApp = new Angel(); testApp.get('/foo', (req, res) async { res.write('pub serve'); }); - testApp.get('/empty', (req, res) => res.end()); + testApp.get('/empty', (req, res) => res.close()); - testApp.responseFinalizers.add((req, ResponseContext res) async { + testApp.responseFinalizers.add((req, res) async { print('OUTGOING: ' + new String.fromCharCodes(res.buffer.toBytes())); }); - testApp.injectEncoders({'gzip': GZIP.encoder}); + testApp.encoders.addAll({'gzip': gzip.encoder}); - var server = await testApp.startServer(); + var server = await AngelHttp(testApp).startServer(); app = new Angel(); app.get('/bar', (req, res) => res.write('normal')); var httpClient = new http.Client(); - var layer = new Proxy( + layer = new Proxy( app, httpClient, server.address.address, port: server.port, publicPath: '/proxy', ); - app.use(layer.handleRequest); + app.all("*", layer.handleRequest); app.responseFinalizers.add((req, ResponseContext res) async { - print('Normal. Buf: ' + - new String.fromCharCodes(res.buffer.toBytes()) + - ', headers: ${res.headers}'); + print('Normal. Buf: ' + new String.fromCharCodes(res.buffer.toBytes()) + ', headers: ${res.headers}'); }); - app.injectEncoders({'gzip': GZIP.encoder}); + app.encoders.addAll({'gzip': gzip.encoder}); client = await connectTo(app); }); @@ -61,11 +61,10 @@ main() { test('proxied', () async { var rq = new MockHttpRequest('GET', Uri.parse('/proxy/foo'))..close(); - await app.handleRequest(rq); - var response = await rq.response - .transform(GZIP.decoder) - .transform(UTF8.decoder) - .join(); + var rqc = await HttpRequestContext.from(rq, app, '/proxy/foo'); + var rsc = HttpResponseContext(rq.response, app); + await app.executeHandler(layer, rqc, rsc); + var response = await rq.response.transform(gzip.decoder).transform(utf8.decoder).join(); expect(response, 'pub serve'); });