From f95de91bf52bb6e997236603baf1c7e8b034d650 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 9 Dec 2018 10:49:59 -0500 Subject: [PATCH] 2.0.0-alpha.15 --- CHANGELOG.md | 7 + example/http2/body_parsing.dart | 2 +- example/http2/main.dart | 2 +- example/http2/server_push.dart | 2 +- example/json.dart | 3 +- lib/src/core/request_context.dart | 66 +++++++-- lib/src/core/server.dart | 6 - lib/src/core/service.dart | 164 ++++++++++------------- lib/src/http/http_request_context.dart | 19 +-- lib/src/http2/http2_request_context.dart | 72 ++++++---- pubspec.yaml | 2 +- test/http2/adapter_test.dart | 16 ++- test/http2/http2_client.dart | 2 +- test/routing_test.dart | 7 +- test/services_test.dart | 2 + 15 files changed, 203 insertions(+), 169 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e618a19a..9ed1eeb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 2.0.0-alpha.15 +* Remove dependency on `body_parser`. +* `RequestContext` now exposes a `Stream> get body` getter. + * Calling `RequestContext.parseBody()` parses its contents. + * Added `bodyAsMap`, `bodyAsList`, `bodyAsObject`, and `uploadedFiles` to `RequestContext`. + * Removed `Angel.keepRawRequestBuffers` and anything that had to do with buffering request bodies. + # 2.0.0-alpha.14 * Patch `HttpResponseContext._openStream` to send content-length. diff --git a/example/http2/body_parsing.dart b/example/http2/body_parsing.dart index b7d0fc77..93c4a78d 100644 --- a/example/http2/body_parsing.dart +++ b/example/http2/body_parsing.dart @@ -16,7 +16,7 @@ main() async { app.get('/', (req, res) => res.streamFile(indexHtml)); - app.post('/', (req, res) => req.parseBody()); + app.post('/', (req, res) => req.parseBody().then((_) => req.bodyAsMap)); var ctx = new SecurityContext() ..useCertificateChain('dev.pem') diff --git a/example/http2/main.dart b/example/http2/main.dart index 04a3ed98..e381c48c 100644 --- a/example/http2/main.dart +++ b/example/http2/main.dart @@ -16,7 +16,7 @@ main() async { app.get('/', (req, res) => 'Hello HTTP/2!!!'); app.fallback((req, res) => throw new AngelHttpException.notFound( - message: 'No file exists at ${req.uri.path}')); + message: 'No file exists at ${req.uri}')); var ctx = new SecurityContext() ..useCertificateChain('dev.pem') diff --git a/example/http2/server_push.dart b/example/http2/server_push.dart index 814acf4f..439b7fb9 100644 --- a/example/http2/server_push.dart +++ b/example/http2/server_push.dart @@ -10,7 +10,7 @@ main() async { var app = new Angel(); app.logger = new Logger('angel')..onRecord.listen(prettyLog); - var publicDir = new Directory('example/public'); + var publicDir = new Directory('example/http2/public'); var indexHtml = const LocalFileSystem().file(publicDir.uri.resolve('index.html')); var styleCss = diff --git a/example/json.dart b/example/json.dart index e5e6dcbe..23dac1a2 100644 --- a/example/json.dart +++ b/example/json.dart @@ -32,7 +32,8 @@ main() async { serverMain(_) async { var app = new Angel(); - var http = new AngelHttp.custom(app, startShared, useZone: false); // Run a cluster + var http = + new AngelHttp.custom(app, startShared, useZone: false); // Run a cluster app.get('/', (req, res) { return res.serialize({ diff --git a/lib/src/core/request_context.dart b/lib/src/core/request_context.dart index 57e12673..0e081106 100644 --- a/lib/src/core/request_context.dart +++ b/lib/src/core/request_context.dart @@ -2,12 +2,12 @@ library angel_framework.http.request_context; import 'dart:async'; import 'dart:convert'; -import 'dart:io' show Cookie, HttpHeaders, HttpSession, InternetAddress; +import 'dart:io' + show Cookie, HeaderValue, HttpHeaders, HttpSession, InternetAddress; import 'package:angel_container/angel_container.dart'; import 'package:http_parser/http_parser.dart'; import 'package:http_server/http_server.dart'; -import 'package:meta/meta.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart' as p; @@ -21,11 +21,11 @@ part 'injection.dart'; /// A convenience wrapper around an incoming [RawRequest]. abstract class RequestContext { String _acceptHeaderCache, _extensionCache; - bool _acceptsAllCache, _hasParsedBody; + bool _acceptsAllCache, _hasParsedBody = false; Map _bodyFields, _queryParameters; List _bodyList; Object _bodyObject; - List _bodyFiles; + List _uploadedFiles; MediaType _contentType; /// The underlying [RawRequest] provided by the driver. @@ -97,7 +97,7 @@ abstract class RequestContext { /// Returns a *mutable* [Map] of the fields parsed from the request [body]. /// /// Note that [parseBody] must be called first. - Map get bodyFields { + Map get bodyAsMap { if (!hasParsedBody) { throw new StateError('The request body has not been parsed yet.'); } else if (_bodyFields == null) { @@ -110,7 +110,7 @@ abstract class RequestContext { /// Returns a *mutable* [List] parsed from the request [body]. /// /// Note that [parseBody] must be called first. - List get bodyList { + List get bodyAsList { if (!hasParsedBody) { throw new StateError('The request body has not been parsed yet.'); } else if (_bodyList == null) { @@ -123,7 +123,7 @@ abstract class RequestContext { /// Returns the parsed request body, whatever it may be (typically a [Map] or [List]). /// /// Note that [parseBody] must be called first. - Object get bodyObject { + Object get bodyAsObject { if (!hasParsedBody) { throw new StateError('The request body has not been parsed yet.'); } @@ -134,12 +134,12 @@ abstract class RequestContext { /// Returns a *mutable* map of the files parsed from the request [body]. /// /// Note that [parseBody] must be called first. - List get bodyFiles { + List get uploadedFiles { if (!hasParsedBody) { throw new StateError('The request body has not been parsed yet.'); } - return _bodyFiles; + return _uploadedFiles; } /// Returns a *mutable* map of the fields contained in the query. @@ -189,7 +189,7 @@ abstract class RequestContext { _hasParsedBody = true; if (contentType.type == 'application' && contentType.subtype == 'json') { - _bodyFiles = []; + _uploadedFiles = []; var parsed = _bodyObject = await body.transform(encoding.decoder).join().then(json.decode); @@ -199,6 +199,14 @@ abstract class RequestContext { } else if (parsed is List) { _bodyList = parsed; } + } else if (contentType.type == 'application' && + contentType.subtype == 'x-www-form-urlencoded') { + _uploadedFiles = []; + var parsed = await body + .transform(encoding.decoder) + .join() + .then((s) => Uri.splitQueryString(s, encoding: encoding)); + _bodyFields = new Map.from(parsed); } else if (contentType.type == 'multipart' && contentType.subtype == 'form-data' && contentType.parameters.containsKey('boundary')) { @@ -207,11 +215,11 @@ abstract class RequestContext { var parts = body.transform(transformer).map((part) => HttpMultipartFormData.parse(part, defaultEncoding: encoding)); _bodyFields = {}; - _bodyFiles = []; + _uploadedFiles = []; await for (var part in parts) { if (part.isBinary) { - _bodyFiles.add(part); + _uploadedFiles.add(new UploadedFile(part)); } else if (part.isText && part.contentDisposition.parameters.containsKey('name')) { // If there is no name, then don't parse it. @@ -222,7 +230,7 @@ abstract class RequestContext { } } else { _bodyFields = {}; - _bodyFiles = []; + _uploadedFiles = []; } } } @@ -236,3 +244,35 @@ abstract class RequestContext { return new Future.value(); } } + +/// Reads information about a binary chunk uploaded to the server. +class UploadedFile { + /// The underlying `form-data` item. + final HttpMultipartFormData formData; + + MediaType _contentType; + + UploadedFile(this.formData); + + /// Returns the binary stream from [formData]. + Stream> get data => formData.cast>(); + + /// The filename associated with the data on the user's system. + /// Returns [:null:] if not present. + String get filename => formData.contentDisposition.parameters['filename']; + + /// The name of the field associated with this data. + /// Returns [:null:] if not present. + String get name => formData.contentDisposition.parameters['name']; + + /// The parsed [:Content-Type:] header of the [:HttpMultipartFormData:]. + /// Returns [:null:] if not present. + MediaType get contentType => _contentType ??= (formData.contentType == null + ? null + : new MediaType.parse(formData.contentType.toString())); + + /// The parsed [:Content-Transfer-Encoding:] header of the + /// [:HttpMultipartFormData:]. This field is used to determine how to decode + /// the data. Returns [:null:] if not present. + HeaderValue get contentTransferEncoding => formData.contentTransferEncoding; +} diff --git a/lib/src/core/server.dart b/lib/src/core/server.dart index 8449052f..a86fc84d 100644 --- a/lib/src/core/server.dart +++ b/lib/src/core/server.dart @@ -114,11 +114,6 @@ class Angel extends Routable { /// for you. final Map configuration = {}; - /// When set to `true` (default: `false`), the request body will be parsed - /// automatically; otherwise, you must call [RequestContext].parseBody() manually, - /// or use `lazyBody()`. - bool eagerParseRequestBodies = false; - /// A function that renders views. /// /// Called by [ResponseContext]@`render`. @@ -358,7 +353,6 @@ class Angel extends Routable { Angel( {Reflector reflector: const EmptyReflector(), this.logger, - this.eagerParseRequestBodies: false, this.allowMethodOverrides: true, this.serializer, this.viewGenerator}) diff --git a/lib/src/core/service.dart b/lib/src/core/service.dart index a5c9306f..a330b0d7 100644 --- a/lib/src/core/service.dart +++ b/lib/src/core/service.dart @@ -200,13 +200,11 @@ class Service extends Routable { Middleware indexMiddleware = getAnnotation(service.index, Middleware, app.container.reflector); get('/', (req, res) { - return req.parseQuery().then((query) { - return this.index(mergeMap([ - {'query': query}, - restProvider, - req.serviceParams - ])); - }); + return this.index(mergeMap([ + {'query': req.queryParameters}, + restProvider, + req.serviceParams + ])); }, middleware: [] ..addAll(handlers) @@ -215,20 +213,18 @@ class Service extends Routable { Middleware createMiddleware = getAnnotation(service.create, Middleware, app.container.reflector); post('/', (req, ResponseContext res) { - return req.parseQuery().then((query) { - return req.parseBody().then((body) { - return this - .create( - body as Data, - mergeMap([ - {'query': query}, - restProvider, - req.serviceParams - ])) - .then((r) { - res.statusCode = 201; - return r; - }); + return req.parseBody().then((_) { + return this + .create( + req.bodyAsMap as Data, + mergeMap([ + {'query': req.queryParameters}, + restProvider, + req.serviceParams + ])) + .then((r) { + res.statusCode = 201; + return r; }); }); }, @@ -241,15 +237,13 @@ class Service extends Routable { getAnnotation(service.read, Middleware, app.container.reflector); get('/:id', (req, res) { - return req.parseQuery().then((query) { - return this.read( - parseId(req.params['id']), - mergeMap([ - {'query': query}, - restProvider, - req.serviceParams - ])); - }); + return this.read( + parseId(req.params['id']), + mergeMap([ + {'query': req.queryParameters}, + restProvider, + req.serviceParams + ])); }, middleware: [] ..addAll(handlers) @@ -257,20 +251,18 @@ class Service extends Routable { Middleware modifyMiddleware = getAnnotation(service.modify, Middleware, app.container.reflector); - patch( - '/:id', - (req, res) => req.parseBody().then((body) { - return req.parseQuery().then((query) { - return this.modify( - parseId(req.params['id']), - body as Data, - mergeMap([ - {'query': query}, - restProvider, - req.serviceParams - ])); - }); - }), + patch('/:id', (req, res) { + return req.parseBody().then((_) { + return this.modify( + parseId(req.params['id']), + req.bodyAsMap as Data, + mergeMap([ + {'query': req.queryParameters}, + restProvider, + req.serviceParams + ])); + }); + }, middleware: [] ..addAll(handlers) ..addAll( @@ -278,38 +270,34 @@ class Service extends Routable { Middleware updateMiddleware = getAnnotation(service.update, Middleware, app.container.reflector); - post( - '/:id', - (req, res) => req.parseBody().then((body) { - return req.parseQuery().then((query) { - return this.update( - parseId(req.params['id']), - body as Data, - mergeMap([ - {'query': query}, - restProvider, - req.serviceParams - ])); - }); - }), + post('/:id', (req, res) { + return req.parseBody().then((_) { + return this.update( + parseId(req.params['id']), + req.bodyAsMap as Data, + mergeMap([ + {'query': req.queryParameters}, + restProvider, + req.serviceParams + ])); + }); + }, middleware: [] ..addAll(handlers) ..addAll( (updateMiddleware == null) ? [] : updateMiddleware.handlers)); - put( - '/:id', - (req, res) => req.parseBody().then((body) { - return req.parseQuery().then((query) { - return this.update( - parseId(req.params['id']), - body as Data, - mergeMap([ - {'query': query}, - restProvider, - req.serviceParams - ])); - }); - }), + put('/:id', (req, res) { + return req.parseBody().then((_) { + return this.update( + parseId(req.params['id']), + req.bodyAsMap as Data, + mergeMap([ + {'query': req.queryParameters}, + restProvider, + req.serviceParams + ])); + }); + }, middleware: [] ..addAll(handlers) ..addAll( @@ -318,30 +306,26 @@ class Service extends Routable { Middleware removeMiddleware = getAnnotation(service.remove, Middleware, app.container.reflector); delete('/', (req, res) { - return req.parseQuery().then((query) { - return this.remove( - null, - mergeMap([ - {'query': query}, - restProvider, - req.serviceParams - ])); - }); + return this.remove( + null, + mergeMap([ + {'query': req.queryParameters}, + restProvider, + req.serviceParams + ])); }, middleware: [] ..addAll(handlers) ..addAll( (removeMiddleware == null) ? [] : removeMiddleware.handlers)); delete('/:id', (req, res) { - return req.parseQuery().then((query) { - return this.remove( - parseId(req.params['id']), - mergeMap([ - {'query': query}, - restProvider, - req.serviceParams - ])); - }); + return this.remove( + parseId(req.params['id']), + mergeMap([ + {'query': req.queryParameters}, + restProvider, + req.serviceParams + ])); }, middleware: [] ..addAll(handlers) diff --git a/lib/src/http/http_request_context.dart b/lib/src/http/http_request_context.dart index e8d21c0d..f9408580 100644 --- a/lib/src/http/http_request_context.dart +++ b/lib/src/http/http_request_context.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:angel_container/angel_container.dart'; -import 'package:body_parser/body_parser.dart'; import 'package:http_parser/http_parser.dart'; import '../core/core.dart'; @@ -40,6 +39,9 @@ class HttpRequestContext extends RequestContext { /// The underlying [HttpRequest] instance underneath this context. HttpRequest get rawRequest => _io; + @override + Stream> get body => _io; + @override String get method { return _override ?? originalMethod; @@ -120,10 +122,6 @@ class HttpRequestContext extends RequestContext { ctx._path = path; ctx._io = request; - if (app.eagerParseRequestBodies == true) { - return ctx.parse().then((_) => ctx); - } - return new Future.value(ctx); } @@ -134,15 +132,4 @@ class HttpRequestContext extends RequestContext { _override = _path = null; return super.close(); } - - @override - Future parseOnce() { - return parseBodyFromStream( - rawRequest, - rawRequest.headers.contentType != null - ? new MediaType.parse(rawRequest.headers.contentType.toString()) - : null, - rawRequest.uri, - storeOriginalBuffer: app.keepRawRequestBuffers == true); - } } diff --git a/lib/src/http2/http2_request_context.dart b/lib/src/http2/http2_request_context.dart index f15b247e..3381383f 100644 --- a/lib/src/http2/http2_request_context.dart +++ b/lib/src/http2/http2_request_context.dart @@ -3,8 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:angel_container/src/container.dart'; import 'package:angel_framework/angel_framework.dart'; -import 'package:body_parser/body_parser.dart'; -import 'package:http_parser/http_parser.dart'; import 'package:http2/transport.dart'; import 'package:mock_request/mock_request.dart'; import 'package:uuid/uuid.dart'; @@ -13,8 +11,8 @@ final RegExp _comma = new RegExp(r',\s*'); final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); class Http2RequestContext extends RequestContext { + final StreamController> _body = new StreamController(); final Container container; - BytesBuilder _buf; List _cookies; HttpHeaders _headers; String _method, _override, _path; @@ -25,27 +23,48 @@ class Http2RequestContext extends RequestContext { Http2RequestContext._(this.container); + @override + Stream> get body => _body.stream; + static Future from( ServerTransportStream stream, Socket socket, Angel app, Map sessions, Uuid uuid) async { + var c = new Completer(); var req = new Http2RequestContext._(app.container.createChild()) ..app = app .._socket = socket .._stream = stream; - var buf = req._buf = new BytesBuilder(); var headers = req._headers = new MockHttpHeaders(); - String scheme = 'https', - authority = '${socket.address.address}:${socket.port}', - path = ''; + String scheme = 'https', host = socket.address.address, path = ''; + int port = socket.port; var cookies = []; - await for (var msg in stream.incomingMessages) { + void finalize() { + req + .._cookies = new List.unmodifiable(cookies) + .._uri = new Uri(scheme: scheme, host: host, port: port, path: path); + if (!c.isCompleted) c.complete(req); + } + + void parseHost(String value) { + var uri = Uri.tryParse(value); + if (uri == null || uri.scheme == 'localhost') return; + scheme = uri.hasScheme ? uri.scheme : scheme; + + if (uri.hasAuthority) { + host = uri.host; + port = uri.hasPort ? uri.port : null; + } + } + + stream.incomingMessages.listen((msg) { if (msg is DataStreamMessage) { - buf.add(msg.bytes); + if (!c.isCompleted) finalize(); + req._body.add(msg.bytes); } else if (msg is HeadersStreamMessage) { for (var header in msg.headers) { var name = ascii.decode(header.name).toLowerCase(); @@ -64,7 +83,7 @@ class Http2RequestContext extends RequestContext { scheme = value; break; case ':authority': - authority = value; + parseHost(value); break; case 'cookie': var cookieStrings = value.split(';').map((s) => s.trim()); @@ -78,18 +97,22 @@ class Http2RequestContext extends RequestContext { } break; default: - headers.add(ascii.decode(header.name), value.split(_comma)); + var name = ascii.decode(header.name).toLowerCase(); + + if (name == 'host') { + parseHost(value); + } + + headers.add(name, value.split(_comma)); break; } } + + if (msg.endStream && !c.isCompleted) finalize(); } - - //if (msg.endStream) break; - } - - req - .._cookies = new List.unmodifiable(cookies) - .._uri = Uri.parse('$scheme://$authority').replace(path: path); + }, onDone: () { + if (!c.isCompleted) finalize(); + }, cancelOnError: true, onError: c.completeError); // Apply session var dartSessId = @@ -104,7 +127,7 @@ class Http2RequestContext extends RequestContext { () => new MockHttpSession(id: dartSessId.value), ); - return req; + return c.future; } @override @@ -147,19 +170,10 @@ class Http2RequestContext extends RequestContext { @override Future close() { + _body.close(); return super.close(); } - @override - Future parseOnce() { - return parseBodyFromStream( - new Stream.fromIterable([_buf.takeBytes()]), - contentType == null ? null : new MediaType.parse(contentType.toString()), - uri, - storeOriginalBuffer: app.keepRawRequestBuffers == true, - ); - } - @override ServerTransportStream get rawRequest => _stream; } diff --git a/pubspec.yaml b/pubspec.yaml index f1529a91..245e192e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_framework -version: 2.0.0-alpha.14 +version: 2.0.0-alpha.15 description: A high-powered HTTP server with dependency injection, routing and much more. author: Tobe O homepage: https://github.com/angel-dart/angel_framework diff --git a/test/http2/adapter_test.dart b/test/http2/adapter_test.dart index 547a7878..d16b5a09 100644 --- a/test/http2/adapter_test.dart +++ b/test/http2/adapter_test.dart @@ -25,9 +25,7 @@ void main() { Uri serverRoot; setUp(() async { - app = new Angel() - ..keepRawRequestBuffers = true - ..encoders['gzip'] = gzip.encoder; + app = new Angel()..encoders['gzip'] = gzip.encoder; app.get('/', (req, res) async { res.write('Hello world'); @@ -50,13 +48,17 @@ void main() { await res.close(); }); - app.post('/body', (req, res) => req.parseBody()); + app.post('/body', (req, res) => req.parseBody().then((_) => req.bodyAsMap)); app.post('/upload', (req, res) async { - var body = await req.parseBody(), files = await req.parseUploadedFiles(); - stdout.add(await req.parseRawRequestBuffer()); + await req.parseBody(); + var body = req.bodyAsMap, files = req.uploadedFiles; var file = files.firstWhere((f) => f.name == 'file'); - return [file.data.length, file.mimeType, body]; + return [ + await file.data.map((l) => l.length).reduce((a, b) => a + b), + file.contentType.mimeType, + body + ]; }); app.get('/push', (req, res) async { diff --git a/test/http2/http2_client.dart b/test/http2/http2_client.dart index 5263d2b7..ebf46bab 100644 --- a/test/http2/http2_client.dart +++ b/test/http2/http2_client.dart @@ -38,7 +38,7 @@ class Http2Client extends BaseClient { headers.add(new Header.ascii(k, v)); }); - var stream = await connection.makeRequest(headers); + var stream = await connection.makeRequest(headers, endStream: body.isEmpty); if (body.isNotEmpty) { stream.sendData(body, endStream: true); diff --git a/test/routing_test.dart b/test/routing_test.dart index e283d708..15424b19 100644 --- a/test/routing_test.dart +++ b/test/routing_test.dart @@ -75,7 +75,10 @@ main() { middleware: [interceptor]); app.get('/hello', (req, res) => 'world'); app.get('/name/:first/last/:last', (req, res) => req.params); - app.post('/lambda', (RequestContext req, res) => req.parseBody()); + app.post( + '/lambda', + (RequestContext req, res) => + req.parseBody().then((_) => req.bodyAsMap)); app.mount('/todos/:id', todos); app .get('/greet/:name', @@ -85,7 +88,7 @@ main() { res.redirectTo('Named routes', {'name': 'tests'}); }); app.get('/log', (RequestContext req, res) async { - print("Query: ${await req.parseQuery()}"); + print("Query: ${req.queryParameters}"); return "Logged"; }); diff --git a/test/services_test.dart b/test/services_test.dart index d5e4e7d0..2afcb38d 100644 --- a/test/services_test.dart +++ b/test/services_test.dart @@ -77,6 +77,7 @@ main() { await client.post("$url/todos", headers: headers as Map, body: postData); postData = json.encode({'text': 'modified'}); + var response = await client.patch("$url/todos/0", headers: headers as Map, body: postData); expect(response.statusCode, 200); @@ -90,6 +91,7 @@ main() { await client.post("$url/todos", headers: headers as Map, body: postData); postData = json.encode({'over': 'write'}); + var response = await client.post("$url/todos/0", headers: headers as Map, body: postData); expect(response.statusCode, 200);