blob: 0a8bba4c042002d1f44530b702274f64d9d8b996 [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of dart._http;
// Global constants.
class _Const {
// Bytes for "HTTP".
static const HTTP = [72, 84, 84, 80];
// Bytes for "HTTP/1.".
static const HTTP1DOT = [72, 84, 84, 80, 47, 49, 46];
// Bytes for "HTTP/1.0".
static const HTTP10 = [72, 84, 84, 80, 47, 49, 46, 48];
// Bytes for "HTTP/1.1".
static const HTTP11 = [72, 84, 84, 80, 47, 49, 46, 49];
static const bool T = true;
static const bool F = false;
// Loopup-map for the following characters: '()<>@,;:\\"/[]?={} \t'.
static const SEPARATOR_MAP = [
F, F, F, F, F, F, F, F, F, T, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
F, F, F, F, F, F, F, F, T, F, T, F, F, F, F, F, T, T, F, F, T, F, F, T, //
F, F, F, F, F, F, F, F, F, F, T, T, T, T, T, T, T, F, F, F, F, F, F, F, //
F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, T, T, T, F, F, //
F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
F, F, F, T, F, T, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F
];
}
// Frequently used character codes.
class _CharCode {
static const int HT = 9;
static const int LF = 10;
static const int CR = 13;
static const int SP = 32;
static const int COMMA = 44;
static const int SLASH = 47;
static const int ZERO = 48;
static const int ONE = 49;
static const int COLON = 58;
static const int SEMI_COLON = 59;
}
// States of the HTTP parser state machine.
class _State {
static const int START = 0;
static const int METHOD_OR_RESPONSE_HTTP_VERSION = 1;
static const int RESPONSE_HTTP_VERSION = 2;
static const int REQUEST_LINE_METHOD = 3;
static const int REQUEST_LINE_URI = 4;
static const int REQUEST_LINE_HTTP_VERSION = 5;
static const int REQUEST_LINE_ENDING = 6;
static const int RESPONSE_LINE_STATUS_CODE = 7;
static const int RESPONSE_LINE_REASON_PHRASE = 8;
static const int RESPONSE_LINE_ENDING = 9;
static const int HEADER_START = 10;
static const int HEADER_FIELD = 11;
static const int HEADER_VALUE_START = 12;
static const int HEADER_VALUE = 13;
static const int HEADER_VALUE_FOLD_OR_END_CR = 14;
static const int HEADER_VALUE_FOLD_OR_END = 15;
static const int HEADER_ENDING = 16;
static const int CHUNK_SIZE_STARTING_CR = 17;
static const int CHUNK_SIZE_STARTING = 18;
static const int CHUNK_SIZE = 19;
static const int CHUNK_SIZE_EXTENSION = 20;
static const int CHUNK_SIZE_ENDING = 21;
static const int CHUNKED_BODY_DONE_CR = 22;
static const int CHUNKED_BODY_DONE = 23;
static const int BODY = 24;
static const int CLOSED = 25;
static const int UPGRADED = 26;
static const int FAILURE = 27;
static const int FIRST_BODY_STATE = CHUNK_SIZE_STARTING_CR;
}
// HTTP version of the request or response being parsed.
class _HttpVersion {
static const int UNDETERMINED = 0;
static const int HTTP10 = 1;
static const int HTTP11 = 2;
}
// States of the HTTP parser state machine.
class _MessageType {
static const int UNDETERMINED = 0;
static const int REQUEST = 1;
static const int RESPONSE = 0;
}
/// The _HttpDetachedStreamSubscription takes a subscription and some extra data,
/// and makes it possible to "inject" the data in from of other data events
/// from the subscription.
///
/// It does so by overriding pause/resume, so that once the
/// _HttpDetachedStreamSubscription is resumed, it'll deliver the data before
/// resuming the underlying subscription.
class _HttpDetachedStreamSubscription implements StreamSubscription<Uint8List> {
final StreamSubscription<Uint8List> _subscription;
Uint8List? _injectData;
void Function(Uint8List data)? _userOnData;
bool _isCanceled = false;
bool _scheduled = false;
int _pauseCount = 1;
_HttpDetachedStreamSubscription(
this._subscription, this._injectData, this._userOnData);
bool get isPaused => _subscription.isPaused;
Future<T> asFuture<T>([T? futureValue]) =>
_subscription.asFuture<T>(futureValue as T);
Future cancel() {
_isCanceled = true;
_injectData = null;
return _subscription.cancel();
}
void onData(void Function(Uint8List data)? handleData) {
_userOnData = handleData;
_subscription.onData(handleData);
}
void onDone(void Function()? handleDone) {
_subscription.onDone(handleDone);
}
void onError(Function? handleError) {
_subscription.onError(handleError);
}
void pause([Future? resumeSignal]) {
if (_injectData == null) {
_subscription.pause(resumeSignal);
} else {
_pauseCount++;
if (resumeSignal != null) {
resumeSignal.whenComplete(resume);
}
}
}
void resume() {
if (_injectData == null) {
_subscription.resume();
} else {
_pauseCount--;
_maybeScheduleData();
}
}
void _maybeScheduleData() {
if (_scheduled) return;
if (_pauseCount != 0) return;
_scheduled = true;
scheduleMicrotask(() {
_scheduled = false;
if (_pauseCount > 0 || _isCanceled) return;
var data = _injectData!;
_injectData = null;
// To ensure that 'subscription.isPaused' is false, we resume the
// subscription here. This is fine as potential events are delayed.
_subscription.resume();
_userOnData?.call(data);
});
}
}
class _HttpDetachedIncoming extends Stream<Uint8List> {
final StreamSubscription<Uint8List>? subscription;
final Uint8List? bufferedData;
_HttpDetachedIncoming(this.subscription, this.bufferedData);
StreamSubscription<Uint8List> listen(void Function(Uint8List event)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
var subscription = this.subscription;
if (subscription != null) {
subscription
..onData(onData)
..onError(onError)
..onDone(onDone);
if (bufferedData == null) {
return subscription..resume();
}
return _HttpDetachedStreamSubscription(subscription, bufferedData, onData)
..resume();
} else {
// TODO(26379): add test for this branch.
return Stream<Uint8List>.fromIterable([bufferedData!]).listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
}
}
/// HTTP parser which parses the data stream given to [consume].
///
/// If an HTTP parser error occurs, the parser will signal an error to either
/// the current _HttpIncoming or the _parser itself.
///
/// The connection upgrades (e.g. switching from HTTP/1.1 to the
/// WebSocket protocol) is handled in a special way. If connection
/// upgrade is specified in the headers, then on the callback to
/// [:responseStart:] the [:upgrade:] property on the [:HttpParser:]
/// object will be [:true:] indicating that from now on the protocol is
/// not HTTP anymore and no more callbacks will happen, that is
/// [:dataReceived:] and [:dataEnd:] are not called in this case as
/// there is no more HTTP data. After the upgrade the method
/// [:readUnparsedData:] can be used to read any remaining bytes in the
/// HTTP parser which are part of the protocol the connection is
/// upgrading to. These bytes cannot be processed by the HTTP parser
/// and should be handled according to whatever protocol is being
/// upgraded to.
class _HttpParser extends Stream<_HttpIncoming> {
// State.
bool _parserCalled = false;
// The data that is currently being parsed.
Uint8List? _buffer;
int _index = -1;
// Whether a HTTP request is being parsed (as opposed to a response).
final bool _requestParser;
int _state = _State.START;
int? _httpVersionIndex;
int _messageType = _MessageType.UNDETERMINED;
int _statusCode = 0;
int _statusCodeLength = 0;
final List<int> _method = [];
final List<int> _uriOrReasonPhrase = [];
final List<int> _headerField = [];
final List<int> _headerValue = [];
static const _headerTotalSizeLimit = 1024 * 1024;
int _headersReceivedSize = 0;
int _httpVersion = _HttpVersion.UNDETERMINED;
int _transferLength = -1;
bool _persistentConnection = false;
bool _connectionUpgrade = false;
bool _chunked = false;
bool _noMessageBody = false;
int _remainingContent = -1;
bool _contentLength = false;
bool _transferEncoding = false;
bool connectMethod = false;
_HttpHeaders? _headers;
// The limit for parsing chunk size
static const _chunkSizeLimit = 0x7FFFFFFF;
// The current incoming connection.
_HttpIncoming? _incoming;
StreamSubscription<Uint8List>? _socketSubscription;
bool _paused = true;
bool _bodyPaused = false;
final StreamController<_HttpIncoming> _controller;
StreamController<Uint8List>? _bodyController;
factory _HttpParser.requestParser() {
return _HttpParser._(true);
}
factory _HttpParser.responseParser() {
return _HttpParser._(false);
}
_HttpParser._(this._requestParser)
: _controller = StreamController<_HttpIncoming>(sync: true) {
_controller
..onListen = () {
_paused = false;
}
..onPause = () {
_paused = true;
_pauseStateChanged();
}
..onResume = () {
_paused = false;
_pauseStateChanged();
}
..onCancel = () {
_socketSubscription?.cancel();
};
_reset();
}
StreamSubscription<_HttpIncoming> listen(
void Function(_HttpIncoming event)? onData,
{Function? onError,
void Function()? onDone,
bool? cancelOnError}) {
return _controller.stream.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
void listenToStream(Stream<Uint8List> stream) {
// Listen to the stream and handle data accordingly. When a
// _HttpIncoming is created, _dataPause, _dataResume, _dataDone is
// given to provide a way of controlling the parser.
// TODO(ajohnsen): Remove _dataPause, _dataResume and _dataDone and clean up
// how the _HttpIncoming signals the parser.
_socketSubscription =
stream.listen(_onData, onError: _controller.addError, onDone: _onDone);
}
void _parse() {
try {
_doParse();
} catch (e, s) {
if (_state >= _State.CHUNK_SIZE_STARTING_CR && _state <= _State.BODY) {
_state = _State.FAILURE;
_reportBodyError(e, s);
} else {
_state = _State.FAILURE;
_reportHttpError(e, s);
}
}
}
// Process end of headers. Returns true if the parser should stop
// parsing and return. This will be in case of either an upgrade
// request or a request or response with an empty body.
bool _headersEnd() {
var headers = _headers!;
// If method is CONNECT, response parser should ignore any Content-Length or
// Transfer-Encoding header fields in a successful response.
// [RFC 7231](https://tools.ietf.org/html/rfc7231#section-4.3.6)
if (!_requestParser &&
_statusCode >= 200 &&
_statusCode < 300 &&
connectMethod) {
_transferLength = -1;
headers.chunkedTransferEncoding = false;
_chunked = false;
headers.removeAll(HttpHeaders.contentLengthHeader);
headers.removeAll(HttpHeaders.transferEncodingHeader);
}
headers._mutable = false;
_transferLength = headers.contentLength;
// Ignore the Content-Length header if Transfer-Encoding
// is chunked (RFC 2616 section 4.4)
if (_chunked) _transferLength = -1;
// If a request message has neither Content-Length nor
// Transfer-Encoding the message must not have a body (RFC
// 2616 section 4.3).
if (_messageType == _MessageType.REQUEST &&
_transferLength < 0 &&
_chunked == false) {
_transferLength = 0;
}
if (_connectionUpgrade) {
_state = _State.UPGRADED;
_transferLength = 0;
}
var incoming = _createIncoming(_transferLength);
if (_requestParser) {
incoming.method = String.fromCharCodes(_method);
incoming.uri = Uri.parse(String.fromCharCodes(_uriOrReasonPhrase));
} else {
incoming.statusCode = _statusCode;
incoming.reasonPhrase = String.fromCharCodes(_uriOrReasonPhrase);
}
_method.clear();
_uriOrReasonPhrase.clear();
if (_connectionUpgrade) {
incoming.upgraded = true;
_parserCalled = false;
_closeIncoming();
_controller.add(incoming);
return true;
}
if (_transferLength == 0 ||
(_messageType == _MessageType.RESPONSE && _noMessageBody)) {
_reset();
_closeIncoming();
_controller.add(incoming);
return false;
} else if (_chunked) {
_state = _State.CHUNK_SIZE;
_remainingContent = 0;
} else if (_transferLength > 0) {
_remainingContent = _transferLength;
_state = _State.BODY;
} else {
// Neither chunked nor content length. End of body
// indicated by close.
_state = _State.BODY;
}
_parserCalled = false;
_controller.add(incoming);
return true;
}
// From RFC 2616.
// generic-message = start-line
// *(message-header CRLF)
// CRLF
// [ message-body ]
// start-line = Request-Line | Status-Line
// Request-Line = Method SP Request-URI SP HTTP-Version CRLF
// Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
// message-header = field-name ":" [ field-value ]
//
// Per section 19.3 "Tolerant Applications" CRLF treats LF as a terminator
// and leading CR is ignored. Use of standalone CR is not allowed.
void _doParse() {
assert(!_parserCalled);
_parserCalled = true;
if (_state == _State.CLOSED) {
throw HttpException("Data on closed connection");
}
if (_state == _State.FAILURE) {
throw HttpException("Data on failed connection");
}
while (_buffer != null &&
_index < _buffer!.length &&
_state != _State.FAILURE &&
_state != _State.UPGRADED) {
// Depending on _incoming, we either break on _bodyPaused or _paused.
if ((_incoming != null && _bodyPaused) ||
(_incoming == null && _paused)) {
_parserCalled = false;
return;
}
int index = _index;
int byte = _buffer![index];
_index = index + 1;
switch (_state) {
case _State.START:
if (byte == _Const.HTTP[0]) {
// Start parsing method or HTTP version.
_httpVersionIndex = 1;
_state = _State.METHOD_OR_RESPONSE_HTTP_VERSION;
} else {
// Start parsing method.
if (!_isTokenChar(byte)) {
throw HttpException("Invalid request method");
}
_addWithValidation(_method, byte);
if (!_requestParser) {
throw HttpException("Invalid response line");
}
_state = _State.REQUEST_LINE_METHOD;
}
break;
case _State.METHOD_OR_RESPONSE_HTTP_VERSION:
var httpVersionIndex = _httpVersionIndex!;
if (httpVersionIndex < _Const.HTTP.length &&
byte == _Const.HTTP[httpVersionIndex]) {
// Continue parsing HTTP version.
_httpVersionIndex = httpVersionIndex + 1;
} else if (httpVersionIndex == _Const.HTTP.length &&
byte == _CharCode.SLASH) {
// HTTP/ parsed. As method is a token this cannot be a
// method anymore.
_httpVersionIndex = httpVersionIndex + 1;
if (_requestParser) {
throw HttpException("Invalid request line");
}
_state = _State.RESPONSE_HTTP_VERSION;
} else {
// Did not parse HTTP version. Expect method instead.
for (int i = 0; i < httpVersionIndex; i++) {
_addWithValidation(_method, _Const.HTTP[i]);
}
if (byte == _CharCode.SP) {
_state = _State.REQUEST_LINE_URI;
} else {
_addWithValidation(_method, byte);
_httpVersion = _HttpVersion.UNDETERMINED;
if (!_requestParser) {
throw HttpException("Invalid response line");
}
_state = _State.REQUEST_LINE_METHOD;
}
}
break;
case _State.RESPONSE_HTTP_VERSION:
var httpVersionIndex = _httpVersionIndex!;
if (httpVersionIndex < _Const.HTTP1DOT.length) {
// Continue parsing HTTP version.
_expect(byte, _Const.HTTP1DOT[httpVersionIndex]);
_httpVersionIndex = httpVersionIndex + 1;
} else if (httpVersionIndex == _Const.HTTP1DOT.length &&
byte == _CharCode.ONE) {
// HTTP/1.1 parsed.
_httpVersion = _HttpVersion.HTTP11;
_persistentConnection = true;
_httpVersionIndex = httpVersionIndex + 1;
} else if (httpVersionIndex == _Const.HTTP1DOT.length &&
byte == _CharCode.ZERO) {
// HTTP/1.0 parsed.
_httpVersion = _HttpVersion.HTTP10;
_persistentConnection = false;
_httpVersionIndex = httpVersionIndex + 1;
} else if (httpVersionIndex == _Const.HTTP1DOT.length + 1) {
_expect(byte, _CharCode.SP);
// HTTP version parsed.
_state = _State.RESPONSE_LINE_STATUS_CODE;
} else {
throw HttpException(
"Invalid response line, failed to parse HTTP version");
}
break;
case _State.REQUEST_LINE_METHOD:
if (byte == _CharCode.SP) {
_state = _State.REQUEST_LINE_URI;
} else {
if (_Const.SEPARATOR_MAP[byte] ||
byte == _CharCode.CR ||
byte == _CharCode.LF) {
throw HttpException("Invalid request method");
}
_addWithValidation(_method, byte);
}
break;
case _State.REQUEST_LINE_URI:
if (byte == _CharCode.SP) {
if (_uriOrReasonPhrase.isEmpty) {
throw HttpException("Invalid request, empty URI");
}
_state = _State.REQUEST_LINE_HTTP_VERSION;
_httpVersionIndex = 0;
} else {
if (byte == _CharCode.CR || byte == _CharCode.LF) {
throw HttpException("Invalid request, unexpected $byte in URI");
}
_addWithValidation(_uriOrReasonPhrase, byte);
}
break;
case _State.REQUEST_LINE_HTTP_VERSION:
var httpVersionIndex = _httpVersionIndex!;
if (httpVersionIndex < _Const.HTTP1DOT.length) {
_expect(byte, _Const.HTTP11[httpVersionIndex]);
_httpVersionIndex = httpVersionIndex + 1;
} else if (_httpVersionIndex == _Const.HTTP1DOT.length) {
if (byte == _CharCode.ONE) {
// HTTP/1.1 parsed.
_httpVersion = _HttpVersion.HTTP11;
_persistentConnection = true;
_httpVersionIndex = httpVersionIndex + 1;
} else if (byte == _CharCode.ZERO) {
// HTTP/1.0 parsed.
_httpVersion = _HttpVersion.HTTP10;
_persistentConnection = false;
_httpVersionIndex = httpVersionIndex + 1;
} else {
throw HttpException("Invalid response, invalid HTTP version");
}
} else {
if (byte == _CharCode.CR) {
_state = _State.REQUEST_LINE_ENDING;
} else if (byte == _CharCode.LF) {
_state = _State.REQUEST_LINE_ENDING;
_index = _index - 1; // Make the new state see the LF again.
}
}
break;
case _State.REQUEST_LINE_ENDING:
_expect(byte, _CharCode.LF);
_messageType = _MessageType.REQUEST;
_state = _State.HEADER_START;
break;
case _State.RESPONSE_LINE_STATUS_CODE:
if (byte == _CharCode.SP) {
_state = _State.RESPONSE_LINE_REASON_PHRASE;
} else if (byte == _CharCode.CR) {
// Some HTTP servers do not follow the spec and send
// \r?\n right after the status code.
_state = _State.RESPONSE_LINE_ENDING;
} else if (byte == _CharCode.LF) {
_state = _State.RESPONSE_LINE_ENDING;
_index = _index - 1; // Make the new state see the LF again.
} else {
_statusCodeLength++;
if (byte < 0x30 || byte > 0x39) {
throw HttpException("Invalid response status code with $byte");
} else if (_statusCodeLength > 3) {
throw HttpException(
"Invalid response, status code is over 3 digits");
} else {
_statusCode = _statusCode * 10 + byte - 0x30;
}
}
break;
case _State.RESPONSE_LINE_REASON_PHRASE:
if (byte == _CharCode.CR) {
_state = _State.RESPONSE_LINE_ENDING;
} else if (byte == _CharCode.LF) {
_state = _State.RESPONSE_LINE_ENDING;
_index = _index - 1; // Make the new state see the LF again.
} else {
_addWithValidation(_uriOrReasonPhrase, byte);
}
break;
case _State.RESPONSE_LINE_ENDING:
_expect(byte, _CharCode.LF);
_messageType == _MessageType.RESPONSE;
// Check whether this response will never have a body.
if (_statusCode <= 199 || _statusCode == 204 || _statusCode == 304) {
_noMessageBody = true;
}
_state = _State.HEADER_START;
break;
case _State.HEADER_START:
_headers = _HttpHeaders(version!);
if (byte == _CharCode.CR) {
_state = _State.HEADER_ENDING;
} else if (byte == _CharCode.LF) {
_state = _State.HEADER_ENDING;
_index = _index - 1; // Make the new state see the LF again.
} else {
// Start of new header field.
_addWithValidation(_headerField, _toLowerCaseByte(byte));
_state = _State.HEADER_FIELD;
}
break;
case _State.HEADER_FIELD:
if (byte == _CharCode.COLON) {
_state = _State.HEADER_VALUE_START;
} else {
if (!_isTokenChar(byte)) {
throw HttpException("Invalid header field name, with $byte");
}
_addWithValidation(_headerField, _toLowerCaseByte(byte));
}
break;
case _State.HEADER_VALUE_START:
if (byte == _CharCode.CR) {
_state = _State.HEADER_VALUE_FOLD_OR_END_CR;
} else if (byte == _CharCode.LF) {
_state = _State.HEADER_VALUE_FOLD_OR_END;
} else if (byte != _CharCode.SP && byte != _CharCode.HT) {
// Start of new header value.
_addWithValidation(_headerValue, byte);
_state = _State.HEADER_VALUE;
}
break;
case _State.HEADER_VALUE:
if (byte == _CharCode.CR) {
_state = _State.HEADER_VALUE_FOLD_OR_END_CR;
} else if (byte == _CharCode.LF) {
_state = _State.HEADER_VALUE_FOLD_OR_END;
} else {
_addWithValidation(_headerValue, byte);
}
break;
case _State.HEADER_VALUE_FOLD_OR_END_CR:
_expect(byte, _CharCode.LF);
_state = _State.HEADER_VALUE_FOLD_OR_END;
break;
case _State.HEADER_VALUE_FOLD_OR_END:
if (byte == _CharCode.SP || byte == _CharCode.HT) {
_state = _State.HEADER_VALUE_START;
} else {
String headerField = String.fromCharCodes(_headerField);
String headerValue = String.fromCharCodes(_headerValue);
const errorIfBothText = "Both Content-Length and Transfer-Encoding "
"are specified, at most one is allowed";
if (headerField == HttpHeaders.contentLengthHeader) {
// Content Length header should not have more than one occurrence
// or coexist with Transfer Encoding header.
if (_contentLength) {
throw HttpException("The Content-Length header occurred "
"more than once, at most one is allowed.");
} else if (_transferEncoding) {
throw HttpException(errorIfBothText);
}
_contentLength = true;
} else if (headerField == HttpHeaders.transferEncodingHeader) {
_transferEncoding = true;
if (_caseInsensitiveCompare("chunked".codeUnits, _headerValue)) {
_chunked = true;
}
if (_contentLength) {
throw HttpException(errorIfBothText);
}
}
var headers = _headers!;
if (headerField == HttpHeaders.connectionHeader) {
List<String> tokens = _tokenizeFieldValue(headerValue);
final bool isResponse = _messageType == _MessageType.RESPONSE;
final bool isUpgradeCode =
(_statusCode == HttpStatus.upgradeRequired) ||
(_statusCode == HttpStatus.switchingProtocols);
for (int i = 0; i < tokens.length; i++) {
final bool isUpgrade = _caseInsensitiveCompare(
"upgrade".codeUnits, tokens[i].codeUnits);
if ((isUpgrade && !isResponse) ||
(isUpgrade && isResponse && isUpgradeCode)) {
_connectionUpgrade = true;
}
headers._add(headerField, tokens[i]);
}
} else {
headers._add(headerField, headerValue);
}
_headerField.clear();
_headerValue.clear();
if (byte == _CharCode.CR) {
_state = _State.HEADER_ENDING;
} else if (byte == _CharCode.LF) {
_state = _State.HEADER_ENDING;
_index = _index - 1; // Make the new state see the LF again.
} else {
// Start of new header field.
_state = _State.HEADER_FIELD;
_addWithValidation(_headerField, _toLowerCaseByte(byte));
}
}
break;
case _State.HEADER_ENDING:
_expect(byte, _CharCode.LF);
if (_headersEnd()) {
return;
}
break;
case _State.CHUNK_SIZE_STARTING_CR:
if (byte == _CharCode.LF) {
_state = _State.CHUNK_SIZE_STARTING;
_index = _index - 1; // Make the new state see the LF again.
break;
}
_expect(byte, _CharCode.CR);
_state = _State.CHUNK_SIZE_STARTING;
break;
case _State.CHUNK_SIZE_STARTING:
_expect(byte, _CharCode.LF);
_state = _State.CHUNK_SIZE;
break;
case _State.CHUNK_SIZE:
if (byte == _CharCode.CR) {
_state = _State.CHUNK_SIZE_ENDING;
} else if (byte == _CharCode.LF) {
_state = _State.CHUNK_SIZE_ENDING;
_index = _index - 1; // Make the new state see the LF again.
} else if (byte == _CharCode.SEMI_COLON) {
_state = _State.CHUNK_SIZE_EXTENSION;
} else {
int value = _expectHexDigit(byte);
// Checks whether (_remainingContent * 16 + value) overflows.
if (_remainingContent > _chunkSizeLimit >> 4) {
throw HttpException('Chunk size overflows the integer');
}
_remainingContent = _remainingContent * 16 + value;
}
break;
case _State.CHUNK_SIZE_EXTENSION:
if (byte == _CharCode.CR) {
_state = _State.CHUNK_SIZE_ENDING;
} else if (byte == _CharCode.LF) {
_state = _State.CHUNK_SIZE_ENDING;
_index = _index - 1; // Make the new state see the LF again.
}
break;
case _State.CHUNK_SIZE_ENDING:
_expect(byte, _CharCode.LF);
if (_remainingContent > 0) {
_state = _State.BODY;
} else {
_state = _State.CHUNKED_BODY_DONE_CR;
}
break;
case _State.CHUNKED_BODY_DONE_CR:
if (byte == _CharCode.LF) {
_state = _State.CHUNKED_BODY_DONE;
_index = _index - 1; // Make the new state see the LF again.
break;
}
_expect(byte, _CharCode.CR);
break;
case _State.CHUNKED_BODY_DONE:
_expect(byte, _CharCode.LF);
_reset();
_closeIncoming();
break;
case _State.BODY:
// The body is not handled one byte at a time but in blocks.
_index = _index - 1;
var buffer = _buffer!;
int dataAvailable = buffer.length - _index;
if (_remainingContent >= 0 && dataAvailable > _remainingContent) {
dataAvailable = _remainingContent;
}
// Always present the data as a view. This way we can handle all
// cases like this, and the user will not experience different data
// typed (which could lead to polymorphic user code).
Uint8List data = Uint8List.view(
buffer.buffer, buffer.offsetInBytes + _index, dataAvailable);
_bodyController!.add(data);
if (_remainingContent != -1) {
_remainingContent -= data.length;
}
_index = _index + data.length;
if (_remainingContent == 0) {
if (!_chunked) {
_reset();
_closeIncoming();
} else {
_state = _State.CHUNK_SIZE_STARTING_CR;
}
}
break;
case _State.FAILURE:
// Should be unreachable.
assert(false);
break;
default:
// Should be unreachable.
assert(false);
break;
}
}
_parserCalled = false;
var buffer = _buffer;
if (buffer != null && _index == buffer.length) {
// If all data is parsed release the buffer and resume receiving
// data.
_releaseBuffer();
if (_state != _State.UPGRADED && _state != _State.FAILURE) {
_socketSubscription!.resume();
}
}
}
void _onData(Uint8List buffer) {
_socketSubscription!.pause();
assert(_buffer == null);
_buffer = buffer;
_index = 0;
_parse();
}
void _onDone() {
// onDone cancels the subscription.
_socketSubscription = null;
if (_state == _State.CLOSED || _state == _State.FAILURE) return;
if (_incoming != null) {
if (_state != _State.UPGRADED &&
!(_state == _State.START && !_requestParser) &&
!(_state == _State.BODY && !_chunked && _transferLength == -1)) {
_reportBodyError(
HttpException("Connection closed while receiving data"));
}
_closeIncoming(true);
_controller.close();
return;
}
// If the connection is idle the HTTP stream is closed.
if (_state == _State.START) {
if (!_requestParser) {
_reportHttpError(
HttpException("Connection closed before full header was received"));
}
_controller.close();
return;
}
if (_state == _State.UPGRADED) {
_controller.close();
return;
}
if (_state < _State.FIRST_BODY_STATE) {
_state = _State.FAILURE;
// Report the error through the error callback if any. Otherwise
// throw the error.
_reportHttpError(
HttpException("Connection closed before full header was received"));
_controller.close();
return;
}
if (!_chunked && _transferLength == -1) {
_state = _State.CLOSED;
} else {
_state = _State.FAILURE;
// Report the error through the error callback if any. Otherwise
// throw the error.
_reportHttpError(
HttpException("Connection closed before full body was received"));
}
_controller.close();
}
String? get version {
switch (_httpVersion) {
case _HttpVersion.HTTP10:
return "1.0";
case _HttpVersion.HTTP11:
return "1.1";
}
return null;
}
int get messageType => _messageType;
int get transferLength => _transferLength;
bool get upgrade => _connectionUpgrade && _state == _State.UPGRADED;
bool get persistentConnection => _persistentConnection;
void set isHead(bool value) {
_noMessageBody = valueOfNonNullableParamWithDefault<bool>(value, false);
}
_HttpDetachedIncoming detachIncoming() {
// Simulate detached by marking as upgraded.
_state = _State.UPGRADED;
return _HttpDetachedIncoming(_socketSubscription, readUnparsedData());
}
Uint8List? readUnparsedData() {
var buffer = _buffer;
if (buffer == null) return null;
var index = _index;
if (index == buffer.length) return null;
var result = buffer.sublist(index);
_releaseBuffer();
return result;
}
void _reset() {
if (_state == _State.UPGRADED) return;
_state = _State.START;
_messageType = _MessageType.UNDETERMINED;
_headerField.clear();
_headerValue.clear();
_headersReceivedSize = 0;
_method.clear();
_uriOrReasonPhrase.clear();
_statusCode = 0;
_statusCodeLength = 0;
_httpVersion = _HttpVersion.UNDETERMINED;
_transferLength = -1;
_persistentConnection = false;
_connectionUpgrade = false;
_chunked = false;
_noMessageBody = false;
_remainingContent = -1;
_contentLength = false;
_transferEncoding = false;
_headers = null;
}
void _releaseBuffer() {
_buffer = null;
_index = -1;
}
static bool _isTokenChar(int byte) {
return byte > 31 && byte < 128 && !_Const.SEPARATOR_MAP[byte];
}
static bool _isValueChar(int byte) {
return (byte > 31 && byte < 128) || (byte == _CharCode.HT);
}
static List<String> _tokenizeFieldValue(String headerValue) {
List<String> tokens = <String>[];
int start = 0;
int index = 0;
while (index < headerValue.length) {
if (headerValue[index] == ",") {
tokens.add(headerValue.substring(start, index));
start = index + 1;
} else if (headerValue[index] == " " || headerValue[index] == "\t") {
start++;
}
index++;
}
tokens.add(headerValue.substring(start, index));
return tokens;
}
static int _toLowerCaseByte(int x) {
// Optimized version:
// - 0x41 is 'A'
// - 0x7f is ASCII mask
// - 26 is the number of alpha characters.
// - 0x20 is the delta between lower and upper chars.
return (((x - 0x41) & 0x7f) < 26) ? (x | 0x20) : x;
}
// expected should already be lowercase.
static bool _caseInsensitiveCompare(List<int> expected, List<int> value) {
if (expected.length != value.length) return false;
for (int i = 0; i < expected.length; i++) {
if (expected[i] != _toLowerCaseByte(value[i])) return false;
}
return true;
}
void _expect(int val1, int val2) {
if (val1 != val2) {
throw HttpException("Failed to parse HTTP, $val1 does not match $val2");
}
}
int _expectHexDigit(int byte) {
if (0x30 <= byte && byte <= 0x39) {
return byte - 0x30; // 0 - 9
} else if (0x41 <= byte && byte <= 0x46) {
return byte - 0x41 + 10; // A - F
} else if (0x61 <= byte && byte <= 0x66) {
return byte - 0x61 + 10; // a - f
} else {
throw HttpException(
"Failed to parse HTTP, $byte is expected to be a Hex digit");
}
}
void _addWithValidation(List<int> list, int byte) {
_headersReceivedSize++;
if (_headersReceivedSize < _headerTotalSizeLimit) {
list.add(byte);
} else {
_reportSizeLimitError();
}
}
void _reportSizeLimitError() {
String method = "";
switch (_state) {
case _State.START:
case _State.METHOD_OR_RESPONSE_HTTP_VERSION:
case _State.REQUEST_LINE_METHOD:
method = "Method";
break;
case _State.REQUEST_LINE_URI:
method = "URI";
break;
case _State.RESPONSE_LINE_REASON_PHRASE:
method = "Reason phrase";
break;
case _State.HEADER_START:
case _State.HEADER_FIELD:
method = "Header field";
break;
case _State.HEADER_VALUE_START:
case _State.HEADER_VALUE:
method = "Header value";
break;
default:
throw UnsupportedError("Unexpected state: $_state");
}
throw HttpException(
"$method exceeds the $_headerTotalSizeLimit size limit");
}
_HttpIncoming _createIncoming(int transferLength) {
assert(_incoming == null);
assert(_bodyController == null);
assert(!_bodyPaused);
var controller = _bodyController = StreamController<Uint8List>(sync: true);
var incoming =
_incoming = _HttpIncoming(_headers!, transferLength, controller.stream);
controller
..onListen = () {
if (incoming != _incoming) return;
assert(_bodyPaused);
_bodyPaused = false;
_pauseStateChanged();
}
..onPause = () {
if (incoming != _incoming) return;
assert(!_bodyPaused);
_bodyPaused = true;
_pauseStateChanged();
}
..onResume = () {
if (incoming != _incoming) return;
assert(_bodyPaused);
_bodyPaused = false;
_pauseStateChanged();
}
..onCancel = () {
if (incoming != _incoming) return;
_socketSubscription?.cancel();
_closeIncoming(true);
_controller.close();
};
_bodyPaused = true;
_pauseStateChanged();
return incoming;
}
void _closeIncoming([bool closing = false]) {
// Ignore multiple close (can happen in re-entrance).
var tmp = _incoming;
if (tmp == null) return;
tmp.close(closing);
_incoming = null;
var controller = _bodyController;
if (controller != null) {
controller.close();
_bodyController = null;
}
_bodyPaused = false;
_pauseStateChanged();
}
void _pauseStateChanged() {
if (_incoming != null) {
if (!_bodyPaused && !_parserCalled) {
_parse();
}
} else {
if (!_paused && !_parserCalled) {
_parse();
}
}
}
void _reportHttpError(error, [stackTrace]) {
_socketSubscription?.cancel();
_state = _State.FAILURE;
_controller.addError(error, stackTrace);
_controller.close();
}
void _reportBodyError(error, [stackTrace]) {
_socketSubscription?.cancel();
_state = _State.FAILURE;
_bodyController?.addError(error, stackTrace);
// In case of drain(), error event will close the stream.
_bodyController?.close();
}
}