Begin rolling in http2
This commit is contained in:
parent
681e2ac596
commit
2d168dd3aa
21 changed files with 1642 additions and 332 deletions
43
example/http2/body_parsing.dart
Normal file
43
example/http2/body_parsing.dart
Normal 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
29
example/http2/dev.key
Normal 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
57
example/http2/dev.pem
Normal 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
43
example/http2/main.dart
Normal 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}');
|
||||
}
|
9
example/http2/pretty_logging.dart
Normal file
9
example/http2/pretty_logging.dart
Normal 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());
|
||||
}
|
27
example/http2/public/app.js
Normal file
27
example/http2/public/app.js
Normal 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);
|
||||
};
|
21
example/http2/public/body_parsing.html
Normal file
21
example/http2/public/body_parsing.html
Normal 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>
|
12
example/http2/public/index.html
Normal file
12
example/http2/public/index.html
Normal 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>
|
20
example/http2/public/style.css
Normal file
20
example/http2/public/style.css
Normal 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;
|
||||
}
|
59
example/http2/server_push.dart
Normal file
59
example/http2/server_push.dart
Normal 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
3
lib/http2.dart
Normal 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';
|
|
@ -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
357
lib/src/core/driver.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
@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);
|
||||
}
|
||||
|
|
132
lib/src/http2/angel_http2.dart
Normal file
132
lib/src/http2/angel_http2.dart
Normal 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);
|
||||
}
|
||||
}
|
174
lib/src/http2/http2_request_context.dart
Normal file
174
lib/src/http2/http2_request_context.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
218
lib/src/http2/http2_response_context.dart
Normal file
218
lib/src/http2/http2_response_context.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
265
test/http2/adapter_test.dart
Normal file
265
test/http2/adapter_test.dart
Normal 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'}
|
||||
]));
|
||||
});
|
||||
});
|
||||
}
|
101
test/http2/http2_client.dart
Normal file
101
test/http2/http2_client.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue