Hostnamerouter done

This commit is contained in:
thosakwe 2019-07-17 14:57:51 -04:00
parent ac71ffad0f
commit 2fdfb848f0
4 changed files with 194 additions and 14 deletions

43
example/hostname.dart Normal file
View file

@ -0,0 +1,43 @@
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:logging/logging.dart';
Future<void> apiConfigurer(Angel app) async {
app.get('/', (req, res) => 'Hello, API!');
app.fallback((req, res) {
return 'fallback on ${req.uri} (within the API)';
});
}
Future<void> frontendConfigurer(Angel app) async {
app.fallback((req, res) => '(usually an index page would be shown here.)');
}
main() async {
// Logging set up/boilerplate
Logger.root.onRecord.listen(print);
var app = Angel(logger: Logger('angel'));
var http = AngelHttp(app);
var multiHost = HostnameRouter.configure({
'api.localhost:3000': apiConfigurer,
'localhost:3000': frontendConfigurer,
});
app
..fallback(multiHost.handleRequest)
..fallback((req, res) {
res.write('Uncaught hostname: ${req.hostname}');
});
app.errorHandler = (e, req, res) {
print(e.message ?? e.error ?? e);
print(e.stackTrace);
return e.toJson();
};
await http.startServer('127.0.0.1', 3000);
print('Listening at ${http.uri}');
print('(See what happens when you visit http://localhost:3000 instead)');
}

View file

@ -3,6 +3,8 @@ export 'controller.dart';
export 'driver.dart';
export 'env.dart';
export 'hooked_service.dart';
export 'hostname_parser.dart';
export 'hostname_router.dart';
export 'map_service.dart';
export 'metadata.dart';
export 'request_context.dart';

View file

