platform/lib/src/http/server.dart

344 lines
11 KiB
Dart
Raw Normal View History

library angel_framework.http.server;
import 'dart:async';
import 'dart:io';
import 'dart:math' show Random;
2016-09-17 16:12:25 +00:00
import 'dart:mirrors';
import 'package:json_god/json_god.dart' as god;
import 'angel_base.dart';
import 'angel_http_exception.dart';
import 'controller.dart';
import 'request_context.dart';
import 'response_context.dart';
import 'routable.dart';
import 'service.dart';
2016-09-17 16:12:25 +00:00
export 'package:container/container.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);
/// Handles an [AngelHttpException].
2016-09-17 16:12:25 +00:00
typedef Future AngelErrorHandler(
AngelHttpException err, RequestContext req, ResponseContext res);
/// A function that configures an [AngelBase] server in some way.
typedef Future AngelConfigurer(AngelBase app);
2016-04-18 03:27:23 +00:00
2016-02-28 13:11:17 +00:00
/// A powerful real-time/REST/MVC server class.
class Angel extends AngelBase {
2016-09-17 16:12:25 +00:00
var _afterProcessed = new StreamController<HttpRequest>.broadcast();
var _beforeProcessed = new StreamController<HttpRequest>.broadcast();
2016-09-19 06:52:21 +00:00
var _fatalErrorStream = new StreamController<Map>.broadcast();
2016-07-05 22:11:54 +00:00
var _onController = new StreamController<Controller>.broadcast();
2016-10-22 20:41:36 +00:00
final Random _rand = new Random.secure();
2016-09-17 16:12:25 +00:00
ServerGenerator _serverGenerator =
(address, port) async => await HttpServer.bind(address, port);
/// Fired after a request is processed. Always runs.
Stream<HttpRequest> get afterProcessed => _afterProcessed.stream;
2016-07-05 22:11:54 +00:00
/// Fired before a request is processed. Always runs.
Stream<HttpRequest> get beforeProcessed => _beforeProcessed.stream;
2016-07-05 22:11:54 +00:00
2016-09-19 06:52:21 +00:00
/// Fired on fatal errors.
Stream<Map> get fatalErrorStream => _fatalErrorStream.stream;
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;
/// Default error handler, show HTML error page
2016-09-17 16:12:25 +00:00
AngelErrorHandler _errorHandler =
(AngelHttpException e, req, ResponseContext res) {
2016-05-01 02:03:31 +00:00
res.header(HttpHeaders.CONTENT_TYPE, ContentType.HTML.toString());
res.status(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>");
for (String error in e.errors) {
res.write("<li>$error</li>");
}
res.write("</ul></body></html>");
res.end();
};
2016-09-21 05:10:21 +00:00
/// The handler currently configured to run on [AngelHttpException]s.
AngelErrorHandler get errorHandler => _errorHandler;
/// [RequestMiddleware] to be run before all requests.
List before = [];
/// [RequestMiddleware] to be run after all requests.
List after = [];
2016-04-18 03:27:23 +00:00
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-10-22 20:41:36 +00:00
/// Handles a server error.
_onError(e, [StackTrace stackTrace]) {
_fatalErrorStream.add({"error": e, "stack": stackTrace});
}
void _printDebug(x) {
if (debug) print(x);
}
String _randomString(int length) {
var codeUnits = new List.generate(length, (index) {
return _rand.nextInt(33) + 89;
});
return new String.fromCharCodes(codeUnits);
}
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 {
final host = address ?? InternetAddress.LOOPBACK_IP_V4;
2016-11-23 09:10:47 +00:00
this.httpServer = await _serverGenerator(host, port ?? 0);
return httpServer..listen(handleRequest);
}
2016-09-17 16:12:25 +00:00
/// Loads some base dependencies into the service container.
void bootstrapContainer() {
container.singleton(this, as: AngelBase);
container.singleton(this);
2016-09-18 02:59:06 +00:00
if (runtimeType != Angel) container.singleton(this, as: Angel);
2016-09-17 16:12:25 +00:00
}
2016-10-22 20:41:36 +00:00
Future<bool> executeHandler(
2016-09-17 16:12:25 +00:00
handler, RequestContext req, ResponseContext res) async {
if (handler is RequestMiddleware) {
var result = await handler(req, res);
2016-09-17 16:12:25 +00:00
if (result is bool)
return result == true;
else if (result != null) {
res.json(result);
return false;
} else
return res.isOpen;
2016-04-18 03:27:23 +00:00
}
if (handler is RequestHandler) {
2016-04-18 03:27:23 +00:00
await handler(req, res);
return res.isOpen;
2016-09-17 16:12:25 +00:00
}
if (handler is RawRequestHandler) {
2016-04-18 03:27:23 +00:00
var result = await handler(req.underlyingRequest);
if (result is bool)
return result == true;
else if (result != null) {
res.json(result);
return false;
} else
return true;
2016-09-17 16:12:25 +00:00
}
if (handler is Future) {
var result = await handler;
if (result is bool)
return result == true;
else if (result != null) {
res.json(result);
return false;
} else
return true;
}
if (handler is Function) {
var result = await runContained(handler, req, res);
if (result is bool)
return result == true;
else if (result != null) {
res.json(result);
return false;
} else
return true;
2016-09-17 16:12:25 +00:00
}
if (requestMiddleware.containsKey(handler)) {
2016-10-22 20:41:36 +00:00
return await executeHandler(requestMiddleware[handler], req, res);
2016-04-18 03:27:23 +00:00
}
2016-09-17 16:12:25 +00:00
res.willCloseItself = true;
res.underlyingResponse.write(god.serialize(handler));
await res.underlyingResponse.close();
return false;
2016-04-18 03:27:23 +00:00
}
2016-10-22 20:41:36 +00:00
Future handleRequest(HttpRequest request) async {
_beforeProcessed.add(request);
final req = await RequestContext.from(request, this);
final res = new ResponseContext(request.response, this);
String requestedUrl = request.uri
2016-11-23 09:10:47 +00:00
.path
2016-10-22 20:41:36 +00:00
.replaceAll(_straySlashes, '');
if (requestedUrl.isEmpty) requestedUrl = '/';
final route = resolve(requestedUrl, method: request.method);
2016-11-23 09:10:47 +00:00
_printDebug('Resolved ${requestedUrl} -> $route');
2016-10-22 20:41:36 +00:00
req.params.addAll(route?.parseParameters(requestedUrl) ?? {});
final handlerSequence = []..addAll(before);
if (route != null) handlerSequence.addAll(route.handlerSequence);
handlerSequence.addAll(after);
_printDebug('Handler sequence on $requestedUrl: $handlerSequence');
for (final handler in handlerSequence) {
try {
_printDebug('Executing handler: $handler');
final result = await executeHandler(handler, req, res);
_printDebug('Result: $result');
if (!result) {
_printDebug('Last executed handler: $handler');
break;
} else {
_printDebug(
'Handler completed successfully, did not terminate response: $handler');
}
} catch (e, st) {
_printDebug('Caught error in handler $handler: $e');
_printDebug(st);
if (e is AngelHttpException) {
// Special handling for AngelHttpExceptions :)
try {
res.status(e.statusCode);
String accept = request.headers.value(HttpHeaders.ACCEPT);
if (accept == "*/*" ||
accept.contains(ContentType.JSON.mimeType) ||
accept.contains("application/javascript")) {
res.json(e.toMap());
} else {
await _errorHandler(e, req, res);
}
_finalizeResponse(request, res);
} catch (_) {
// Todo: This exception needs to be caught as well.
}
} else {
// Todo: Uncaught exceptions need to be... Caught.
}
_onError(e, st);
break;
}
}
2016-05-01 01:42:52 +00:00
try {
2016-10-07 21:39:41 +00:00
_afterProcessed.add(request);
2016-10-22 20:41:36 +00:00
2016-05-01 01:42:52 +00:00
if (!res.willCloseItself) {
2016-10-22 20:41:36 +00:00
request.response.add(res.buffer.takeBytes());
2016-05-01 01:42:52 +00:00
await request.response.close();
}
} catch (e) {
2016-09-17 16:12:25 +00:00
failSilently(request, res);
2016-04-22 01:42:39 +00:00
}
}
2016-09-17 16:12:25 +00:00
// Run a function after injecting from service container
2016-09-18 02:59:06 +00:00
Future runContained(Function handler, RequestContext req, ResponseContext res,
{Map<String, dynamic> namedParameters,
Map<Type, dynamic> injecting}) async {
2016-09-17 16:12:25 +00:00
ClosureMirror closureMirror = reflect(handler);
List args = [];
for (ParameterMirror parameter in closureMirror.function.parameters) {
if (parameter.type.reflectedType == RequestContext)
args.add(req);
else if (parameter.type.reflectedType == ResponseContext)
args.add(res);
else {
// First, search to see if we can map this to a type
if (parameter.type.reflectedType != dynamic) {
2016-09-18 02:59:06 +00:00
args.add(container.make(parameter.type.reflectedType,
namedParameters: namedParameters, injecting: injecting));
2016-09-17 16:12:25 +00:00
} else {
String name = MirrorSystem.getName(parameter.simpleName);
2016-09-18 02:59:06 +00:00
if (req.params.containsKey(name))
args.add(req.params[name]);
else if (name == "req")
2016-09-17 16:12:25 +00:00
args.add(req);
else if (name == "res")
args.add(res);
else {
2016-09-18 02:59:06 +00:00
throw new Exception(
"Cannot resolve parameter '$name' within handler.");
2016-09-17 16:12:25 +00:00
}
}
}
}
return await closureMirror.apply(args).reflectee;
}
2016-04-18 03:27:23 +00:00
/// Applies an [AngelConfigurer] to this instance.
Future configure(AngelConfigurer configurer) async {
await configurer(this);
2016-07-05 22:11:54 +00:00
2016-09-17 16:12:25 +00:00
if (configurer is Controller) _onController.add(configurer);
2016-02-28 13:11:17 +00:00
}
2016-09-17 16:12:25 +00:00
/// Fallback when an error is thrown while handling a request.
void failSilently(HttpRequest request, ResponseContext res) {}
2016-02-28 13:11:17 +00:00
/// Starts the server.
void listen({InternetAddress address, int port: 3000}) {
runZoned(() async {
2016-04-18 03:27:23 +00:00
await startServer(address, port);
}, onError: _onError);
}
@override
use(Pattern path, Routable routable,
2016-10-22 20:41:36 +00:00
{bool hooked: true, String namespace: null}) {
if (routable is Service) {
routable.app = this;
}
2016-10-22 20:41:36 +00:00
return super.use(path, routable, hooked: hooked, namespace: namespace);
2016-02-28 13:11:17 +00:00
}
2016-06-21 22:56:04 +00:00
/// Registers a callback to run upon errors.
onError(AngelErrorHandler handler) {
_errorHandler = handler;
}
2016-04-22 01:42:39 +00:00
2016-10-22 20:41:36 +00:00
Angel({bool debug: false}) : super(debug: debug) {
2016-09-17 16:12:25 +00:00
bootstrapContainer();
}
2016-02-28 13:11:17 +00:00
2016-04-18 03:27:23 +00:00
/// 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.
Angel.secure(String certificateChainPath, String serverKeyPath,
2016-10-22 20:41:36 +00:00
{bool debug: false, String password})
: super(debug: debug) {
2016-09-17 16:12:25 +00:00
bootstrapContainer();
_serverGenerator = (InternetAddress address, int port) async {
var certificateChain =
2016-11-23 09:10:47 +00:00
Platform.script.resolve(certificateChainPath).toFilePath();
var serverKey = Platform.script.resolve(serverKeyPath).toFilePath();
var serverContext = new SecurityContext();
serverContext.useCertificateChain(certificateChain);
serverContext.usePrivateKey(serverKey,
password: password ?? _randomString(8));
return await HttpServer.bindSecure(address, port, serverContext);
};
}
}