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 '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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue