platform/lib/src/http/server.dart

703 lines
22 KiB
Dart
Raw Normal View History

library angel_framework.http.server;
import 'dart:async';
2017-11-28 18:14:50 +00:00
import 'dart:collection' show HashMap;
2017-08-15 23:01:16 +00:00
import 'dart:convert';
import 'dart:io';
2017-09-22 04:48:22 +00:00
import 'package:angel_http_exception/angel_http_exception.dart';
2017-11-28 18:14:50 +00:00
import 'package:angel_route/angel_route.dart';
import 'package:combinator/combinator.dart';
2017-04-01 01:00:24 +00:00
export 'package:container/container.dart';
import 'package:json_god/json_god.dart' as god;
2017-09-22 04:48:22 +00:00
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
2017-11-28 18:14:50 +00:00
import 'package:pool/pool.dart';
2017-08-28 15:29:27 +00:00
import 'package:tuple/tuple.dart';
import 'angel_base.dart';
import 'controller.dart';
import 'request_context.dart';
import 'response_context.dart';
import 'routable.dart';
import 'service.dart';
2016-02-28 13:11:17 +00:00
2016-10-22 20:41:36 +00:00
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
2016-04-18 03:27:23 +00:00
/// A function that binds an [Angel] server to an Internet address and port.
2017-10-04 14:09:12 +00:00
typedef Future<HttpServer> ServerGenerator(address, int port);
2016-02-28 13:11:17 +00:00
2017-02-25 00:16:31 +00:00
/// A function that configures an [Angel] server in some way.
typedef Future AngelConfigurer(Angel app);
2016-02-28 13:11:17 +00:00
/// A powerful real-time/REST/MVC server class.
class Angel extends AngelBase {
2016-12-21 18:18:26 +00:00
final List<Angel> _children = [];
2017-11-28 18:14:50 +00:00
final Map<String, Tuple3<List, Map, ParseResult<Map<String, String>>>>
handlerCache = new HashMap();
2017-08-15 23:01:16 +00:00
2017-04-01 01:00:24 +00:00
Router _flattened;
2017-11-28 18:14:50 +00:00
bool _isProduction;
2016-12-21 18:18:26 +00:00
Angel _parent;
2017-11-28 18:14:50 +00:00
Pool _pool;
2017-10-28 08:50:16 +00:00
StreamSubscription<HttpRequest> _sub;
2016-11-24 07:12:53 +00:00
ServerGenerator _serverGenerator = HttpServer.bind;
2016-09-17 16:12:25 +00:00
2017-08-15 23:01:16 +00:00
/// A global Map of converters that can transform responses bodies.
final Map<String, Converter<List<int>, List<int>>> encoders = {};
2017-09-22 04:48:22 +00:00
final Map _injections = {};
/// Creates a safe zone within which a request can be handled, without crashing the application.
Future<ZoneSpecification> Function(
HttpRequest request, RequestContext req, ResponseContext res)
createZoneForRequest;
2017-08-15 23:01:16 +00:00
2016-12-31 01:46:41 +00:00
final Map<dynamic, InjectionRequest> _preContained = {};
2017-04-01 01:00:24 +00:00
ResponseSerializer _serializer;
2016-12-31 01:46:41 +00:00
2017-06-06 12:42:33 +00:00
/// A [Map] of dependency data obtained via reflection.
///
/// You may modify this [Map] yourself if you intend to avoid reflection entirely.
Map<dynamic, InjectionRequest> get preContained => _preContained;
2017-03-02 22:06:02 +00:00
/// Determines whether to allow HTTP request method overrides.
bool allowMethodOverrides = true;
2016-12-21 18:18:26 +00:00
/// All child application mounted on this instance.
List<Angel> get children => new List<Angel>.unmodifiable(_children);
2017-10-04 14:09:12 +00:00
final Map<Pattern, Controller> _controllers = {};
/// A set of [Controller] objects that have been loaded into the application.
Map<Pattern, Controller> get controllers => _controllers;
2016-12-21 18:18:26 +00:00
/// Indicates whether the application is running in a production environment.
///
/// The criteria for this is the `ANGEL_ENV` environment variable being set to
/// `'production'`.
2017-04-01 01:00:24 +00:00
///
/// This value is memoized the first time you call it, so do not change environment
/// configuration at runtime!
bool get isProduction {
2017-11-28 18:14:50 +00:00
return _isProduction ??=
(Platform.environment['ANGEL_ENV'] == 'production');
2017-04-01 01:00:24 +00:00
}
2016-12-21 18:18:26 +00:00
2017-03-28 23:29:22 +00:00
/// The function used to bind this instance to an HTTP server.
ServerGenerator get serverGenerator => _serverGenerator;
2016-12-21 18:18:26 +00:00
/// Returns the parent instance of this application, if any.
Angel get parent => _parent;
2017-09-22 04:48:22 +00:00
/// Outputs diagnostics and debug messages.
Logger logger;
2017-02-01 21:43:18 +00:00
/// Plug-ins to be called right before server startup.
///
/// If the server is never started, they will never be called.
2017-09-22 04:48:22 +00:00
final List<AngelConfigurer> startupHooks = [];
2017-02-01 21:43:18 +00:00
2017-09-22 04:48:22 +00:00
/// Plug-ins to be called right before server shutdown.
2017-04-04 08:35:36 +00:00
///
/// If the server is never [close]d, they will never be called.
2017-09-22 04:48:22 +00:00
final List<AngelConfigurer> shutdownHooks = [];
2017-04-04 08:35:36 +00:00
2016-12-19 01:38:23 +00:00
/// Always run before responses are sent.
///
2017-09-22 14:03:23 +00:00
/// These will only not run if a response's `willCloseItself` is set to `true`.
2016-12-19 01:38:23 +00:00
final List<RequestHandler> responseFinalizers = [];
2017-10-04 14:09:12 +00:00
/// Use [configuration] instead.
@deprecated
Map get properties {
try {
throw new Error();
} catch (e, st) {
logger?.warning(
'`properties` is deprecated, and should not be used.',
new UnsupportedError('`properties` is deprecated.'),
st,
);
}
return configuration;
}
2017-03-04 21:12:39 +00:00
/// The handler currently configured to run on [AngelHttpException]s.
2017-09-22 04:48:22 +00:00
Function(AngelHttpException e, RequestContext req, ResponseContext res)
2017-10-04 14:09:12 +00:00
errorHandler =
(AngelHttpException e, RequestContext req, ResponseContext res) {
if (!req.accepts('text/html') &&
(req.accepts('application/json') ||
req.accepts('application/javascript'))) {
res.json(e.toJson());
return;
}
2016-12-21 20:27:07 +00:00
res.headers[HttpHeaders.CONTENT_TYPE] = ContentType.HTML.toString();
2016-12-19 01:38:23 +00:00
res.statusCode = e.statusCode;
2016-07-03 22:23:55 +00:00
res.write("<!DOCTYPE html><html><head><title>${e.message}</title>");
res.write("</head><body><h1>${e.message}</h1><ul>");
2016-12-31 01:46:41 +00:00
for (String error in e.errors) {
res.write("<li>$error</li>");
}
2016-12-31 01:46:41 +00:00
res.write("</ul></body></html>");
res.end();
};
2016-06-21 22:56:04 +00:00
/// The native HttpServer running this instancce.
2016-04-18 03:27:23 +00:00
HttpServer httpServer;
2016-06-21 22:56:04 +00:00
/// Starts the server.
///
/// Returns false on failure; otherwise, returns the HttpServer.
2017-10-04 14:09:12 +00:00
Future<HttpServer> startServer([address, int port]) async {
2017-01-20 22:40:48 +00:00
var host = address ?? InternetAddress.LOOPBACK_IP_V4;
2016-11-23 09:10:47 +00:00
this.httpServer = await _serverGenerator(host, port ?? 0);
2017-03-02 04:04:37 +00:00
2017-09-22 04:48:22 +00:00
for (var configurer in startupHooks) {
2017-03-02 04:04:37 +00:00
await configure(configurer);
}
2017-04-01 01:00:24 +00:00
optimizeForProduction();
2017-10-28 08:50:16 +00:00
_sub = httpServer.listen(handleRequest);
return httpServer;
}
2017-04-01 01:00:24 +00:00
@override
Route addRoute(String method, Pattern path, Object handler,
2017-04-01 01:00:24 +00:00
{List middleware: const []}) {
if (_flattened != null) {
2017-09-22 14:53:49 +00:00
logger?.warning(
2017-04-02 19:14:10 +00:00
'WARNING: You added a route ($method $path) to the router, after it had been optimized.');
2017-09-22 14:53:49 +00:00
logger?.warning(
'This route will be ignored, and no requests will ever reach it.');
2017-04-01 01:00:24 +00:00
}
return super.addRoute(method, path, handler, middleware: middleware ?? []);
}
2017-04-02 19:14:10 +00:00
@override
mount(Pattern path, Router router, {bool hooked: true, String namespace}) {
if (_flattened != null) {
2017-09-22 14:53:49 +00:00
logger?.warning(
2017-04-02 19:14:10 +00:00
'WARNING: You added mounted a child router ($path) on the router, after it had been optimized.');
2017-09-22 14:53:49 +00:00
logger?.warning(
'This route will be ignored, and no requests will ever reach it.');
2017-04-02 19:14:10 +00:00
}
return super
.mount(path, router, hooked: hooked != false, namespace: namespace);
}
2016-09-17 16:12:25 +00:00
/// Loads some base dependencies into the service container.
void bootstrapContainer() {
2017-01-20 22:11:20 +00:00
if (runtimeType != Angel) container.singleton(this, as: Angel);
2016-09-17 16:12:25 +00:00
container.singleton(this, as: AngelBase);
2016-11-23 19:50:17 +00:00
container.singleton(this, as: Routable);
container.singleton(this, as: Router);
2016-09-17 16:12:25 +00:00
container.singleton(this);
}
2017-04-04 08:35:36 +00:00
/// Shuts down the server, and closes any open [StreamController]s.
2017-10-28 08:50:16 +00:00
///
/// The server will be **COMPLETE DEFUNCT** after this operation!
2017-04-04 08:35:36 +00:00
Future<HttpServer> close() async {
HttpServer server;
2017-10-28 08:50:16 +00:00
_sub?.cancel();
2017-04-04 08:35:36 +00:00
if (httpServer != null) {
server = httpServer;
await httpServer.close(force: true);
}
2017-04-25 02:44:22 +00:00
await Future.forEach(services.values, (Service service) async {
await service.close();
2017-04-04 08:35:36 +00:00
});
2017-09-22 04:48:22 +00:00
for (var plugin in shutdownHooks) await plugin(this);
2017-04-04 08:35:36 +00:00
2017-10-28 08:50:16 +00:00
await super.close();
_preContained.clear();
2017-11-18 17:42:31 +00:00
handlerCache.clear();
2017-10-28 08:50:16 +00:00
_injections.clear();
encoders.clear();
_serializer = god.serialize;
_children.clear();
_parent = null;
logger = null;
startupHooks.clear();
shutdownHooks.clear();
responseFinalizers.clear();
_flattened = null;
2017-04-04 08:35:36 +00:00
return server;
}
2017-04-01 01:00:24 +00:00
@override
2017-08-28 15:29:27 +00:00
void dumpTree(
{callback(String tree),
String header: 'Dumping route tree:',
String tab: ' ',
bool showMatchers: false}) {
2017-04-01 01:00:24 +00:00
if (isProduction) {
2017-11-28 18:14:50 +00:00
_flattened ??= flatten(this);
2017-04-01 01:00:24 +00:00
_flattened.dumpTree(
callback: callback,
header: header?.isNotEmpty == true
? header
: (isProduction
2017-08-28 15:29:27 +00:00
? 'Dumping flattened route tree:'
: 'Dumping route tree:'),
2017-11-28 18:14:50 +00:00
tab: tab ?? ' ');
2017-04-01 01:00:24 +00:00
} else {
super.dumpTree(
callback: callback,
header: header?.isNotEmpty == true
? header
: (isProduction
2017-08-28 15:29:27 +00:00
? 'Dumping flattened route tree:'
: 'Dumping route tree:'),
2017-11-28 18:14:50 +00:00
tab: tab ?? ' ');
2017-04-01 01:00:24 +00:00
}
}
2017-03-06 00:52:16 +00:00
/// Shortcut for adding a middleware to inject a key/value pair on every request.
void inject(key, value) {
2017-09-22 04:48:22 +00:00
_injections[key] = value;
2017-03-06 00:52:16 +00:00
}
2017-08-15 23:01:16 +00:00
/// Shortcuts for adding converters to transform the response buffer/stream of any request.
void injectEncoders(Map<String, Converter<List<int>, List<int>>> encoders) {
this.encoders.addAll(encoders);
}
2017-03-06 00:52:16 +00:00
/// Shortcut for adding a middleware to inject a serialize on every request.
void injectSerializer(ResponseSerializer serializer) {
2017-04-01 01:00:24 +00:00
_serializer = serializer;
2017-03-06 00:52:16 +00:00
}
2017-08-28 15:29:27 +00:00
Future getHandlerResult(
handler, RequestContext req, ResponseContext res) async {
if (handler is RequestHandler) {
2017-02-25 00:16:31 +00:00
var result = await handler(req, res);
return await getHandlerResult(result, req, res);
2016-09-17 16:12:25 +00:00
}
if (handler is Future) {
var result = await handler;
return await getHandlerResult(result, req, res);
2016-09-17 16:12:25 +00:00
}
if (handler is Function) {
var result = await runContained(handler, req, res);
return await getHandlerResult(result, req, res);
}
if (handler is Stream) {
return await getHandlerResult(await handler.toList(), req, res);
2016-09-17 16:12:25 +00:00
}
2017-06-06 12:42:33 +00:00
var middleware = (req.app ?? this).findMiddleware(handler);
if (middleware != null) {
return await getHandlerResult(middleware, req, res);
2016-04-18 03:27:23 +00:00
}
2016-09-17 16:12:25 +00:00
2017-04-15 17:42:21 +00:00
return handler;
}
/// Runs some [handler]. Returns `true` if request execution should continue.
2017-08-28 15:29:27 +00:00
Future<bool> executeHandler(
handler, RequestContext req, ResponseContext res) async {
2017-04-15 17:42:21 +00:00
var result = await getHandlerResult(handler, req, res);
2017-10-28 08:50:16 +00:00
if (result == null)
return false;
else if (result is bool) {
2017-04-15 17:42:21 +00:00
return result;
} else if (result != null) {
res.serialize(result,
contentType: res.headers[HttpHeaders.CONTENT_TYPE] ??
ContentType.JSON.mimeType);
return false;
} else
return res.isOpen;
2016-04-18 03:27:23 +00:00
}
2017-01-20 22:40:48 +00:00
Future<RequestContext> createRequestContext(HttpRequest request) {
2017-11-28 18:14:50 +00:00
var path = request.uri.path.replaceAll(_straySlashes, '');
if (path.length == 0) path = '/';
return RequestContext.from(request, this, path).then((req) async {
if (_pool != null) req.inject(PoolResource, await _pool.request());
if (_injections.isNotEmpty) _injections.forEach(req.inject);
2017-09-24 19:43:14 +00:00
return req;
2017-04-01 01:00:24 +00:00
});
2017-01-20 22:40:48 +00:00
}
2017-08-15 23:01:16 +00:00
Future<ResponseContext> createResponseContext(HttpResponse response,
2017-08-28 15:29:27 +00:00
[RequestContext correspondingRequest]) =>
2017-08-15 23:01:16 +00:00
new Future<ResponseContext>.value(
new ResponseContext(response, this, correspondingRequest)
..serializer = (_serializer ?? god.serialize)
..encoders.addAll(encoders ?? {}));
2017-01-20 22:40:48 +00:00
2017-06-06 12:42:33 +00:00
/// Attempts to find a middleware by the given name within this application.
findMiddleware(key) {
if (requestMiddleware.containsKey(key)) return requestMiddleware[key];
return parent != null ? parent.findMiddleware(key) : null;
}
/// Attempts to find a property by the given name within this application.
findProperty(key) {
2017-09-24 19:43:14 +00:00
if (configuration.containsKey(key)) return configuration[key];
2017-06-06 12:42:33 +00:00
return parent != null ? parent.findProperty(key) : null;
}
2017-07-10 23:08:05 +00:00
/// Handles an [AngelHttpException].
2017-09-22 04:48:22 +00:00
Future handleAngelHttpException(AngelHttpException e, StackTrace st,
RequestContext req, ResponseContext res, HttpRequest request,
{bool ignoreFinalizers: false}) async {
if (req == null || res == null) {
2017-09-22 04:48:22 +00:00
try {
logger?.severe(e, st);
request.response
..statusCode = HttpStatus.INTERNAL_SERVER_ERROR
..write('500 Internal Server Error')
..close();
} finally {
return null;
}
2017-06-19 01:53:51 +00:00
}
res.statusCode = e.statusCode;
2017-10-10 16:55:42 +00:00
var result = await errorHandler(e, req, res);
await executeHandler(result, req, res);
res.end();
2017-09-22 04:48:22 +00:00
return await sendResponse(request, req, res,
ignoreFinalizers: ignoreFinalizers == true);
2017-06-19 01:53:51 +00:00
}
2017-01-20 22:11:20 +00:00
/// Handles a single request.
2016-10-22 20:41:36 +00:00
Future handleRequest(HttpRequest request) async {
2017-10-04 14:09:12 +00:00
var req = await createRequestContext(request);
var res = await createResponseContext(request.response, req);
2017-09-22 04:48:22 +00:00
2017-11-28 18:14:50 +00:00
try {
var path = req.path;
if (path == '/') path = '';
2017-11-28 18:14:50 +00:00
Tuple3<List, Map, ParseResult<Map<String, String>>> resolveTuple() {
Router r = _flattened ?? this;
var resolved =
2017-11-28 18:14:50 +00:00
r.resolveAbsolute(path, method: req.method, strip: false);
2017-10-10 16:55:42 +00:00
2017-08-28 15:29:27 +00:00
return new Tuple3(
2017-10-28 08:50:16 +00:00
new MiddlewarePipeline(resolved).handlers,
2017-08-28 15:29:27 +00:00
resolved.fold<Map>({}, (out, r) => out..addAll(r.allParams)),
2017-11-28 18:14:50 +00:00
resolved.isEmpty ? null : resolved.first.parseResult,
2017-08-28 15:29:27 +00:00
);
2017-10-28 08:50:16 +00:00
}
2017-11-28 18:14:50 +00:00
var cacheKey = req.method + path;
2017-10-28 08:50:16 +00:00
var tuple = isProduction
2017-11-28 18:14:50 +00:00
? handlerCache.putIfAbsent(cacheKey, resolveTuple)
2017-10-28 08:50:16 +00:00
: resolveTuple();
2017-11-28 18:14:50 +00:00
//req.inject(Zone, zone);
//req.inject(ZoneSpecification, zoneSpec);
2017-08-28 15:29:27 +00:00
req.params.addAll(tuple.item2);
2017-11-28 18:14:50 +00:00
req.inject(ParseResult, tuple.item3);
2017-10-28 08:50:16 +00:00
if (logger != null) req.inject(Stopwatch, new Stopwatch()..start());
var pipeline = tuple.item1;
2016-11-23 19:50:17 +00:00
2017-01-20 22:40:48 +00:00
for (var handler in pipeline) {
2017-01-14 15:39:11 +00:00
try {
2017-10-28 08:50:16 +00:00
if (handler == null || !await executeHandler(handler, req, res))
break;
2017-06-19 01:53:51 +00:00
} on AngelHttpException catch (e, st) {
2017-07-02 01:56:23 +00:00
e.stackTrace ??= st;
return await handleAngelHttpException(e, st, req, res, request);
2017-01-14 15:39:11 +00:00
}
2016-10-22 20:41:36 +00:00
}
2017-01-14 15:39:11 +00:00
try {
2017-02-13 00:38:33 +00:00
await sendResponse(request, req, res);
2017-06-19 01:53:51 +00:00
} on AngelHttpException catch (e, st) {
2017-07-02 01:56:23 +00:00
e.stackTrace ??= st;
2017-09-22 04:48:22 +00:00
return await handleAngelHttpException(
e,
st,
req,
res,
request,
ignoreFinalizers: true,
);
2016-05-01 01:42:52 +00:00
}
2017-11-28 18:14:50 +00:00
} on FormatException catch (error, stackTrace) {
var e = new AngelHttpException.badRequest(message: error.message);
if (logger != null) {
logger.severe(e.message ?? e.toString(), error, stackTrace);
}
return await handleAngelHttpException(e, stackTrace, req, res, request);
} catch (error, stackTrace) {
2017-10-10 16:55:42 +00:00
var e = new AngelHttpException(error,
stackTrace: stackTrace, message: error?.toString());
if (logger != null) {
logger.severe(e.message ?? e.toString(), error, stackTrace);
}
2017-11-28 18:14:50 +00:00
return await handleAngelHttpException(e, stackTrace, req, res, request);
} finally {
res.dispose();
}
2016-04-22 01:42:39 +00:00
}
2017-04-01 01:00:24 +00:00
/// Runs several optimizations, *if* [isProduction] is `true`.
///
/// * Preprocesses all dependency injection, and eliminates the burden of reflecting handlers
2016-12-31 02:00:52 +00:00
/// at run-time.
2017-04-01 01:00:24 +00:00
/// * [flatten]s the route tree into a linear one.
2017-06-06 12:42:33 +00:00
///
/// You may [force] the optimization to run, if you are not running in production.
void optimizeForProduction({bool force: false}) {
if (isProduction == true || force == true) {
_isProduction = true;
2017-04-01 01:00:24 +00:00
_add(v) {
if (v is Function && !_preContained.containsKey(v)) {
_preContained[v] = preInject(v);
}
2016-12-31 02:00:52 +00:00
}
2017-04-01 01:00:24 +00:00
void _walk(Router router) {
router.requestMiddleware.forEach((k, v) => _add(v));
router.middleware.forEach(_add);
2017-06-06 12:42:33 +00:00
router.routes.forEach((r) {
r.handlers.forEach(_add);
if (r is SymlinkRoute) _walk(r.router);
});
2017-01-15 19:52:14 +00:00
}
2017-11-28 18:14:50 +00:00
_flattened ??= flatten(this);
2017-04-02 19:14:10 +00:00
_walk(_flattened);
2017-09-22 14:53:49 +00:00
logger?.config('Angel is running in production mode.');
2016-12-31 02:00:52 +00:00
}
}
2016-12-31 01:46:41 +00:00
/// Run a function after injecting from service container.
/// If this function has been reflected before, then
/// the execution will be faster, as the injection requirements were stored beforehand.
2017-08-28 15:29:27 +00:00
Future runContained(
Function handler, RequestContext req, ResponseContext res) {
2016-12-31 01:46:41 +00:00
if (_preContained.containsKey(handler)) {
return handleContained(handler, _preContained[handler])(req, res);
}
return runReflected(handler, req, res);
}
/// Runs with DI, and *always* reflects. Prefer [runContained].
2017-08-28 15:29:27 +00:00
Future runReflected(
Function handler, RequestContext req, ResponseContext res) async {
2017-01-12 01:52:06 +00:00
var h =
2017-08-28 15:29:27 +00:00
handleContained(handler, _preContained[handler] = preInject(handler));
2017-01-12 01:52:06 +00:00
return await h(req, res);
2016-12-31 01:46:41 +00:00
// return await closureMirror.apply(args).reflectee;
2016-09-17 16:12:25 +00:00
}
2017-02-13 00:38:33 +00:00
/// Sends a response.
2017-08-28 15:29:27 +00:00
Future sendResponse(
HttpRequest request, RequestContext req, ResponseContext res,
{bool ignoreFinalizers: false}) {
2017-11-28 18:14:50 +00:00
if (res.willCloseItself) return new Future.value();
2017-01-20 22:40:48 +00:00
2017-11-28 18:14:50 +00:00
Future finalizers = ignoreFinalizers == true
? new Future.value()
: responseFinalizers.fold<Future>(
new Future.value(), (out, f) => out.then((_) => f(req, res)));
2017-04-02 19:14:10 +00:00
2017-11-28 18:14:50 +00:00
if (res.isOpen) res.end();
2017-01-20 22:40:48 +00:00
2017-11-28 18:14:50 +00:00
for (var key in res.headers.keys) {
request.response.headers.add(key, res.headers[key]);
}
2017-01-20 22:40:48 +00:00
2017-11-28 18:14:50 +00:00
request.response.contentLength = res.buffer.length;
request.response.headers.chunkedTransferEncoding = res.chunked ?? true;
2017-08-15 23:01:16 +00:00
2017-11-28 18:14:50 +00:00
List<int> outputBuffer = res.buffer.toBytes();
if (res.encoders.isNotEmpty) {
var allowedEncodings =
req.headers[HttpHeaders.ACCEPT_ENCODING]?.map((str) {
// Ignore quality specifications in accept-encoding
// ex. gzip;q=0.8
if (!str.contains(';')) return str;
return str.split(';')[0];
});
2017-08-15 23:01:16 +00:00
2017-11-28 18:14:50 +00:00
if (allowedEncodings != null) {
2017-08-15 23:01:16 +00:00
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) {
2017-08-28 15:29:27 +00:00
request.response.headers.set(HttpHeaders.CONTENT_ENCODING, key);
2017-08-15 23:01:16 +00:00
outputBuffer = res.encoders[key].convert(outputBuffer);
2017-09-24 17:56:33 +00:00
request.response.contentLength = outputBuffer.length;
2017-08-15 23:01:16 +00:00
break;
}
}
}
2017-11-28 18:14:50 +00:00
}
2017-08-15 23:01:16 +00:00
2017-11-28 18:14:50 +00:00
request.response
..statusCode = res.statusCode
..cookies.addAll(res.cookies)
..add(outputBuffer);
2017-11-28 18:14:50 +00:00
return finalizers.then((_) async {
request.response.close();
2017-09-24 19:43:14 +00:00
2017-11-28 18:14:50 +00:00
if (req.injections.containsKey(PoolResource)) {
req.injections[PoolResource].release();
}
if (logger != null) {
var sw = req.grab<Stopwatch>(Stopwatch);
if (sw.isRunning) {
sw?.stop();
logger.info("${res.statusCode} ${req.method} ${req.uri} (${sw
?.elapsedMilliseconds ?? 'unknown'} ms)");
2017-09-22 14:53:49 +00:00
}
2017-11-28 18:14:50 +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);
2017-01-20 22:40:48 +00:00
}
2016-04-18 03:27:23 +00:00
/// Applies an [AngelConfigurer] to this instance.
Future configure(AngelConfigurer configurer) async {
await configurer(this);
2016-02-28 13:11:17 +00:00
}
2017-09-22 04:48:22 +00:00
/// Mounts the child on this router. If [routable] is `null`,
/// then this method will add a handler as a global middleware instead.
2016-12-21 18:18:26 +00:00
///
/// If the router is an [Angel] instance, all controllers
/// will be copied, as well as services and response finalizers.
///
/// [before] and [after] will be preserved.
2016-12-31 01:46:41 +00:00
///
2016-12-21 18:18:26 +00:00
/// NOTE: The above will not be properly copied if [path] is
/// a [RegExp].
@override
2017-09-22 04:48:22 +00:00
use(path, [@checked Routable routable, String namespace = null]) {
if (routable == null) return all('*', path);
2017-01-20 22:40:48 +00:00
var head = path.toString().replaceAll(_straySlashes, '');
2016-12-21 18:18:26 +00:00
2016-11-28 00:49:27 +00:00
if (routable is Angel) {
2016-12-21 18:18:26 +00:00
_children.add(routable.._parent = this);
2017-01-15 19:52:14 +00:00
_preContained.addAll(routable._preContained);
2016-12-21 18:18:26 +00:00
if (routable.responseFinalizers.isNotEmpty) {
responseFinalizers.add((req, res) async {
if (req.path.replaceAll(_straySlashes, '').startsWith(head)) {
for (var finalizer in routable.responseFinalizers)
await finalizer(req, res);
}
return true;
});
}
2016-11-28 00:49:27 +00:00
2017-10-04 14:09:12 +00:00
routable._controllers.forEach((k, v) {
2017-01-20 22:40:48 +00:00
var tail = k.toString().replaceAll(_straySlashes, '');
2017-10-04 14:09:12 +00:00
_controllers['$head/$tail'.replaceAll(_straySlashes, '')] = v;
2016-11-28 00:49:27 +00:00
});
2016-12-21 18:18:26 +00:00
routable.services.forEach((k, v) {
2017-01-20 22:40:48 +00:00
var tail = k.toString().replaceAll(_straySlashes, '');
2016-12-21 18:18:26 +00:00
services['$head/$tail'.replaceAll(_straySlashes, '')] = v;
});
2016-11-28 00:49:27 +00:00
}
if (routable is Service) {
routable.app = this;
}
2016-10-22 20:41:36 +00:00
2017-09-22 04:48:22 +00:00
return super.use(path, routable, namespace);
}
2016-04-22 01:42:39 +00:00
2017-01-20 22:11:20 +00:00
/// Default constructor. ;)
2017-09-22 04:48:22 +00:00
Angel() : super() {
2016-09-17 16:12:25 +00:00
bootstrapContainer();
2017-11-28 18:14:50 +00:00
createZoneForRequest = defaultZoneCreator;
}
2017-09-22 04:48:22 +00:00
2017-11-28 18:14:50 +00:00
Future<ZoneSpecification> defaultZoneCreator(request, req, res) async {
return new ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
if (logger != null) {
logger.info(line);
} else {
return parent.print(zone, line);
}
},
);
2016-09-17 16:12:25 +00:00
}
2016-02-28 13:11:17 +00:00
2017-01-20 22:11:20 +00:00
/// An instance mounted on a server started by the [serverGenerator].
2017-09-22 04:48:22 +00:00
factory Angel.custom(ServerGenerator serverGenerator) {
return new Angel().._serverGenerator = serverGenerator;
}
2017-01-20 22:11:20 +00:00
2017-09-22 04:48:22 +00:00
factory Angel.fromSecurityContext(SecurityContext context) {
var app = new Angel();
2017-02-26 21:31:09 +00:00
app._serverGenerator = (InternetAddress address, int port) async {
return await HttpServer.bindSecure(address, port, context);
};
return app;
}
2016-04-18 03:27:23 +00:00
/// Creates an HTTPS server.
2017-01-20 22:11:20 +00:00
///
/// 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.
2016-11-23 19:50:17 +00:00
factory Angel.secure(String certificateChainPath, String serverKeyPath,
{bool debug: false, String password}) {
2017-02-26 21:31:09 +00:00
var certificateChain =
2017-08-28 15:29:27 +00:00
Platform.script.resolve(certificateChainPath).toFilePath();
2017-02-26 21:31:09 +00:00
var serverKey = Platform.script.resolve(serverKeyPath).toFilePath();
var serverContext = new SecurityContext();
2017-03-02 04:04:37 +00:00
serverContext.useCertificateChain(certificateChain, password: password);
2017-02-26 21:31:09 +00:00
serverContext.usePrivateKey(serverKey, password: password);
return new Angel.fromSecurityContext(serverContext);
}
2017-11-28 18:14:50 +00:00
}