from static method
Implementation
static Future<Http2RequestContext> from(
ServerTransportStream stream,
Socket socket,
Angel app,
Map<String, MockHttpSession> sessions,
Uuid uuid,
) {
var c = Completer<Http2RequestContext>();
var req = Http2RequestContext._(app.container.createChild())
..app = app
.._socket = socket
.._stream = stream;
var headers = req._headers = MockHttpHeaders();
// String scheme = 'https', host = socket.address.address, path = '';
var uri = Uri(
scheme: 'https',
host: socket.address.address,
port: socket.port,
);
var cookies = <Cookie>[];
void finalize() {
if (c.isCompleted) return;
req
.._cookies = List.unmodifiable(cookies)
.._uri = uri;
// Apply the session only now that the cookie headers have been read.
// An unknown id gets a fresh session rather than adopting the
// client-chosen id, which would allow session fixation.
var sessionId = cookies
.firstWhereOrNull((c) => c.name == 'DARTSESSID')
?.value;
var session = sessionId == null ? null : sessions[sessionId];
if (session == null) {
var id = uuid.v4();
session = sessions[id] = MockHttpSession(id: id);
}
req._session = session;
c.complete(req);
}
void fail(Object error, StackTrace stackTrace) {
if (!c.isCompleted) {
c.completeError(error, stackTrace);
} else if (!req._body.isClosed) {
req._body.addError(error, stackTrace);
}
}
void parseHost(String value) {
// Parse as an authority ("host[:port]"), not as a full URI, where
// "example.com:8443" would be read as a scheme.
var inUri = Uri.tryParse('//$value');
if (inUri == null || inUri.host.isEmpty) return;
uri = uri.replace(host: inUri.host);
if (inUri.hasPort) uri = uri.replace(port: inUri.port);
}
void handleHeader(Header header) {
// Header bytes come straight from the client: decode them without
// throwing (Latin-1 accepts every byte, matching dart:io), and do not
// percent-decode or comma-split values, which would corrupt them.
var name = latin1.decode(header.name).toLowerCase();
var value = latin1.decode(header.value);
switch (name) {
case ':method':
req._method = value;
break;
case ':path':
var inUri = Uri.tryParse(value);
if (inUri == null) {
throw AngelHttpException.badRequest(message: 'Invalid :path.');
}
uri = uri.replace(path: inUri.path);
if (inUri.hasQuery) uri = uri.replace(query: inUri.query);
var path = uri.path.replaceAll(_straySlashes, '');
req._path = path;
if (path.isEmpty) req._path = '/';
break;
case ':scheme':
uri = uri.replace(scheme: value);
break;
case ':authority':
// HTTP/2 clients send :authority instead of Host.
parseHost(value);
if (headers.value('host') == null) headers.set('host', value);
break;
case 'cookie':
var cookieStrings = value.split(';').map((s) => s.trim());
for (var cookieString in cookieStrings) {
try {
cookies.add(Cookie.fromSetCookieValue(cookieString));
} catch (_) {
// Ignore malformed cookies, and just don't add them to the container.
}
}
break;
case 'host':
parseHost(value);
headers.set('host', value);
break;
default:
headers.add(name, value);
break;
}
}
stream.incomingMessages.listen(
(msg) {
// Nothing may throw out of this listener: it runs outside the
// request's error zone, so an exception here would be unhandled
// and terminate the whole server.
try {
if (msg is DataStreamMessage) {
finalize();
req._body.add(msg.bytes);
} else if (msg is HeadersStreamMessage) {
msg.headers.forEach(handleHeader);
if (msg.endStream) finalize();
}
} catch (e, st) {
fail(e, st);
}
},
onDone: () {
finalize();
req._body.close();
},
cancelOnError: true,
onError: fail,
);
return c.future;
}