2.0.0-alpha.11
This commit is contained in:
parent
2d168dd3aa
commit
bf0f01a5b9
23 changed files with 418 additions and 233 deletions
29
dev.key
Normal file
29
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
dev.pem
Normal file
57
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-----
|
|
@ -13,7 +13,7 @@ main() async {
|
||||||
});
|
});
|
||||||
app.logger = new Logger('angel')..onRecord.listen(prettyLog);
|
app.logger = new Logger('angel')..onRecord.listen(prettyLog);
|
||||||
|
|
||||||
app.get('/', (_, __) => 'Hello HTTP/2!!!');
|
app.get('/', (req, res) => 'Hello HTTP/2!!!');
|
||||||
|
|
||||||
app.fallback((req, res) => throw new AngelHttpException.notFound(
|
app.fallback((req, res) => throw new AngelHttpException.notFound(
|
||||||
message: 'No file exists at ${req.uri.path}'));
|
message: 'No file exists at ${req.uri.path}'));
|
||||||
|
@ -38,6 +38,6 @@ main() async {
|
||||||
// HTTP/1.x requests will fallback to `AngelHttp`
|
// HTTP/1.x requests will fallback to `AngelHttp`
|
||||||
http2.onHttp1.listen(http1.handleRequest);
|
http2.onHttp1.listen(http1.handleRequest);
|
||||||
|
|
||||||
var server = await http2.startServer('127.0.0.1', 3000);
|
await http2.startServer('127.0.0.1', 3000);
|
||||||
print('Listening at https://${server.address.address}:${server.port}');
|
print('Listening at ${http2.uri}');
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ main() async {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Index route. Returns JSON.
|
// Index route. Returns JSON.
|
||||||
app.get('/', (req, res) => res.write('Welcome to Angel!'));
|
app.get('/', (req, res) => 'Welcome to Angel!');
|
||||||
|
|
||||||
// Accepts a URL like /greet/foo or /greet/bob.
|
// Accepts a URL like /greet/foo or /greet/bob.
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
@ -34,15 +34,22 @@ abstract class Driver<
|
||||||
/// The native server running this instance.
|
/// The native server running this instance.
|
||||||
Server get server => _server;
|
Server get server => _server;
|
||||||
|
|
||||||
|
Future<Server> generateServer(address, int port) =>
|
||||||
|
serverGenerator(address, port);
|
||||||
|
|
||||||
/// Starts, and returns the server.
|
/// Starts, and returns the server.
|
||||||
Future<Server> startServer([address, int port]) {
|
Future<Server> startServer([address, int port]) {
|
||||||
var host = address ?? '127.0.0.1';
|
var host = address ?? '127.0.0.1';
|
||||||
return serverGenerator(host, port ?? 0).then((server) {
|
return generateServer(host, port ?? 0).then((server) {
|
||||||
_server = server;
|
_server = server;
|
||||||
return Future.wait(app.startupHooks.map(app.configure)).then((_) {
|
return Future.wait(app.startupHooks.map(app.configure)).then((_) {
|
||||||
app.optimizeForProduction();
|
app.optimizeForProduction();
|
||||||
_sub = server.listen((request) =>
|
_sub = server.listen((request) {
|
||||||
handleRawRequest(request, createResponseFromRawRequest(request)));
|
var stream = createResponseStreamFromRawRequest(request);
|
||||||
|
stream.listen((response) {
|
||||||
|
return handleRawRequest(request, response);
|
||||||
|
});
|
||||||
|
});
|
||||||
return _server;
|
return _server;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -78,11 +85,9 @@ abstract class Driver<
|
||||||
|
|
||||||
void writeToResponse(Response response, List<int> data);
|
void writeToResponse(Response response, List<int> data);
|
||||||
|
|
||||||
Uri getUriFromRequest(Request request);
|
|
||||||
|
|
||||||
Future closeResponse(Response response);
|
Future closeResponse(Response response);
|
||||||
|
|
||||||
Response createResponseFromRawRequest(Request request);
|
Stream<Response> createResponseStreamFromRawRequest(Request request);
|
||||||
|
|
||||||
/// Handles a single request.
|
/// Handles a single request.
|
||||||
Future handleRawRequest(Request request, Response response) {
|
Future handleRawRequest(Request request, Response response) {
|
||||||
|
@ -123,26 +128,17 @@ abstract class Driver<
|
||||||
}
|
}
|
||||||
|
|
||||||
var pipeline = tuple.item1;
|
var pipeline = tuple.item1;
|
||||||
|
var it = pipeline.iterator;
|
||||||
|
|
||||||
Future Function() runPipeline;
|
var runPipeline = pipeline.isEmpty
|
||||||
|
? null
|
||||||
for (var handler in pipeline) {
|
: Future.doWhile(() => !it.moveNext()
|
||||||
if (handler == null) break;
|
? new Future.value(false)
|
||||||
|
: app.executeHandler(it.current, req, res));
|
||||||
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
|
return runPipeline == null
|
||||||
? sendResponse(request, response, req, res)
|
? sendResponse(request, response, req, res)
|
||||||
: runPipeline()
|
: runPipeline
|
||||||
.then((_) => sendResponse(request, response, req, res));
|
.then((_) => sendResponse(request, response, req, res));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,7 +207,6 @@ abstract class Driver<
|
||||||
e, trace, req, res, request, response);
|
e, trace, req, res, request, response);
|
||||||
}).catchError((e, StackTrace st) {
|
}).catchError((e, StackTrace st) {
|
||||||
var trace = new Trace.from(st ?? StackTrace.current).terse;
|
var trace = new Trace.from(st ?? StackTrace.current).terse;
|
||||||
var uri = getUriFromRequest(request);
|
|
||||||
closeResponse(response);
|
closeResponse(response);
|
||||||
// Ideally, we won't be in a position where an absolutely fatal error occurs,
|
// Ideally, we won't be in a position where an absolutely fatal error occurs,
|
||||||
// but if so, we'll need to log it.
|
// but if so, we'll need to log it.
|
||||||
|
@ -221,7 +216,7 @@ abstract class Driver<
|
||||||
} else {
|
} else {
|
||||||
stderr
|
stderr
|
||||||
..writeln('Fatal error occurred when processing '
|
..writeln('Fatal error occurred when processing '
|
||||||
'$uri:')
|
'${req.uri}:')
|
||||||
..writeln(e)
|
..writeln(e)
|
||||||
..writeln(trace);
|
..writeln(trace);
|
||||||
}
|
}
|
||||||
|
@ -299,11 +294,10 @@ abstract class Driver<
|
||||||
|
|
||||||
Future finalizers = ignoreFinalizers == true
|
Future finalizers = ignoreFinalizers == true
|
||||||
? new Future.value()
|
? new Future.value()
|
||||||
: app.responseFinalizers.fold<Future>(
|
: Future.forEach(app.responseFinalizers, (f) => f(req, res));
|
||||||
new Future.value(), (out, f) => out.then((_) => f(req, res)));
|
|
||||||
|
|
||||||
return finalizers.then((_) {
|
return finalizers.then((_) {
|
||||||
if (res.isOpen) res.close();
|
//if (res.isOpen) res.close();
|
||||||
|
|
||||||
for (var key in res.headers.keys) {
|
for (var key in res.headers.keys) {
|
||||||
setHeader(response, key, res.headers[key]);
|
setHeader(response, key, res.headers[key]);
|
||||||
|
|
|
@ -59,7 +59,8 @@ abstract class RequestContext<RawRequest> {
|
||||||
String get originalMethod;
|
String get originalMethod;
|
||||||
|
|
||||||
/// The content type of an incoming request.
|
/// The content type of an incoming request.
|
||||||
MediaType get contentType;
|
MediaType get contentType =>
|
||||||
|
new MediaType.parse(headers.contentType.toString());
|
||||||
|
|
||||||
/// The URL parameters extracted from the request URI.
|
/// The URL parameters extracted from the request URI.
|
||||||
Map<String, dynamic> params = <String, dynamic>{};
|
Map<String, dynamic> params = <String, dynamic>{};
|
||||||
|
@ -67,6 +68,12 @@ abstract class RequestContext<RawRequest> {
|
||||||
/// The requested path.
|
/// The requested path.
|
||||||
String get path;
|
String get path;
|
||||||
|
|
||||||
|
/// Is this an **XMLHttpRequest**?
|
||||||
|
bool get isXhr {
|
||||||
|
return headers.value("X-Requested-With")?.trim()?.toLowerCase() ==
|
||||||
|
'xmlhttprequest';
|
||||||
|
}
|
||||||
|
|
||||||
/// The remote address requesting this resource.
|
/// The remote address requesting this resource.
|
||||||
InternetAddress get remoteAddress;
|
InternetAddress get remoteAddress;
|
||||||
|
|
||||||
|
@ -76,9 +83,6 @@ abstract class RequestContext<RawRequest> {
|
||||||
/// The [Uri] instance representing the path this request is responding to.
|
/// The [Uri] instance representing the path this request is responding to.
|
||||||
Uri get uri;
|
Uri get uri;
|
||||||
|
|
||||||
/// Is this an **XMLHttpRequest**?
|
|
||||||
bool get xhr;
|
|
||||||
|
|
||||||
/// Returns the file extension of the requested path, if any.
|
/// Returns the file extension of the requested path, if any.
|
||||||
///
|
///
|
||||||
/// Includes the leading `.`, if there is one.
|
/// Includes the leading `.`, if there is one.
|
||||||
|
|
|
@ -20,7 +20,10 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||||
abstract class ResponseContext<RawResponse>
|
abstract class ResponseContext<RawResponse>
|
||||||
implements StreamSink<List<int>>, StringSink {
|
implements StreamSink<List<int>>, StringSink {
|
||||||
final Map properties = {};
|
final Map properties = {};
|
||||||
final Map<String, String> _headers = {'server': 'angel'};
|
final Map<String, String> _headers = new CaseInsensitiveMap.from({
|
||||||
|
'content-type': 'text/plain',
|
||||||
|
'server': 'angel',
|
||||||
|
});
|
||||||
|
|
||||||
Completer _done;
|
Completer _done;
|
||||||
int _statusCode = 200;
|
int _statusCode = 200;
|
||||||
|
@ -101,7 +104,18 @@ abstract class ResponseContext<RawResponse>
|
||||||
FutureOr<RawResponse> detach();
|
FutureOr<RawResponse> detach();
|
||||||
|
|
||||||
/// Gets or sets the content type to send back to a client.
|
/// Gets or sets the content type to send back to a client.
|
||||||
MediaType contentType = new MediaType('text', 'plain');
|
MediaType get contentType {
|
||||||
|
try {
|
||||||
|
return new MediaType.parse(headers['content-type']);
|
||||||
|
} catch (_) {
|
||||||
|
return new MediaType('text', 'plain');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets or sets the content type to send back to a client.
|
||||||
|
void set contentType(MediaType value) {
|
||||||
|
headers['content-type'] = value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
static StateError closed() =>
|
static StateError closed() =>
|
||||||
new StateError('Cannot modify a closed response.');
|
new StateError('Cannot modify a closed response.');
|
||||||
|
@ -124,8 +138,6 @@ abstract class ResponseContext<RawResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prevents more data from being written to the response, and locks it entire from further editing.
|
/// Prevents more data from being written to the response, and locks it entire from further editing.
|
||||||
///
|
|
||||||
/// This method should be overwritten, setting [streaming] to `false`, **after** a `super` call.
|
|
||||||
Future close() {
|
Future close() {
|
||||||
if (buffer is LockableBytesBuilder) {
|
if (buffer is LockableBytesBuilder) {
|
||||||
(buffer as LockableBytesBuilder).lock();
|
(buffer as LockableBytesBuilder).lock();
|
||||||
|
@ -145,21 +157,21 @@ abstract class ResponseContext<RawResponse>
|
||||||
/// You can override the [contentType] sent; by default it is `application/javascript`.
|
/// You can override the [contentType] sent; by default it is `application/javascript`.
|
||||||
void jsonp(value, {String callbackName: "callback", MediaType contentType}) {
|
void jsonp(value, {String callbackName: "callback", MediaType contentType}) {
|
||||||
if (!isOpen) throw closed();
|
if (!isOpen) throw closed();
|
||||||
write("$callbackName(${serializer(value)})");
|
|
||||||
this.contentType =
|
this.contentType =
|
||||||
contentType ?? new MediaType('application', 'javascript');
|
contentType ?? new MediaType('application', 'javascript');
|
||||||
|
write("$callbackName(${serializer(value)})");
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a view to the response stream, and closes the response.
|
/// Renders a view to the response stream, and closes the response.
|
||||||
Future render(String view, [Map<String, dynamic> data]) {
|
Future render(String view, [Map<String, dynamic> data]) {
|
||||||
if (!isOpen) throw closed();
|
if (!isOpen) throw closed();
|
||||||
|
contentType = new MediaType('text', 'html');
|
||||||
return Future<String>.sync(() => app.viewGenerator(
|
return Future<String>.sync(() => app.viewGenerator(
|
||||||
view,
|
view,
|
||||||
new Map<String, dynamic>.from(renderParams)
|
new Map<String, dynamic>.from(renderParams)
|
||||||
..addAll(data ?? <String, dynamic>{}))).then((content) {
|
..addAll(data ?? <String, dynamic>{}))).then((content) {
|
||||||
write(content);
|
write(content);
|
||||||
contentType = new MediaType('text', 'html');
|
|
||||||
close();
|
close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -296,8 +308,6 @@ abstract class ResponseContext<RawResponse>
|
||||||
|
|
||||||
/// Adds a stream directly the underlying response.
|
/// Adds a stream directly the underlying response.
|
||||||
///
|
///
|
||||||
/// This will also set [willCloseItself] to `true`, thus canceling out response finalizers.
|
|
||||||
///
|
|
||||||
/// If this instance has access to a [correspondingRequest], then it will attempt to transform
|
/// If this instance has access to a [correspondingRequest], then it will attempt to transform
|
||||||
/// the content using at most one of the response [encoders].
|
/// the content using at most one of the response [encoders].
|
||||||
@override
|
@override
|
||||||
|
@ -305,7 +315,9 @@ abstract class ResponseContext<RawResponse>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addError(Object error, [StackTrace stackTrace]) {
|
void addError(Object error, [StackTrace stackTrace]) {
|
||||||
if (_done?.isCompleted == false) _done.completeError(error, stackTrace);
|
if (_done?.isCompleted == false)
|
||||||
|
_done.completeError(error, stackTrace);
|
||||||
|
else if (_done == null) Zone.current.handleUncaughtError(error, stackTrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes data to the response.
|
/// Writes data to the response.
|
||||||
|
|
|
@ -73,6 +73,12 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
|
||||||
void addCookies(HttpResponse response, Iterable<Cookie> cookies) =>
|
void addCookies(HttpResponse response, Iterable<Cookie> cookies) =>
|
||||||
response.cookies.addAll(cookies);
|
response.cookies.addAll(cookies);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<HttpServer> close() async {
|
||||||
|
await server?.close();
|
||||||
|
return await super.close();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future closeResponse(HttpResponse response) => response.close();
|
Future closeResponse(HttpResponse response) => response.close();
|
||||||
|
|
||||||
|
@ -95,11 +101,9 @@ class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
HttpResponse createResponseFromRawRequest(HttpRequest request) =>
|
Stream<HttpResponse> createResponseStreamFromRawRequest(
|
||||||
request.response;
|
HttpRequest request) =>
|
||||||
|
new Stream.fromIterable([request.response]);
|
||||||
@override
|
|
||||||
Uri getUriFromRequest(HttpRequest request) => request.uri;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void setChunkedEncoding(HttpResponse response, bool value) =>
|
void setChunkedEncoding(HttpResponse response, bool value) =>
|
||||||
|
|
|
@ -70,15 +70,6 @@ class HttpRequestContext extends RequestContext<HttpRequest> {
|
||||||
return rawRequest.uri;
|
return rawRequest.uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
bool get xhr {
|
|
||||||
return rawRequest.headers
|
|
||||||
.value("X-Requested-With")
|
|
||||||
?.trim()
|
|
||||||
?.toLowerCase() ==
|
|
||||||
'xmlhttprequest';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Magically transforms an [HttpRequest] into a [RequestContext].
|
/// Magically transforms an [HttpRequest] into a [RequestContext].
|
||||||
static Future<HttpRequestContext> from(
|
static Future<HttpRequestContext> from(
|
||||||
HttpRequest request, Angel app, String path) {
|
HttpRequest request, Angel app, String path) {
|
||||||
|
|
|
@ -2,6 +2,8 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
import '../core/core.dart';
|
import '../core/core.dart';
|
||||||
import 'http_request_context.dart';
|
import 'http_request_context.dart';
|
||||||
|
|
||||||
|
@ -68,6 +70,14 @@ class HttpResponseContext extends ResponseContext<HttpResponse> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void set contentType(MediaType value) {
|
||||||
|
super.contentType = value;
|
||||||
|
if (!_streamInitialized)
|
||||||
|
rawResponse.headers.contentType =
|
||||||
|
ContentType(value.type, value.subtype, parameters: value.parameters);
|
||||||
|
}
|
||||||
|
|
||||||
bool _openStream() {
|
bool _openStream() {
|
||||||
if (!_streamInitialized) {
|
if (!_streamInitialized) {
|
||||||
// If this is the first stream added to this response,
|
// If this is the first stream added to this response,
|
||||||
|
|
|
@ -2,34 +2,32 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel_framework/angel_framework.dart' hide Header;
|
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:http2/transport.dart';
|
||||||
import 'package:mock_request/mock_request.dart';
|
import 'package:mock_request/mock_request.dart';
|
||||||
import 'http2_request_context.dart';
|
import 'http2_request_context.dart';
|
||||||
import 'http2_response_context.dart';
|
import 'http2_response_context.dart';
|
||||||
import 'package:pool/pool.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
|
|
||||||
class AngelHttp2 extends Driver<Socket, ServerTransportStream,
|
class AngelHttp2 extends Driver<Socket, ServerTransportStream,
|
||||||
ArtificialServerSocket, Http2RequestContext, Http2ResponseContext> {
|
SecureServerSocket, Http2RequestContext, Http2ResponseContext> {
|
||||||
final ServerSettings settings;
|
final ServerSettings settings;
|
||||||
final StreamController<HttpRequest> _onHttp1 = new StreamController();
|
final StreamController<HttpRequest> _onHttp1 = new StreamController();
|
||||||
final Map<String, MockHttpSession> _sessions = {};
|
final Map<String, MockHttpSession> _sessions = {};
|
||||||
final Uuid _uuid = new Uuid();
|
final Uuid _uuid = new Uuid();
|
||||||
ArtificialServerSocket _artificial;
|
_AngelHttp2ServerSocket _artificial;
|
||||||
HttpServer _httpServer;
|
|
||||||
StreamController<SecureSocket> _http1;
|
SecureServerSocket get socket => _artificial;
|
||||||
SecureServerSocket _socket;
|
|
||||||
StreamSubscription _sub;
|
|
||||||
|
|
||||||
AngelHttp2._(
|
AngelHttp2._(
|
||||||
Angel app,
|
Angel app,
|
||||||
Future<ArtificialServerSocket> Function(dynamic, int) serverGenerator,
|
Future<SecureServerSocket> Function(dynamic, int) serverGenerator,
|
||||||
bool useZone,
|
bool useZone,
|
||||||
this.settings)
|
this.settings)
|
||||||
: super(app, serverGenerator, useZone: useZone);
|
: super(
|
||||||
|
app,
|
||||||
|
serverGenerator,
|
||||||
|
useZone: useZone,
|
||||||
|
);
|
||||||
|
|
||||||
factory AngelHttp2(Angel app, SecurityContext securityContext,
|
factory AngelHttp2(Angel app, SecurityContext securityContext,
|
||||||
{bool useZone: true, ServerSettings settings}) {
|
{bool useZone: true, ServerSettings settings}) {
|
||||||
|
@ -48,14 +46,25 @@ class AngelHttp2 extends Driver<Socket, ServerTransportStream,
|
||||||
var addr = address is InternetAddress
|
var addr = address is InternetAddress
|
||||||
? address
|
? address
|
||||||
: new InternetAddress(address.toString());
|
: new InternetAddress(address.toString());
|
||||||
return SecureServerSocket.bind(addr, port, ctx)
|
return SecureServerSocket.bind(addr, port, ctx);
|
||||||
.then((s) => ArtificialServerSocket(addr, port, s));
|
|
||||||
}, useZone, settings);
|
}, useZone, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fires when an HTTP/1.x request is received.
|
/// Fires when an HTTP/1.x request is received.
|
||||||
Stream<HttpRequest> get onHttp1 => _onHttp1.stream;
|
Stream<HttpRequest> get onHttp1 => _onHttp1.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SecureServerSocket> generateServer([address, int port]) async {
|
||||||
|
var s = await serverGenerator(address ?? '127.0.0.1', port ?? 0);
|
||||||
|
return _artificial = new _AngelHttp2ServerSocket(s, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SecureServerSocket> close() async {
|
||||||
|
await _artificial.close();
|
||||||
|
return await super.close();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addCookies(ServerTransportStream response, Iterable<Cookie> cookies) {
|
void addCookies(ServerTransportStream response, Iterable<Cookie> cookies) {
|
||||||
var headers = cookies
|
var headers = cookies
|
||||||
|
@ -84,14 +93,11 @@ class AngelHttp2 extends Driver<Socket, ServerTransportStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ServerTransportStream createResponseFromRawRequest(Socket request) {
|
Stream<ServerTransportStream> createResponseStreamFromRawRequest(
|
||||||
|
Socket request) {
|
||||||
var connection =
|
var connection =
|
||||||
new ServerTransportConnection.viaSocket(request, settings: settings);
|
new ServerTransportConnection.viaSocket(request, settings: settings);
|
||||||
}
|
return connection.incomingStreams;
|
||||||
|
|
||||||
@override
|
|
||||||
Uri getUriFromRequest(Socket request) {
|
|
||||||
// TODO: implement getUriFromRequest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -130,3 +136,85 @@ class AngelHttp2 extends Driver<Socket, ServerTransportStream,
|
||||||
response.sendData(data);
|
response.sendData(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FakeServerSocket extends Stream<Socket> implements ServerSocket {
|
||||||
|
final _AngelHttp2ServerSocket angel;
|
||||||
|
final _ctrl = new StreamController<Socket>();
|
||||||
|
|
||||||
|
_FakeServerSocket(this.angel);
|
||||||
|
|
||||||
|
@override
|
||||||
|
InternetAddress get address => angel.address;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ServerSocket> close() async {
|
||||||
|
_ctrl.close();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get port => angel.port;
|
||||||
|
|
||||||
|
@override
|
||||||
|
StreamSubscription<Socket> listen(void Function(Socket event) onData,
|
||||||
|
{Function onError, void Function() onDone, bool cancelOnError}) {
|
||||||
|
return _ctrl.stream.listen(onData,
|
||||||
|
cancelOnError: cancelOnError, onError: onError, onDone: onDone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AngelHttp2ServerSocket extends Stream<SecureSocket>
|
||||||
|
implements SecureServerSocket {
|
||||||
|
final SecureServerSocket socket;
|
||||||
|
final AngelHttp2 driver;
|
||||||
|
final _ctrl = new StreamController<SecureSocket>();
|
||||||
|
_FakeServerSocket _fake;
|
||||||
|
StreamSubscription _sub;
|
||||||
|
|
||||||
|
_AngelHttp2ServerSocket(this.socket, this.driver) {
|
||||||
|
_fake = new _FakeServerSocket(this);
|
||||||
|
new HttpServer.listenOn(_fake).pipe(driver._onHttp1);
|
||||||
|
_sub = socket.listen(
|
||||||
|
(socket) {
|
||||||
|
if (socket.selectedProtocol == null ||
|
||||||
|
socket.selectedProtocol == 'http/1.0' ||
|
||||||
|
socket.selectedProtocol == 'http/1.1') {
|
||||||
|
_fake._ctrl.add(socket);
|
||||||
|
} else if (socket.selectedProtocol == 'h2' ||
|
||||||
|
socket.selectedProtocol == 'h2-14') {
|
||||||
|
_ctrl.add(socket);
|
||||||
|
} else {
|
||||||
|
socket.destroy();
|
||||||
|
throw new Exception(
|
||||||
|
'AngelHttp2 does not support ${socket.selectedProtocol} as an ALPN protocol.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: _ctrl.close,
|
||||||
|
onError: (e, st) {
|
||||||
|
driver.app.logger.warning(
|
||||||
|
'HTTP/2 incoming connection failure: ', e, st as StackTrace);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InternetAddress get address => socket.address;
|
||||||
|
|
||||||
|
int get port => socket.port;
|
||||||
|
|
||||||
|
Future<SecureServerSocket> close() async {
|
||||||
|
_sub?.cancel();
|
||||||
|
_fake.close();
|
||||||
|
_ctrl.close();
|
||||||
|
return await socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
StreamSubscription<SecureSocket> listen(
|
||||||
|
void Function(SecureSocket event) onData,
|
||||||
|
{Function onError,
|
||||||
|
void Function() onDone,
|
||||||
|
bool cancelOnError}) {
|
||||||
|
return _ctrl.stream.listen(onData,
|
||||||
|
cancelOnError: cancelOnError, onError: onError, onDone: onDone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:angel_container/src/container.dart';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:body_parser/body_parser.dart';
|
import 'package:body_parser/body_parser.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
@ -11,9 +12,9 @@ import 'package:uuid/uuid.dart';
|
||||||
final RegExp _comma = new RegExp(r',\s*');
|
final RegExp _comma = new RegExp(r',\s*');
|
||||||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||||
|
|
||||||
class Http2RequestContext extends RequestContext {
|
class Http2RequestContext extends RequestContext<ServerTransportStream> {
|
||||||
|
final Container container;
|
||||||
BytesBuilder _buf;
|
BytesBuilder _buf;
|
||||||
ContentType _contentType;
|
|
||||||
List<Cookie> _cookies;
|
List<Cookie> _cookies;
|
||||||
HttpHeaders _headers;
|
HttpHeaders _headers;
|
||||||
String _method, _override, _path;
|
String _method, _override, _path;
|
||||||
|
@ -22,13 +23,15 @@ class Http2RequestContext extends RequestContext {
|
||||||
ServerTransportStream _stream;
|
ServerTransportStream _stream;
|
||||||
Uri _uri;
|
Uri _uri;
|
||||||
|
|
||||||
|
Http2RequestContext._(this.container);
|
||||||
|
|
||||||
static Future<Http2RequestContext> from(
|
static Future<Http2RequestContext> from(
|
||||||
ServerTransportStream stream,
|
ServerTransportStream stream,
|
||||||
Socket socket,
|
Socket socket,
|
||||||
Angel app,
|
Angel app,
|
||||||
Map<String, MockHttpSession> sessions,
|
Map<String, MockHttpSession> sessions,
|
||||||
Uuid uuid) async {
|
Uuid uuid) async {
|
||||||
var req = new Http2RequestContext()
|
var req = new Http2RequestContext._(app.container.createChild())
|
||||||
..app = app
|
..app = app
|
||||||
.._socket = socket
|
.._socket = socket
|
||||||
.._stream = stream;
|
.._stream = stream;
|
||||||
|
@ -93,7 +96,7 @@ class Http2RequestContext extends RequestContext {
|
||||||
cookies.firstWhere((c) => c.name == 'DARTSESSID', orElse: () => null);
|
cookies.firstWhere((c) => c.name == 'DARTSESSID', orElse: () => null);
|
||||||
|
|
||||||
if (dartSessId == null) {
|
if (dartSessId == null) {
|
||||||
dartSessId = new Cookie('DARTSESSID', uuid.v4());
|
dartSessId = new Cookie('DARTSESSID', uuid.v4() as String);
|
||||||
}
|
}
|
||||||
|
|
||||||
req._session = sessions.putIfAbsent(
|
req._session = sessions.putIfAbsent(
|
||||||
|
@ -110,12 +113,6 @@ class Http2RequestContext extends RequestContext {
|
||||||
/// The underlying HTTP/2 [ServerTransportStream].
|
/// The underlying HTTP/2 [ServerTransportStream].
|
||||||
ServerTransportStream get stream => _stream;
|
ServerTransportStream get stream => _stream;
|
||||||
|
|
||||||
@override
|
|
||||||
bool get xhr {
|
|
||||||
return headers.value("X-Requested-With")?.trim()?.toLowerCase() ==
|
|
||||||
'xmlhttprequest';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Uri get uri => _uri;
|
Uri get uri => _uri;
|
||||||
|
|
||||||
|
@ -132,12 +129,6 @@ class Http2RequestContext extends RequestContext {
|
||||||
return _path;
|
return _path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
ContentType get contentType =>
|
|
||||||
_contentType ??= (headers['content-type'] == null
|
|
||||||
? null
|
|
||||||
: ContentType.parse(headers.value('content-type')));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get originalMethod {
|
String get originalMethod {
|
||||||
return _method;
|
return _method;
|
||||||
|
@ -148,9 +139,6 @@ class Http2RequestContext extends RequestContext {
|
||||||
return _override ?? _method;
|
return _override ?? _method;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
HttpRequest get io => null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get hostname => _headers.value('host');
|
String get hostname => _headers.value('host');
|
||||||
|
|
||||||
|
@ -168,7 +156,10 @@ class Http2RequestContext extends RequestContext {
|
||||||
new Stream.fromIterable([_buf.takeBytes()]),
|
new Stream.fromIterable([_buf.takeBytes()]),
|
||||||
contentType == null ? null : new MediaType.parse(contentType.toString()),
|
contentType == null ? null : new MediaType.parse(contentType.toString()),
|
||||||
uri,
|
uri,
|
||||||
storeOriginalBuffer: app.storeOriginalBuffer,
|
storeOriginalBuffer: app.keepRawRequestBuffers == true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ServerTransportStream get rawRequest => _stream;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,21 @@ import 'package:angel_framework/angel_framework.dart' hide Header;
|
||||||
import 'package:http2/transport.dart';
|
import 'package:http2/transport.dart';
|
||||||
import 'http2_request_context.dart';
|
import 'http2_request_context.dart';
|
||||||
|
|
||||||
class Http2ResponseContext extends ResponseContext {
|
class Http2ResponseContext extends ResponseContext<ServerTransportStream> {
|
||||||
final Angel app;
|
final Angel app;
|
||||||
final ServerTransportStream stream;
|
final ServerTransportStream stream;
|
||||||
|
|
||||||
|
ServerTransportStream get rawResponse => stream;
|
||||||
|
|
||||||
|
LockableBytesBuilder _buffer;
|
||||||
|
|
||||||
final Http2RequestContext _req;
|
final Http2RequestContext _req;
|
||||||
bool _useStream = false, _isClosed = false, _isPush = false;
|
|
||||||
|
bool _isDetached = false,
|
||||||
|
_isClosed = false,
|
||||||
|
_streamInitialized = false,
|
||||||
|
_isPush = false;
|
||||||
|
|
||||||
Uri _targetUri;
|
Uri _targetUri;
|
||||||
|
|
||||||
Http2ResponseContext(this.app, this.stream, this._req) {
|
Http2ResponseContext(this.app, this.stream, this._req) {
|
||||||
|
@ -26,46 +36,65 @@ class Http2ResponseContext extends ResponseContext {
|
||||||
/// Returns a [List] of all resources that have [push]ed to the client.
|
/// Returns a [List] of all resources that have [push]ed to the client.
|
||||||
List<Http2ResponseContext> get pushes => new List.unmodifiable(_pushes);
|
List<Http2ResponseContext> get pushes => new List.unmodifiable(_pushes);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ServerTransportStream detach() {
|
||||||
|
_isDetached = true;
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RequestContext get correspondingRequest => _req;
|
RequestContext get correspondingRequest => _req;
|
||||||
|
|
||||||
Uri get targetUri => _targetUri;
|
Uri get targetUri => _targetUri;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
HttpResponse get io => null;
|
bool get isOpen {
|
||||||
|
return !_isClosed && !_isDetached;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get streaming => _useStream;
|
bool get isBuffered => _buffer != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get isOpen => !_isClosed;
|
BytesBuilder get buffer => _buffer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addError(Object error, [StackTrace stackTrace]) {
|
||||||
|
super.addError(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void useBuffer() {
|
||||||
|
_buffer = new LockableBytesBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
/// Write headers, status, etc. to the underlying [stream].
|
/// Write headers, status, etc. to the underlying [stream].
|
||||||
void finalize() {
|
bool _openStream() {
|
||||||
if (_isPush) return;
|
if (_isPush || _streamInitialized) return false;
|
||||||
|
|
||||||
var headers = <Header>[
|
var headers = <Header>[
|
||||||
new Header.ascii(':status', statusCode.toString()),
|
new Header.ascii(':status', statusCode.toString()),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (encoders.isNotEmpty && correspondingRequest != null) {
|
if (encoders.isNotEmpty && correspondingRequest != null) {
|
||||||
var allowedEncodings =
|
if (_allowedEncodings != null) {
|
||||||
(correspondingRequest.headers['accept-encoding'] ?? []).map((str) {
|
for (var encodingName in _allowedEncodings) {
|
||||||
// Ignore quality specifications in accept-encoding
|
Converter<List<int>, List<int>> encoder;
|
||||||
// ex. gzip;q=0.8
|
|
||||||
if (!str.contains(';')) return str;
|
|
||||||
return str.split(';')[0];
|
|
||||||
});
|
|
||||||
|
|
||||||
for (var encodingName in allowedEncodings) {
|
|
||||||
String key = encodingName;
|
String key = encodingName;
|
||||||
|
|
||||||
if (encoders.containsKey(encodingName)) {
|
if (encoders.containsKey(encodingName))
|
||||||
|
encoder = encoders[encodingName];
|
||||||
|
else if (encodingName == '*') {
|
||||||
|
encoder = encoders[key = encoders.keys.first];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoder != null) {
|
||||||
this.headers['content-encoding'] = key;
|
this.headers['content-encoding'] = key;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add all normal headers
|
// Add all normal headers
|
||||||
for (var key in this.headers.keys) {
|
for (var key in this.headers.keys) {
|
||||||
|
@ -81,54 +110,35 @@ class Http2ResponseContext extends ResponseContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.sendHeaders(headers);
|
stream.sendHeaders(headers);
|
||||||
|
return _streamInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Iterable<String> __allowedEncodings;
|
||||||
void addError(Object error, [StackTrace stackTrace]) {
|
|
||||||
Zone.current.handleUncaughtError(error, stackTrace);
|
|
||||||
super.addError(error, stackTrace);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
Iterable<String> get _allowedEncodings {
|
||||||
bool useStream() {
|
return __allowedEncodings ??= correspondingRequest.headers
|
||||||
if (!_useStream) {
|
.value('accept-encoding')
|
||||||
// If this is the first stream added to this response,
|
?.split(',')
|
||||||
// then add headers, status code, etc.
|
?.map((s) => s.trim())
|
||||||
finalize();
|
?.where((s) => s.isNotEmpty)
|
||||||
|
?.map((str) {
|
||||||
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
|
// Ignore quality specifications in accept-encoding
|
||||||
// ex. gzip;q=0.8
|
// ex. gzip;q=0.8
|
||||||
if (!str.contains(';')) return str;
|
if (!str.contains(';')) return str;
|
||||||
return str.split(';')[0];
|
return str.split(';')[0];
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (var encodingName in allowedEncodings) {
|
@override
|
||||||
|
Future addStream(Stream<List<int>> stream) {
|
||||||
|
if (!isOpen && isBuffered) throw ResponseContext.closed();
|
||||||
|
_openStream();
|
||||||
|
|
||||||
|
Stream<List<int>> output = stream;
|
||||||
|
|
||||||
|
if (encoders.isNotEmpty && correspondingRequest != null) {
|
||||||
|
if (_allowedEncodings != null) {
|
||||||
|
for (var encodingName in _allowedEncodings) {
|
||||||
Converter<List<int>, List<int>> encoder;
|
Converter<List<int>, List<int>> encoder;
|
||||||
String key = encodingName;
|
String key = encodingName;
|
||||||
|
|
||||||
|
@ -139,58 +149,62 @@ class Http2ResponseContext extends ResponseContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encoder != null) {
|
if (encoder != null) {
|
||||||
/*
|
|
||||||
if (firstStream) {
|
|
||||||
this.stream.sendHeaders([
|
|
||||||
new Header.ascii(
|
|
||||||
'content-encoding', headers['content-encoding'] = key)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
output = encoders[key].bind(output);
|
output = encoders[key].bind(output);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return output.forEach(this.stream.sendData);
|
return output.forEach(this.stream.sendData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void add(List<int> data) {
|
void add(List<int> data) {
|
||||||
if (_isClosed && !_useStream)
|
if (!isOpen && isBuffered)
|
||||||
throw ResponseContext.closed();
|
throw ResponseContext.closed();
|
||||||
else if (_useStream)
|
else if (!isBuffered) {
|
||||||
//stream.sendData(data);
|
_openStream();
|
||||||
addStream(new Stream.fromIterable([data]));
|
|
||||||
else
|
if (encoders.isNotEmpty && correspondingRequest != null) {
|
||||||
|
if (_allowedEncodings != null) {
|
||||||
|
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) {
|
||||||
|
data = encoders[key].convert(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.sendData(data);
|
||||||
|
} else
|
||||||
buffer.add(data);
|
buffer.add(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future close() async {
|
Future close() async {
|
||||||
if (_useStream) {
|
if (!_isDetached && !_isClosed && !isBuffered) {
|
||||||
try {
|
_openStream();
|
||||||
await stream.outgoingMessages.close();
|
await stream.outgoingMessages.close();
|
||||||
} catch (_) {
|
|
||||||
// This only seems to occur on `MockHttpRequest`, but
|
|
||||||
// this try/catch prevents a crash.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_isClosed = true;
|
_isClosed = true;
|
||||||
await super.close();
|
await super.close();
|
||||||
_useStream = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes a resource to the client.
|
/// Pushes a resource to the client.
|
||||||
Http2ResponseContext push(String path,
|
Http2ResponseContext push(String path,
|
||||||
{Map<String, String> headers: const {}, String method: 'GET'}) {
|
{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 targetUri = _req.uri.replace(path: path);
|
||||||
|
|
||||||
var h = <Header>[
|
var h = <Header>[
|
||||||
|
@ -211,8 +225,4 @@ class Http2ResponseContext extends ResponseContext {
|
||||||
_pushes.add(r);
|
_pushes.add(r);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
void internalReopen() {
|
|
||||||
_isClosed = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
name: angel_framework
|
name: angel_framework
|
||||||
version: 2.0.0-alpha.10
|
version: 2.0.0-alpha.11
|
||||||
description: >
|
description: A high-powered HTTP server with dependency injection, routing and much more.
|
||||||
A high-powered HTTP server with DI, routing and more.
|
|
||||||
When combined with the other packages in the Angel ecosystem, this
|
|
||||||
package can be used to make robust application servers for API's,
|
|
||||||
traditional server-side applications, and much more, along with
|
|
||||||
a rich client API.
|
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/angel_framework
|
homepage: https://github.com/angel-dart/angel_framework
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -64,5 +64,5 @@ Future<RequestContext> acceptContentTypes(
|
||||||
rq.close();
|
rq.close();
|
||||||
var app = new Angel(reflector: MirrorsReflector());
|
var app = new Angel(reflector: MirrorsReflector());
|
||||||
var http = new AngelHttp(app);
|
var http = new AngelHttp(app);
|
||||||
return http.createRequestContext(rq);
|
return http.createRequestContext(rq, rq.response);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,12 +38,14 @@ class NamedController extends Controller {
|
||||||
optional() => 2;
|
optional() => 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
void foo(RequestContext req, ResponseContext res) {
|
bool foo(RequestContext req, ResponseContext res) {
|
||||||
res.write("Hello, ");
|
res.write("Hello, ");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void bar(RequestContext req, ResponseContext res) {
|
bool bar(RequestContext req, ResponseContext res) {
|
||||||
res.write("world!");
|
res.write("world!");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
|
|
@ -67,7 +67,7 @@ void encodingTests(Angel getApp()) {
|
||||||
await http.handleRequest(rq);
|
await http.handleRequest(rq);
|
||||||
|
|
||||||
var body = await getBody(rs);
|
var body = await getBody(rs);
|
||||||
print(rs.headers);
|
//print(rs.headers);
|
||||||
expect(rs.headers.value('content-encoding'), 'deflate');
|
expect(rs.headers.value('content-encoding'), 'deflate');
|
||||||
expect(body, zlib.encode(utf8.encode('Hello, world!')));
|
expect(body, zlib.encode(utf8.encode('Hello, world!')));
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,5 +28,5 @@ Future<RequestContext> makeRequest(String path) {
|
||||||
var rq = new MockHttpRequest('GET', ENDPOINT.replace(path: path))..close();
|
var rq = new MockHttpRequest('GET', ENDPOINT.replace(path: path))..close();
|
||||||
var app = new Angel(reflector: MirrorsReflector());
|
var app = new Angel(reflector: MirrorsReflector());
|
||||||
var http = new AngelHttp(app);
|
var http = new AngelHttp(app);
|
||||||
return http.createRequestContext(rq);
|
return http.createRequestContext(rq, rq.response);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,10 +29,9 @@ void main() {
|
||||||
..keepRawRequestBuffers = true
|
..keepRawRequestBuffers = true
|
||||||
..encoders['gzip'] = gzip.encoder;
|
..encoders['gzip'] = gzip.encoder;
|
||||||
|
|
||||||
app.get('/', (req, res) {
|
app.get('/', (req, res) async {
|
||||||
res
|
res.write('Hello world');
|
||||||
..write('Hello world')
|
await res.close();
|
||||||
..close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.all('/method', (req, res) => req.method);
|
app.all('/method', (req, res) => req.method);
|
||||||
|
@ -41,16 +40,14 @@ void main() {
|
||||||
|
|
||||||
app.get('/stream', (req, res) => jfkStream().pipe(res));
|
app.get('/stream', (req, res) => jfkStream().pipe(res));
|
||||||
|
|
||||||
app.get('/headers', (req, res) {
|
app.get('/headers', (req, res) async {
|
||||||
res
|
res.headers.addAll({'foo': 'bar', 'x-angel': 'http2'});
|
||||||
..headers.addAll({'foo': 'bar', 'x-angel': 'http2'})
|
await res.close();
|
||||||
..close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/status', (req, res) {
|
app.get('/status', (req, res) async {
|
||||||
res
|
res.statusCode = 1337;
|
||||||
..statusCode = 1337
|
await res.close();
|
||||||
..close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/body', (req, res) => req.parseBody());
|
app.post('/body', (req, res) => req.parseBody());
|
||||||
|
@ -63,9 +60,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/push', (req, res) async {
|
app.get('/push', (req, res) async {
|
||||||
res
|
res.write('ok');
|
||||||
..write('ok')
|
|
||||||
..close();
|
|
||||||
|
|
||||||
if (res is Http2ResponseContext && res.canPush) {
|
if (res is Http2ResponseContext && res.canPush) {
|
||||||
res.push('a')
|
res.push('a')
|
||||||
|
@ -76,6 +71,8 @@ void main() {
|
||||||
..write('b')
|
..write('b')
|
||||||
..close();
|
..close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await res.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
var ctx = new SecurityContext()
|
var ctx = new SecurityContext()
|
||||||
|
@ -211,7 +208,6 @@ void main() {
|
||||||
expect(getPath(pushA), '/a');
|
expect(getPath(pushA), '/a');
|
||||||
expect(getPath(pushB), '/b');
|
expect(getPath(pushB), '/b');
|
||||||
|
|
||||||
// TODO: Dart http/2 client seems to not be able to get body
|
|
||||||
// However, Chrome, Firefox, Edge all can
|
// However, Chrome, Firefox, Edge all can
|
||||||
//expect(await getBody(pushA.stream), 'a');
|
//expect(await getBody(pushA.stream), 'a');
|
||||||
//expect(await getBody(pushB.stream), 'b');
|
//expect(await getBody(pushB.stream), 'b');
|
||||||
|
|
|
@ -62,8 +62,8 @@ main() {
|
||||||
try {
|
try {
|
||||||
var rq = new MockHttpRequest('GET', Uri.parse('/num/unparsed/32'))
|
var rq = new MockHttpRequest('GET', Uri.parse('/num/unparsed/32'))
|
||||||
..close();
|
..close();
|
||||||
var req = await http.createRequestContext(rq);
|
var req = await http.createRequestContext(rq, rq.response);
|
||||||
var res = await http.createResponseContext(rq.response, req);
|
var res = await http.createResponseContext(rq, rq.response, req);
|
||||||
await app.runContained((num unparsed) => unparsed, req, res);
|
await app.runContained((num unparsed) => unparsed, req, res);
|
||||||
throw new StateError(
|
throw new StateError(
|
||||||
'ArgumentError should be thrown if a parameter cannot be resolved.');
|
'ArgumentError should be thrown if a parameter cannot be resolved.');
|
||||||
|
|
|
@ -29,8 +29,9 @@ void interceptor(RequestContext req, ResponseContext res) {
|
||||||
..close();
|
..close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void interceptService(RequestContext req, ResponseContext res) {
|
bool interceptService(RequestContext req, ResponseContext res) {
|
||||||
res.write("Service with ");
|
res.write("Service with ");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
@ -96,6 +97,7 @@ main() {
|
||||||
RequestHandler write(String message) {
|
RequestHandler write(String message) {
|
||||||
return (req, res) {
|
return (req, res) {
|
||||||
res.write(message);
|
res.write(message);
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +106,7 @@ main() {
|
||||||
|
|
||||||
app.fallback((req, res) => 'MJ');
|
app.fallback((req, res) => 'MJ');
|
||||||
|
|
||||||
app.dumpTree(header: "DUMPING ROUTES:", showMatchers: true);
|
//app.dumpTree(header: "DUMPING ROUTES:", showMatchers: true);
|
||||||
|
|
||||||
client = new http.Client();
|
client = new http.Client();
|
||||||
var server = await new AngelHttp(app).startServer('127.0.0.1', 0);
|
var server = await new AngelHttp(app).startServer('127.0.0.1', 0);
|
||||||
|
|
|
@ -44,12 +44,12 @@ main() {
|
||||||
var rq = new MockHttpRequest('GET', $foo);
|
var rq = new MockHttpRequest('GET', $foo);
|
||||||
rq.close();
|
rq.close();
|
||||||
var rs = rq.response;
|
var rs = rq.response;
|
||||||
var req = await http.createRequestContext(rq);
|
var req = await http.createRequestContext(rq, rs);
|
||||||
var res = await http.createResponseContext(rs);
|
var res = await http.createResponseContext(rq, rs);
|
||||||
var e = new AngelHttpException(null,
|
var e = new AngelHttpException(null,
|
||||||
statusCode: 321, message: 'Hello', errors: ['foo', 'bar']);
|
statusCode: 321, message: 'Hello', errors: ['foo', 'bar']);
|
||||||
await app.errorHandler(e, req, res);
|
await app.errorHandler(e, req, res);
|
||||||
await http.sendResponse(rq, req, res);
|
await http.sendResponse(rq, rs, req, res);
|
||||||
expect(
|
expect(
|
||||||
ContentType.parse(rs.headers.value('content-type')).mimeType,
|
ContentType.parse(rs.headers.value('content-type')).mimeType,
|
||||||
'text/html',
|
'text/html',
|
||||||
|
@ -120,8 +120,8 @@ main() {
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
var rq = new MockHttpRequest('GET', $foo)..close();
|
var rq = new MockHttpRequest('GET', $foo)..close();
|
||||||
req = await http.createRequestContext(rq);
|
req = await http.createRequestContext(rq, rq.response);
|
||||||
res = await http.createResponseContext(rq.response);
|
res = await http.createResponseContext(rq, rq.response);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('getHandlerResult', () {
|
group('getHandlerResult', () {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -e
|
||||||
pub run test
|
pub run test --timeout 5s
|
||||||
ANGEL_ENV=production pub run test
|
ANGEL_ENV=production pub run test --timeout 5s
|
Loading…
Reference in a new issue