@ -4,9 +4,16 @@ import 'package:string_scanner/string_scanner.dart';
/// Parses a string into a [RegExp] that is matched against hostnames.
class HostnameSyntaxParser {
final SpanScanner _scanner;
var _safe = RegExp(r"[0-9a-zA-Z-_]+");
var _safe = RegExp(r"[0-9a-zA-Z-_:]+");
HostnameSyntaxParser(String hostname) : _scanner = SpanScanner(hostname);
HostnameSyntaxParser(String hostname)
: _scanner = SpanScanner(hostname, sourceUrl: hostname);
FormatException _formatExc(String message) {
var span = _scanner.lastSpan ?? _scanner.emptySpan;
return FormatException(
'${span.start.toolString}: $message\n' + span.highlight(color: true));
}
RegExp parse() {
var b = StringBuffer();
@ -15,13 +22,11 @@ class HostnameSyntaxParser {
while (!_scanner.isDone) {
if (_scanner.scan('|')) {
if (parts.isEmpty) {
throw FormatException(
'${_scanner.emptySpan.end.toolString}: No hostname parts found before "|".');
throw _formatExc('No hostname parts found before "|".');
} else {
var next = _parseHostnamePart();
if (next == null) {
throw FormatException(
'${_scanner.emptySpan.end.toolString}: No hostname parts found after "|".');
throw _formatExc('No hostname parts found after "|".');
} else {
var prev = parts.removeLast();
parts.addLast('(($prev)|($next))');
@ -30,6 +35,18 @@ class HostnameSyntaxParser {
} else {
var part = _parseHostnamePart();
if (part != null) {
if (_scanner.scan('.')) {
var subPart = _parseHostnamePart(shouldThrow: false);
while (subPart != null) {
part += '\\.' + subPart;
if (_scanner.scan('.')) {
subPart = _parseHostnamePart(shouldThrow: false);
} else {
break;
}
}
}
parts.add(part);
}
}
@ -40,23 +57,24 @@ class HostnameSyntaxParser {
}
if (b.isEmpty) {
throw FormatException('Invalid or empty hostname.');
throw _formatExc('Invalid or empty hostname.');
} else {
return RegExp(b.toString(), caseSensitive: false);
return RegExp('^$b\$', caseSensitive: false);
}
}
String _parseHostnamePart() {
String _parseHostnamePart({bool shouldThrow = true}) {
if (_scanner.scan('*.')) {
return r'([^$]+\.)?';
return r'([^$.]+\.)?';
} else if (_scanner.scan('*')) {
return r'[^$]*';
} else if (_scanner.scan('+')) {
return r'[^$]+';
} else if (_scanner.scan(_safe)) {
return _scanner.lastMatch[0];
} else if (!_scanner.isDone) {
} else if (!_scanner.isDone && shouldThrow) {
var s = String.fromCharCode(_scanner.peekChar());
throw FormatException(
'${_scanner.emptySpan.end.toolString}: Unexpected character "$s".');
throw _formatExc('Unexpected character "$s".');
} else {
return null;
}

View file

@ -1,5 +1,122 @@
import 'dart:async';
import 'package:string_scanner/string_scanner.dart';
import 'package:angel_container/angel_container.dart';
import 'package:angel_route/angel_route.dart';
import 'package:logging/logging.dart';
import 'env.dart';
import 'hostname_parser.dart';
import 'request_context.dart';
import 'response_context.dart';
import 'routable.dart';
import 'server.dart';
/// A utility that allows requests to be handled based on their
/// origin's hostname.
///
/// For example, an application could handle example.com and api.example.com
/// separately.
///
/// The provided patterns can be any `Pattern`. If a `String` is provided, a simple
/// grammar (see [HostnameSyntaxParser]) is used to create [RegExp].
///
/// For example:
/// * `example.com` -> `/example\.com/`
/// * `*.example.com` -> `/([^$.]\.)?example\.com/`
/// * `example.*` -> `/example\./[^$]*`
/// * `example.+` -> `/example\./[^$]+`
class HostnameRouter {
final Map<Pattern, Angel> _apps = {};
final Map<Pattern, FutureOr<Angel> Function()> _creators = {};
final List<Pattern> _patterns = [];
HostnameRouter(
{Map<Pattern, Angel> apps = const {},
Map<Pattern, FutureOr<Angel> Function()> creators = const {}}) {
Map<Pattern, V> _parseMap<V>(Map<Pattern, V> map) {
return map.map((p, c) {
Pattern pp;
if (p is String) {
pp = HostnameSyntaxParser(p).parse();
} else {
pp = p;
}
return MapEntry(pp, c);
});
}
apps ??= {};
creators ??= {};
apps = _parseMap(apps);
creators = _parseMap(creators);
var patterns = apps.keys.followedBy(creators.keys).toSet().toList();
_apps.addAll(apps);
_creators.addAll(creators);
_patterns.addAll(patterns);
// print(_creators);
}
factory HostnameRouter.configure(
Map<Pattern, FutureOr<void> Function(Angel)> configurers,
{Reflector reflector = const EmptyReflector(),
AngelEnvironment environment = angelEnv,
Logger logger,
bool allowMethodOverrides = true,
FutureOr<String> Function(dynamic) serializer,
ViewGenerator viewGenerator}) {
var creators = configurers.map((p, c) {
return MapEntry(p, () async {
var app = Angel(
reflector: reflector,
environment: environment,
logger: logger,
allowMethodOverrides: allowMethodOverrides,
serializer: serializer,
viewGenerator: viewGenerator);
await app.configure(c);
return app;
});
});
return HostnameRouter(creators: creators);
}
/// Attempts to handle a request, according to its hostname.
///
/// If none is matched, then `true` is returned.
/// Also returns `true` if all of the sub-app's handlers returned
/// `true`.
Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
if (req.hostname != null) {
for (var pattern in _patterns) {
// print('${req.hostname} vs $_creators');
if (pattern.allMatches(req.hostname).isNotEmpty) {
// Resolve the entire pipeline within the context of the selected app.
var app = _apps[pattern] ??= (await _creators[pattern]());
// print('App for ${req.hostname} = $app from $pattern');
// app.dumpTree();
var r = app.optimizedRouter;
var resolved =
r.resolveAbsolute(req.path, method: req.method);
var pipeline = MiddlewarePipeline<RequestHandler>(resolved);
// print('Pipeline: $pipeline');
for (var handler in pipeline.handlers) {
// print(handler);
// Avoid stack overflow.
if (handler == handleRequest) {
continue;
} else if (!await app.executeHandler(handler, req, res)) {
// print('$handler TERMINATED');
return false;
} else {
// print('$handler CONTINUED');
}
}
}
}
}
// Otherwise, return true.
return true;
}
}