2016-09-15 19:53:01 +00:00
|
|
|
library angel_framework.http.server;
|
|
|
|
|
|
|
|
import 'dart:async';
|
2017-08-15 23:01:16 +00:00
|
|
|
import 'dart:convert';
|
2016-09-15 19:53:01 +00:00
|
|
|
import 'dart:io';
|
2016-09-17 16:12:25 +00:00
|
|
|
import 'dart:mirrors';
|
2017-09-22 04:48:22 +00:00
|
|
|
import 'package:angel_http_exception/angel_http_exception.dart';
|
2017-08-03 16:40:21 +00:00
|
|
|
import 'package:angel_route/angel_route.dart' hide Extensible;
|
|
|
|
import 'package:charcode/charcode.dart';
|
2017-04-01 01:00:24 +00:00
|
|
|
export 'package:container/container.dart';
|
|
|
|
import 'package:flatten/flatten.dart';
|
|
|
|
import 'package:json_god/json_god.dart' as god;
|
2017-09-22 04:48:22 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2017-08-03 16:40:21 +00:00
|
|
|
import 'package:meta/meta.dart';
|
2017-08-28 15:29:27 +00:00
|
|
|
import 'package:tuple/tuple.dart';
|
2017-08-03 16:40:21 +00:00
|
|
|
import '../safe_stream_controller.dart';
|
2016-09-15 19:53:01 +00:00
|
|
|
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.
|
2016-02-28 13:11:17 +00:00
|
|
|
typedef Future<HttpServer> ServerGenerator(InternetAddress address, int port);
|
|
|
|
|
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.
|
2016-09-15 19:53:01 +00:00
|
|
|
class Angel extends AngelBase {
|
2017-08-15 23:01:16 +00:00
|
|
|
final SafeCtrl<Controller> _onController =
|
2017-08-28 15:29:27 +00:00
|
|
|
new SafeCtrl<Controller>.broadcast();
|
2017-08-03 16:40:21 +00:00
|
|
|
|
2016-12-21 18:18:26 +00:00
|
|
|
final List<Angel> _children = [];
|
2017-08-28 15:29:27 +00:00
|
|
|
final Map<String, Tuple3<MiddlewarePipeline, Map, Match>> _handlerCache = {};
|
2017-08-15 23:01:16 +00:00
|
|
|
|
2017-04-01 01:00:24 +00:00
|
|
|
Router _flattened;
|
|
|
|
bool _isProduction;
|
2016-12-21 18:18:26 +00:00
|
|
|
Angel _parent;
|
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);
|
|
|
|
|
|
|
|
/// 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 {
|
|
|
|
if (_isProduction != null)
|
|
|
|
return _isProduction;
|
|
|
|
else
|
|
|
|
return _isProduction = Platform.environment['ANGEL_ENV'] == 'production';
|
|
|
|
}
|
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-07-05 22:11:54 +00:00
|
|
|
/// Fired whenever a controller is added to this instance.
|
|
|
|
///
|
|
|
|
/// **NOTE**: This is a broadcast stream.
|
|
|
|
Stream<Controller> get onController => _onController.stream;
|
|
|
|
|
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-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)
|
|
|
|
errorHandler = (AngelHttpException e, req, ResponseContext res) {
|
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>");
|
Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
2016-04-29 00:01:58 +00:00
|
|
|
res.write("</head><body><h1>${e.message}</h1><ul>");
|
2016-12-31 01:46:41 +00:00
|
|
|
|
Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
2016-04-29 00:01:58 +00:00
|
|
|
for (String error in e.errors) {
|
|
|
|
res.write("<li>$error</li>");
|
|
|
|
}
|
2016-12-31 01:46:41 +00:00
|
|
|
|
Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
2016-04-29 00:01:58 +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.
|
2016-09-17 16:12:25 +00:00
|
|
|
Future<HttpServer> startServer([InternetAddress 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();
|
2016-11-23 09:10:47 +00:00
|
|
|
return httpServer..listen(handleRequest);
|
2016-07-04 18:06:31 +00:00
|
|
|
}
|
|
|
|
|
2017-04-01 01:00:24 +00:00
|
|
|
@override
|
2017-08-03 16:40:21 +00:00
|
|
|
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.
|
|
|
|
Future<HttpServer> close() async {
|
|
|
|
HttpServer server;
|
|
|
|
|
|
|
|
if (httpServer != null) {
|
|
|
|
server = httpServer;
|
|
|
|
await httpServer.close(force: true);
|
|
|
|
}
|
|
|
|
|
|
|
|
_onController.close();
|
|
|
|
|
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
|
|
|
|
|
|
|
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) {
|
|
|
|
if (_flattened == null) _flattened = flatten(this);
|
|
|
|
|
|
|
|
_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-04-01 01:00:24 +00:00
|
|
|
tab: tab ?? ' ',
|
|
|
|
showMatchers: showMatchers == true);
|
|
|
|
} 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-04-01 01:00:24 +00:00
|
|
|
tab: tab ?? ' ',
|
|
|
|
showMatchers: showMatchers == true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 {
|
2016-04-21 20:37:02 +00:00
|
|
|
if (handler is RequestHandler) {
|
2017-02-25 00:16:31 +00:00
|
|
|
var result = await handler(req, res);
|
2017-06-30 13:49:45 +00:00
|
|
|
return await getHandlerResult(result, req, res);
|
2016-09-17 16:12:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (handler is Future) {
|
|
|
|
var result = await handler;
|
2017-06-30 13:49:45 +00:00
|
|
|
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);
|
2017-06-30 13:49:45 +00:00
|
|
|
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-06-30 13:49:45 +00:00
|
|
|
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-04-01 01:00:24 +00:00
|
|
|
return RequestContext.from(request, this).then((req) {
|
2017-09-22 04:48:22 +00:00
|
|
|
return req..injections.addAll(_injections ?? {});
|
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) {
|
|
|
|
if (properties.containsKey(key)) return properties[key];
|
|
|
|
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,
|
2017-06-30 13:49:45 +00:00
|
|
|
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
|
|
|
}
|
2017-06-30 13:49:45 +00:00
|
|
|
|
|
|
|
res.statusCode = e.statusCode;
|
2017-09-22 04:48:22 +00:00
|
|
|
|
2017-07-10 23:08:05 +00:00
|
|
|
if (req.headers.value(HttpHeaders.ACCEPT) == null ||
|
|
|
|
req.acceptsAll ||
|
|
|
|
req.accepts(ContentType.JSON) ||
|
|
|
|
req.accepts('application/javascript')) {
|
2017-06-30 13:49:45 +00:00
|
|
|
res.serialize(e.toMap(),
|
|
|
|
contentType: res.headers[HttpHeaders.CONTENT_TYPE] ??
|
|
|
|
ContentType.JSON.mimeType);
|
|
|
|
} else {
|
|
|
|
await errorHandler(e, req, res);
|
|
|
|
}
|
|
|
|
|
|
|
|
res.end();
|
2017-09-22 04:48:22 +00:00
|
|
|
return await sendResponse(request, req, res,
|
2017-06-30 13:49:45 +00:00
|
|
|
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-09-22 04:48:22 +00:00
|
|
|
var zoneSpec = await createZoneForRequest(request, req, res);
|
|
|
|
var zone = Zone.current.fork(specification: zoneSpec);
|
|
|
|
|
|
|
|
return zone.run(() async {
|
2017-01-20 22:40:48 +00:00
|
|
|
var req = await createRequestContext(request);
|
2017-08-15 23:01:16 +00:00
|
|
|
var res = await createResponseContext(request.response, req);
|
2017-08-03 16:40:21 +00:00
|
|
|
String requestedUrl;
|
2016-10-22 20:41:36 +00:00
|
|
|
|
2017-08-03 16:40:21 +00:00
|
|
|
// Faster way to get path
|
|
|
|
List<int> _path = request.uri.path.codeUnits;
|
2016-10-22 20:41:36 +00:00
|
|
|
|
2017-08-03 16:40:21 +00:00
|
|
|
// Remove trailing slashes
|
|
|
|
int lastSlash = -1;
|
2016-11-23 19:50:17 +00:00
|
|
|
|
2017-08-03 16:40:21 +00:00
|
|
|
for (int i = _path.length - 1; i >= 0; i--) {
|
|
|
|
if (_path[i] == $slash)
|
|
|
|
lastSlash = i;
|
|
|
|
else
|
|
|
|
break;
|
2017-01-14 15:39:11 +00:00
|
|
|
}
|
2016-10-22 20:41:36 +00:00
|
|
|
|
2017-08-03 16:40:21 +00:00
|
|
|
if (lastSlash > -1)
|
|
|
|
requestedUrl = new String.fromCharCodes(_path.take(lastSlash));
|
|
|
|
else
|
|
|
|
requestedUrl = new String.fromCharCodes(_path);
|
2016-12-19 01:38:23 +00:00
|
|
|
|
2017-08-03 16:40:21 +00:00
|
|
|
if (requestedUrl.isEmpty) requestedUrl = '/';
|
|
|
|
|
2017-08-28 15:29:27 +00:00
|
|
|
var tuple = _handlerCache.putIfAbsent(requestedUrl, () {
|
|
|
|
Router r = isProduction ? (_flattened ??= flatten(this)) : this;
|
2017-08-03 16:40:21 +00:00
|
|
|
var resolved =
|
2017-08-28 15:29:27 +00:00
|
|
|
r.resolveAll(requestedUrl, requestedUrl, method: req.method);
|
|
|
|
return new Tuple3(
|
|
|
|
new MiddlewarePipeline(resolved),
|
|
|
|
resolved.fold<Map>({}, (out, r) => out..addAll(r.allParams)),
|
|
|
|
resolved.isEmpty ? null : resolved.first.route.match(requestedUrl),
|
|
|
|
);
|
|
|
|
});
|
2017-08-03 16:40:21 +00:00
|
|
|
|
2017-09-22 04:48:22 +00:00
|
|
|
req.inject(Zone, zone);
|
|
|
|
req.inject(ZoneSpecification, zoneSpec);
|
2017-08-28 15:29:27 +00:00
|
|
|
req.inject(MiddlewarePipeline, tuple.item1);
|
|
|
|
req.params.addAll(tuple.item2);
|
|
|
|
req.inject(Match, tuple.item3);
|
2017-09-22 14:53:49 +00:00
|
|
|
req.inject(Stopwatch, new Stopwatch()..start());
|
2017-08-03 16:40:21 +00:00
|
|
|
|
2017-09-22 04:48:22 +00:00
|
|
|
var pipeline = tuple.item1.handlers;
|
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-08-03 16:40:21 +00:00
|
|
|
if (!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;
|
2017-06-30 13:49:45 +00:00
|
|
|
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-09-22 04:48:22 +00:00
|
|
|
});
|
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) {
|
2017-06-30 13:49:45 +00:00
|
|
|
_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) {
|
|
|
|
if (router is Angel) {
|
2017-08-28 15:29:27 +00:00
|
|
|
router..before.forEach(_add)..after.forEach(_add);
|
2017-04-01 01:00:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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-04-02 19:14:10 +00:00
|
|
|
if (_flattened == null) _flattened = flatten(this);
|
|
|
|
|
|
|
|
_walk(_flattened);
|
2017-08-03 16:40:21 +00:00
|
|
|
|
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,
|
2017-08-03 16:40:21 +00:00
|
|
|
{bool ignoreFinalizers: false}) {
|
|
|
|
if (res.willCloseItself) {
|
|
|
|
return new Future.value();
|
|
|
|
} else {
|
|
|
|
Future finalizers = ignoreFinalizers == true
|
|
|
|
? new Future.value()
|
|
|
|
: responseFinalizers.fold<Future>(
|
2017-08-28 15:29:27 +00:00
|
|
|
new Future.value(), (out, f) => out.then((_) => f(req, res)));
|
2017-01-20 22:40:48 +00:00
|
|
|
|
2017-04-02 19:14:10 +00:00
|
|
|
if (res.isOpen) res.end();
|
|
|
|
|
2017-01-20 22:40:48 +00:00
|
|
|
for (var key in res.headers.keys) {
|
|
|
|
request.response.headers.add(key, res.headers[key]);
|
|
|
|
}
|
|
|
|
|
|
|
|
request.response.headers
|
|
|
|
..chunkedTransferEncoding = res.chunked ?? true
|
|
|
|
..set(HttpHeaders.CONTENT_LENGTH, res.buffer.length);
|
|
|
|
|
2017-08-15 23:01:16 +00:00
|
|
|
List<int> outputBuffer = res.buffer.toBytes();
|
|
|
|
|
|
|
|
if (res.encoders.isNotEmpty) {
|
|
|
|
var allowedEncodings =
|
2017-08-28 15:29:27 +00:00
|
|
|
(req.headers[HttpHeaders.ACCEPT_ENCODING] ?? []).map((str) {
|
2017-08-15 23:01:16 +00:00
|
|
|
// Ignore quality specifications in accept-encoding
|
|
|
|
// ex. gzip;q=0.8
|
|
|
|
if (!str.contains(';')) return str;
|
|
|
|
return str.split(';')[0];
|
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-20 22:40:48 +00:00
|
|
|
request.response
|
|
|
|
..statusCode = res.statusCode
|
|
|
|
..cookies.addAll(res.cookies)
|
2017-08-15 23:01:16 +00:00
|
|
|
..add(outputBuffer);
|
2017-08-03 16:40:21 +00:00
|
|
|
|
2017-09-22 14:53:49 +00:00
|
|
|
return finalizers.then((_) => request.response.close()).then((_) {
|
|
|
|
if (logger != null) {
|
|
|
|
var sw = req.injections[Stopwatch];
|
|
|
|
sw?.stop();
|
|
|
|
logger.info(
|
|
|
|
"${res.statusCode} ${req.method} ${req.uri} (${sw?.elapsedMilliseconds ?? 'unknown'} ms)");
|
|
|
|
}
|
|
|
|
});
|
2017-01-20 22:40:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-18 03:27:23 +00:00
|
|
|
/// Applies an [AngelConfigurer] to this instance.
|
Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
2016-04-29 00:01:58 +00:00
|
|
|
Future configure(AngelConfigurer configurer) async {
|
|
|
|
await configurer(this);
|
2016-07-05 22:11:54 +00:00
|
|
|
|
2016-11-28 00:49:27 +00:00
|
|
|
if (configurer is Controller)
|
2017-08-28 15:29:27 +00:00
|
|
|
_onController.add(controllers[configurer.findExpose().path] = configurer);
|
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].
|
Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
2016-04-29 00:01:58 +00:00
|
|
|
@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
|
|
|
|
|
|
|
routable.controllers.forEach((k, v) {
|
2017-01-20 22:40:48 +00:00
|
|
|
var tail = k.toString().replaceAll(_straySlashes, '');
|
2016-11-28 00:49:27 +00:00
|
|
|
controllers['$head/$tail'.replaceAll(_straySlashes, '')] = v;
|
|
|
|
});
|
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;
|
|
|
|
});
|
2017-08-03 16:40:21 +00:00
|
|
|
|
|
|
|
_onController.whenInitialized(() {
|
|
|
|
routable.onController.listen(_onController.add);
|
|
|
|
});
|
2016-11-28 00:49:27 +00:00
|
|
|
}
|
|
|
|
|
Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
2016-04-29 00:01:58 +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);
|
Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
2016-04-29 00:01:58 +00:00
|
|
|
}
|
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-09-22 04:48:22 +00:00
|
|
|
|
|
|
|
createZoneForRequest = (request, req, res) async {
|
|
|
|
return new ZoneSpecification(
|
|
|
|
handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, error,
|
|
|
|
StackTrace stackTrace) {
|
|
|
|
var e = new AngelHttpException(error,
|
|
|
|
stackTrace: stackTrace, message: error?.toString());
|
|
|
|
return handleAngelHttpException(
|
|
|
|
e,
|
|
|
|
stackTrace,
|
|
|
|
req,
|
|
|
|
res,
|
|
|
|
request,
|
|
|
|
ignoreFinalizers: true,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
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) {
|
2017-08-03 16:40:21 +00:00
|
|
|
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
|
|
|
///
|
Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
2016-04-29 00:01:58 +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);
|
Angel.secure, fallback routes, 404, app.addRoute, app.all, services are a go (just missing params, i.e. $sort?), now have service.app, app.before, app.after, angel.configure now uses futures, errors are implemented
2016-04-29 00:01:58 +00:00
|
|
|
}
|
|
|
|
}
|
2016-12-31 02:00:52 +00:00
|
|
|
|
|
|
|
/// Predetermines what needs to be injected for a handler to run.
|
|
|
|
InjectionRequest preInject(Function handler) {
|
|
|
|
var injection = new InjectionRequest();
|
2017-02-25 00:16:31 +00:00
|
|
|
|
|
|
|
ClosureMirror closureMirror = reflect(handler);
|
2016-12-31 02:00:52 +00:00
|
|
|
|
2017-02-25 20:57:28 +00:00
|
|
|
if (closureMirror.function.parameters.isEmpty) return injection;
|
|
|
|
|
2016-12-31 02:00:52 +00:00
|
|
|
// Load parameters
|
|
|
|
for (var parameter in closureMirror.function.parameters) {
|
|
|
|
var name = MirrorSystem.getName(parameter.simpleName);
|
|
|
|
var type = parameter.type.reflectedType;
|
|
|
|
|
2017-01-20 22:11:20 +00:00
|
|
|
if (!parameter.isNamed) {
|
|
|
|
if (parameter.isOptional) injection.optional.add(name);
|
|
|
|
|
|
|
|
if (type == RequestContext || type == ResponseContext) {
|
|
|
|
injection.required.add(type);
|
|
|
|
} else if (name == 'req') {
|
|
|
|
injection.required.add(RequestContext);
|
|
|
|
} else if (name == 'res') {
|
|
|
|
injection.required.add(ResponseContext);
|
|
|
|
} else if (type == dynamic) {
|
|
|
|
injection.required.add(name);
|
|
|
|
} else {
|
|
|
|
injection.required.add([name, type]);
|
|
|
|
}
|
2016-12-31 02:00:52 +00:00
|
|
|
} else {
|
2017-01-20 22:11:20 +00:00
|
|
|
injection.named[name] = type;
|
2016-12-31 02:00:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return injection;
|
|
|
|
}
|