Begin rolling in http2

This commit is contained in:
Tobe O 2018-11-07 23:11:10 -05:00
parent 681e2ac596
commit 2d168dd3aa
21 changed files with 1642 additions and 332 deletions

View file

@ -0,0 +1,43 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_framework/http2.dart';
import 'package:file/local.dart';
import 'package:logging/logging.dart';
import 'pretty_logging.dart';
main() async {
var app = new Angel();
app.logger = new Logger('angel')..onRecord.listen(prettyLog);
var publicDir = new Directory('example/public');
var indexHtml =
const LocalFileSystem().file(publicDir.uri.resolve('body_parsing.html'));
app.get('/', (req, res) => res.streamFile(indexHtml));
app.post('/', (req, res) => req.parseBody());
var ctx = new SecurityContext()
..useCertificateChain('dev.pem')
..usePrivateKey('dev.key', password: 'dartdart');
try {
ctx.setAlpnProtocols(['h2'], true);
} catch (e, st) {
app.logger.severe(
'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.',
e,
st,
);
}
var http1 = new AngelHttp(app);
var http2 = new AngelHttp2(app, ctx);
// HTTP/1.x requests will fallback to `AngelHttp`
http2.onHttp1.listen(http1.handleRequest);
var server = await http2.startServer('127.0.0.1', 3000);
print('Listening at https://${server.address.address}:${server.port}');
}

29
example/http2/dev.key Normal file
View file

@ -0,0 +1,29 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP
xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE
ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5
Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1
qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc
gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU
0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF
gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS
oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn
oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ
kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh
zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa
J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe
d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX
TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76
ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW
HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN
goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im
EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j
ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS
YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3
q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT
Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z
Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH
QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE
xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w
AUukhVtTNn4=
-----END ENCRYPTED PRIVATE KEY-----

57
example/http2/dev.pem Normal file
View file

@ -0,0 +1,57 @@
-----BEGIN CERTIFICATE-----
MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa
MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq
Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu
EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki
we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb
N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI
7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg
hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O
BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS
YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd
AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4
CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM
4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG
MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5
V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx
EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP
DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE
YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu
MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7
B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd
IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb
oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC
cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8
x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ
e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX
NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4
0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh
FKvRDxsW
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv
dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw
siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj
kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2
hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV
DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU
ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD
26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ
lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X
J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/
uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE
4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k
t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W
r6AL284qtw==
-----END CERTIFICATE-----

43
example/http2/main.dart Normal file
View file

@ -0,0 +1,43 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_framework/http2.dart';
import 'package:logging/logging.dart';
import 'pretty_logging.dart';
main() async {
var app = new Angel()
..encoders.addAll({
'gzip': gzip.encoder,
'deflate': zlib.encoder,
});
app.logger = new Logger('angel')..onRecord.listen(prettyLog);
app.get('/', (_, __) => 'Hello HTTP/2!!!');
app.fallback((req, res) => throw new AngelHttpException.notFound(
message: 'No file exists at ${req.uri.path}'));
var ctx = new SecurityContext()
..useCertificateChain('dev.pem')
..usePrivateKey('dev.key', password: 'dartdart');
try {
ctx.setAlpnProtocols(['h2'], true);
} catch (e, st) {
app.logger.severe(
'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.',
e,
st,
);
}
var http1 = new AngelHttp(app);
var http2 = new AngelHttp2(app, ctx);
// HTTP/1.x requests will fallback to `AngelHttp`
http2.onHttp1.listen(http1.handleRequest);
var server = await http2.startServer('127.0.0.1', 3000);
print('Listening at https://${server.address.address}:${server.port}');
}

View file

@ -0,0 +1,9 @@
import 'package:logging/logging.dart';
/// Prints the contents of a [LogRecord] with pretty colors.
void prettyLog(LogRecord record) {
print(record.toString());
if (record.error != null) print(record.error.toString());
if (record.stackTrace != null) print(record.stackTrace.toString());
}

View file

@ -0,0 +1,27 @@
window.onload = function() {
var $app = document.getElementById('app');
var $loading = document.getElementById('loading');
$app.removeChild($loading);
var $button = document.createElement('button');
var $h1 = document.createElement('h1');
$app.appendChild($h1);
$app.appendChild($button);
$h1.textContent = '~Angel HTTP/2 server push~';
$button.textContent = 'Change color';
$button.onclick = function() {
var color = Math.floor(Math.random() * 0xffffff);
$h1.style.color = '#' + color.toString(16);
};
$button.onclick();
window.setInterval($button.onclick, 2000);
var rotation = 0;
window.setInterval(function() {
rotation += .6;
$button.style.transform = 'rotate(' + rotation + 'deg)';
}, 10);
};

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Angel HTTP/2</title>
<style>
input:not([type="submit"]) {
margin-bottom: 2em;
}
</style>
</head>
<body>
<form action="/" method="post">
<input name="name" placeholder="Your Name" type="text">
<input name="password" placeholder="Secret Field" type="password">
<input name="age" placeholder="Your Age" type="number">
<input name="birthday" placeholder="Your Birthday" type="datetime-local">
<input type="submit" value="Submit">
</form>
</body>
</html>

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Angel HTTP/2</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app"><span id="loading">Loading...</span></div>
<script src="app.js"></script>
</body>
</html>

View file

@ -0,0 +1,20 @@
button {
margin-top: 2em;
}
html, body {
background-color: #000;
}
#app {
text-align: center;
}
#app h1 {
font-style: italic;
text-decoration: underline;
}
#loading {
color: red;
}

View file

@ -0,0 +1,59 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_framework/http2.dart';
import 'package:file/local.dart';
import 'package:logging/logging.dart';
import 'pretty_logging.dart';
main() async {
var app = new Angel();
app.logger = new Logger('angel')..onRecord.listen(prettyLog);
var publicDir = new Directory('example/public');
var indexHtml =
const LocalFileSystem().file(publicDir.uri.resolve('index.html'));
var styleCss =
const LocalFileSystem().file(publicDir.uri.resolve('style.css'));
var appJs = const LocalFileSystem().file(publicDir.uri.resolve('app.js'));
// Send files when requested
app
..get('/style.css', (req, res) => res.streamFile(styleCss))
..get('/app.js', (req, res) => res.streamFile(appJs));
app.get('/', (req, res) async {
// Regardless of whether we pushed other resources, let's still send /index.html.
await res.streamFile(indexHtml);
// If the client is HTTP/2 and supports server push, let's
// send down /style.css and /app.js as well, to improve initial load time.
if (res is Http2ResponseContext && res.canPush) {
await res.push('/style.css').streamFile(styleCss);
await res.push('/app.js').streamFile(appJs);
}
});
var ctx = new SecurityContext()
..useCertificateChain('dev.pem')
..usePrivateKey('dev.key', password: 'dartdart');
try {
ctx.setAlpnProtocols(['h2'], true);
} catch (e, st) {
app.logger.severe(
'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.',
e,
st,
);
}
var http1 = new AngelHttp(app);
var http2 = new AngelHttp2(app, ctx);
// HTTP/1.x requests will fallback to `AngelHttp`
http2.onHttp1.listen(http1.handleRequest);
var server = await http2.startServer('127.0.0.1', 3000);
print('Listening at https://${server.address.address}:${server.port}');
}

