Hostnamerouter done
This commit is contained in:
parent
ac71ffad0f
commit
2fdfb848f0
4 changed files with 194 additions and 14 deletions
43
example/hostname.dart
Normal file
43
example/hostname.dart
Normal 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)');
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ export 'controller.dart';
|
||||||
export 'driver.dart';
|
export 'driver.dart';
|
||||||
export 'env.dart';
|
export 'env.dart';
|
||||||
export 'hooked_service.dart';
|
export 'hooked_service.dart';
|
||||||
|
export 'hostname_parser.dart';
|
||||||
|
export 'hostname_router.dart';
|
||||||
export 'map_service.dart';
|
export 'map_service.dart';
|
||||||
export 'metadata.dart';
|
export 'metadata.dart';
|
||||||
export 'request_context.dart';
|
export 'request_context.dart';
|
||||||
|
|
|
@ -4,9 +4,16 @@ import 'package:string_scanner/string_scanner.dart';
|
||||||
/// Parses a string into a [RegExp] that is matched against hostnames.
|
/// Parses a string into a [RegExp] that is matched against hostnames.
|
||||||
class HostnameSyntaxParser {
|
class HostnameSyntaxParser {
|
||||||
final SpanScanner _scanner;
|
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() {
|
RegExp parse() {
|
||||||
var b = StringBuffer();
|
var b = StringBuffer();
|
||||||
|
@ -15,13 +22,11 @@ class HostnameSyntaxParser {
|
||||||
while (!_scanner.isDone) {
|
while (!_scanner.isDone) {
|
||||||
if (_scanner.scan('|')) {
|
if (_scanner.scan('|')) {
|
||||||
if (parts.isEmpty) {
|
if (parts.isEmpty) {
|
||||||
throw FormatException(
|
throw _formatExc('No hostname parts found before "|".');
|
||||||
'${_scanner.emptySpan.end.toolString}: No hostname parts found before "|".');
|
|
||||||
} else {
|
} else {
|
||||||
var next = _parseHostnamePart();
|
var next = _parseHostnamePart();
|
||||||
if (next == null) {
|
if (next == null) {
|
||||||
throw FormatException(
|
throw _formatExc('No hostname parts found after "|".');
|
||||||
'${_scanner.emptySpan.end.toolString}: No hostname parts found after "|".');
|
|
||||||
} else {
|
} else {
|
||||||
var prev = parts.removeLast();
|
var prev = parts.removeLast();
|
||||||
parts.addLast('(($prev)|($next))');
|
parts.addLast('(($prev)|($next))');
|
||||||
|
@ -30,6 +35,18 @@ class HostnameSyntaxParser {
|
||||||
} else {
|
} else {
|
||||||
var part = _parseHostnamePart();
|
var part = _parseHostnamePart();
|
||||||
if (part != null) {
|
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);
|
parts.add(part);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,23 +57,24 @@ class HostnameSyntaxParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (b.isEmpty) {
|
if (b.isEmpty) {
|
||||||
throw FormatException('Invalid or empty hostname.');
|
throw _formatExc('Invalid or empty hostname.');
|
||||||
} else {
|
} else {
|
||||||
return RegExp(b.toString(), caseSensitive: false);
|
return RegExp('^$b\$', caseSensitive: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _parseHostnamePart() {
|
String _parseHostnamePart({bool shouldThrow = true}) {
|
||||||
if (_scanner.scan('*.')) {
|
if (_scanner.scan('*.')) {
|
||||||
return r'([^$]+\.)?';
|
return r'([^$.]+\.)?';
|
||||||
} else if (_scanner.scan('*')) {
|
} else if (_scanner.scan('*')) {
|
||||||
return r'[^$]*';
|
return r'[^$]*';
|
||||||
|
} else if (_scanner.scan('+')) {
|
||||||
|
return r'[^$]+';
|
||||||
} else if (_scanner.scan(_safe)) {
|
} else if (_scanner.scan(_safe)) {
|
||||||
return _scanner.lastMatch[0];
|
return _scanner.lastMatch[0];
|
||||||
} else if (!_scanner.isDone) {
|
} else if (!_scanner.isDone && shouldThrow) {
|
||||||
var s = String.fromCharCode(_scanner.peekChar());
|
var s = String.fromCharCode(_scanner.peekChar());
|
||||||
throw FormatException(
|
throw _formatExc('Unexpected character "$s".');
|
||||||
'${_scanner.emptySpan.end.toolString}: Unexpected character "$s".');
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,122 @@
|
||||||
import 'dart:async';
|
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 'request_context.dart';
|
||||||
import 'response_context.dart';
|
import 'response_context.dart';
|
||||||
|
import 'routable.dart';
|
||||||
import 'server.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue