platform/lib/src/http/angel_http.dart

370 lines
12 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:convert';
2018-06-08 07:06:26 +00:00
import 'dart:io'
show
stderr,
HttpRequest,
HttpResponse,
HttpServer,
Platform,
SecurityContext;
import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:angel_route/angel_route.dart';
import 'package:combinator/combinator.dart';
2018-06-08 07:06:26 +00:00
import 'package:dart2_constant/io.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:pool/pool.dart';
2018-04-06 19:43:10 +00:00
import 'package:stack_trace/stack_trace.dart';
import 'package:tuple/tuple.dart';
2018-02-07 04:59:05 +00:00
import 'http_request_context.dart';
import 'http_response_context.dart';
2018-02-07 05:44:21 +00:00
import '../core/core.dart';
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
2018-02-07 03:32:31 +00:00
/// Adapts `dart:io`'s [HttpServer] to serve Angel.
class AngelHttp {
final Angel app;
2018-06-08 07:06:26 +00:00
final bool useZone;
bool _closed = false;
HttpServer _server;
2018-02-07 04:17:40 +00:00
Future<HttpServer> Function(dynamic, int) _serverGenerator = HttpServer.bind;
StreamSubscription<HttpRequest> _sub;
Pool _pool;
2018-06-08 07:06:26 +00:00
AngelHttp(this.app, {this.useZone: true});
2018-02-07 04:17:40 +00:00
/// The function used to bind this instance to an HTTP server.
2018-04-06 19:43:10 +00:00
Future<HttpServer> Function(dynamic, int) get serverGenerator =>
_serverGenerator;
2018-02-07 04:17:40 +00:00
/// An instance mounted on a server started by the [serverGenerator].
2018-02-07 04:17:40 +00:00
factory AngelHttp.custom(
2018-06-08 07:06:26 +00:00
Angel app, Future<HttpServer> Function(dynamic, int) serverGenerator,
{bool useZone: true}) {
return new AngelHttp(app, useZone: useZone)
.._serverGenerator = serverGenerator;
}
2018-06-08 07:06:26 +00:00
factory AngelHttp.fromSecurityContext(Angel app, SecurityContext context,
{bool useZone: true}) {
var http = new AngelHttp(app, useZone: useZone);
2018-06-08 07:06:26 +00:00
http._serverGenerator = (address, int port) {
return HttpServer.bindSecure(address, port, context);
};
return http;
}
/// Creates an HTTPS server.
///
/// Provide paths to a certificate chain and server key (both .pem).
/// If no password is provided, a random one will be generated upon running
/// the server.
2018-02-07 04:17:40 +00:00
factory AngelHttp.secure(
Angel app, String certificateChainPath, String serverKeyPath,
2018-06-08 07:06:26 +00:00
{bool debug: false, String password, bool useZone: true}) {
var certificateChain =
2018-02-07 04:17:40 +00:00
Platform.script.resolve(certificateChainPath).toFilePath();
var serverKey = Platform.script.resolve(serverKeyPath).toFilePath();
var serverContext = new SecurityContext();
serverContext.useCertificateChain(certificateChain, password: password);
serverContext.usePrivateKey(serverKey, password: password);
2018-06-08 07:06:26 +00:00
return new AngelHttp.fromSecurityContext(app, serverContext,
useZone: useZone);
}
/// The native HttpServer running this instance.
HttpServer get httpServer => _server;
/// Starts the server.
///
/// Returns false on failure; otherwise, returns the HttpServer.
2018-06-08 07:06:26 +00:00
Future<HttpServer> startServer([address, int port]) {
var host = address ?? '127.0.0.1';
return _serverGenerator(host, port ?? 0).then((server) {
_server = server;
return Future.wait(app.startupHooks.map(app.configure)).then((_) {
app.optimizeForProduction();
_sub = _server.listen(handleRequest);
return _server;
});
});
}
/// Shuts down the underlying server.
2018-06-08 07:06:26 +00:00
Future<HttpServer> close() {
if (_closed) return new Future.value(_server);
_closed = true;
_sub?.cancel();
2018-06-10 23:17:11 +00:00
return app.close().then((_) =>
2018-06-08 07:06:26 +00:00
Future.wait(app.shutdownHooks.map(app.configure)).then((_) => _server));
}
/// Handles a single request.
2018-06-08 07:06:26 +00:00
Future handleRequest(HttpRequest request) {
return createRequestContext(request).then((req) {
return createResponseContext(request.response, req).then((res) {
handle() {
var path = req.path;
if (path == '/') path = '';
2018-06-23 03:29:38 +00:00
Tuple3<List, Map, ParseResult<Map<String, dynamic>>> resolveTuple() {
2018-06-08 07:06:26 +00:00
Router r = app.optimizedRouter;
var resolved =
r.resolveAbsolute(path, method: req.method, strip: false);
return new Tuple3(
new MiddlewarePipeline(resolved).handlers,
resolved.fold<Map>({}, (out, r) => out..addAll(r.allParams)),
resolved.isEmpty ? null : resolved.first.parseResult,
);
2018-04-06 19:43:10 +00:00
}
2018-06-08 07:06:26 +00:00
var cacheKey = req.method + path;
var tuple = app.isProduction
? app.handlerCache.putIfAbsent(cacheKey, resolveTuple)
: resolveTuple();
2018-04-06 19:43:10 +00:00
2018-06-08 07:06:26 +00:00
req.params.addAll(tuple.item2);
req.inject(ParseResult, tuple.item3);
2018-05-16 02:40:57 +00:00
2018-06-10 23:17:11 +00:00
if (!app.isProduction && app.logger != null)
2018-06-08 07:06:26 +00:00
req.inject(Stopwatch, new Stopwatch()..start());
2018-06-08 07:06:26 +00:00
var pipeline = tuple.item1;
2018-06-08 07:06:26 +00:00
Future<bool> Function() runPipeline;
2018-06-08 07:06:26 +00:00
for (var handler in pipeline) {
if (handler == null) break;
2018-06-08 07:06:26 +00:00
if (runPipeline == null)
runPipeline = () => app.executeHandler(handler, req, res);
else {
var current = runPipeline;
2018-06-23 03:59:41 +00:00
runPipeline = () => current().then((result) => !result
? new Future.value(result)
: app.executeHandler(handler, req, res));
2018-06-08 07:06:26 +00:00
}
}
2018-06-08 07:06:26 +00:00
return runPipeline == null
? sendResponse(request, req, res)
: runPipeline().then((_) => sendResponse(request, req, res));
}
2018-06-08 07:06:26 +00:00
if (useZone == false) {
2018-06-23 03:29:38 +00:00
return handle().catchError((e, StackTrace st) {
2018-06-08 07:20:09 +00:00
if (e is FormatException)
throw new AngelHttpException.badRequest(message: e.message)
..stackTrace = st;
throw new AngelHttpException(e, stackTrace: st, statusCode: 500);
}, test: (e) => e is! AngelHttpException).catchError(
2018-07-09 15:59:17 +00:00
(ee, StackTrace st) {
var e = ee as AngelHttpException;
2018-06-08 07:20:09 +00:00
return handleAngelHttpException(
e, e.stackTrace ?? st, req, res, request);
}).whenComplete(() => res.dispose());
2018-06-08 07:06:26 +00:00
} else {
var zoneSpec = new ZoneSpecification(
print: (self, parent, zone, line) {
if (app.logger != null)
app.logger.info(line);
else
parent.print(zone, line);
},
handleUncaughtError: (self, parent, zone, error, stackTrace) {
2018-06-10 23:17:11 +00:00
var trace =
new Trace.from(stackTrace ?? StackTrace.current).terse;
2018-06-08 07:06:26 +00:00
return new Future(() {
AngelHttpException e;
if (error is FormatException) {
e = new AngelHttpException.badRequest(message: error.message);
} else if (error is AngelHttpException) {
e = error;
} else {
e = new AngelHttpException(error,
stackTrace: stackTrace, message: error?.toString());
}
if (app.logger != null) {
app.logger.severe(e.message ?? e.toString(), error, trace);
}
return handleAngelHttpException(e, trace, req, res, request);
2018-06-23 03:29:38 +00:00
}).catchError((e, StackTrace st) {
2018-06-10 22:34:05 +00:00
var trace = new Trace.from(st ?? StackTrace.current).terse;
2018-06-08 07:06:26 +00:00
request.response.close();
// Ideally, we won't be in a position where an absolutely fatal error occurs,
// but if so, we'll need to log it.
if (app.logger != null) {
app.logger.severe(
'Fatal error occurred when processing ${request.uri}.',
e,
trace);
} else {
stderr
..writeln('Fatal error occurred when processing '
'${request.uri}:')
..writeln(e)
..writeln(trace);
}
});
},
);
var zone = Zone.current.fork(specification: zoneSpec);
req.inject(Zone, zone);
req.inject(ZoneSpecification, zoneSpec);
return zone.run(handle).whenComplete(() {
res.dispose();
});
}
});
2018-04-06 19:43:10 +00:00
});
}
/// Handles an [AngelHttpException].
Future handleAngelHttpException(AngelHttpException e, StackTrace st,
RequestContext req, ResponseContext res, HttpRequest request,
2018-06-08 07:06:26 +00:00
{bool ignoreFinalizers: false}) {
if (req == null || res == null) {
try {
app.logger?.severe(e, st);
request.response
2018-06-08 07:06:26 +00:00
..statusCode = HttpStatus.internalServerError
..write('500 Internal Server Error')
..close();
} finally {
return null;
}
}
2018-06-10 23:17:11 +00:00
Future handleError;
if (!res.isOpen)
handleError = new Future.value();
else {
res.statusCode = e.statusCode;
2018-06-10 23:17:11 +00:00
handleError =
new Future.sync(() => app.errorHandler(e, req, res)).then((result) {
return app.executeHandler(result, req, res).then((_) => res.end());
});
}
2018-06-10 23:17:11 +00:00
return handleError.then((_) => sendResponse(request, req, res,
ignoreFinalizers: ignoreFinalizers == true));
}
/// Sends a response.
Future sendResponse(
HttpRequest request, RequestContext req, ResponseContext res,
{bool ignoreFinalizers: false}) {
if (res.willCloseItself) return new Future.value();
Future finalizers = ignoreFinalizers == true
? new Future.value()
: app.responseFinalizers.fold<Future>(
2018-02-07 04:17:40 +00:00
new Future.value(), (out, f) => out.then((_) => f(req, res)));
if (res.isOpen) res.end();
for (var key in res.headers.keys) {
request.response.headers.add(key, res.headers[key]);
}
request.response.contentLength = res.buffer.length;
request.response.headers.chunkedTransferEncoding = res.chunked ?? true;
List<int> outputBuffer = res.buffer.toBytes();
if (res.encoders.isNotEmpty) {
2018-06-08 07:06:26 +00:00
var allowedEncodings = req.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];
});
if (allowedEncodings != null) {
for (var encodingName in allowedEncodings) {
Converter<List<int>, List<int>> encoder;
String key = encodingName;
if (res.encoders.containsKey(encodingName))
encoder = res.encoders[encodingName];
else if (encodingName == '*') {
encoder = res.encoders[key = res.encoders.keys.first];
}
if (encoder != null) {
2018-06-08 07:06:26 +00:00
request.response.headers.set('content-encoding', key);
outputBuffer = res.encoders[key].convert(outputBuffer);
request.response.contentLength = outputBuffer.length;
break;
}
}
}
}
request.response
..statusCode = res.statusCode
..cookies.addAll(res.cookies)
..add(outputBuffer);
2018-06-08 07:06:26 +00:00
return finalizers.then((_) {
return request.response.close().then((_) {
if (req.injections.containsKey(PoolResource)) {
req.injections[PoolResource].release();
}
2018-06-10 23:17:11 +00:00
if (!app.isProduction && app.logger != null) {
2018-06-08 07:06:26 +00:00
var sw = req.grab<Stopwatch>(Stopwatch);
2018-06-08 07:06:26 +00:00
if (sw.isRunning) {
sw?.stop();
app.logger.info("${res.statusCode} ${req.method} ${req.uri} (${sw
?.elapsedMilliseconds ?? 'unknown'} ms)");
}
}
2018-06-08 07:06:26 +00:00
});
});
}
Future<HttpRequestContextImpl> createRequestContext(HttpRequest request) {
var path = request.uri.path.replaceAll(_straySlashes, '');
if (path.length == 0) path = '/';
2018-06-08 07:06:26 +00:00
return HttpRequestContextImpl.from(request, app, path).then((req) {
if (_pool != null) req.inject(PoolResource, _pool.request());
if (app.injections.isNotEmpty) app.injections.forEach(req.inject);
return req;
});
}
Future<ResponseContext> createResponseContext(HttpResponse response,
2018-02-07 04:17:40 +00:00
[RequestContext correspondingRequest]) =>
2018-06-23 03:59:41 +00:00
new Future<ResponseContext>.value(new HttpResponseContextImpl(
response, app, correspondingRequest as HttpRequestContextImpl)
..serializer = (app.serializer ?? god.serialize)
..encoders.addAll(app.encoders ?? {}));
2018-02-07 03:32:31 +00:00
/// Limits the maximum number of requests to be handled concurrently by this instance.
///
/// You can optionally provide a [timeout] to limit the amount of time a request can be
/// handled before.
void throttle(int maxConcurrentRequests, {Duration timeout}) {
_pool = new Pool(maxConcurrentRequests, timeout: timeout);
}
2018-02-07 04:17:40 +00:00
}