3
lib/http2.dart Normal file
View file

@ -0,0 +1,3 @@
export 'src/http2/angel_http2.dart';
export 'src/http2/http2_request_context.dart';
export 'src/http2/http2_response_context.dart';

View file

@ -1,5 +1,6 @@
export 'anonymous_service.dart';
export 'controller.dart';
export 'driver.dart';
export 'hooked_service.dart';
export 'map_service.dart';
export 'metadata.dart';

357
lib/src/core/driver.dart Normal file
View file

@ -0,0 +1,357 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io' show stderr, Cookie;
import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:angel_route/angel_route.dart';
import 'package:combinator/combinator.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:tuple/tuple.dart';
import 'core.dart';
/// Base driver class for Angel implementations.
///
/// Powers both AngelHttp and AngelHttp2.
abstract class Driver<
Request,
Response,
Server extends Stream<Request>,
RequestContextType extends RequestContext,
ResponseContextType extends ResponseContext> {
final Angel app;
final bool useZone;
bool _closed = false;
Server _server;
StreamSubscription<Request> _sub;
/// The function used to bind this instance to a server..
final Future<Server> Function(dynamic, int) serverGenerator;
Driver(this.app, this.serverGenerator, {this.useZone: true});
/// The path at which this server is listening for requests.
Uri get uri;
/// The native server running this instance.
Server get server => _server;
/// Starts, and returns the server.
Future<Server> startServer([address, int port]) {
var host = address ?? '127.0.0.1';
return serverGenerator(host, port ?? 0).then((server) {
_server = server;
return Future.wait(app.startupHooks.map(app.configure)).then((_) {
app.optimizeForProduction();
_sub = server.listen((request) =>
handleRawRequest(request, createResponseFromRawRequest(request)));
return _server;
});
});
}
/// Shuts down the underlying server.
Future<Server> close() {
if (_closed) return new Future.value(_server);
_closed = true;
_sub?.cancel();
return app.close().then((_) =>
Future.wait(app.shutdownHooks.map(app.configure)).then((_) => _server));
}
Future<RequestContextType> createRequestContext(
Request request, Response response);
Future<ResponseContextType> createResponseContext(
Request request, Response response,
[RequestContextType correspondingRequest]);
void setHeader(Response response, String key, String value);
void setContentLength(Response response, int length);
void setChunkedEncoding(Response response, bool value);
void setStatusCode(Response response, int value);
void addCookies(Response response, Iterable<Cookie> cookies);
void writeStringToResponse(Response response, String value);
void writeToResponse(Response response, List<int> data);
Uri getUriFromRequest(Request request);
Future closeResponse(Response response);
Response createResponseFromRawRequest(Request request);
/// Handles a single request.
Future handleRawRequest(Request request, Response response) {
return createRequestContext(request, response).then((req) {
return createResponseContext(request, response, req).then((res) {
handle() {
var path = req.path;
if (path == '/') path = '';
Tuple3<List, Map<String, dynamic>, ParseResult<Map<String, dynamic>>>
resolveTuple() {
Router r = app.optimizedRouter;
var resolved =
r.resolveAbsolute(path, method: req.method, strip: false);
return new Tuple3(
new MiddlewarePipeline(resolved).handlers,
resolved.fold<Map<String, dynamic>>(
<String, dynamic>{}, (out, r) => out..addAll(r.allParams)),
resolved.isEmpty ? null : resolved.first.parseResult,
);
}
var cacheKey = req.method + path;
var tuple = app.isProduction
? app.handlerCache.putIfAbsent(cacheKey, resolveTuple)
: resolveTuple();
req.params.addAll(tuple.item2);
req.container.registerSingleton<ParseResult<Map<String, dynamic>>>(
tuple.item3);
req.container.registerSingleton<ParseResult>(tuple.item3);
if (!app.isProduction && app.logger != null) {
req.container
.registerSingleton<Stopwatch>(new Stopwatch()..start());
}
var pipeline = tuple.item1;
Future Function() runPipeline;
for (var handler in pipeline) {
if (handler == null) break;
if (runPipeline == null)
runPipeline = () =>
Future.sync(() => app.executeHandler(handler, req, res));
else {
var current = runPipeline;
runPipeline = () => current().then((result) => !res.isOpen
? new Future.value(result)
: app.executeHandler(handler, req, res));
}
}
return runPipeline == null
? sendResponse(request, response, req, res)
: runPipeline()
.then((_) => sendResponse(request, response, req, res));
}
if (useZone == false) {
Future f;
try {
f = handle();
} catch (e, st) {
f = Future.error(e, st);
}
return f.catchError((e, StackTrace st) {
if (e is FormatException)
throw new AngelHttpException.badRequest(message: e.message)
..stackTrace = st;
throw new AngelHttpException(e,
stackTrace: st,
statusCode: 500,
message: e?.toString() ?? '500 Internal Server Error');
}, test: (e) => e is! AngelHttpException).catchError(
(ee, StackTrace st) {
var e = ee as AngelHttpException;
if (app.logger != null) {
var error = e.error ?? e;
var trace =
new Trace.from(e.stackTrace ?? StackTrace.current).terse;
app.logger.severe(e.message ?? e.toString(), error, trace);
}
return handleAngelHttpException(
e, e.stackTrace ?? st, req, res, request, response);
});
} else {
var zoneSpec = new ZoneSpecification(
print: (self, parent, zone, line) {
if (app.logger != null)
app.logger.info(line);
else
parent.print(zone, line);
},
handleUncaughtError: (self, parent, zone, error, stackTrace) {
var trace =
new Trace.from(stackTrace ?? StackTrace.current).terse;
return new Future(() {
AngelHttpException e;
if (error is FormatException) {
e = new AngelHttpException.badRequest(message: error.message);
} else if (error is AngelHttpException) {
e = error;
} else {
e = new AngelHttpException(error,
stackTrace: stackTrace,
message:
error?.toString() ?? '500 Internal Server Error');
}
if (app.logger != null) {
app.logger.severe(e.message ?? e.toString(), error, trace);
}
return handleAngelHttpException(
e, trace, req, res, request, response);
}).catchError((e, StackTrace st) {
var trace = new Trace.from(st ?? StackTrace.current).terse;
var uri = getUriFromRequest(request);
closeResponse(response);
// Ideally, we won't be in a position where an absolutely fatal error occurs,
// but if so, we'll need to log it.
if (app.logger != null) {
app.logger.severe(
'Fatal error occurred when processing $uri.', e, trace);
} else {
stderr
..writeln('Fatal error occurred when processing '
'$uri:')
..writeln(e)
..writeln(trace);
}
});
},
);
var zone = Zone.current.fork(specification: zoneSpec);
req.container.registerSingleton<Zone>(zone);
req.container.registerSingleton<ZoneSpecification>(zoneSpec);
// If a synchronous error is thrown, it's not caught by `zone.run`,
// so use a try/catch, and recover when need be.
try {
return zone.run(handle);
} catch (e, st) {
zone.handleUncaughtError(e, st);
return Future.value();
}
}
});
});
}
/// Handles an [AngelHttpException].
Future handleAngelHttpException(
AngelHttpException e,
StackTrace st,
RequestContext req,
ResponseContext res,
Request request,
Response response,
{bool ignoreFinalizers: false}) {
if (req == null || res == null) {
try {
app.logger?.severe(e, st);
setStatusCode(response, 500);
writeStringToResponse(response, '500 Internal Server Error');
closeResponse(response);
} finally {
return null;
}
}
Future handleError;
if (!res.isOpen)
handleError = new Future.value();
else {
res.statusCode = e.statusCode;
handleError =
new Future.sync(() => app.errorHandler(e, req, res)).then((result) {
return app.executeHandler(result, req, res).then((_) => res.close());
});
}
return handleError.then((_) => sendResponse(request, response, req, res,
ignoreFinalizers: ignoreFinalizers == true));
}
/// Sends a response.
Future sendResponse(Request request, Response response, RequestContext req,
ResponseContext res,
{bool ignoreFinalizers: false}) {
void _cleanup(_) {
if (!app.isProduction && app.logger != null) {
var sw = req.container.make<Stopwatch>();
app.logger.info(
"${res.statusCode} ${req.method} ${req.uri} (${sw?.elapsedMilliseconds ?? 'unknown'} ms)");
}
}
if (!res.isBuffered) return res.close().then(_cleanup);
Future finalizers = ignoreFinalizers == true
? new Future.value()
: app.responseFinalizers.fold<Future>(
new Future.value(), (out, f) => out.then((_) => f(req, res)));
return finalizers.then((_) {
if (res.isOpen) res.close();
for (var key in res.headers.keys) {
setHeader(response, key, res.headers[key]);
}
setContentLength(response, res.buffer.length);
setChunkedEncoding(response, res.chunked ?? true);
List<int> outputBuffer = res.buffer.toBytes();
if (res.encoders.isNotEmpty) {
var allowedEncodings = req.headers
.value('accept-encoding')
?.split(',')
?.map((s) => s.trim())
?.where((s) => s.isNotEmpty)
?.map((str) {
// Ignore quality specifications in accept-encoding
// ex. gzip;q=0.8
if (!str.contains(';')) return str;
return str.split(';')[0];
});
if (allowedEncodings != null) {
for (var encodingName in allowedEncodings) {
Converter<List<int>, List<int>> encoder;
String key = encodingName;
if (res.encoders.containsKey(encodingName))
encoder = res.encoders[encodingName];
else if (encodingName == '*') {
encoder = res.encoders[key = res.encoders.keys.first];
}
if (encoder != null) {
setHeader(response, 'content-encoding', key);
outputBuffer = res.encoders[key].convert(outputBuffer);
setContentLength(response, outputBuffer.length);
break;
}
}
}
}
setStatusCode(response, res.statusCode);
addCookies(response, res.cookies);
writeToResponse(response, outputBuffer);
return closeResponse(response).then(_cleanup);
});
}
}

View file

@ -2,19 +2,13 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io'
show
stderr,
Cookie,
HttpRequest,
HttpResponse,
HttpServer,
Platform,
SecurityContext;
import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:angel_route/angel_route.dart';
import 'package:combinator/combinator.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:tuple/tuple.dart';
import 'package:angel_framework/angel_framework.dart';
import '../core/core.dart';
import 'http_request_context.dart';
import 'http_response_context.dart';
@ -22,41 +16,32 @@ import 'http_response_context.dart';
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
/// Adapts `dart:io`'s [HttpServer] to serve Angel.
class AngelHttp {
final Angel app;
final bool useZone;
bool _closed = false;
HttpServer _server;
Future<HttpServer> Function(dynamic, int) _serverGenerator = HttpServer.bind;
StreamSubscription<HttpRequest> _sub;
class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
HttpRequestContext, HttpResponseContext> {
@override
Uri get uri =>
new Uri(scheme: 'http', host: server.address.address, port: server.port);
AngelHttp(this.app, {this.useZone: false});
AngelHttp._(Angel app,
Future<HttpServer> Function(dynamic, int) serverGenerator, bool useZone)
: super(app, serverGenerator, useZone: useZone);
/// The path at which this server is listening for requests.
Uri get uri => new Uri(
scheme: 'http', host: _server.address.address, port: _server.port);
/// The function used to bind this instance to an HTTP server.
Future<HttpServer> Function(dynamic, int) get serverGenerator =>
_serverGenerator;
factory AngelHttp(Angel app, {bool useZone: true}) {
return new AngelHttp._(app, HttpServer.bind, useZone);
}
/// An instance mounted on a server started by the [serverGenerator].
factory AngelHttp.custom(
Angel app, Future<HttpServer> Function(dynamic, int) serverGenerator,
{bool useZone: true}) {
return new AngelHttp(app, useZone: useZone)
.._serverGenerator = serverGenerator;
return new AngelHttp._(app, serverGenerator, useZone);
}
factory AngelHttp.fromSecurityContext(Angel app, SecurityContext context,
{bool useZone: true}) {
var http = new AngelHttp(app, useZone: useZone);
http._serverGenerator = (address, int port) {
return new AngelHttp._(app, (address, int port) {
return HttpServer.bindSecure(address, port, context);
};
return http;
}, useZone);
}
/// Creates an HTTPS server.
@ -73,316 +58,70 @@ class AngelHttp {
var serverContext = new SecurityContext();
serverContext.useCertificateChain(certificateChain, password: password);
serverContext.usePrivateKey(serverKey, password: password);
return new AngelHttp.fromSecurityContext(app, serverContext,
useZone: useZone);
}
/// The native HttpServer running this instance.
HttpServer get httpServer => _server;
/// Use [server] instead.
@deprecated
HttpServer get httpServer => server;
/// Starts the server.
///
/// Returns false on failure; otherwise, returns the HttpServer.
Future<HttpServer> startServer([address, int port]) {
var host = address ?? '127.0.0.1';
return _serverGenerator(host, port ?? 0).then((server) {
_server = server;
return Future.wait(app.startupHooks.map(app.configure)).then((_) {
app.optimizeForProduction();
_sub = _server.listen(handleRequest);
return _server;
});
});
}
Future handleRequest(HttpRequest request) =>
handleRawRequest(request, request.response);
/// Shuts down the underlying server.
Future<HttpServer> close() {
if (_closed) return new Future.value(_server);
_closed = true;
_sub?.cancel();
return app.close().then((_) =>
Future.wait(app.shutdownHooks.map(app.configure)).then((_) => _server));
}
@override
void addCookies(HttpResponse response, Iterable<Cookie> cookies) =>
response.cookies.addAll(cookies);
/// Handles a single request.
Future handleRequest(HttpRequest request) {
return createRequestContext(request).then((req) {
return createResponseContext(request.response, req).then((res) {
handle() {
var path = req.path;
if (path == '/') path = '';
@override
Future closeResponse(HttpResponse response) => response.close();
Tuple3<List, Map<String, dynamic>, ParseResult<Map<String, dynamic>>>
resolveTuple() {
Router r = app.optimizedRouter;
var resolved =
r.resolveAbsolute(path, method: req.method, strip: false);
return new Tuple3(
new MiddlewarePipeline(resolved).handlers,
resolved.fold<Map<String, dynamic>>(
<String, dynamic>{}, (out, r) => out..addAll(r.allParams)),
resolved.isEmpty ? null : resolved.first.parseResult,
);
}
var cacheKey = req.method + path;
var tuple = app.isProduction
? app.handlerCache.putIfAbsent(cacheKey, resolveTuple)
: resolveTuple();
req.params.addAll(tuple.item2);
req.container.registerSingleton<ParseResult<Map<String, dynamic>>>(
tuple.item3);
req.container.registerSingleton<ParseResult>(tuple.item3);
if (!app.isProduction && app.logger != null) {
req.container
.registerSingleton<Stopwatch>(new Stopwatch()..start());
}
var pipeline = tuple.item1;
Future Function() runPipeline;
for (var handler in pipeline) {
if (handler == null) break;
if (runPipeline == null)
runPipeline = () =>
Future.sync(() => app.executeHandler(handler, req, res));
else {
var current = runPipeline;
runPipeline = () => current().then((result) => !res.isOpen
? new Future.value(result)
: app.executeHandler(handler, req, res));
}
}
return runPipeline == null
? sendResponse(request, req, res)
: runPipeline().then((_) => sendResponse(request, req, res));
}
if (useZone == false) {
Future f;
try {
f = handle();
} catch (e, st) {
f = Future.error(e, st);
}
return f.catchError((e, StackTrace st) {
if (e is FormatException)
throw new AngelHttpException.badRequest(message: e.message)
..stackTrace = st;
throw new AngelHttpException(e,
stackTrace: st,
statusCode: 500,
message: e?.toString() ?? '500 Internal Server Error');
}, test: (e) => e is! AngelHttpException).catchError(
(ee, StackTrace st) {
var e = ee as AngelHttpException;
if (app.logger != null) {
var error = e.error ?? e;
var trace =
new Trace.from(e.stackTrace ?? StackTrace.current).terse;
app.logger.severe(e.message ?? e.toString(), error, trace);
}
return handleAngelHttpException(
e, e.stackTrace ?? st, req, res, request);
});
} else {
var zoneSpec = new ZoneSpecification(
print: (self, parent, zone, line) {
if (app.logger != null)
app.logger.info(line);
else
parent.print(zone, line);
},
handleUncaughtError: (self, parent, zone, error, stackTrace) {
var trace =
new Trace.from(stackTrace ?? StackTrace.current).terse;
return new Future(() {
AngelHttpException e;
if (error is FormatException) {
e = new AngelHttpException.badRequest(message: error.message);
} else if (error is AngelHttpException) {
e = error;
} else {
e = new AngelHttpException(error,
stackTrace: stackTrace,
message:
error?.toString() ?? '500 Internal Server Error');
}
if (app.logger != null) {
app.logger.severe(e.message ?? e.toString(), error, trace);
}
return handleAngelHttpException(e, trace, req, res, request);
}).catchError((e, StackTrace st) {
var trace = new Trace.from(st ?? StackTrace.current).terse;
request.response.close();
// Ideally, we won't be in a position where an absolutely fatal error occurs,
// but if so, we'll need to log it.
if (app.logger != null) {
app.logger.severe(
'Fatal error occurred when processing ${request.uri}.',
e,
trace);
} else {
stderr
..writeln('Fatal error occurred when processing '
'${request.uri}:')
..writeln(e)
..writeln(trace);
}
});
},
);
var zone = Zone.current.fork(specification: zoneSpec);
req.container.registerSingleton<Zone>(zone);
req.container.registerSingleton<ZoneSpecification>(zoneSpec);
// If a synchronous error is thrown, it's not caught by `zone.run`,
// so use a try/catch, and recover when need be.
try {
return zone.run(handle);
} catch (e, st) {
zone.handleUncaughtError(e, st);
return Future.value();
}
}
});
});
}
/// Handles an [AngelHttpException].
Future handleAngelHttpException(AngelHttpException e, StackTrace st,
RequestContext req, ResponseContext res, HttpRequest request,
{bool ignoreFinalizers: false}) {
if (req == null || res == null) {
try {
app.logger?.severe(e, st);
request.response
..statusCode = 500
..write('500 Internal Server Error')
..close();
} finally {
return null;
}
}
Future handleError;
if (!res.isOpen)
handleError = new Future.value();
else {
res.statusCode = e.statusCode;
handleError =
new Future.sync(() => app.errorHandler(e, req, res)).then((result) {
return app.executeHandler(result, req, res).then((_) => res.close());
});
}
return handleError.then((_) => sendResponse(request, req, res,
ignoreFinalizers: ignoreFinalizers == true));
}
/// Sends a response.
Future sendResponse(
HttpRequest request, RequestContext req, ResponseContext res,
{bool ignoreFinalizers: false}) {
void _cleanup(_) {
if (!app.isProduction && app.logger != null) {
var sw = req.container.make<Stopwatch>();
app.logger.info(
"${res.statusCode} ${req.method} ${req.uri} (${sw?.elapsedMilliseconds ?? 'unknown'} ms)");
}
}
if (!res.isBuffered) return res.close().then(_cleanup);
Future finalizers = ignoreFinalizers == true
? new Future.value()
: app.responseFinalizers.fold<Future>(
new Future.value(), (out, f) => out.then((_) => f(req, res)));
return finalizers.then((_) {
if (res.isOpen) res.close();
for (var key in res.headers.keys) {
request.response.headers.add(key, res.headers[key]);
}
request.response.contentLength = res.buffer.length;
request.response.headers.chunkedTransferEncoding = res.chunked ?? true;
List<int> outputBuffer = res.buffer.toBytes();
if (res.encoders.isNotEmpty) {
var allowedEncodings = req.headers
.value('accept-encoding')
?.split(',')
?.map((s) => s.trim())
?.where((s) => s.isNotEmpty)
?.map((str) {
// Ignore quality specifications in accept-encoding
// ex. gzip;q=0.8
if (!str.contains(';')) return str;
return str.split(';')[0];
});
if (allowedEncodings != null) {
for (var encodingName in allowedEncodings) {
Converter<List<int>, List<int>> encoder;
String key = encodingName;
if (res.encoders.containsKey(encodingName))
encoder = res.encoders[encodingName];
else if (encodingName == '*') {
encoder = res.encoders[key = res.encoders.keys.first];
}
if (encoder != null) {
request.response.headers.set('content-encoding', key);
outputBuffer = res.encoders[key].convert(outputBuffer);
request.response.contentLength = outputBuffer.length;
break;
}
}
}
}
request.response
..statusCode = res.statusCode
..cookies.addAll(res.cookies)
..add(outputBuffer);
return request.response.close().then(_cleanup);
});
}
Future<HttpRequestContext> createRequestContext(HttpRequest request) {
@override
Future<HttpRequestContext> createRequestContext(
HttpRequest request, HttpResponse response) {
var path = request.uri.path.replaceAll(_straySlashes, '');
if (path.length == 0) path = '/';
return HttpRequestContext.from(request, app, path);
}
Future<ResponseContext> createResponseContext(HttpResponse response,
[RequestContext correspondingRequest]) =>
new Future<ResponseContext>.value(new HttpResponseContext(
response, app, correspondingRequest as HttpRequestContext)
..serializer = (app.serializer ?? json.encode)
..encoders.addAll(app.encoders ?? {}));
@override
Future<HttpResponseContext> createResponseContext(
HttpRequest request, HttpResponse response,
[HttpRequestContext correspondingRequest]) {
return new Future<HttpResponseContext>.value(
new HttpResponseContext(response, app, correspondingRequest)
..serializer = (app.serializer ?? json.encode)
..encoders.addAll(app.encoders ?? {}));
}
@override
HttpResponse createResponseFromRawRequest(HttpRequest request) =>
request.response;
@override
Uri getUriFromRequest(HttpRequest request) => request.uri;
@override
void setChunkedEncoding(HttpResponse response, bool value) =>
response.headers.chunkedTransferEncoding = value;
@override
void setContentLength(HttpResponse response, int length) =>
response.headers.contentLength = length;
@override
void setHeader(HttpResponse response, String key, String value) =>
response.headers.set(key, value);
@override
void setStatusCode(HttpResponse response, int value) =>
response.statusCode = value;
@override
void writeStringToResponse(HttpResponse response, String value) =>
response.write(value);
@override
void writeToResponse(HttpResponse response, List<int> data) =>
response.add(data);
}

View file

@ -0,0 +1,132 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart' hide Header;
import 'package:combinator/combinator.dart';
import 'package:http2/src/artificial_server_socket.dart';
import 'package:http2/transport.dart';
import 'package:mock_request/mock_request.dart';
import 'http2_request_context.dart';
import 'http2_response_context.dart';
import 'package:pool/pool.dart';
import 'package:uuid/uuid.dart';
import 'package:tuple/tuple.dart';
class AngelHttp2 extends Driver<Socket, ServerTransportStream,
ArtificialServerSocket, Http2RequestContext, Http2ResponseContext> {
final ServerSettings settings;
final StreamController<HttpRequest> _onHttp1 = new StreamController();
final Map<String, MockHttpSession> _sessions = {};
final Uuid _uuid = new Uuid();
ArtificialServerSocket _artificial;
HttpServer _httpServer;
StreamController<SecureSocket> _http1;
SecureServerSocket _socket;
StreamSubscription _sub;
AngelHttp2._(
Angel app,
Future<ArtificialServerSocket> Function(dynamic, int) serverGenerator,
bool useZone,
this.settings)
: super(app, serverGenerator, useZone: useZone);
factory AngelHttp2(Angel app, SecurityContext securityContext,
{bool useZone: true, ServerSettings settings}) {
return new AngelHttp2.custom(app, securityContext, SecureServerSocket.bind,
settings: settings);
}
factory AngelHttp2.custom(
Angel app,
SecurityContext ctx,
Future<SecureServerSocket> serverGenerator(
address, int port, SecurityContext ctx),
{bool useZone: true,
ServerSettings settings}) {
return new AngelHttp2._(app, (address, port) {
var addr = address is InternetAddress
? address
: new InternetAddress(address.toString());
return SecureServerSocket.bind(addr, port, ctx)
.then((s) => ArtificialServerSocket(addr, port, s));
}, useZone, settings);
}
/// Fires when an HTTP/1.x request is received.
Stream<HttpRequest> get onHttp1 => _onHttp1.stream;
@override
void addCookies(ServerTransportStream response, Iterable<Cookie> cookies) {
var headers = cookies
.map((cookie) => new Header.ascii('set-cookie', cookie.toString()));
response.sendHeaders(headers.toList());
}
@override
Future closeResponse(ServerTransportStream response) {
response.terminate();
return new Future.value();
}
@override
Future<Http2RequestContext> createRequestContext(
Socket request, ServerTransportStream response) {
return Http2RequestContext.from(response, request, app, _sessions, _uuid);
}
@override
Future<Http2ResponseContext> createResponseContext(
Socket request, ServerTransportStream response,
[Http2RequestContext correspondingRequest]) async {
return new Http2ResponseContext(app, response, correspondingRequest)
..encoders.addAll(app.encoders);
}
@override
ServerTransportStream createResponseFromRawRequest(Socket request) {
var connection =
new ServerTransportConnection.viaSocket(request, settings: settings);
}
@override
Uri getUriFromRequest(Socket request) {
// TODO: implement getUriFromRequest
}
@override
void setChunkedEncoding(ServerTransportStream response, bool value) {
// Do nothing in HTTP/2
}
@override
void setContentLength(ServerTransportStream response, int length) {
setHeader(response, 'content-length', length.toString());
}
@override
void setHeader(ServerTransportStream response, String key, String value) {
response.sendHeaders([new Header.ascii(key, value)]);
}
@override
void setStatusCode(ServerTransportStream response, int value) {
response.sendHeaders([new Header.ascii(':status', value.toString())]);
}
@override
Uri get uri => Uri(
scheme: 'https',
host: server.address.address,
port: server.port != 443 ? server.port : null);
@override
void writeStringToResponse(ServerTransportStream response, String value) {
writeToResponse(response, utf8.encode(value));
}
@override
void writeToResponse(ServerTransportStream response, List<int> data) {
response.sendData(data);
}
}

View file

@ -0,0 +1,174 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:body_parser/body_parser.dart';
import 'package:http_parser/http_parser.dart';
import 'package:http2/transport.dart';
import 'package:mock_request/mock_request.dart';
import 'package:uuid/uuid.dart';
final RegExp _comma = new RegExp(r',\s*');
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
class Http2RequestContext extends RequestContext {
BytesBuilder _buf;
ContentType _contentType;
List<Cookie> _cookies;
HttpHeaders _headers;
String _method, _override, _path;
HttpSession _session;
Socket _socket;
ServerTransportStream _stream;
Uri _uri;
static Future<Http2RequestContext> from(
ServerTransportStream stream,
Socket socket,
Angel app,
Map<String, MockHttpSession> sessions,
Uuid uuid) async {
var req = new Http2RequestContext()
..app = app
.._socket = socket
.._stream = stream;
var buf = req._buf = new BytesBuilder();
var headers = req._headers = new MockHttpHeaders();
String scheme = 'https',
authority = '${socket.address.address}:${socket.port}',
path = '';
var cookies = <Cookie>[];
await for (var msg in stream.incomingMessages) {
if (msg is DataStreamMessage) {
buf.add(msg.bytes);
} else if (msg is HeadersStreamMessage) {
for (var header in msg.headers) {
var name = ascii.decode(header.name).toLowerCase();
var value = ascii.decode(header.value);
switch (name) {
case ':method':
req._method = value;
break;
case ':path':
path = value.replaceAll(_straySlashes, '');
req._path = path;
if (path.isEmpty) req._path = '/';
break;
case ':scheme':
scheme = value;
break;
case ':authority':
authority = value;
break;
case 'cookie':
var cookieStrings = value.split(';').map((s) => s.trim());
for (var cookieString in cookieStrings) {
try {
cookies.add(new Cookie.fromSetCookieValue(cookieString));
} catch (_) {
// Ignore malformed cookies, and just don't add them to the container.
}
}
break;
default:
headers.add(ascii.decode(header.name), value.split(_comma));
break;
}
}
}
//if (msg.endStream) break;
}
req
.._cookies = new List.unmodifiable(cookies)
.._uri = Uri.parse('$scheme://$authority').replace(path: path);
// Apply session
var dartSessId =
cookies.firstWhere((c) => c.name == 'DARTSESSID', orElse: () => null);
if (dartSessId == null) {
dartSessId = new Cookie('DARTSESSID', uuid.v4());
}
req._session = sessions.putIfAbsent(
dartSessId.value,
() => new MockHttpSession(id: dartSessId.value),
);
return req;
}
@override
List<Cookie> get cookies => _cookies;
/// The underlying HTTP/2 [ServerTransportStream].
ServerTransportStream get stream => _stream;
@override
bool get xhr {
return headers.value("X-Requested-With")?.trim()?.toLowerCase() ==
'xmlhttprequest';
}
@override
Uri get uri => _uri;
@override
HttpSession get session {
return _session;
}
@override
InternetAddress get remoteAddress => _socket.remoteAddress;
@override
String get path {
return _path;
}
@override
ContentType get contentType =>
_contentType ??= (headers['content-type'] == null
? null
: ContentType.parse(headers.value('content-type')));
@override
String get originalMethod {
return _method;
}
@override
String get method {
return _override ?? _method;
}
@override
HttpRequest get io => null;
@override
String get hostname => _headers.value('host');
@override
HttpHeaders get headers => _headers;
@override
Future close() {
return super.close();
}
@override
Future<BodyParseResult> parseOnce() {
return parseBodyFromStream(
new Stream.fromIterable([_buf.takeBytes()]),
contentType == null ? null : new MediaType.parse(contentType.toString()),
uri,
storeOriginalBuffer: app.storeOriginalBuffer,
);
}
}

View file

@ -0,0 +1,218 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart' hide Header;
import 'package:http2/transport.dart';
import 'http2_request_context.dart';
class Http2ResponseContext extends ResponseContext {
final Angel app;
final ServerTransportStream stream;
final Http2RequestContext _req;
bool _useStream = false, _isClosed = false, _isPush = false;
Uri _targetUri;
Http2ResponseContext(this.app, this.stream, this._req) {
_targetUri = _req.uri;
}
final List<Http2ResponseContext> _pushes = [];
/// Returns `true` if an attempt to [push] a resource will succeed.
///
/// See [ServerTransportStream].`push`.
bool get canPush => stream.canPush;
/// Returns a [List] of all resources that have [push]ed to the client.
List<Http2ResponseContext> get pushes => new List.unmodifiable(_pushes);
@override
RequestContext get correspondingRequest => _req;
Uri get targetUri => _targetUri;
@override
HttpResponse get io => null;
@override
bool get streaming => _useStream;
@override
bool get isOpen => !_isClosed;
/// Write headers, status, etc. to the underlying [stream].
void finalize() {
if (_isPush) return;
var headers = <Header>[
new Header.ascii(':status', statusCode.toString()),
];
if (encoders.isNotEmpty && correspondingRequest != null) {
var allowedEncodings =
(correspondingRequest.headers['accept-encoding'] ?? []).map((str) {
// Ignore quality specifications in accept-encoding
// ex. gzip;q=0.8
if (!str.contains(';')) return str;
return str.split(';')[0];
});
for (var encodingName in allowedEncodings) {
String key = encodingName;
if (encoders.containsKey(encodingName)) {
this.headers['content-encoding'] = key;
break;
}
}
}
// Add all normal headers
for (var key in this.headers.keys) {
headers.add(new Header.ascii(key.toLowerCase(), this.headers[key]));
}
// Persist session ID
cookies.add(new Cookie('DARTSESSID', _req.session.id));
// Send all cookies
for (var cookie in cookies) {
headers.add(new Header.ascii('set-cookie', cookie.toString()));
}
stream.sendHeaders(headers);
}
@override
void addError(Object error, [StackTrace stackTrace]) {
Zone.current.handleUncaughtError(error, stackTrace);
super.addError(error, stackTrace);
}
@override
bool useStream() {
if (!_useStream) {
// If this is the first stream added to this response,
// then add headers, status code, etc.
finalize();
willCloseItself = _useStream = _isClosed = true;
releaseCorrespondingRequest();
return true;
}
return false;
}
@override
void end() {
_isClosed = true;
super.end();
}
@override
Future addStream(Stream<List<int>> stream) {
if (_isClosed && !_useStream) throw ResponseContext.closed();
var firstStream = useStream();
Stream<List<int>> output = stream;
if ((firstStream || !headers.containsKey('content-encoding')) &&
encoders.isNotEmpty &&
correspondingRequest != null) {
var allowedEncodings =
(correspondingRequest.headers['accept-encoding'] ?? []).map((str) {
// Ignore quality specifications in accept-encoding
// ex. gzip;q=0.8
if (!str.contains(';')) return str;
return str.split(';')[0];
});
for (var encodingName in allowedEncodings) {
Converter<List<int>, List<int>> encoder;
String key = encodingName;
if (encoders.containsKey(encodingName))
encoder = encoders[encodingName];
else if (encodingName == '*') {
encoder = encoders[key = encoders.keys.first];
}
if (encoder != null) {
/*
if (firstStream) {
this.stream.sendHeaders([
new Header.ascii(
'content-encoding', headers['content-encoding'] = key)
]);
}
*/
output = encoders[key].bind(output);
break;
}
}
}
return output.forEach(this.stream.sendData);
}
@override
void add(List<int> data) {
if (_isClosed && !_useStream)
throw ResponseContext.closed();
else if (_useStream)
//stream.sendData(data);
addStream(new Stream.fromIterable([data]));
else
buffer.add(data);
}
@override
Future close() async {
if (_useStream) {
try {
await stream.outgoingMessages.close();
} catch (_) {
// This only seems to occur on `MockHttpRequest`, but
// this try/catch prevents a crash.
}
}
_isClosed = true;
await super.close();
_useStream = false;
}
/// Pushes a resource to the client.
Http2ResponseContext push(String path,
{Map<String, String> headers: const {}, String method: 'GET'}) {
if (isOpen)
throw new StateError(
'You can only push resources after the main response context is closed. You will need to use streaming methods, i.e. `addStream`.');
var targetUri = _req.uri.replace(path: path);
var h = <Header>[
new Header.ascii(':authority', targetUri.authority),
new Header.ascii(':method', method),
new Header.ascii(':path', targetUri.path),
new Header.ascii(':scheme', targetUri.scheme),
];
for (var key in headers.keys) {
h.add(new Header.ascii(key, headers[key]));
}
var s = stream.push(h);
var r = new Http2ResponseContext(app, s, _req)
.._isPush = true
.._targetUri = targetUri;
_pushes.add(r);
return r;
}
void internalReopen() {
_isClosed = false;
}
}

View file

@ -21,6 +21,7 @@ dependencies:
dart2_constant: ^1.0.0
file: ^5.0.0
http_parser: ^3.0.0
http2: ">=0.1.7 <2.0.0"
logging: ">=0.11.3 <1.0.0"
matcher: ^0.12.0
merge_map: ^1.0.0
@ -29,6 +30,7 @@ dependencies:
path: ^1.0.0
stack_trace: ^1.0.0
tuple: ^1.0.0
uuid: ^1.0.0
dev_dependencies:
http: ^0.11.3
io: ^0.3.0

View file

@ -0,0 +1,265 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart' hide Header;
import 'package:angel_framework/http2.dart';
import 'package:http/src/multipart_file.dart' as http;
import 'package:http/src/multipart_request.dart' as http;
import 'package:http/http.dart' as http;
import 'package:http2/transport.dart';
import 'package:http_parser/http_parser.dart';
import 'package:test/test.dart';
import 'http2_client.dart';
const String jfk =
'Ask not what your country can do for you, but what you can do for your country.';
Stream<List<int>> jfkStream() {
return new Stream.fromIterable([utf8.encode(jfk)]);
}
void main() {
var client = new Http2Client();
Angel app;
AngelHttp2 http2;
Uri serverRoot;
setUp(() async {
app = new Angel()
..keepRawRequestBuffers = true
..encoders['gzip'] = gzip.encoder;
app.get('/', (req, res) {
res
..write('Hello world')
..close();
});
app.all('/method', (req, res) => req.method);
app.get('/json', (_, __) => {'foo': 'bar'});
app.get('/stream', (req, res) => jfkStream().pipe(res));
app.get('/headers', (req, res) {
res
..headers.addAll({'foo': 'bar', 'x-angel': 'http2'})
..close();
});
app.get('/status', (req, res) {
res
..statusCode = 1337
..close();
});
app.post('/body', (req, res) => req.parseBody());
app.post('/upload', (req, res) async {
var body = await req.parseBody(), files = await req.parseUploadedFiles();
stdout.add(await req.parseRawRequestBuffer());
var file = files.firstWhere((f) => f.name == 'file');
return [file.data.length, file.mimeType, body];
});
app.get('/push', (req, res) async {
res
..write('ok')
..close();
if (res is Http2ResponseContext && res.canPush) {
res.push('a')
..write('a')
..close();
res.push('b')
..write('b')
..close();
}
});
var ctx = new SecurityContext()
..useCertificateChain('dev.pem')
..usePrivateKey('dev.key', password: 'dartdart')
..setAlpnProtocols(['h2'], true);
http2 = new AngelHttp2(app, ctx);
var server = await http2.startServer();
serverRoot = Uri.parse('https://127.0.0.1:${server.port}');
});
tearDown(() async {
await http2.close();
});
test('buffered response', () async {
var response = await client.get(serverRoot);
expect(response.body, 'Hello world');
});
test('streamed response', () async {
var response = await client.get(serverRoot.replace(path: '/stream'));
expect(response.body, jfk);
});
group('gzip', () {
test('buffered response', () async {
var response = await client
.get(serverRoot, headers: {'accept-encoding': 'gzip, deflate, br'});
expect(response.headers['content-encoding'], 'gzip');
var decoded = gzip.decode(response.bodyBytes);
expect(utf8.decode(decoded), 'Hello world');
});
test('streamed response', () async {
var response = await client.get(serverRoot.replace(path: '/stream'),
headers: {'accept-encoding': 'gzip'});
expect(response.headers['content-encoding'], 'gzip');
//print(response.body);
var decoded = gzip.decode(response.bodyBytes);
expect(utf8.decode(decoded), jfk);
});
});
test('method parsed', () async {
var response = await client.delete(serverRoot.replace(path: '/method'));
expect(response.body, json.encode('DELETE'));
});
test('json response', () async {
var response = await client.get(serverRoot.replace(path: '/json'));
expect(response.body, json.encode({'foo': 'bar'}));
expect(ContentType.parse(response.headers['content-type']).mimeType,
ContentType.json.mimeType);
});
test('status sent', () async {
var response = await client.get(serverRoot.replace(path: '/status'));
expect(response.statusCode, 1337);
});
test('headers sent', () async {
var response = await client.get(serverRoot.replace(path: '/headers'));
expect(response.headers['foo'], 'bar');
expect(response.headers['x-angel'], 'http2');
});
test('server push', () async {
var socket = await SecureSocket.connect(
serverRoot.host,
serverRoot.port ?? 443,
onBadCertificate: (_) => true,
supportedProtocols: ['h2'],
);
var connection = new ClientTransportConnection.viaSocket(
socket,
settings: new ClientSettings(allowServerPushes: true),
);
var headers = <Header>[
new Header.ascii(':authority', serverRoot.authority),
new Header.ascii(':method', 'GET'),
new Header.ascii(':path', serverRoot.replace(path: '/push').path),
new Header.ascii(':scheme', serverRoot.scheme),
];
var stream = await connection.makeRequest(headers, endStream: true);
var bb = await stream.incomingMessages
.where((s) => s is DataStreamMessage)
.cast<DataStreamMessage>()
.fold<BytesBuilder>(
new BytesBuilder(), (out, msg) => out..add(msg.bytes));
// Check that main body was sent
expect(utf8.decode(bb.takeBytes()), 'ok');
var pushes = await stream.peerPushes.toList();
expect(pushes, hasLength(2));
var pushA = pushes[0], pushB = pushes[1];
String getPath(TransportStreamPush p) => ascii.decode(p.requestHeaders
.firstWhere((h) => ascii.decode(h.name) == ':path')
.value);
/*
Future<String> getBody(ClientTransportStream stream) async {
await stream.outgoingMessages.close();
var bb = await stream.incomingMessages
.map((s) {
if (s is HeadersStreamMessage) {
for (var h in s.headers) {
print('${ASCII.decode(h.name)}: ${ASCII.decode(h.value)}');
}
} else if (s is DataStreamMessage) {
print(UTF8.decode(s.bytes));
}
return s;
})
.where((s) => s is DataStreamMessage)
.cast<DataStreamMessage>()
.fold<BytesBuilder>(
new BytesBuilder(), (out, msg) => out..add(msg.bytes));
return UTF8.decode(bb.takeBytes());
}
*/
expect(getPath(pushA), '/a');
expect(getPath(pushB), '/b');
// TODO: Dart http/2 client seems to not be able to get body
// However, Chrome, Firefox, Edge all can
//expect(await getBody(pushA.stream), 'a');
//expect(await getBody(pushB.stream), 'b');
});
group('body parsing', () {
test('urlencoded body parsed', () async {
var response = await client.post(
serverRoot.replace(path: '/body'),
headers: {
'accept': 'application/json',
'content-type': 'application/x-www-form-urlencoded'
},
body: 'foo=bar',
);
expect(response.body, json.encode({'foo': 'bar'}));
});
test('json body parsed', () async {
var response = await client.post(serverRoot.replace(path: '/body'),
headers: {
'accept': 'application/json',
'content-type': 'application/json'
},
body: json.encode({'foo': 'bar'}));
expect(response.body, json.encode({'foo': 'bar'}));
});
test('multipart body parsed', () async {
var rq = new http.MultipartRequest(
'POST', serverRoot.replace(path: '/upload'));
rq.headers.addAll({'accept': 'application/json'});
rq.fields['foo'] = 'bar';
rq.files.add(new http.MultipartFile(
'file', new Stream.fromIterable([utf8.encode('hello world')]), 11,
contentType: new MediaType('angel', 'framework')));
var response = await client.send(rq);
var responseBody = await response.stream.transform(utf8.decoder).join();
expect(
responseBody,
json.encode([
11,
'angel/framework',
{'foo': 'bar'}
]));
});
});
}

View file

@ -0,0 +1,101 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:http2/transport.dart';
/// Simple HTTP/2 client
class Http2Client extends BaseClient {
static Future<ClientTransportStream> convertRequestToStream(
BaseRequest request) async {
// Connect a socket
var socket = await SecureSocket.connect(
request.url.host,
request.url.port ?? 443,
onBadCertificate: (_) => true,
supportedProtocols: ['h2'],
);
var connection = new ClientTransportConnection.viaSocket(socket);
var headers = <Header>[
new Header.ascii(':authority', request.url.authority),
new Header.ascii(':method', request.method),
new Header.ascii(':path', request.url.path),
new Header.ascii(':scheme', request.url.scheme),
];
var bb = await request
.finalize()
.fold<BytesBuilder>(new BytesBuilder(), (out, list) => out..add(list));
var body = bb.takeBytes();
if (body.isNotEmpty) {
headers.add(new Header.ascii('content-length', body.length.toString()));
}
request.headers.forEach((k, v) {
headers.add(new Header.ascii(k, v));
});
var stream = await connection.makeRequest(headers);
if (body.isNotEmpty) {
stream.sendData(body, endStream: true);
} else {
stream.outgoingMessages.close();
}
return stream;
}
/// Returns `true` if the response stream was closed.
static Future<bool> readResponse(ClientTransportStream stream,
Map<String, String> headers, BytesBuilder body) {
var c = new Completer<bool>();
var closed = false;
stream.incomingMessages.listen(
(msg) {
if (msg is HeadersStreamMessage) {
for (var header in msg.headers) {
var name = ascii.decode(header.name).toLowerCase(),
value = ascii.decode(header.value);
headers[name] = value;
//print('$name: $value');
}
} else if (msg is DataStreamMessage) {
body.add(msg.bytes);
}
if (!closed && msg.endStream) closed = true;
},
cancelOnError: true,
onError: c.completeError,
onDone: () => c.complete(closed),
);
return c.future;
}
@override
Future<StreamedResponse> send(BaseRequest request) async {
var stream = await convertRequestToStream(request);
var headers = <String, String>{};
var body = new BytesBuilder();
var closed = await readResponse(stream, headers, body);
return new StreamedResponse(
new Stream.fromIterable([body.takeBytes()]),
int.parse(headers[':status']),
headers: headers,
isRedirect: headers.containsKey('location'),
contentLength: headers.containsKey('content-length')
? int.parse(headers['content-length'])
: null,
request: request,
reasonPhrase: null,
// doesn't exist in HTTP/2
persistentConnection: !closed,
);
}
}

View file

@ -1,5 +1,3 @@
import 'dart:async';
import 'package:angel_container/mirrors.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';