From bf0f01a5b905b9377eee733356548f1d90e7108b Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 10 Nov 2018 20:07:09 -0500 Subject: [PATCH] 2.0.0-alpha.11 --- dev.key | 29 ++++ dev.pem | 57 ++++++ example/http2/main.dart | 6 +- example/main.dart | 2 +- lib/src/core/driver.dart | 48 +++-- lib/src/core/request_context.dart | 12 +- lib/src/core/response_context.dart | 30 +++- lib/src/http/angel_http.dart | 14 +- lib/src/http/http_request_context.dart | 9 - lib/src/http/http_response_context.dart | 10 ++ lib/src/http2/angel_http2.dart | 128 +++++++++++--- lib/src/http2/http2_request_context.dart | 31 ++-- lib/src/http2/http2_response_context.dart | 202 ++++++++++++---------- pubspec.yaml | 9 +- test/accepts_test.dart | 2 +- test/controller_test.dart | 6 +- test/encoders_buffer_test.dart | 2 +- test/extension_test.dart | 2 +- test/http2/adapter_test.dart | 28 ++- test/primitives_test.dart | 4 +- test/routing_test.dart | 6 +- test/server_test.dart | 10 +- tool/travis.sh | 4 +- 23 files changed, 418 insertions(+), 233 deletions(-) create mode 100644 dev.key create mode 100644 dev.pem diff --git a/dev.key b/dev.key new file mode 100644 index 00000000..5d49ae7e --- /dev/null +++ b/dev.key @@ -0,0 +1,29 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP +xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE +ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5 +Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1 +qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc +gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU +0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF +gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS +oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn +oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ +kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh +zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa +J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe +d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX +TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76 +ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW +HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN +goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im +EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j +ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS +YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3 +q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT +Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z +Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH +QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE +xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w +AUukhVtTNn4= +-----END ENCRYPTED PRIVATE KEY----- \ No newline at end of file diff --git a/dev.pem b/dev.pem new file mode 100644 index 00000000..01756b25 --- /dev/null +++ b/dev.pem @@ -0,0 +1,57 @@ +-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV +BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa +MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq +Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu +EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki +we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb +N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI +7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg +hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O +BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS +YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd +AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4 +CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM +4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG +MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5 +V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV +BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw +WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx +EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP +DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE +YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu +MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7 +B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd +IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb +oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC +cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8 +x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ +e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX +NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4 +0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh +FKvRDxsW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV +BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw +WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv +dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw +siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj +kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2 +hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV +DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU +ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD +26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ +lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X +J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/ +uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE +4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k +t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W +r6AL284qtw== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/example/http2/main.dart b/example/http2/main.dart index cb37957a..04a3ed98 100644 --- a/example/http2/main.dart +++ b/example/http2/main.dart @@ -13,7 +13,7 @@ main() async { }); app.logger = new Logger('angel')..onRecord.listen(prettyLog); - app.get('/', (_, __) => 'Hello HTTP/2!!!'); + app.get('/', (req, res) => 'Hello HTTP/2!!!'); app.fallback((req, res) => throw new AngelHttpException.notFound( message: 'No file exists at ${req.uri.path}')); @@ -38,6 +38,6 @@ main() async { // HTTP/1.x requests will fallback to `AngelHttp` http2.onHttp1.listen(http1.handleRequest); - var server = await http2.startServer('127.0.0.1', 3000); - print('Listening at https://${server.address.address}:${server.port}'); + await http2.startServer('127.0.0.1', 3000); + print('Listening at ${http2.uri}'); } diff --git a/example/main.dart b/example/main.dart index 8beb3aea..6d0934eb 100644 --- a/example/main.dart +++ b/example/main.dart @@ -18,7 +18,7 @@ main() async { }); // Index route. Returns JSON. - app.get('/', (req, res) => res.write('Welcome to Angel!')); + app.get('/', (req, res) => 'Welcome to Angel!'); // Accepts a URL like /greet/foo or /greet/bob. app.get( diff --git a/lib/src/core/driver.dart b/lib/src/core/driver.dart index 2f2acca2..1bb9d50b 100644 --- a/lib/src/core/driver.dart +++ b/lib/src/core/driver.dart @@ -34,15 +34,22 @@ abstract class Driver< /// The native server running this instance. Server get server => _server; + Future generateServer(address, int port) => + serverGenerator(address, port); + /// Starts, and returns the server. Future startServer([address, int port]) { var host = address ?? '127.0.0.1'; - return serverGenerator(host, port ?? 0).then((server) { + return generateServer(host, port ?? 0).then((server) { _server = server; return Future.wait(app.startupHooks.map(app.configure)).then((_) { app.optimizeForProduction(); - _sub = server.listen((request) => - handleRawRequest(request, createResponseFromRawRequest(request))); + _sub = server.listen((request) { + var stream = createResponseStreamFromRawRequest(request); + stream.listen((response) { + return handleRawRequest(request, response); + }); + }); return _server; }); }); @@ -78,11 +85,9 @@ abstract class Driver< void writeToResponse(Response response, List data); - Uri getUriFromRequest(Request request); - Future closeResponse(Response response); - Response createResponseFromRawRequest(Request request); + Stream createResponseStreamFromRawRequest(Request request); /// Handles a single request. Future handleRawRequest(Request request, Response response) { @@ -123,26 +128,17 @@ abstract class Driver< } var pipeline = tuple.item1; + var it = pipeline.iterator; - Future Function() runPipeline; - - for (var handler in pipeline) { - if (handler == null) break; - - if (runPipeline == null) - runPipeline = () => - Future.sync(() => app.executeHandler(handler, req, res)); - else { - var current = runPipeline; - runPipeline = () => current().then((result) => !res.isOpen - ? new Future.value(result) - : app.executeHandler(handler, req, res)); - } - } + var runPipeline = pipeline.isEmpty + ? null + : Future.doWhile(() => !it.moveNext() + ? new Future.value(false) + : app.executeHandler(it.current, req, res)); return runPipeline == null ? sendResponse(request, response, req, res) - : runPipeline() + : runPipeline .then((_) => sendResponse(request, response, req, res)); } @@ -211,7 +207,6 @@ abstract class Driver< e, trace, req, res, request, response); }).catchError((e, StackTrace st) { var trace = new Trace.from(st ?? StackTrace.current).terse; - var uri = getUriFromRequest(request); closeResponse(response); // Ideally, we won't be in a position where an absolutely fatal error occurs, // but if so, we'll need to log it. @@ -221,7 +216,7 @@ abstract class Driver< } else { stderr ..writeln('Fatal error occurred when processing ' - '$uri:') + '${req.uri}:') ..writeln(e) ..writeln(trace); } @@ -299,11 +294,10 @@ abstract class Driver< Future finalizers = ignoreFinalizers == true ? new Future.value() - : app.responseFinalizers.fold( - new Future.value(), (out, f) => out.then((_) => f(req, res))); + : Future.forEach(app.responseFinalizers, (f) => f(req, res)); return finalizers.then((_) { - if (res.isOpen) res.close(); + //if (res.isOpen) res.close(); for (var key in res.headers.keys) { setHeader(response, key, res.headers[key]); diff --git a/lib/src/core/request_context.dart b/lib/src/core/request_context.dart index 6f030f2a..b0e1fac1 100644 --- a/lib/src/core/request_context.dart +++ b/lib/src/core/request_context.dart @@ -59,7 +59,8 @@ abstract class RequestContext { String get originalMethod; /// The content type of an incoming request. - MediaType get contentType; + MediaType get contentType => + new MediaType.parse(headers.contentType.toString()); /// The URL parameters extracted from the request URI. Map params = {}; @@ -67,6 +68,12 @@ abstract class RequestContext { /// The requested path. String get path; + /// Is this an **XMLHttpRequest**? + bool get isXhr { + return headers.value("X-Requested-With")?.trim()?.toLowerCase() == + 'xmlhttprequest'; + } + /// The remote address requesting this resource. InternetAddress get remoteAddress; @@ -76,9 +83,6 @@ abstract class RequestContext { /// The [Uri] instance representing the path this request is responding to. Uri get uri; - /// Is this an **XMLHttpRequest**? - bool get xhr; - /// Returns the file extension of the requested path, if any. /// /// Includes the leading `.`, if there is one. diff --git a/lib/src/core/response_context.dart b/lib/src/core/response_context.dart index a76aaccf..8ad806f3 100644 --- a/lib/src/core/response_context.dart +++ b/lib/src/core/response_context.dart @@ -20,7 +20,10 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); abstract class ResponseContext implements StreamSink>, StringSink { final Map properties = {}; - final Map _headers = {'server': 'angel'}; + final Map _headers = new CaseInsensitiveMap.from({ + 'content-type': 'text/plain', + 'server': 'angel', + }); Completer _done; int _statusCode = 200; @@ -101,7 +104,18 @@ abstract class ResponseContext FutureOr detach(); /// Gets or sets the content type to send back to a client. - MediaType contentType = new MediaType('text', 'plain'); + MediaType get contentType { + try { + return new MediaType.parse(headers['content-type']); + } catch (_) { + return new MediaType('text', 'plain'); + } + } + + /// Gets or sets the content type to send back to a client. + void set contentType(MediaType value) { + headers['content-type'] = value.toString(); + } static StateError closed() => new StateError('Cannot modify a closed response.'); @@ -124,8 +138,6 @@ abstract class ResponseContext } /// Prevents more data from being written to the response, and locks it entire from further editing. - /// - /// This method should be overwritten, setting [streaming] to `false`, **after** a `super` call. Future close() { if (buffer is LockableBytesBuilder) { (buffer as LockableBytesBuilder).lock(); @@ -145,21 +157,21 @@ abstract class ResponseContext /// You can override the [contentType] sent; by default it is `application/javascript`. void jsonp(value, {String callbackName: "callback", MediaType contentType}) { if (!isOpen) throw closed(); - write("$callbackName(${serializer(value)})"); this.contentType = contentType ?? new MediaType('application', 'javascript'); + write("$callbackName(${serializer(value)})"); close(); } /// Renders a view to the response stream, and closes the response. Future render(String view, [Map data]) { if (!isOpen) throw closed(); + contentType = new MediaType('text', 'html'); return Future.sync(() => app.viewGenerator( view, new Map.from(renderParams) ..addAll(data ?? {}))).then((content) { write(content); - contentType = new MediaType('text', 'html'); close(); }); } @@ -296,8 +308,6 @@ abstract class ResponseContext /// Adds a stream directly the underlying response. /// - /// This will also set [willCloseItself] to `true`, thus canceling out response finalizers. - /// /// If this instance has access to a [correspondingRequest], then it will attempt to transform /// the content using at most one of the response [encoders]. @override @@ -305,7 +315,9 @@ abstract class ResponseContext @override void addError(Object error, [StackTrace stackTrace]) { - if (_done?.isCompleted == false) _done.completeError(error, stackTrace); + if (_done?.isCompleted == false) + _done.completeError(error, stackTrace); + else if (_done == null) Zone.current.handleUncaughtError(error, stackTrace); } /// Writes data to the response. diff --git a/lib/src/http/angel_http.dart b/lib/src/http/angel_http.dart index 678356cd..cb6ff71a 100644 --- a/lib/src/http/angel_http.dart +++ b/lib/src/http/angel_http.dart @@ -73,6 +73,12 @@ class AngelHttp extends Driver cookies) => response.cookies.addAll(cookies); + @override + Future close() async { + await server?.close(); + return await super.close(); + } + @override Future closeResponse(HttpResponse response) => response.close(); @@ -95,11 +101,9 @@ class AngelHttp extends Driver - request.response; - - @override - Uri getUriFromRequest(HttpRequest request) => request.uri; + Stream createResponseStreamFromRawRequest( + HttpRequest request) => + new Stream.fromIterable([request.response]); @override void setChunkedEncoding(HttpResponse response, bool value) => diff --git a/lib/src/http/http_request_context.dart b/lib/src/http/http_request_context.dart index c9f210f2..e8d21c0d 100644 --- a/lib/src/http/http_request_context.dart +++ b/lib/src/http/http_request_context.dart @@ -70,15 +70,6 @@ class HttpRequestContext extends RequestContext { return rawRequest.uri; } - @override - bool get xhr { - return rawRequest.headers - .value("X-Requested-With") - ?.trim() - ?.toLowerCase() == - 'xmlhttprequest'; - } - /// Magically transforms an [HttpRequest] into a [RequestContext]. static Future from( HttpRequest request, Angel app, String path) { diff --git a/lib/src/http/http_response_context.dart b/lib/src/http/http_response_context.dart index 4438282f..1f48c686 100644 --- a/lib/src/http/http_response_context.dart +++ b/lib/src/http/http_response_context.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:http_parser/http_parser.dart'; + import '../core/core.dart'; import 'http_request_context.dart'; @@ -68,6 +70,14 @@ class HttpResponseContext extends ResponseContext { }); } + @override + void set contentType(MediaType value) { + super.contentType = value; + if (!_streamInitialized) + rawResponse.headers.contentType = + ContentType(value.type, value.subtype, parameters: value.parameters); + } + bool _openStream() { if (!_streamInitialized) { // If this is the first stream added to this response, diff --git a/lib/src/http2/angel_http2.dart b/lib/src/http2/angel_http2.dart index e96c1f62..3a90e58f 100644 --- a/lib/src/http2/angel_http2.dart +++ b/lib/src/http2/angel_http2.dart @@ -2,34 +2,32 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:angel_framework/angel_framework.dart' hide Header; -import 'package:combinator/combinator.dart'; -import 'package:http2/src/artificial_server_socket.dart'; import 'package:http2/transport.dart'; import 'package:mock_request/mock_request.dart'; import 'http2_request_context.dart'; import 'http2_response_context.dart'; -import 'package:pool/pool.dart'; import 'package:uuid/uuid.dart'; -import 'package:tuple/tuple.dart'; class AngelHttp2 extends Driver { + SecureServerSocket, Http2RequestContext, Http2ResponseContext> { final ServerSettings settings; final StreamController _onHttp1 = new StreamController(); final Map _sessions = {}; final Uuid _uuid = new Uuid(); - ArtificialServerSocket _artificial; - HttpServer _httpServer; - StreamController _http1; - SecureServerSocket _socket; - StreamSubscription _sub; + _AngelHttp2ServerSocket _artificial; + + SecureServerSocket get socket => _artificial; AngelHttp2._( Angel app, - Future Function(dynamic, int) serverGenerator, + Future Function(dynamic, int) serverGenerator, bool useZone, this.settings) - : super(app, serverGenerator, useZone: useZone); + : super( + app, + serverGenerator, + useZone: useZone, + ); factory AngelHttp2(Angel app, SecurityContext securityContext, {bool useZone: true, ServerSettings settings}) { @@ -48,14 +46,25 @@ class AngelHttp2 extends Driver ArtificialServerSocket(addr, port, s)); + return SecureServerSocket.bind(addr, port, ctx); }, useZone, settings); } /// Fires when an HTTP/1.x request is received. Stream get onHttp1 => _onHttp1.stream; + @override + Future generateServer([address, int port]) async { + var s = await serverGenerator(address ?? '127.0.0.1', port ?? 0); + return _artificial = new _AngelHttp2ServerSocket(s, this); + } + + @override + Future close() async { + await _artificial.close(); + return await super.close(); + } + @override void addCookies(ServerTransportStream response, Iterable cookies) { var headers = cookies @@ -84,14 +93,11 @@ class AngelHttp2 extends Driver createResponseStreamFromRawRequest( + Socket request) { var connection = new ServerTransportConnection.viaSocket(request, settings: settings); - } - - @override - Uri getUriFromRequest(Socket request) { - // TODO: implement getUriFromRequest + return connection.incomingStreams; } @override @@ -130,3 +136,85 @@ class AngelHttp2 extends Driver implements ServerSocket { + final _AngelHttp2ServerSocket angel; + final _ctrl = new StreamController(); + + _FakeServerSocket(this.angel); + + @override + InternetAddress get address => angel.address; + + @override + Future close() async { + _ctrl.close(); + return this; + } + + @override + int get port => angel.port; + + @override + StreamSubscription listen(void Function(Socket event) onData, + {Function onError, void Function() onDone, bool cancelOnError}) { + return _ctrl.stream.listen(onData, + cancelOnError: cancelOnError, onError: onError, onDone: onDone); + } +} + +class _AngelHttp2ServerSocket extends Stream + implements SecureServerSocket { + final SecureServerSocket socket; + final AngelHttp2 driver; + final _ctrl = new StreamController(); + _FakeServerSocket _fake; + StreamSubscription _sub; + + _AngelHttp2ServerSocket(this.socket, this.driver) { + _fake = new _FakeServerSocket(this); + new HttpServer.listenOn(_fake).pipe(driver._onHttp1); + _sub = socket.listen( + (socket) { + if (socket.selectedProtocol == null || + socket.selectedProtocol == 'http/1.0' || + socket.selectedProtocol == 'http/1.1') { + _fake._ctrl.add(socket); + } else if (socket.selectedProtocol == 'h2' || + socket.selectedProtocol == 'h2-14') { + _ctrl.add(socket); + } else { + socket.destroy(); + throw new Exception( + 'AngelHttp2 does not support ${socket.selectedProtocol} as an ALPN protocol.'); + } + }, + onDone: _ctrl.close, + onError: (e, st) { + driver.app.logger.warning( + 'HTTP/2 incoming connection failure: ', e, st as StackTrace); + }, + ); + } + + InternetAddress get address => socket.address; + + int get port => socket.port; + + Future close() async { + _sub?.cancel(); + _fake.close(); + _ctrl.close(); + return await socket.close(); + } + + @override + StreamSubscription listen( + void Function(SecureSocket event) onData, + {Function onError, + void Function() onDone, + bool cancelOnError}) { + return _ctrl.stream.listen(onData, + cancelOnError: cancelOnError, onError: onError, onDone: onDone); + } +} diff --git a/lib/src/http2/http2_request_context.dart b/lib/src/http2/http2_request_context.dart index f207067b..f15b247e 100644 --- a/lib/src/http2/http2_request_context.dart +++ b/lib/src/http2/http2_request_context.dart @@ -1,6 +1,7 @@ import 'dart:async'; 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'; @@ -11,9 +12,9 @@ import 'package:uuid/uuid.dart'; final RegExp _comma = new RegExp(r',\s*'); final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); -class Http2RequestContext extends RequestContext { +class Http2RequestContext extends RequestContext { + final Container container; BytesBuilder _buf; - ContentType _contentType; List _cookies; HttpHeaders _headers; String _method, _override, _path; @@ -22,13 +23,15 @@ class Http2RequestContext extends RequestContext { ServerTransportStream _stream; Uri _uri; + Http2RequestContext._(this.container); + static Future from( ServerTransportStream stream, Socket socket, Angel app, Map sessions, Uuid uuid) async { - var req = new Http2RequestContext() + var req = new Http2RequestContext._(app.container.createChild()) ..app = app .._socket = socket .._stream = stream; @@ -93,7 +96,7 @@ class Http2RequestContext extends RequestContext { cookies.firstWhere((c) => c.name == 'DARTSESSID', orElse: () => null); if (dartSessId == null) { - dartSessId = new Cookie('DARTSESSID', uuid.v4()); + dartSessId = new Cookie('DARTSESSID', uuid.v4() as String); } req._session = sessions.putIfAbsent( @@ -110,12 +113,6 @@ class Http2RequestContext extends RequestContext { /// The underlying HTTP/2 [ServerTransportStream]. ServerTransportStream get stream => _stream; - @override - bool get xhr { - return headers.value("X-Requested-With")?.trim()?.toLowerCase() == - 'xmlhttprequest'; - } - @override Uri get uri => _uri; @@ -132,12 +129,6 @@ class Http2RequestContext extends RequestContext { return _path; } - @override - ContentType get contentType => - _contentType ??= (headers['content-type'] == null - ? null - : ContentType.parse(headers.value('content-type'))); - @override String get originalMethod { return _method; @@ -148,9 +139,6 @@ class Http2RequestContext extends RequestContext { return _override ?? _method; } - @override - HttpRequest get io => null; - @override String get hostname => _headers.value('host'); @@ -168,7 +156,10 @@ class Http2RequestContext extends RequestContext { new Stream.fromIterable([_buf.takeBytes()]), contentType == null ? null : new MediaType.parse(contentType.toString()), uri, - storeOriginalBuffer: app.storeOriginalBuffer, + storeOriginalBuffer: app.keepRawRequestBuffers == true, ); } + + @override + ServerTransportStream get rawRequest => _stream; } diff --git a/lib/src/http2/http2_response_context.dart b/lib/src/http2/http2_response_context.dart index 05cf07b5..0e1224db 100644 --- a/lib/src/http2/http2_response_context.dart +++ b/lib/src/http2/http2_response_context.dart @@ -5,11 +5,21 @@ import 'package:angel_framework/angel_framework.dart' hide Header; import 'package:http2/transport.dart'; import 'http2_request_context.dart'; -class Http2ResponseContext extends ResponseContext { +class Http2ResponseContext extends ResponseContext { final Angel app; final ServerTransportStream stream; + + ServerTransportStream get rawResponse => stream; + + LockableBytesBuilder _buffer; + final Http2RequestContext _req; - bool _useStream = false, _isClosed = false, _isPush = false; + + bool _isDetached = false, + _isClosed = false, + _streamInitialized = false, + _isPush = false; + Uri _targetUri; Http2ResponseContext(this.app, this.stream, this._req) { @@ -26,43 +36,62 @@ class Http2ResponseContext extends ResponseContext { /// Returns a [List] of all resources that have [push]ed to the client. List get pushes => new List.unmodifiable(_pushes); + @override + ServerTransportStream detach() { + _isDetached = true; + return stream; + } + @override RequestContext get correspondingRequest => _req; Uri get targetUri => _targetUri; @override - HttpResponse get io => null; + bool get isOpen { + return !_isClosed && !_isDetached; + } @override - bool get streaming => _useStream; + bool get isBuffered => _buffer != null; @override - bool get isOpen => !_isClosed; + BytesBuilder get buffer => _buffer; + + @override + void addError(Object error, [StackTrace stackTrace]) { + super.addError(error, stackTrace); + } + + @override + void useBuffer() { + _buffer = new LockableBytesBuilder(); + } /// Write headers, status, etc. to the underlying [stream]. - void finalize() { - if (_isPush) return; + bool _openStream() { + if (_isPush || _streamInitialized) return false; var headers =
[ new Header.ascii(':status', statusCode.toString()), ]; if (encoders.isNotEmpty && correspondingRequest != null) { - var allowedEncodings = - (correspondingRequest.headers['accept-encoding'] ?? []).map((str) { - // Ignore quality specifications in accept-encoding - // ex. gzip;q=0.8 - if (!str.contains(';')) return str; - return str.split(';')[0]; - }); + if (_allowedEncodings != null) { + for (var encodingName in _allowedEncodings) { + Converter, List> encoder; + String key = encodingName; - for (var encodingName in allowedEncodings) { - String key = encodingName; + if (encoders.containsKey(encodingName)) + encoder = encoders[encodingName]; + else if (encodingName == '*') { + encoder = encoders[key = encoders.keys.first]; + } - if (encoders.containsKey(encodingName)) { - this.headers['content-encoding'] = key; - break; + if (encoder != null) { + this.headers['content-encoding'] = key; + break; + } } } } @@ -81,75 +110,48 @@ class Http2ResponseContext extends ResponseContext { } stream.sendHeaders(headers); + return _streamInitialized = true; } - @override - void addError(Object error, [StackTrace stackTrace]) { - Zone.current.handleUncaughtError(error, stackTrace); - super.addError(error, stackTrace); - } + Iterable __allowedEncodings; - @override - bool useStream() { - if (!_useStream) { - // If this is the first stream added to this response, - // then add headers, status code, etc. - finalize(); - - willCloseItself = _useStream = _isClosed = true; - releaseCorrespondingRequest(); - return true; - } - - return false; - } - - @override - void end() { - _isClosed = true; - super.end(); + Iterable get _allowedEncodings { + return __allowedEncodings ??= correspondingRequest.headers + .value('accept-encoding') + ?.split(',') + ?.map((s) => s.trim()) + ?.where((s) => s.isNotEmpty) + ?.map((str) { + // Ignore quality specifications in accept-encoding + // ex. gzip;q=0.8 + if (!str.contains(';')) return str; + return str.split(';')[0]; + }); } @override Future addStream(Stream> stream) { - if (_isClosed && !_useStream) throw ResponseContext.closed(); - var firstStream = useStream(); + if (!isOpen && isBuffered) throw ResponseContext.closed(); + _openStream(); Stream> output = stream; - if ((firstStream || !headers.containsKey('content-encoding')) && - encoders.isNotEmpty && - correspondingRequest != null) { - var allowedEncodings = - (correspondingRequest.headers['accept-encoding'] ?? []).map((str) { - // Ignore quality specifications in accept-encoding - // ex. gzip;q=0.8 - if (!str.contains(';')) return str; - return str.split(';')[0]; - }); + if (encoders.isNotEmpty && correspondingRequest != null) { + if (_allowedEncodings != null) { + for (var encodingName in _allowedEncodings) { + Converter, List> encoder; + String key = encodingName; - for (var encodingName in allowedEncodings) { - Converter, List> encoder; - String key = encodingName; - - if (encoders.containsKey(encodingName)) - encoder = encoders[encodingName]; - else if (encodingName == '*') { - encoder = encoders[key = encoders.keys.first]; - } - - if (encoder != null) { - /* - if (firstStream) { - this.stream.sendHeaders([ - new Header.ascii( - 'content-encoding', headers['content-encoding'] = key) - ]); + if (encoders.containsKey(encodingName)) + encoder = encoders[encodingName]; + else if (encodingName == '*') { + encoder = encoders[key = encoders.keys.first]; } - */ - output = encoders[key].bind(output); - break; + if (encoder != null) { + output = encoders[key].bind(output); + break; + } } } } @@ -159,38 +161,50 @@ class Http2ResponseContext extends ResponseContext { @override void add(List data) { - if (_isClosed && !_useStream) + if (!isOpen && isBuffered) throw ResponseContext.closed(); - else if (_useStream) - //stream.sendData(data); - addStream(new Stream.fromIterable([data])); - else + else if (!isBuffered) { + _openStream(); + + if (encoders.isNotEmpty && correspondingRequest != null) { + if (_allowedEncodings != null) { + for (var encodingName in _allowedEncodings) { + Converter, List> encoder; + String key = encodingName; + + if (encoders.containsKey(encodingName)) + encoder = encoders[encodingName]; + else if (encodingName == '*') { + encoder = encoders[key = encoders.keys.first]; + } + + if (encoder != null) { + data = encoders[key].convert(data); + break; + } + } + } + } + + stream.sendData(data); + } else buffer.add(data); } @override Future close() async { - if (_useStream) { - try { - await stream.outgoingMessages.close(); - } catch (_) { - // This only seems to occur on `MockHttpRequest`, but - // this try/catch prevents a crash. - } + if (!_isDetached && !_isClosed && !isBuffered) { + _openStream(); + await stream.outgoingMessages.close(); } _isClosed = true; await super.close(); - _useStream = false; } /// Pushes a resource to the client. Http2ResponseContext push(String path, {Map headers: const {}, String method: 'GET'}) { - if (isOpen) - throw new StateError( - 'You can only push resources after the main response context is closed. You will need to use streaming methods, i.e. `addStream`.'); - var targetUri = _req.uri.replace(path: path); var h =
[ @@ -211,8 +225,4 @@ class Http2ResponseContext extends ResponseContext { _pushes.add(r); return r; } - - void internalReopen() { - _isClosed = false; - } } diff --git a/pubspec.yaml b/pubspec.yaml index a9b080fb..6259aead 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,6 @@ name: angel_framework -version: 2.0.0-alpha.10 -description: > - A high-powered HTTP server with DI, routing and more. - When combined with the other packages in the Angel ecosystem, this - package can be used to make robust application servers for API's, - traditional server-side applications, and much more, along with - a rich client API. +version: 2.0.0-alpha.11 +description: A high-powered HTTP server with dependency injection, routing and much more. author: Tobe O homepage: https://github.com/angel-dart/angel_framework environment: diff --git a/test/accepts_test.dart b/test/accepts_test.dart index 7503f816..72cd4aae 100644 --- a/test/accepts_test.dart +++ b/test/accepts_test.dart @@ -64,5 +64,5 @@ Future acceptContentTypes( rq.close(); var app = new Angel(reflector: MirrorsReflector()); var http = new AngelHttp(app); - return http.createRequestContext(rq); + return http.createRequestContext(rq, rq.response); } diff --git a/test/controller_test.dart b/test/controller_test.dart index 1f1a42ba..72277a20 100644 --- a/test/controller_test.dart +++ b/test/controller_test.dart @@ -38,12 +38,14 @@ class NamedController extends Controller { optional() => 2; } -void foo(RequestContext req, ResponseContext res) { +bool foo(RequestContext req, ResponseContext res) { res.write("Hello, "); + return true; } -void bar(RequestContext req, ResponseContext res) { +bool bar(RequestContext req, ResponseContext res) { res.write("world!"); + return true; } main() { diff --git a/test/encoders_buffer_test.dart b/test/encoders_buffer_test.dart index 0cc0ca09..09fa887d 100644 --- a/test/encoders_buffer_test.dart +++ b/test/encoders_buffer_test.dart @@ -67,7 +67,7 @@ void encodingTests(Angel getApp()) { await http.handleRequest(rq); var body = await getBody(rs); - print(rs.headers); + //print(rs.headers); expect(rs.headers.value('content-encoding'), 'deflate'); expect(body, zlib.encode(utf8.encode('Hello, world!'))); }); diff --git a/test/extension_test.dart b/test/extension_test.dart index 23f05d77..0ef019a7 100644 --- a/test/extension_test.dart +++ b/test/extension_test.dart @@ -28,5 +28,5 @@ Future makeRequest(String path) { var rq = new MockHttpRequest('GET', ENDPOINT.replace(path: path))..close(); var app = new Angel(reflector: MirrorsReflector()); var http = new AngelHttp(app); - return http.createRequestContext(rq); + return http.createRequestContext(rq, rq.response); } diff --git a/test/http2/adapter_test.dart b/test/http2/adapter_test.dart index 1543516a..547a7878 100644 --- a/test/http2/adapter_test.dart +++ b/test/http2/adapter_test.dart @@ -29,10 +29,9 @@ void main() { ..keepRawRequestBuffers = true ..encoders['gzip'] = gzip.encoder; - app.get('/', (req, res) { - res - ..write('Hello world') - ..close(); + app.get('/', (req, res) async { + res.write('Hello world'); + await res.close(); }); app.all('/method', (req, res) => req.method); @@ -41,16 +40,14 @@ void main() { app.get('/stream', (req, res) => jfkStream().pipe(res)); - app.get('/headers', (req, res) { - res - ..headers.addAll({'foo': 'bar', 'x-angel': 'http2'}) - ..close(); + app.get('/headers', (req, res) async { + res.headers.addAll({'foo': 'bar', 'x-angel': 'http2'}); + await res.close(); }); - app.get('/status', (req, res) { - res - ..statusCode = 1337 - ..close(); + app.get('/status', (req, res) async { + res.statusCode = 1337; + await res.close(); }); app.post('/body', (req, res) => req.parseBody()); @@ -63,9 +60,7 @@ void main() { }); app.get('/push', (req, res) async { - res - ..write('ok') - ..close(); + res.write('ok'); if (res is Http2ResponseContext && res.canPush) { res.push('a') @@ -76,6 +71,8 @@ void main() { ..write('b') ..close(); } + + await res.close(); }); var ctx = new SecurityContext() @@ -211,7 +208,6 @@ void main() { expect(getPath(pushA), '/a'); expect(getPath(pushB), '/b'); - // TODO: Dart http/2 client seems to not be able to get body // However, Chrome, Firefox, Edge all can //expect(await getBody(pushA.stream), 'a'); //expect(await getBody(pushB.stream), 'b'); diff --git a/test/primitives_test.dart b/test/primitives_test.dart index c91c19b9..200fd8eb 100644 --- a/test/primitives_test.dart +++ b/test/primitives_test.dart @@ -62,8 +62,8 @@ main() { try { var rq = new MockHttpRequest('GET', Uri.parse('/num/unparsed/32')) ..close(); - var req = await http.createRequestContext(rq); - var res = await http.createResponseContext(rq.response, req); + var req = await http.createRequestContext(rq, rq.response); + var res = await http.createResponseContext(rq, rq.response, req); await app.runContained((num unparsed) => unparsed, req, res); throw new StateError( 'ArgumentError should be thrown if a parameter cannot be resolved.'); diff --git a/test/routing_test.dart b/test/routing_test.dart index c81cfca7..e283d708 100644 --- a/test/routing_test.dart +++ b/test/routing_test.dart @@ -29,8 +29,9 @@ void interceptor(RequestContext req, ResponseContext res) { ..close(); } -void interceptService(RequestContext req, ResponseContext res) { +bool interceptService(RequestContext req, ResponseContext res) { res.write("Service with "); + return true; } main() { @@ -96,6 +97,7 @@ main() { RequestHandler write(String message) { return (req, res) { res.write(message); + return true; }; } @@ -104,7 +106,7 @@ main() { app.fallback((req, res) => 'MJ'); - app.dumpTree(header: "DUMPING ROUTES:", showMatchers: true); + //app.dumpTree(header: "DUMPING ROUTES:", showMatchers: true); client = new http.Client(); var server = await new AngelHttp(app).startServer('127.0.0.1', 0); diff --git a/test/server_test.dart b/test/server_test.dart index e0717bf0..079cf1e5 100644 --- a/test/server_test.dart +++ b/test/server_test.dart @@ -44,12 +44,12 @@ main() { var rq = new MockHttpRequest('GET', $foo); rq.close(); var rs = rq.response; - var req = await http.createRequestContext(rq); - var res = await http.createResponseContext(rs); + var req = await http.createRequestContext(rq, rs); + var res = await http.createResponseContext(rq, rs); var e = new AngelHttpException(null, statusCode: 321, message: 'Hello', errors: ['foo', 'bar']); await app.errorHandler(e, req, res); - await http.sendResponse(rq, req, res); + await http.sendResponse(rq, rs, req, res); expect( ContentType.parse(rs.headers.value('content-type')).mimeType, 'text/html', @@ -120,8 +120,8 @@ main() { setUp(() async { var rq = new MockHttpRequest('GET', $foo)..close(); - req = await http.createRequestContext(rq); - res = await http.createResponseContext(rq.response); + req = await http.createRequestContext(rq, rq.response); + res = await http.createResponseContext(rq, rq.response); }); group('getHandlerResult', () { diff --git a/tool/travis.sh b/tool/travis.sh index bac6b671..ac63f578 100644 --- a/tool/travis.sh +++ b/tool/travis.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -e -pub run test -ANGEL_ENV=production pub run test \ No newline at end of file +pub run test --timeout 5s +ANGEL_ENV=production pub run test --timeout 5s \ No newline at end of file