Init shelf driver

This commit is contained in:
Tobe O 2019-10-14 11:28:50 -04:00
parent aacd91e267
commit b115aae975
11 changed files with 160 additions and 84 deletions

View file

@ -1,3 +1,7 @@
# 2.1.0
* `pedantic` lints.
* Add the `AngelShelf` driver class, which allows you to embed Angel within shelf.
# 2.0.0 # 2.0.0
* Removed `supportShelf`. * Removed `supportShelf`.

View file

@ -30,8 +30,8 @@ import 'package:shelf/shelf.dart' as shelf;
import 'api/api.dart'; import 'api/api.dart';
main() async { main() async {
var app = new Angel(); var app = Angel();
var http = new AngelHttp(app); var http = AngelHttp(app);
// Angel routes on top // Angel routes on top
await app.mountController<ApiController>(); await app.mountController<ApiController>();
@ -39,7 +39,7 @@ main() async {
// Re-route all other traffic to an // Re-route all other traffic to an
// existing application. // existing application.
app.fallback(embedShelf( app.fallback(embedShelf(
new shelf.Pipeline() shelf.Pipeline()
.addMiddleware(shelf.logRequests()) .addMiddleware(shelf.logRequests())
.addHandler(_echoRequest) .addHandler(_echoRequest)
)); ));
@ -62,7 +62,7 @@ handleRequest(shelf.Request request) {
var req = request.context['angel_shelf.request'] as RequestContext; var req = request.context['angel_shelf.request'] as RequestContext;
// ... And then interact with it. // ... And then interact with it.
req.container.registerNamedSingleton<Foo>('from_shelf', new Foo()); req.container.registerNamedSingleton<Foo>('from_shelf', Foo());
// `req.container` is also available. // `req.container` is also available.
var container = request.context['angel_shelf.container'] as Container; var container = request.context['angel_shelf.container'] as Container;

View file

@ -1,3 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer: analyzer:
strong-mode: strong-mode:
implicit-casts: false implicit-casts: false

View file

@ -2,11 +2,17 @@ import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel_framework/http.dart';
import 'package:angel_shelf/angel_shelf.dart'; import 'package:angel_shelf/angel_shelf.dart';
import 'package:logging/logging.dart';
import 'package:pretty_logging/pretty_logging.dart';
import 'package:shelf_static/shelf_static.dart'; import 'package:shelf_static/shelf_static.dart';
main() async { main() async {
var app = new Angel(); Logger.root
var http = new AngelHttp(app); ..level = Level.ALL
..onRecord.listen(prettyLog);
var app = Angel(logger: Logger('angel_shelf_demo'));
var http = AngelHttp(app);
// `shelf` request handler // `shelf` request handler
var shelfHandler = createStaticHandler('.', var shelfHandler = createStaticHandler('.',
@ -21,9 +27,9 @@ main() async {
return false; // End execution of handlers, so we don't proxy to dartlang.org when we don't need to. return false; // End execution of handlers, so we don't proxy to dartlang.org when we don't need to.
}); });
// Proxy any other request through to the static file handler // Pass any other request through to the static file handler
app.fallback(wrappedHandler); app.fallback(wrappedHandler);
await http.startServer(InternetAddress.loopbackIPv4, 8080); await http.startServer(InternetAddress.loopbackIPv4, 8080);
print('Proxying at ${http.uri}'); print('Running at ${http.uri}');
} }

View file

@ -1,15 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_framework/http2.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf.dart' as shelf;
import 'package:stream_channel/stream_channel.dart'; import 'package:stream_channel/stream_channel.dart';
/// Creates a [shelf.Request] analogous to the input [req]. /// Creates a [shelf.Request] analogous to the input [req].
/// ///
/// The new request's `context` will contain [req.container] as `angel_shelf.container`, as well as /// The request's `context` will contain [req.container] as `angel_shelf.container`, as well as
/// the provided [context], if any. /// the provided [context], if any.
/// ///
/// The context will also have the original request available as `angel_shelf.request`. /// The context will also have the original request available as `angel_shelf.request`.
@ -30,47 +28,26 @@ Future<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
String protocolVersion; String protocolVersion;
Uri requestedUri; Uri requestedUri;
if (req is HttpRequestContext && res is HttpResponseContext) { protocolVersion = '1.1';
protocolVersion = req.rawRequest.protocolVersion; requestedUri = Uri.parse('http://${req.hostname}');
requestedUri = req.rawRequest.requestedUri; requestedUri = requestedUri.replace(path: req.uri.path);
onHijack = (void hijack(StreamChannel<List<int>> channel)) { onHijack = (void hijack(StreamChannel<List<int>> channel)) {
new Future(() async { Future.sync(res.detach).then((_) {
var rs = res.detach(); var ctrl = StreamChannelController<List<int>>();
var socket = await rs.detachSocket(writeHeaders: false); if (req.hasParsedBody) {
var ctrl = new StreamChannelController<List<int>>(); req.body.listen(ctrl.local.sink.add,
var body = await req.parseRawRequestBuffer() ?? [];
ctrl.local.sink.add(body ?? []);
socket.listen(ctrl.local.sink.add,
onError: ctrl.local.sink.addError, onDone: ctrl.local.sink.close); onError: ctrl.local.sink.addError, onDone: ctrl.local.sink.close);
ctrl.local.stream.pipe(socket); } else {
hijack(ctrl.foreign); ctrl.local.sink.close();
}).catchError((e, st) { }
app.logger?.severe('An error occurred while hijacking a shelf request', ctrl.local.stream.pipe(res);
e, st as StackTrace); hijack(ctrl.foreign);
}); }).catchError((e, st) {
}; app.logger?.severe('An error occurred while hijacking a shelf request', e,
} else if (req is Http2RequestContext && res is Http2ResponseContext) { st as StackTrace);
protocolVersion = '2.0'; });
requestedUri = req.uri; };
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
new Future(() async {
var rs = await res.detach();
var ctrl = new StreamChannelController<List<int>>();
var body = await req.parseRawRequestBuffer() ?? [];
ctrl.local.sink.add(body ?? []);
ctrl.local.stream.listen(rs.sendData, onDone: rs.terminate);
hijack(ctrl.foreign);
}).catchError((e, st) {
stderr.writeln('An error occurred while hijacking a shelf request: $e');
stderr.writeln(st);
});
};
} else {
throw new UnsupportedError(
'`embedShelf` is only supported for HTTP and HTTP2 requests in Angel.');
}
var url = req.uri; var url = req.uri;
@ -78,12 +55,12 @@ Future<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
url = url.replace(path: url.path.substring(1)); url = url.replace(path: url.path.substring(1));
} }
return new shelf.Request(req.method, requestedUri, return shelf.Request(req.method, requestedUri,
protocolVersion: protocolVersion, protocolVersion: protocolVersion,
headers: headers, headers: headers,
handlerPath: handlerPath, handlerPath: handlerPath,
url: url, url: url,
body: (await req.parseRawRequestBuffer()) ?? [], body: req.body,
context: {'angel_shelf.request': req} context: {'angel_shelf.request': req}
..addAll({'angel_shelf.container': req.container}) ..addAll({'angel_shelf.container': req.container})
..addAll(context ?? {}), ..addAll(context ?? {}),

View file

@ -13,19 +13,21 @@ import 'convert.dart';
RequestHandler embedShelf(shelf.Handler handler, RequestHandler embedShelf(shelf.Handler handler,
{String handlerPath, {String handlerPath,
Map<String, Object> context, Map<String, Object> context,
bool throwOnNullResponse: false}) { bool throwOnNullResponse = false}) {
return (RequestContext req, ResponseContext res) async { return (RequestContext req, ResponseContext res) async {
var shelfRequest = await convertRequest(req, res, var shelfRequest = await convertRequest(req, res,
handlerPath: handlerPath, context: context); handlerPath: handlerPath, context: context);
try { try {
var result = await handler(shelfRequest); var result = await handler(shelfRequest);
if (result is! shelf.Response && result != null) return result; if (result is! shelf.Response && result != null) return result;
if (result == null && throwOnNullResponse == true) if (result == null && throwOnNullResponse == true) {
throw new AngelHttpException('Internal Server Error'); throw AngelHttpException('Internal Server Error');
}
await mergeShelfResponse(result, res); await mergeShelfResponse(result, res);
return false; return false;
} on shelf.HijackException { } on shelf.HijackException {
// On hijack, do nothing, because the hijack handlers already call res.detach(); // On hijack, do nothing, because the hijack handlers already call res.detach();
return null;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }

View file

View file

@ -0,0 +1,77 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_container/angel_container.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:mock_request/mock_request.dart';
import 'package:shelf/shelf.dart' as shelf;
class ShelfRequestContext extends RequestContext {
@override
final Angel app;
@override
final Container container;
@override
final shelf.Request rawRequest;
@override
final String path;
List<Cookie> _cookies;
@override
final MockHttpHeaders headers = MockHttpHeaders();
ShelfRequestContext(this.app, this.container, this.rawRequest, this.path) {
rawRequest.headers.forEach(headers.add);
}
@override
Stream<List<int>> get body => rawRequest.read();
@override
List<Cookie> get cookies {
if (_cookies == null) {
// Parse cookies
_cookies = [];
var value = headers.value('cookie');
if (value != null) {
var cookieStrings = value.split(';').map((s) => s.trim());
for (var cookieString in cookieStrings) {
try {
_cookies.add(Cookie.fromSetCookieValue(cookieString));
} catch (_) {
// Ignore malformed cookies, and just don't add them to the container.
}
}
}
}
return _cookies;
}
@override
String get hostname => rawRequest.headers['host'];
@override
String get method {
if (!app.allowMethodOverrides) {
return originalMethod;
} else {
return headers.value('x-http-method-override')?.toUpperCase() ??
originalMethod;
}
}
@override
String get originalMethod => rawRequest.method;
@override
// TODO: implement remoteAddress
InternetAddress get remoteAddress => null;
@override
// TODO: implement session
HttpSession get session => null;
@override
Uri get uri => rawRequest.url;
}

View file

View file

@ -2,7 +2,7 @@ author: Tobe O <thosakwe@gmail.com>
description: Shelf interop with Angel. Use this to wrap existing server code. description: Shelf interop with Angel. Use this to wrap existing server code.
homepage: https://github.com/angel-dart/shelf homepage: https://github.com/angel-dart/shelf
name: angel_shelf name: angel_shelf
version: 2.0.0 version: 2.1.0
dependencies: dependencies:
angel_framework: ^2.0.0-alpha angel_framework: ^2.0.0-alpha
path: ^1.0.0 path: ^1.0.0
@ -10,6 +10,8 @@ dependencies:
stream_channel: ^1.0.0 stream_channel: ^1.0.0
dev_dependencies: dev_dependencies:
angel_test: ^2.0.0-alpha angel_test: ^2.0.0-alpha
pedantic: ^1.0.0
pretty_logging: ^1.0.0
shelf_static: ^0.2.8 shelf_static: ^0.2.8
test: ^1.0.0 test: ^1.0.0
environment: environment:

View file

@ -7,6 +7,7 @@ import 'package:angel_shelf/angel_shelf.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel_test/angel_test.dart';
import 'package:charcode/charcode.dart'; import 'package:charcode/charcode.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:pretty_logging/pretty_logging.dart';
import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf.dart' as shelf;
import 'package:stream_channel/stream_channel.dart'; import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -16,15 +17,24 @@ main() {
HttpServer server; HttpServer server;
String url; String url;
String _path(String p) {
return Uri(
scheme: 'http',
host: server.address.address,
port: server.port,
path: p)
.toString();
}
setUp(() async { setUp(() async {
var handler = new shelf.Pipeline().addHandler((shelf.Request request) { var handler = shelf.Pipeline().addHandler((shelf.Request request) {
if (request.url.path == 'two') if (request.url.path == 'two') {
return new shelf.Response(200, body: json.encode(2)); return shelf.Response(200, body: json.encode(2));
else if (request.url.path == 'error') } else if (request.url.path == 'error') {
throw new AngelHttpException.notFound(); throw AngelHttpException.notFound();
else if (request.url.path == 'status') } else if (request.url.path == 'status') {
return new shelf.Response.notModified(headers: {'foo': 'bar'}); return shelf.Response.notModified(headers: {'foo': 'bar'});
else if (request.url.path == 'hijack') { } else if (request.url.path == 'hijack') {
request.hijack((StreamChannel<List<int>> channel) { request.hijack((StreamChannel<List<int>> channel) {
var sink = channel.sink; var sink = channel.sink;
sink.add(utf8.encode('HTTP/1.1 200 OK\r\n')); sink.add(utf8.encode('HTTP/1.1 200 OK\r\n'));
@ -32,25 +42,22 @@ main() {
sink.add(utf8.encode(json.encode({'error': 'crime'}))); sink.add(utf8.encode(json.encode({'error': 'crime'})));
sink.close(); sink.close();
}); });
} else if (request.url.path == 'throw')
return null; return null;
else } else if (request.url.path == 'throw') {
return new shelf.Response.ok('Request for "${request.url}"'); return null;
} else {
return shelf.Response.ok('Request for "${request.url}"');
}
}); });
var app = new Angel(); var logger = Logger.detached('angel_shelf')..onRecord.listen(prettyLog);
var http = new AngelHttp(app); var app = Angel(logger: logger);
var http = AngelHttp(app);
app.get('/angel', (req, res) => 'Angel'); app.get('/angel', (req, res) => 'Angel');
app.fallback(embedShelf(handler, throwOnNullResponse: true)); app.fallback(embedShelf(handler, throwOnNullResponse: true));
app.logger = new Logger.detached('angel_shelf')
..onRecord.listen((rec) {
stdout.writeln(rec);
if (rec.error != null) stdout.writeln(rec.error);
if (rec.stackTrace != null) stdout.writeln(rec.stackTrace);
});
server = await http.startServer(InternetAddress.loopbackIPv4, 0); server = await http.startServer(InternetAddress.loopbackIPv4, 0);
client = new c.Rest(url = http.uri.toString()); client = c.Rest(url = http.uri.toString());
}); });
tearDown(() async { tearDown(() async {
@ -59,24 +66,24 @@ main() {
}); });
test('expose angel side', () async { test('expose angel side', () async {
var response = await client.get('/angel'); var response = await client.get(_path('/angel'));
expect(json.decode(response.body), equals('Angel')); expect(json.decode(response.body), equals('Angel'));
}); });
test('expose shelf side', () async { test('expose shelf side', () async {
var response = await client.get('/foo'); var response = await client.get(_path('/foo'));
expect(response, hasStatus(200)); expect(response, hasStatus(200));
expect(response.body, equals('Request for "foo"')); expect(response.body, equals('Request for "foo"'));
}); });
test('shelf can return arbitrary values', () async { test('shelf can return arbitrary values', () async {
var response = await client.get('/two'); var response = await client.get(_path('/two'));
expect(response, isJson(2)); expect(response, isJson(2));
}); });
test('shelf can hijack', () async { test('shelf can hijack', () async {
try { try {
var client = new HttpClient(); var client = HttpClient();
var rq = await client.openUrl('GET', Uri.parse('$url/hijack')); var rq = await client.openUrl('GET', Uri.parse('$url/hijack'));
var rs = await rq.close(); var rs = await rq.close();
var body = await rs.cast<List<int>>().transform(utf8.decoder).join(); var body = await rs.cast<List<int>>().transform(utf8.decoder).join();
@ -90,17 +97,17 @@ main() {
}, skip: ''); }, skip: '');
test('shelf can set status code', () async { test('shelf can set status code', () async {
var response = await client.get('/status'); var response = await client.get(_path('/status'));
expect(response, allOf(hasStatus(304), hasHeader('foo', 'bar'))); expect(response, allOf(hasStatus(304), hasHeader('foo', 'bar')));
}); });
test('shelf can throw error', () async { test('shelf can throw error', () async {
var response = await client.get('/error'); var response = await client.get(_path('/error'));
expect(response, hasStatus(404)); expect(response, hasStatus(404));
}); });
test('throw on null', () async { test('throw on null', () async {
var response = await client.get('/throw'); var response = await client.get(_path('/throw'));
expect(response, hasStatus(500)); expect(response, hasStatus(500));
}); });
} }