diff --git a/lib/angel_framework.dart b/lib/angel_framework.dart index 3f63c839..60a495e6 100644 --- a/lib/angel_framework.dart +++ b/lib/angel_framework.dart @@ -4,5 +4,4 @@ library angel_framework; export 'package:angel_http_exception/angel_http_exception.dart'; export 'package:angel_model/angel_model.dart'; export 'package:angel_route/angel_route.dart'; -export 'package:body_parser/body_parser.dart' show FileUploadInfo; export 'src/core/core.dart'; diff --git a/lib/src/core/request_context.dart b/lib/src/core/request_context.dart index b0e1fac1..57e12673 100644 --- a/lib/src/core/request_context.dart +++ b/lib/src/core/request_context.dart @@ -1,12 +1,14 @@ library angel_framework.http.request_context; import 'dart:async'; +import 'dart:convert'; import 'dart:io' show Cookie, HttpHeaders, HttpSession, InternetAddress; import 'package:angel_container/angel_container.dart'; -import 'package:body_parser/body_parser.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; import 'metadata.dart'; @@ -19,9 +21,12 @@ part 'injection.dart'; /// A convenience wrapper around an incoming [RawRequest]. abstract class RequestContext { String _acceptHeaderCache, _extensionCache; - bool _acceptsAllCache; - BodyParseResult _body; - Map _provisionalQuery; + bool _acceptsAllCache, _hasParsedBody; + Map _bodyFields, _queryParameters; + List _bodyList; + Object _bodyObject; + List _bodyFiles; + MediaType _contentType; /// The underlying [RawRequest] provided by the driver. RawRequest get rawRequest; @@ -60,7 +65,7 @@ abstract class RequestContext { /// The content type of an incoming request. MediaType get contentType => - new MediaType.parse(headers.contentType.toString()); + _contentType ??= new MediaType.parse(headers.contentType.toString()); /// The URL parameters extracted from the request URI. Map params = {}; @@ -83,6 +88,64 @@ abstract class RequestContext { /// The [Uri] instance representing the path this request is responding to. Uri get uri; + /// The [Stream] of incoming binary data sent from the client. + Stream> get body; + + /// Returns `true` if [parseBody] has been called so far. + bool get hasParsedBody => _hasParsedBody; + + /// Returns a *mutable* [Map] of the fields parsed from the request [body]. + /// + /// Note that [parseBody] must be called first. + Map get bodyFields { + if (!hasParsedBody) { + throw new StateError('The request body has not been parsed yet.'); + } else if (_bodyFields == null) { + throw new StateError('The request body, $_bodyObject, is not a Map.'); + } + + return _bodyFields; + } + + /// Returns a *mutable* [List] parsed from the request [body]. + /// + /// Note that [parseBody] must be called first. + List get bodyList { + if (!hasParsedBody) { + throw new StateError('The request body has not been parsed yet.'); + } else if (_bodyList == null) { + throw new StateError('The request body, $_bodyObject, is not a List.'); + } + + return _bodyList; + } + + /// Returns the parsed request body, whatever it may be (typically a [Map] or [List]). + /// + /// Note that [parseBody] must be called first. + Object get bodyObject { + if (!hasParsedBody) { + throw new StateError('The request body has not been parsed yet.'); + } + + return _bodyObject; + } + + /// Returns a *mutable* map of the files parsed from the request [body]. + /// + /// Note that [parseBody] must be called first. + List get bodyFiles { + if (!hasParsedBody) { + throw new StateError('The request body has not been parsed yet.'); + } + + return _bodyFiles; + } + + /// Returns a *mutable* map of the fields contained in the query. + Map get queryParameters => + _queryParameters ??= new Map.from(uri.queryParameters); + /// Returns the file extension of the requested path, if any. /// /// Includes the leading `.`, if there is one. @@ -120,49 +183,54 @@ abstract class RequestContext { /// Returns as `true` if the client's `Accept` header indicates that it will accept any response content type. bool get acceptsAll => _acceptsAllCache ??= accepts('*/*'); - /// Retrieves the request body if it has already been parsed, or lazy-parses it before returning the body. - Future parseBody() => parse().then((b) => b.body); - - /// Retrieves a list of all uploaded files if it has already been parsed, or lazy-parses it before returning the files. - Future> parseUploadedFiles() => - parse().then((b) => b.files); - - /// Retrieves the original request buffer if it has already been parsed, or lazy-parses it before returning the buffer.. - /// - /// This will return an empty `List` if you have not enabled `keepRawRequestBuffers` on your [Angel] instance. - Future> parseRawRequestBuffer() => - parse().then((b) => b.originalBuffer); - - /// Retrieves the request body if it has already been parsed, or lazy-parses it before returning the query. - /// - /// If [forceParse] is not `true`, then [uri].query will be returned, and no parsing will be performed. - Future> parseQuery({bool forceParse: false}) { - if (_body == null && forceParse != true) - return new Future.value( - new Map.from(uri.queryParameters)); - else - return parse().then((b) => b.query); - } - /// Manually parses the request body, if it has not already been parsed. - Future parse() { - if (_body != null) - return new Future.value(_body); - else - _provisionalQuery = null; - return parseOnce().then((body) => _body = body); - } + Future parseBody({Encoding encoding: utf8}) async { + if (!_hasParsedBody) { + _hasParsedBody = true; - /// Override this method to one-time parse an incoming request. - @virtual - Future parseOnce(); + if (contentType.type == 'application' && contentType.subtype == 'json') { + _bodyFiles = []; + + var parsed = _bodyObject = + await body.transform(encoding.decoder).join().then(json.decode); + + if (parsed is Map) { + _bodyFields = new Map.from(parsed); + } else if (parsed is List) { + _bodyList = parsed; + } + } else if (contentType.type == 'multipart' && + contentType.subtype == 'form-data' && + contentType.parameters.containsKey('boundary')) { + var boundary = contentType.parameters['boundary']; + var transformer = new MimeMultipartTransformer(boundary); + var parts = body.transform(transformer).map((part) => + HttpMultipartFormData.parse(part, defaultEncoding: encoding)); + _bodyFields = {}; + _bodyFiles = []; + + await for (var part in parts) { + if (part.isBinary) { + _bodyFiles.add(part); + } else if (part.isText && + part.contentDisposition.parameters.containsKey('name')) { + // If there is no name, then don't parse it. + var key = part.contentDisposition.parameters['name']; + var value = await part.join(); + _bodyFields[key] = value; + } + } + } else { + _bodyFields = {}; + _bodyFiles = []; + } + } + } /// Disposes of all resources. Future close() { - _body = null; _acceptsAllCache = null; _acceptHeaderCache = null; - _provisionalQuery?.clear(); serviceParams.clear(); params.clear(); return new Future.value(); diff --git a/lib/src/core/server.dart b/lib/src/core/server.dart index 0dd00111..8449052f 100644 --- a/lib/src/core/server.dart +++ b/lib/src/core/server.dart @@ -119,10 +119,6 @@ class Angel extends Routable { /// or use `lazyBody()`. bool eagerParseRequestBodies = false; - /// When set to `true`, the original body bytes will be stored - /// on requests. `false` by default. - bool keepRawRequestBuffers = false; - /// A function that renders views. /// /// Called by [ResponseContext]@`render`. @@ -364,7 +360,6 @@ class Angel extends Routable { this.logger, this.eagerParseRequestBodies: false, this.allowMethodOverrides: true, - this.keepRawRequestBuffers: false, this.serializer, this.viewGenerator}) : super(reflector) { diff --git a/performance/hello/main.dart b/performance/hello/main.dart index bd7d96db..12d3d81a 100644 --- a/performance/hello/main.dart +++ b/performance/hello/main.dart @@ -1,29 +1,10 @@ /// A basic server that prints "Hello, world!" library performance.hello; -import 'dart:async'; -import 'dart:io'; -import 'dart:isolate'; -import 'package:angel_container/mirrors.dart'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/http.dart'; main() async { - var isolates = []; - - for (int i = 0; i < Platform.numberOfProcessors; i++) { - isolates.add(await Isolate.spawn(start, i + 1)); - } - - await Future.wait(isolates.map((i) { - var rcv = new ReceivePort(); - i.addOnExitListener(rcv.sendPort); - return rcv.first; - })); - //start(0); -} - -void start(int id) { var app = new Angel(); var http = new AngelHttp.custom(app, startShared, useZone: false); @@ -37,8 +18,6 @@ void start(int id) { return oldHandler(e, req, res); }; - http.startServer('127.0.0.1', 3000).then((server) { - print( - 'Instance #$id listening at http://${server.address.address}:${server.port}'); - }); + await http.startServer('127.0.0.1', 3000); + print('Listening at ${http.uri}'); } diff --git a/performance/hello/raw.dart b/performance/hello/raw.dart index 956056b2..423ba843 100644 --- a/performance/hello/raw.dart +++ b/performance/hello/raw.dart @@ -2,18 +2,10 @@ library performance.hello; import 'dart:io'; -import 'dart:isolate'; main() { - for (int i = 0; i < Platform.numberOfProcessors - 1; i++) - Isolate.spawn(start, i + 1); - start(0); -} - -void start(int id) { - HttpServer.bind('127.0.0.1', 3000, shared: true).then((server) { - print( - 'Instance #$id listening at http://${server.address.address}:${server.port}'); + return HttpServer.bind('127.0.0.1', 3000, shared: true).then((server) { + print('Listening at http://${server.address.address}:${server.port}'); server.listen((request) { if (request.uri.path == '/') { diff --git a/pubspec.yaml b/pubspec.yaml index ee352e0f..f1529a91 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,11 +10,11 @@ dependencies: angel_http_exception: ^1.0.0 angel_model: ^1.0.0 angel_route: ^3.0.0 - body_parser: ^1.0.0 charcode: ^1.0.0 combinator: ^1.0.0 file: ^5.0.0 http_parser: ^3.0.0 + http_server: ^0.9.0 http2: ">=0.1.7 <2.0.0" logging: ">=0.11.3 <1.0.0" matcher: ^0.12.0