diff --git a/CHANGELOG.md b/CHANGELOG.md index 794165ef..70ea8a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.1.0 +* `pedantic` lints. +* Add the `AngelShelf` driver class, which allows you to embed Angel within shelf. + # 2.0.0 * Removed `supportShelf`. diff --git a/README.md b/README.md index 3f4855f9..c0f4203c 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ import 'package:shelf/shelf.dart' as shelf; import 'api/api.dart'; main() async { - var app = new Angel(); - var http = new AngelHttp(app); + var app = Angel(); + var http = AngelHttp(app); // Angel routes on top await app.mountController(); @@ -39,7 +39,7 @@ main() async { // Re-route all other traffic to an // existing application. app.fallback(embedShelf( - new shelf.Pipeline() + shelf.Pipeline() .addMiddleware(shelf.logRequests()) .addHandler(_echoRequest) )); @@ -62,7 +62,7 @@ handleRequest(shelf.Request request) { var req = request.context['angel_shelf.request'] as RequestContext; // ... And then interact with it. - req.container.registerNamedSingleton('from_shelf', new Foo()); + req.container.registerNamedSingleton('from_shelf', Foo()); // `req.container` is also available. var container = request.context['angel_shelf.container'] as Container; diff --git a/analysis_options.yaml b/analysis_options.yaml index eae1e42a..a4f33350 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1,4 @@ +include: package:pedantic/analysis_options.yaml analyzer: strong-mode: - implicit-casts: false \ No newline at end of file + implicit-casts: false diff --git a/example/main.dart b/example/main.dart index 83221986..1803eae3 100644 --- a/example/main.dart +++ b/example/main.dart @@ -2,11 +2,17 @@ import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/http.dart'; import 'package:angel_shelf/angel_shelf.dart'; +import 'package:logging/logging.dart'; +import 'package:pretty_logging/pretty_logging.dart'; import 'package:shelf_static/shelf_static.dart'; main() async { - var app = new Angel(); - var http = new AngelHttp(app); + Logger.root + ..level = Level.ALL + ..onRecord.listen(prettyLog); + + var app = Angel(logger: Logger('angel_shelf_demo')); + var http = AngelHttp(app); // `shelf` request handler var shelfHandler = createStaticHandler('.', @@ -21,9 +27,9 @@ main() async { return false; // End execution of handlers, so we don't proxy to dartlang.org when we don't need to. }); - // Proxy any other request through to the static file handler + // Pass any other request through to the static file handler app.fallback(wrappedHandler); await http.startServer(InternetAddress.loopbackIPv4, 8080); - print('Proxying at ${http.uri}'); + print('Running at ${http.uri}'); } diff --git a/lib/src/convert.dart b/lib/src/convert.dart index 44b1b40a..9d0a2482 100644 --- a/lib/src/convert.dart +++ b/lib/src/convert.dart @@ -1,15 +1,13 @@ import 'dart:async'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; -import 'package:angel_framework/http2.dart'; import 'package:path/path.dart' as p; import 'package:shelf/shelf.dart' as shelf; import 'package:stream_channel/stream_channel.dart'; /// Creates a [shelf.Request] analogous to the input [req]. /// -/// The new request's `context` will contain [req.container] as `angel_shelf.container`, as well as +/// The request's `context` will contain [req.container] as `angel_shelf.container`, as well as /// the provided [context], if any. /// /// The context will also have the original request available as `angel_shelf.request`. @@ -30,47 +28,26 @@ Future convertRequest(RequestContext req, ResponseContext res, String protocolVersion; Uri requestedUri; - if (req is HttpRequestContext && res is HttpResponseContext) { - protocolVersion = req.rawRequest.protocolVersion; - requestedUri = req.rawRequest.requestedUri; + protocolVersion = '1.1'; + requestedUri = Uri.parse('http://${req.hostname}'); + requestedUri = requestedUri.replace(path: req.uri.path); - onHijack = (void hijack(StreamChannel> channel)) { - new Future(() async { - var rs = res.detach(); - var socket = await rs.detachSocket(writeHeaders: false); - var ctrl = new StreamChannelController>(); - var body = await req.parseRawRequestBuffer() ?? []; - ctrl.local.sink.add(body ?? []); - socket.listen(ctrl.local.sink.add, + onHijack = (void hijack(StreamChannel> channel)) { + Future.sync(res.detach).then((_) { + var ctrl = StreamChannelController>(); + if (req.hasParsedBody) { + req.body.listen(ctrl.local.sink.add, onError: ctrl.local.sink.addError, onDone: ctrl.local.sink.close); - ctrl.local.stream.pipe(socket); - hijack(ctrl.foreign); - }).catchError((e, st) { - app.logger?.severe('An error occurred while hijacking a shelf request', - e, st as StackTrace); - }); - }; - } else if (req is Http2RequestContext && res is Http2ResponseContext) { - protocolVersion = '2.0'; - requestedUri = req.uri; - - onHijack = (void hijack(StreamChannel> channel)) { - new Future(() async { - var rs = await res.detach(); - var ctrl = new StreamChannelController>(); - var body = await req.parseRawRequestBuffer() ?? []; - ctrl.local.sink.add(body ?? []); - ctrl.local.stream.listen(rs.sendData, onDone: rs.terminate); - hijack(ctrl.foreign); - }).catchError((e, st) { - stderr.writeln('An error occurred while hijacking a shelf request: $e'); - stderr.writeln(st); - }); - }; - } else { - throw new UnsupportedError( - '`embedShelf` is only supported for HTTP and HTTP2 requests in Angel.'); - } + } else { + ctrl.local.sink.close(); + } + ctrl.local.stream.pipe(res); + hijack(ctrl.foreign); + }).catchError((e, st) { + app.logger?.severe('An error occurred while hijacking a shelf request', e, + st as StackTrace); + }); + }; var url = req.uri; @@ -78,12 +55,12 @@ Future convertRequest(RequestContext req, ResponseContext res, url = url.replace(path: url.path.substring(1)); } - return new shelf.Request(req.method, requestedUri, + return shelf.Request(req.method, requestedUri, protocolVersion: protocolVersion, headers: headers, handlerPath: handlerPath, url: url, - body: (await req.parseRawRequestBuffer()) ?? [], + body: req.body, context: {'angel_shelf.request': req} ..addAll({'angel_shelf.container': req.container}) ..addAll(context ?? {}), diff --git a/lib/src/embed_shelf.dart b/lib/src/embed_shelf.dart index 9f34b97a..9b42642c 100644 --- a/lib/src/embed_shelf.dart +++ b/lib/src/embed_shelf.dart @@ -13,19 +13,21 @@ import 'convert.dart'; RequestHandler embedShelf(shelf.Handler handler, {String handlerPath, Map context, - bool throwOnNullResponse: false}) { + bool throwOnNullResponse = false}) { return (RequestContext req, ResponseContext res) async { var shelfRequest = await convertRequest(req, res, handlerPath: handlerPath, context: context); try { var result = await handler(shelfRequest); if (result is! shelf.Response && result != null) return result; - if (result == null && throwOnNullResponse == true) - throw new AngelHttpException('Internal Server Error'); + if (result == null && throwOnNullResponse == true) { + throw AngelHttpException('Internal Server Error'); + } await mergeShelfResponse(result, res); return false; } on shelf.HijackException { // On hijack, do nothing, because the hijack handlers already call res.detach(); + return null; } catch (e) { rethrow; } diff --git a/lib/src/shelf_driver.dart b/lib/src/shelf_driver.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/src/shelf_request.dart b/lib/src/shelf_request.dart new file mode 100644 index 00000000..fffd6223 --- /dev/null +++ b/lib/src/shelf_request.dart @@ -0,0 +1,77 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel_container/angel_container.dart'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:mock_request/mock_request.dart'; +import 'package:shelf/shelf.dart' as shelf; + +class ShelfRequestContext extends RequestContext { + @override + final Angel app; + @override + final Container container; + @override + final shelf.Request rawRequest; + @override + final String path; + + List _cookies; + + @override + final MockHttpHeaders headers = MockHttpHeaders(); + + ShelfRequestContext(this.app, this.container, this.rawRequest, this.path) { + rawRequest.headers.forEach(headers.add); + } + + @override + Stream> get body => rawRequest.read(); + + @override + List get cookies { + if (_cookies == null) { + // Parse cookies + _cookies = []; + var value = headers.value('cookie'); + if (value != null) { + var cookieStrings = value.split(';').map((s) => s.trim()); + + for (var cookieString in cookieStrings) { + try { + _cookies.add(Cookie.fromSetCookieValue(cookieString)); + } catch (_) { + // Ignore malformed cookies, and just don't add them to the container. + } + } + } + } + return _cookies; + } + + @override + String get hostname => rawRequest.headers['host']; + + @override + String get method { + if (!app.allowMethodOverrides) { + return originalMethod; + } else { + return headers.value('x-http-method-override')?.toUpperCase() ?? + originalMethod; + } + } + + @override + String get originalMethod => rawRequest.method; + + @override + // TODO: implement remoteAddress + InternetAddress get remoteAddress => null; + + @override + // TODO: implement session + HttpSession get session => null; + + @override + Uri get uri => rawRequest.url; +} diff --git a/lib/src/shelf_response.dart b/lib/src/shelf_response.dart new file mode 100644 index 00000000..e69de29b diff --git a/pubspec.yaml b/pubspec.yaml index 8b875dea..d4c5d66b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ author: Tobe O description: Shelf interop with Angel. Use this to wrap existing server code. homepage: https://github.com/angel-dart/shelf name: angel_shelf -version: 2.0.0 +version: 2.1.0 dependencies: angel_framework: ^2.0.0-alpha path: ^1.0.0 @@ -10,6 +10,8 @@ dependencies: stream_channel: ^1.0.0 dev_dependencies: angel_test: ^2.0.0-alpha + pedantic: ^1.0.0 + pretty_logging: ^1.0.0 shelf_static: ^0.2.8 test: ^1.0.0 environment: diff --git a/test/embed_shelf_test.dart b/test/embed_shelf_test.dart index 6f9c015a..53c09bd9 100644 --- a/test/embed_shelf_test.dart +++ b/test/embed_shelf_test.dart @@ -7,6 +7,7 @@ import 'package:angel_shelf/angel_shelf.dart'; import 'package:angel_test/angel_test.dart'; import 'package:charcode/charcode.dart'; import 'package:logging/logging.dart'; +import 'package:pretty_logging/pretty_logging.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; @@ -16,15 +17,24 @@ main() { HttpServer server; String url; + String _path(String p) { + return Uri( + scheme: 'http', + host: server.address.address, + port: server.port, + path: p) + .toString(); + } + setUp(() async { - var handler = new shelf.Pipeline().addHandler((shelf.Request request) { - if (request.url.path == 'two') - return new shelf.Response(200, body: json.encode(2)); - else if (request.url.path == 'error') - throw new AngelHttpException.notFound(); - else if (request.url.path == 'status') - return new shelf.Response.notModified(headers: {'foo': 'bar'}); - else if (request.url.path == 'hijack') { + var handler = shelf.Pipeline().addHandler((shelf.Request request) { + if (request.url.path == 'two') { + return shelf.Response(200, body: json.encode(2)); + } else if (request.url.path == 'error') { + throw AngelHttpException.notFound(); + } else if (request.url.path == 'status') { + return shelf.Response.notModified(headers: {'foo': 'bar'}); + } else if (request.url.path == 'hijack') { request.hijack((StreamChannel> channel) { var sink = channel.sink; sink.add(utf8.encode('HTTP/1.1 200 OK\r\n')); @@ -32,25 +42,22 @@ main() { sink.add(utf8.encode(json.encode({'error': 'crime'}))); sink.close(); }); - } else if (request.url.path == 'throw') return null; - else - return new shelf.Response.ok('Request for "${request.url}"'); + } else if (request.url.path == 'throw') { + return null; + } else { + return shelf.Response.ok('Request for "${request.url}"'); + } }); - var app = new Angel(); - var http = new AngelHttp(app); + var logger = Logger.detached('angel_shelf')..onRecord.listen(prettyLog); + var app = Angel(logger: logger); + var http = AngelHttp(app); app.get('/angel', (req, res) => 'Angel'); app.fallback(embedShelf(handler, throwOnNullResponse: true)); - app.logger = new Logger.detached('angel_shelf') - ..onRecord.listen((rec) { - stdout.writeln(rec); - if (rec.error != null) stdout.writeln(rec.error); - if (rec.stackTrace != null) stdout.writeln(rec.stackTrace); - }); server = await http.startServer(InternetAddress.loopbackIPv4, 0); - client = new c.Rest(url = http.uri.toString()); + client = c.Rest(url = http.uri.toString()); }); tearDown(() async { @@ -59,24 +66,24 @@ main() { }); test('expose angel side', () async { - var response = await client.get('/angel'); + var response = await client.get(_path('/angel')); expect(json.decode(response.body), equals('Angel')); }); test('expose shelf side', () async { - var response = await client.get('/foo'); + var response = await client.get(_path('/foo')); expect(response, hasStatus(200)); expect(response.body, equals('Request for "foo"')); }); test('shelf can return arbitrary values', () async { - var response = await client.get('/two'); + var response = await client.get(_path('/two')); expect(response, isJson(2)); }); test('shelf can hijack', () async { try { - var client = new HttpClient(); + var client = HttpClient(); var rq = await client.openUrl('GET', Uri.parse('$url/hijack')); var rs = await rq.close(); var body = await rs.cast>().transform(utf8.decoder).join(); @@ -90,17 +97,17 @@ main() { }, skip: ''); test('shelf can set status code', () async { - var response = await client.get('/status'); + var response = await client.get(_path('/status')); expect(response, allOf(hasStatus(304), hasHeader('foo', 'bar'))); }); test('shelf can throw error', () async { - var response = await client.get('/error'); + var response = await client.get(_path('/error')); expect(response, hasStatus(404)); }); test('throw on null', () async { - var response = await client.get('/throw'); + var response = await client.get(_path('/throw')); expect(response, hasStatus(500)); }); }