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 'anonymous_service.dart';
|
||||||
export 'controller.dart';
|
export 'controller.dart';
|
||||||
|
export 'driver.dart';
|
||||||
export 'hooked_service.dart';
|
export 'hooked_service.dart';
|
||||||
export 'map_service.dart';
|
export 'map_service.dart';
|
||||||
export 'metadata.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:convert';
|
||||||
import 'dart:io'
|
import 'dart:io'
|
||||||
show
|
show
|
||||||
stderr,
|
Cookie,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpServer,
|
HttpServer,
|
||||||
Platform,
|
Platform,
|
||||||
SecurityContext;
|
SecurityContext;
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
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/core.dart';
|
import '../core/core.dart';
|
||||||
import 'http_request_context.dart';
|
import 'http_request_context.dart';
|
||||||
import 'http_response_context.dart';
|
import 'http_response_context.dart';
|
||||||
|
@ -22,41 +16,32 @@ import 'http_response_context.dart';
|
||||||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||||
|
|
||||||
/// Adapts `dart:io`'s [HttpServer] to serve Angel.
|
/// Adapts `dart:io`'s [HttpServer] to serve Angel.
|
||||||
class AngelHttp {
|
class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
|
||||||
final Angel app;
|
HttpRequestContext, HttpResponseContext> {
|
||||||
final bool useZone;
|
@override
|
||||||
bool _closed = false;
|
Uri get uri =>
|
||||||
HttpServer _server;
|
new Uri(scheme: 'http', host: server.address.address, port: server.port);
|
||||||
Future<HttpServer> Function(dynamic, int) _serverGenerator = HttpServer.bind;
|
|
||||||
StreamSubscription<HttpRequest> _sub;
|
|
||||||
|
|
||||||
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.
|
factory AngelHttp(Angel app, {bool useZone: true}) {
|
||||||
Uri get uri => new Uri(
|
return new AngelHttp._(app, HttpServer.bind, useZone);
|
||||||
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;
|
|
||||||
|
|
||||||
/// An instance mounted on a server started by the [serverGenerator].
|
/// An instance mounted on a server started by the [serverGenerator].
|
||||||
factory AngelHttp.custom(
|
factory AngelHttp.custom(
|
||||||
Angel app, Future<HttpServer> Function(dynamic, int) serverGenerator,
|
Angel app, Future<HttpServer> Function(dynamic, int) serverGenerator,
|
||||||
{bool useZone: true}) {
|
{bool useZone: true}) {
|
||||||
return new AngelHttp(app, useZone: useZone)
|
return new AngelHttp._(app, serverGenerator, useZone);
|
||||||
.._serverGenerator = serverGenerator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AngelHttp.fromSecurityContext(Angel app, SecurityContext context,
|
factory AngelHttp.fromSecurityContext(Angel app, SecurityContext context,
|
||||||
{bool useZone: true}) {
|
{bool useZone: true}) {
|
||||||
var http = new AngelHttp(app, useZone: useZone);
|
return new AngelHttp._(app, (address, int port) {
|
||||||
|
|
||||||
http._serverGenerator = (address, int port) {
|
|
||||||
return HttpServer.bindSecure(address, port, context);
|
return HttpServer.bindSecure(address, port, context);
|
||||||
};
|
}, useZone);
|
||||||
|
|
||||||
return http;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an HTTPS server.
|
/// Creates an HTTPS server.
|
||||||
|
@ -73,316 +58,70 @@ class AngelHttp {
|
||||||
var serverContext = new SecurityContext();
|
var serverContext = new SecurityContext();
|
||||||
serverContext.useCertificateChain(certificateChain, password: password);
|
serverContext.useCertificateChain(certificateChain, password: password);
|
||||||
serverContext.usePrivateKey(serverKey, password: password);
|
serverContext.usePrivateKey(serverKey, password: password);
|
||||||
|
|
||||||
return new AngelHttp.fromSecurityContext(app, serverContext,
|
return new AngelHttp.fromSecurityContext(app, serverContext,
|
||||||
useZone: useZone);
|
useZone: useZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The native HttpServer running this instance.
|
/// Use [server] instead.
|
||||||
HttpServer get httpServer => _server;
|
@deprecated
|
||||||
|
HttpServer get httpServer => server;
|
||||||
|
|
||||||
/// Starts the server.
|
Future handleRequest(HttpRequest request) =>
|
||||||
///
|
handleRawRequest(request, request.response);
|
||||||
/// 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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shuts down the underlying server.
|
@override
|
||||||
Future<HttpServer> close() {
|
void addCookies(HttpResponse response, Iterable<Cookie> cookies) =>
|
||||||
if (_closed) return new Future.value(_server);
|
response.cookies.addAll(cookies);
|
||||||
_closed = true;
|
|
||||||
_sub?.cancel();
|
|
||||||
return app.close().then((_) =>
|
|
||||||
Future.wait(app.shutdownHooks.map(app.configure)).then((_) => _server));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles a single request.
|
@override
|
||||||
Future handleRequest(HttpRequest request) {
|
Future closeResponse(HttpResponse response) => response.close();
|
||||||
return createRequestContext(request).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>>>
|
@override
|
||||||
resolveTuple() {
|
Future<HttpRequestContext> createRequestContext(
|
||||||
Router r = app.optimizedRouter;
|
HttpRequest request, HttpResponse response) {
|
||||||
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) {
|
|
||||||
var path = request.uri.path.replaceAll(_straySlashes, '');
|
var path = request.uri.path.replaceAll(_straySlashes, '');
|
||||||
if (path.length == 0) path = '/';
|
if (path.length == 0) path = '/';
|
||||||
return HttpRequestContext.from(request, app, path);
|
return HttpRequestContext.from(request, app, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ResponseContext> createResponseContext(HttpResponse response,
|
@override
|
||||||
[RequestContext correspondingRequest]) =>
|
Future<HttpResponseContext> createResponseContext(
|
||||||
new Future<ResponseContext>.value(new HttpResponseContext(
|
HttpRequest request, HttpResponse response,
|
||||||
response, app, correspondingRequest as HttpRequestContext)
|
[HttpRequestContext correspondingRequest]) {
|
||||||
..serializer = (app.serializer ?? json.encode)
|
return new Future<HttpResponseContext>.value(
|
||||||
..encoders.addAll(app.encoders ?? {}));
|
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
|
dart2_constant: ^1.0.0
|
||||||
file: ^5.0.0
|
file: ^5.0.0
|
||||||
http_parser: ^3.0.0
|
http_parser: ^3.0.0
|
||||||
|
http2: ">=0.1.7 <2.0.0"
|
||||||
logging: ">=0.11.3 <1.0.0"
|
logging: ">=0.11.3 <1.0.0"
|
||||||
matcher: ^0.12.0
|
matcher: ^0.12.0
|
||||||
merge_map: ^1.0.0
|
merge_map: ^1.0.0
|
||||||
|
@ -29,6 +30,7 @@ dependencies:
|
||||||
path: ^1.0.0
|
path: ^1.0.0
|
||||||
stack_trace: ^1.0.0
|
stack_trace: ^1.0.0
|
||||||
tuple: ^1.0.0
|
tuple: ^1.0.0
|
||||||
|
uuid: ^1.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
http: ^0.11.3
|
http: ^0.11.3
|
||||||
io: ^0.3.0
|
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_container/mirrors.dart';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_framework/http.dart';
|
import 'package:angel_framework/http.dart';
|
||||||
|
|
Loading…
Reference in a new issue