d968304570
This is a reland of d84f359786
Original change's description:
> Revert commits part of #40548 which still has some design debates and breaks valid local network http traffic
>
> Bug: https://github.com/flutter/flutter/issues/72723
> Change-Id: Ib5f809fccf1fad69add441fd40c463da8dc8be36
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/192953
> Auto-Submit: Xiao Yu <xster@google.com>
> Commit-Queue: Vyacheslav Egorov <vegorov@google.com>
> Reviewed-by: Vyacheslav Egorov <vegorov@google.com>
> Reviewed-by: Lasse R.H. Nielsen <lrn@google.com>
Bug: https://github.com/flutter/flutter/issues/72723
Change-Id: I466d888b3f324a996f0bef0463e7ab3df3179c56
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/195485
Reviewed-by: Vyacheslav Egorov <vegorov@google.com>
Reviewed-by: Lasse R.H. Nielsen <lrn@google.com>
Commit-Queue: Xiao Yu <xster@google.com>
847 lines
26 KiB
Dart
847 lines
26 KiB
Dart
// 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.
|
|
|
|
library dart._http;
|
|
|
|
import "dart:async";
|
|
import "dart:collection";
|
|
import "dart:convert";
|
|
import "dart:developer";
|
|
import "dart:io";
|
|
import "dart:isolate";
|
|
import "dart:math";
|
|
import "dart:typed_data";
|
|
|
|
import "package:expect/expect.dart";
|
|
|
|
import "../../../sdk/lib/internal/internal.dart"
|
|
show Since, valueOfNonNullableParamWithDefault, HttpStatus;
|
|
|
|
part "../../../sdk/lib/_http/crypto.dart";
|
|
part "../../../sdk/lib/_http/embedder_config.dart";
|
|
part "../../../sdk/lib/_http/http_impl.dart";
|
|
part "../../../sdk/lib/_http/http_date.dart";
|
|
part "../../../sdk/lib/_http/http_parser.dart";
|
|
part "../../../sdk/lib/_http/http_headers.dart";
|
|
part "../../../sdk/lib/_http/http_session.dart";
|
|
|
|
class HttpParserTest {
|
|
final String Function(String) transform;
|
|
HttpParserTest(this.transform);
|
|
|
|
static void runAllTests() {
|
|
final testCRLF = HttpParserTest((String s) => s);
|
|
testCRLF.testParseRequest();
|
|
testCRLF.testParseResponse();
|
|
testCRLF.testParseInvalidRequest();
|
|
testCRLF.testParseInvalidResponse();
|
|
|
|
// Ensure http parser is CR?LF tolerant.
|
|
final testLF = HttpParserTest((String s) => s.replaceAll('\r', ''));
|
|
testLF.testParseRequest();
|
|
testLF.testParseResponse();
|
|
testLF.testParseInvalidRequest();
|
|
testLF.testParseInvalidResponse();
|
|
}
|
|
|
|
void _testParseRequest(
|
|
String request, String expectedMethod, String expectedUri,
|
|
{int expectedTransferLength: 0,
|
|
int expectedBytesReceived: 0,
|
|
Map<String, String>? expectedHeaders: null,
|
|
bool chunked: false,
|
|
bool upgrade: false,
|
|
int unparsedLength: 0,
|
|
bool connectionClose: false,
|
|
String expectedVersion: "1.1"}) {
|
|
late StreamController<Uint8List> controller;
|
|
void reset() {
|
|
_HttpParser httpParser = new _HttpParser.requestParser();
|
|
controller = new StreamController(sync: true);
|
|
var port1 = new ReceivePort();
|
|
var port2 = new ReceivePort();
|
|
|
|
String? method = null;
|
|
Uri? uri = null;
|
|
_HttpHeaders? headers = null;
|
|
int bytesReceived = 0;
|
|
int unparsedBytesReceived = 0;
|
|
bool upgraded = false;
|
|
|
|
httpParser.listenToStream(controller.stream);
|
|
var subscription = httpParser.listen((incoming) {
|
|
method = incoming.method;
|
|
uri = incoming.uri;
|
|
headers = incoming.headers;
|
|
upgraded = incoming.upgraded;
|
|
Expect.equals(upgrade, upgraded);
|
|
|
|
if (!chunked) {
|
|
Expect.equals(expectedTransferLength, incoming.transferLength);
|
|
} else {
|
|
Expect.equals(-1, incoming.transferLength);
|
|
}
|
|
if (expectedHeaders != null) {
|
|
expectedHeaders.forEach((String name, String value) =>
|
|
Expect.equals(value, headers![name]![0]));
|
|
}
|
|
incoming.listen((List<int> data) {
|
|
Expect.isFalse(upgraded);
|
|
bytesReceived += data.length;
|
|
}, onDone: () {
|
|
port2.close();
|
|
Expect.equals(expectedMethod, method);
|
|
Expect.stringEquals(expectedUri, uri.toString());
|
|
Expect.equals(expectedVersion, headers!.protocolVersion);
|
|
if (upgrade) {
|
|
Expect.equals(0, bytesReceived);
|
|
// port1 is closed by the listener on the detached data.
|
|
} else {
|
|
Expect.equals(expectedBytesReceived, bytesReceived);
|
|
}
|
|
});
|
|
|
|
if (upgraded) {
|
|
port1.close();
|
|
httpParser.detachIncoming().listen((List<int> data) {
|
|
unparsedBytesReceived += data.length;
|
|
}, onDone: () {
|
|
Expect.equals(unparsedLength, unparsedBytesReceived);
|
|
port2.close();
|
|
});
|
|
}
|
|
|
|
incoming.dataDone.then((_) {
|
|
port1.close();
|
|
});
|
|
});
|
|
}
|
|
|
|
void testWrite(List<int> requestData, [int chunkSize = -1]) {
|
|
if (chunkSize == -1) chunkSize = requestData.length;
|
|
reset();
|
|
for (int pos = 0; pos < requestData.length; pos += chunkSize) {
|
|
int end = min(requestData.length, pos + chunkSize);
|
|
controller.add(requestData.sublist(pos, end) as Uint8List);
|
|
}
|
|
controller.close();
|
|
}
|
|
|
|
// Test parsing the request three times delivering the data in
|
|
// different chunks.
|
|
List<int> requestData =
|
|
new Uint8List.fromList(transform(request).codeUnits);
|
|
testWrite(requestData);
|
|
testWrite(requestData, 10);
|
|
testWrite(requestData, 1);
|
|
}
|
|
|
|
void _testParseRequestLean(
|
|
String request, String expectedMethod, String expectedUri,
|
|
{int expectedTransferLength: 0,
|
|
int expectedBytesReceived: 0,
|
|
Map<String, String>? expectedHeaders: null,
|
|
bool chunked: false,
|
|
bool upgrade: false,
|
|
int unparsedLength: 0,
|
|
bool connectionClose: false,
|
|
String expectedVersion: "1.1"}) {
|
|
_testParseRequest(request, expectedMethod, expectedUri,
|
|
expectedTransferLength: expectedTransferLength,
|
|
expectedBytesReceived: expectedBytesReceived,
|
|
expectedHeaders: expectedHeaders,
|
|
chunked: chunked,
|
|
upgrade: upgrade,
|
|
unparsedLength: unparsedLength,
|
|
connectionClose: connectionClose,
|
|
expectedVersion: expectedVersion);
|
|
// Same test but with only \n instead of \r\n terminating each header line.
|
|
_testParseRequest(request.replaceAll('\r', ''), expectedMethod, expectedUri,
|
|
expectedTransferLength: expectedTransferLength,
|
|
expectedBytesReceived: expectedBytesReceived,
|
|
expectedHeaders: expectedHeaders,
|
|
chunked: chunked,
|
|
upgrade: upgrade,
|
|
unparsedLength: unparsedLength,
|
|
connectionClose: connectionClose,
|
|
expectedVersion: expectedVersion);
|
|
}
|
|
|
|
void _testParseInvalidRequest(String request) {
|
|
_HttpParser httpParser;
|
|
bool errorCalled = false;
|
|
late StreamController<Uint8List> controller;
|
|
|
|
void reset() {
|
|
httpParser = new _HttpParser.requestParser();
|
|
controller = new StreamController(sync: true);
|
|
var port = new ReceivePort();
|
|
httpParser.listenToStream(controller.stream);
|
|
var subscription = httpParser.listen((incoming) {
|
|
Expect.fail("Expected request");
|
|
});
|
|
subscription.onError((e) {
|
|
errorCalled = true;
|
|
});
|
|
subscription.onDone(() {
|
|
port.close();
|
|
Expect.isTrue(errorCalled);
|
|
});
|
|
errorCalled = false;
|
|
}
|
|
|
|
void testWrite(List<int> requestData, [int chunkSize = -1]) {
|
|
if (chunkSize == -1) chunkSize = requestData.length;
|
|
reset();
|
|
for (int pos = 0;
|
|
pos < requestData.length && !errorCalled;
|
|
pos += chunkSize) {
|
|
int end = min(requestData.length, pos + chunkSize);
|
|
controller.add(requestData.sublist(pos, end) as Uint8List);
|
|
}
|
|
controller.close();
|
|
}
|
|
|
|
// Test parsing the request three times delivering the data in
|
|
// different chunks.
|
|
List<int> requestData =
|
|
new Uint8List.fromList(transform(request).codeUnits);
|
|
testWrite(requestData);
|
|
testWrite(requestData, 10);
|
|
testWrite(requestData, 1);
|
|
}
|
|
|
|
void _testParseResponse(
|
|
String response, int expectedStatusCode, String expectedReasonPhrase,
|
|
{int expectedTransferLength: 0,
|
|
int expectedBytesReceived: 0,
|
|
Map<String, String>? expectedHeaders: null,
|
|
bool chunked: false,
|
|
bool close: false,
|
|
String? responseToMethod: null,
|
|
bool connectionClose: false,
|
|
bool upgrade: false,
|
|
int unparsedLength: 0,
|
|
String expectedVersion: "1.1"}) {
|
|
late StreamController<Uint8List> controller;
|
|
bool upgraded;
|
|
|
|
void reset() {
|
|
_HttpParser httpParser;
|
|
bool headersCompleteCalled = false;
|
|
bool dataEndCalled = false;
|
|
bool? dataEndClose = null;
|
|
int statusCode = -1;
|
|
String? reasonPhrase = null;
|
|
_HttpHeaders? headers = null;
|
|
int bytesReceived = 0;
|
|
|
|
httpParser = new _HttpParser.responseParser();
|
|
controller = new StreamController(sync: true);
|
|
var port = new ReceivePort();
|
|
httpParser.listenToStream(controller.stream);
|
|
int doneCallCount = 0;
|
|
// Called when done parsing entire message and done parsing body.
|
|
// Only executed when both are done.
|
|
void whenDone() {
|
|
doneCallCount++;
|
|
if (doneCallCount < 2) return;
|
|
Expect.equals(expectedVersion, headers!.protocolVersion);
|
|
Expect.equals(expectedStatusCode, statusCode);
|
|
Expect.equals(expectedReasonPhrase, reasonPhrase);
|
|
Expect.isTrue(headersCompleteCalled);
|
|
Expect.equals(expectedBytesReceived, bytesReceived);
|
|
if (!upgrade) {
|
|
Expect.isTrue(dataEndCalled);
|
|
if (close) Expect.isTrue(dataEndClose);
|
|
Expect.equals(dataEndClose, connectionClose);
|
|
}
|
|
}
|
|
|
|
var subscription = httpParser.listen((incoming) {
|
|
port.close();
|
|
statusCode = incoming.statusCode!;
|
|
reasonPhrase = incoming.reasonPhrase;
|
|
headers = incoming.headers;
|
|
Expect.isFalse(headersCompleteCalled);
|
|
if (!chunked && !close) {
|
|
Expect.equals(expectedTransferLength, incoming.transferLength);
|
|
} else {
|
|
Expect.equals(-1, incoming.transferLength);
|
|
}
|
|
if (expectedHeaders != null) {
|
|
expectedHeaders.forEach((String name, String value) {
|
|
Expect.equals(value, headers![name]![0]);
|
|
});
|
|
}
|
|
Expect.equals(upgrade, httpParser.upgrade);
|
|
headersCompleteCalled = true;
|
|
incoming.listen((List<int> data) {
|
|
Expect.isTrue(headersCompleteCalled);
|
|
bytesReceived += data.length;
|
|
}, onDone: () {
|
|
dataEndCalled = true;
|
|
dataEndClose = close;
|
|
whenDone();
|
|
});
|
|
}, onDone: whenDone);
|
|
}
|
|
|
|
void testWrite(List<int> requestData, [int chunkSize = -1]) {
|
|
if (chunkSize == -1) chunkSize = requestData.length;
|
|
reset();
|
|
for (int pos = 0; pos < requestData.length; pos += chunkSize) {
|
|
int end = min(requestData.length, pos + chunkSize);
|
|
controller.add(requestData.sublist(pos, end) as Uint8List);
|
|
}
|
|
if (close) controller.close();
|
|
}
|
|
|
|
// Test parsing the request three times delivering the data in
|
|
// different chunks.
|
|
List<int> responseData =
|
|
new Uint8List.fromList(transform(response).codeUnits);
|
|
testWrite(responseData);
|
|
testWrite(responseData, 10);
|
|
testWrite(responseData, 1);
|
|
}
|
|
|
|
void _testParseInvalidResponse(String response, [bool close = false]) {
|
|
void testWrite(List<int> requestData, [int chunkSize = -1]) {
|
|
_HttpParser httpParser = new _HttpParser.responseParser();
|
|
StreamController<Uint8List> controller = new StreamController(sync: true);
|
|
bool errorCalled = false;
|
|
|
|
if (chunkSize == -1) chunkSize = requestData.length;
|
|
|
|
var port = new ReceivePort();
|
|
httpParser.listenToStream(controller.stream);
|
|
var subscription = httpParser.listen((incoming) {
|
|
incoming.listen((data) {}, onError: (e) {
|
|
Expect.isFalse(errorCalled);
|
|
errorCalled = true;
|
|
});
|
|
});
|
|
subscription.onError((e) {
|
|
Expect.isFalse(errorCalled);
|
|
errorCalled = true;
|
|
});
|
|
subscription.onDone(() {
|
|
port.close();
|
|
Expect.isTrue(errorCalled);
|
|
});
|
|
|
|
errorCalled = false;
|
|
for (int pos = 0;
|
|
pos < requestData.length && !errorCalled;
|
|
pos += chunkSize) {
|
|
int end = min(requestData.length, pos + chunkSize);
|
|
controller.add(requestData.sublist(pos, end) as Uint8List);
|
|
}
|
|
controller.close();
|
|
}
|
|
|
|
// Test parsing the request three times delivering the data in
|
|
// different chunks.
|
|
List<int> responseData =
|
|
new Uint8List.fromList(transform(response).codeUnits);
|
|
testWrite(responseData);
|
|
testWrite(responseData, 10);
|
|
testWrite(responseData, 1);
|
|
}
|
|
|
|
void testParseRequest() {
|
|
String request;
|
|
Map<String, String> headers;
|
|
var methods = [
|
|
// RFC 2616 methods.
|
|
"OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT",
|
|
// WebDAV methods from RFC 4918.
|
|
"PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK",
|
|
// WebDAV methods from RFC 5323.
|
|
"SEARCH",
|
|
// Methods with HTTP prefix.
|
|
"H", "HT", "HTT", "HTTP", "HX", "HTX", "HTTX", "HTTPX"
|
|
];
|
|
methods = ['GET'];
|
|
methods.forEach((method) {
|
|
request = "$method / HTTP/1.1\r\n\r\n";
|
|
_testParseRequestLean(request, method, "/");
|
|
request = "$method /index.html HTTP/1.1\r\n\r\n";
|
|
_testParseRequestLean(request, method, "/index.html");
|
|
});
|
|
request = "GET / HTTP/1.0\r\n\r\n";
|
|
_testParseRequestLean(request, "GET", "/",
|
|
expectedVersion: "1.0", connectionClose: true);
|
|
|
|
request = "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n";
|
|
_testParseRequestLean(request, "GET", "/", expectedVersion: "1.0");
|
|
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
AAA: AAA\r
|
|
\r
|
|
""";
|
|
_testParseRequestLean(request, "POST", "/test");
|
|
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
\r
|
|
""";
|
|
_testParseRequestLean(request, "POST", "/test");
|
|
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Header-A: AAA\r
|
|
X-Header-B: bbb\r
|
|
\r
|
|
""";
|
|
headers = new Map();
|
|
headers["header-a"] = "AAA";
|
|
headers["x-header-b"] = "bbb";
|
|
_testParseRequestLean(request, "POST", "/test", expectedHeaders: headers);
|
|
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Empty-Header-1:\r
|
|
Empty-Header-2:\r
|
|
\r
|
|
\r
|
|
""";
|
|
headers = new Map();
|
|
headers["empty-header-1"] = "";
|
|
headers["empty-header-2"] = "";
|
|
_testParseRequestLean(request, "POST", "/test", expectedHeaders: headers);
|
|
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Header-A: AAA\r
|
|
X-Header-B:\t \t bbb\r
|
|
\r
|
|
""";
|
|
headers = new Map();
|
|
headers["header-a"] = "AAA";
|
|
headers["x-header-b"] = "bbb";
|
|
_testParseRequestLean(request, "POST", "/test", expectedHeaders: headers);
|
|
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Header-A: AA\r
|
|
A\r
|
|
X-Header-B: b\r
|
|
b\r
|
|
\t b\r
|
|
\r
|
|
""";
|
|
|
|
headers = new Map();
|
|
headers["header-a"] = "AAA";
|
|
headers["x-header-b"] = "bbb";
|
|
_testParseRequestLean(request, "POST", "/test", expectedHeaders: headers);
|
|
|
|
// _testParseRequestLean encodes the request as ISO-8859-1. Test that the
|
|
// HTTP parser decodes header values as ISO-8859-1.
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
latin1: blåbærgrød\r
|
|
\r
|
|
""";
|
|
|
|
headers = new Map();
|
|
headers["latin1"] = "blåbærgrød";
|
|
_testParseRequestLean(request, "POST", "/test", expectedHeaders: headers);
|
|
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Content-Length: 10\r
|
|
\r
|
|
0123456789""";
|
|
_testParseRequestLean(request, "POST", "/test",
|
|
expectedTransferLength: 10, expectedBytesReceived: 10);
|
|
|
|
// Test connection close header.
|
|
request = "GET /test HTTP/1.1\r\nConnection: close\r\n\r\n";
|
|
_testParseRequest(request, "GET", "/test", connectionClose: true);
|
|
|
|
// Test chunked encoding.
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Transfer-Encoding: chunked\r
|
|
\r
|
|
5\r
|
|
01234\r
|
|
5\r
|
|
56789\r
|
|
0\r\n\r\n""";
|
|
_testParseRequest(request, "POST", "/test",
|
|
expectedTransferLength: -1, expectedBytesReceived: 10, chunked: true);
|
|
|
|
// Test upper and lower case hex digits in chunked encoding.
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Transfer-Encoding: chunked\r
|
|
\r
|
|
1E\r
|
|
012345678901234567890123456789\r
|
|
1e\r
|
|
012345678901234567890123456789\r
|
|
0\r\n\r\n""";
|
|
_testParseRequest(request, "POST", "/test",
|
|
expectedTransferLength: -1, expectedBytesReceived: 60, chunked: true);
|
|
|
|
// Test chunk extensions in chunked encoding.
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Transfer-Encoding: chunked\r
|
|
\r
|
|
1E;xxx\r
|
|
012345678901234567890123456789\r
|
|
1E;yyy=zzz\r
|
|
012345678901234567890123456789\r
|
|
0\r\n\r\n""";
|
|
_testParseRequest(request, "POST", "/test",
|
|
expectedTransferLength: -1, expectedBytesReceived: 60, chunked: true);
|
|
|
|
// Test HTTP upgrade.
|
|
request = """
|
|
GET /irc HTTP/1.1\r
|
|
Upgrade: irc/1.2\r
|
|
Connection: Upgrade\r
|
|
\r\n\x01\x01\x01\x01\x01\x02\x02\x02\x02\xFF""";
|
|
headers = new Map();
|
|
headers["upgrade"] = "irc/1.2";
|
|
_testParseRequest(request, "GET", "/irc",
|
|
expectedHeaders: headers, upgrade: true, unparsedLength: 10);
|
|
|
|
// Test HTTP upgrade with protocol data.
|
|
request = """
|
|
GET /irc HTTP/1.1\r
|
|
Upgrade: irc/1.2\r
|
|
Connection: Upgrade\r
|
|
\r\n""";
|
|
headers = new Map();
|
|
headers["upgrade"] = "irc/1.2";
|
|
_testParseRequest(request, "GET", "/irc",
|
|
expectedHeaders: headers, upgrade: true);
|
|
|
|
// Test websocket upgrade.
|
|
request = """
|
|
GET /chat HTTP/1.1\r
|
|
Host: server.example.com\r
|
|
Upgrade: websocket\r
|
|
Connection: Upgrade\r
|
|
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
|
Origin: http://example.com\r
|
|
Sec-WebSocket-Version: 13\r
|
|
\r\n""";
|
|
headers = new Map();
|
|
headers["host"] = "server.example.com";
|
|
headers["upgrade"] = "websocket";
|
|
headers["sec-websocket-key"] = "dGhlIHNhbXBsZSBub25jZQ==";
|
|
headers["origin"] = "http://example.com";
|
|
headers["sec-websocket-version"] = "13";
|
|
_testParseRequest(request, "GET", "/chat",
|
|
expectedHeaders: headers, upgrade: true);
|
|
|
|
// Test websocket upgrade with protocol data. NOTE: When using the
|
|
// WebSocket protocol this should never happen as the client
|
|
// should not send protocol data before processing the request
|
|
// part of the opening handshake. However the HTTP parser should
|
|
// still handle this.
|
|
request = """
|
|
GET /chat HTTP/1.1\r
|
|
Host: server.example.com\r
|
|
Upgrade: websocket\r
|
|
Connection: Upgrade\r
|
|
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
|
Origin: http://example.com\r
|
|
Sec-WebSocket-Version: 13\r
|
|
\r\n0123456""";
|
|
headers = new Map();
|
|
headers["host"] = "server.example.com";
|
|
headers["upgrade"] = "websocket";
|
|
headers["sec-websocket-key"] = "dGhlIHNhbXBsZSBub25jZQ==";
|
|
headers["origin"] = "http://example.com";
|
|
headers["sec-websocket-version"] = "13";
|
|
_testParseRequest(request, "GET", "/chat",
|
|
expectedHeaders: headers, upgrade: true, unparsedLength: 7);
|
|
}
|
|
|
|
void testParseResponse() {
|
|
String response;
|
|
Map<String, String> headers;
|
|
response = "HTTP/1.1 100 Continue\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseResponse(response, 100, "Continue");
|
|
|
|
response = "HTTP/1.1 100 Continue\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseResponse(response, 100, "Continue");
|
|
|
|
response = "HTTP/1.1 100 Continue\r\nContent-Length: 10\r\n\r\n";
|
|
_testParseResponse(response, 100, "Continue",
|
|
expectedTransferLength: 10, expectedBytesReceived: 0);
|
|
|
|
response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n"
|
|
"Connection: Close\r\n\r\n";
|
|
_testParseResponse(response, 200, "OK", connectionClose: true);
|
|
|
|
response = "HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseResponse(response, 200, "OK",
|
|
expectedVersion: "1.0", connectionClose: true);
|
|
|
|
response = "HTTP/1.0 200 OK\r\nContent-Length: 0\r\n"
|
|
"Connection: Keep-Alive\r\n\r\n";
|
|
_testParseResponse(response, 200, "OK", expectedVersion: "1.0");
|
|
|
|
response = "HTTP/1.1 204 No Content\r\nContent-Length: 11\r\n\r\n";
|
|
_testParseResponse(response, 204, "No Content",
|
|
expectedTransferLength: 11, expectedBytesReceived: 0);
|
|
|
|
response = "HTTP/1.1 304 Not Modified\r\nContent-Length: 12\r\n\r\n";
|
|
_testParseResponse(response, 304, "Not Modified",
|
|
expectedTransferLength: 12, expectedBytesReceived: 0);
|
|
|
|
response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseResponse(response, 200, "OK");
|
|
|
|
response = "HTTP/1.1 404 Not found\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseResponse(response, 404, "Not found");
|
|
|
|
response = "HTTP/1.1 500 Server error\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseResponse(response, 500, "Server error");
|
|
|
|
// Test response to HEAD request.
|
|
response = """
|
|
HTTP/1.1 200 OK\r
|
|
Content-Length: 20\r
|
|
Content-Type: text/html\r
|
|
\r\n""";
|
|
headers = new Map();
|
|
headers["content-length"] = "20";
|
|
headers["content-type"] = "text/html";
|
|
_testParseResponse(response, 200, "OK",
|
|
responseToMethod: "HEAD",
|
|
expectedTransferLength: 20,
|
|
expectedBytesReceived: 0,
|
|
expectedHeaders: headers);
|
|
|
|
// _testParseRequestLean encodes the request as ISO-8859-1. Test that the
|
|
// HTTP parser decodes header values as ISO-8859-1.
|
|
response = """
|
|
HTTP/1.1 200 OK\r
|
|
Content-Length: 0\r
|
|
test-latin1: blåbærgrød\r
|
|
\r\n""";
|
|
headers = new Map();
|
|
headers["content-length"] = "0";
|
|
headers["test-latin1"] = "blåbærgrød";
|
|
_testParseResponse(response, 200, "OK", expectedHeaders: headers);
|
|
|
|
// Test content.
|
|
response = """
|
|
HTTP/1.1 200 OK\r
|
|
Content-Length: 20\r
|
|
\r
|
|
01234567890123456789""";
|
|
_testParseResponse(response, 200, "OK",
|
|
expectedTransferLength: 20, expectedBytesReceived: 20);
|
|
|
|
// Test upper and lower case hex digits in chunked encoding.
|
|
response = """
|
|
HTTP/1.1 200 OK\r
|
|
Transfer-Encoding: chunked\r
|
|
\r
|
|
1A\r
|
|
01234567890123456789012345\r
|
|
1f\r
|
|
0123456789012345678901234567890\r
|
|
0\r\n\r\n""";
|
|
_testParseResponse(response, 200, "OK",
|
|
expectedTransferLength: -1, expectedBytesReceived: 57, chunked: true);
|
|
|
|
// Test connection close header.
|
|
response = """
|
|
HTTP/1.1 200 OK\r
|
|
Content-Length: 0\r
|
|
Connection: close\r
|
|
\r\n""";
|
|
_testParseResponse(response, 200, "OK", connectionClose: true);
|
|
|
|
// Test HTTP response without any transfer length indications
|
|
// where closing the connections indicates end of body.
|
|
response = """
|
|
HTTP/1.1 200 OK\r
|
|
\r
|
|
01234567890123456789012345
|
|
0123456789012345678901234567890
|
|
""";
|
|
_testParseResponse(response, 200, "OK",
|
|
expectedTransferLength: -1,
|
|
expectedBytesReceived: 59,
|
|
close: true,
|
|
connectionClose: true);
|
|
|
|
// Test HTTP upgrade.
|
|
response = """
|
|
HTTP/1.1 101 Switching Protocols\r
|
|
Upgrade: irc/1.2\r
|
|
Connection: Upgrade\r
|
|
\r\n""";
|
|
headers = new Map();
|
|
headers["upgrade"] = "irc/1.2";
|
|
_testParseResponse(response, 101, "Switching Protocols",
|
|
expectedHeaders: headers, upgrade: true);
|
|
|
|
// Test HTTP upgrade with protocol data.
|
|
response = """
|
|
HTTP/1.1 101 Switching Protocols\r
|
|
Upgrade: irc/1.2\r
|
|
Connection: Upgrade\r
|
|
\r\n\x00\x10\x20\x30\x40\x50\x60\x70\x80\x90\xA0\xB0\xC0\xD0\xE0\xF0""";
|
|
headers = new Map();
|
|
headers["upgrade"] = "irc/1.2";
|
|
_testParseResponse(response, 101, "Switching Protocols",
|
|
expectedHeaders: headers, upgrade: true, unparsedLength: 16);
|
|
|
|
// Test websocket upgrade.
|
|
response = """
|
|
HTTP/1.1 101 Switching Protocols\r
|
|
Upgrade: websocket\r
|
|
Connection: Upgrade\r
|
|
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
|
|
\r\n""";
|
|
headers = new Map();
|
|
headers["upgrade"] = "websocket";
|
|
headers["sec-websocket-accept"] = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
|
|
_testParseResponse(response, 101, "Switching Protocols",
|
|
expectedHeaders: headers, upgrade: true);
|
|
|
|
// Test websocket upgrade with protocol data.
|
|
response = """
|
|
HTTP/1.1 101 Switching Protocols\r
|
|
Upgrade: websocket\r
|
|
Connection: Upgrade\r
|
|
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
|
|
\r\nABCD""";
|
|
headers = new Map();
|
|
headers["upgrade"] = "websocket";
|
|
headers["sec-websocket-accept"] = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
|
|
_testParseResponse(response, 101, "Switching Protocols",
|
|
expectedHeaders: headers, upgrade: true, unparsedLength: 4);
|
|
}
|
|
|
|
void testParseInvalidRequest() {
|
|
String request;
|
|
request = "GET /\r\n\r\n";
|
|
_testParseInvalidRequest(request);
|
|
|
|
request = "GET / \r\n\r\n";
|
|
_testParseInvalidRequest(request);
|
|
|
|
request = "/ HTTP/1.1\r\n\r\n";
|
|
_testParseInvalidRequest(request);
|
|
|
|
request = "GET HTTP/1.1\r\n\r\n";
|
|
_testParseInvalidRequest(request);
|
|
|
|
request = " / HTTP/1.1\r\n\r\n";
|
|
_testParseInvalidRequest(request);
|
|
|
|
request = "@ / HTTP/1.1\r\n\r\n";
|
|
_testParseInvalidRequest(request);
|
|
|
|
request = "GET / TTP/1.1\r\n\r\n";
|
|
_testParseInvalidRequest(request);
|
|
|
|
request = "GET / HTTP/1.\r\n\r\n";
|
|
_testParseInvalidRequest(request);
|
|
|
|
request = "GET / HTTP/1.1\r\nKeep-Alive: False\r\nbadheader\r\n\r\n";
|
|
_testParseInvalidRequest(request);
|
|
|
|
// Content-Length and "Transfer-Encoding: chunked" are specified (error
|
|
// per RFC-7320).
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Content-Length: 7\r
|
|
Transfer-Encoding: chunked\r
|
|
\r
|
|
5\r
|
|
01234\r
|
|
5\r
|
|
56789\r
|
|
0\r\n\r\n""";
|
|
_testParseInvalidRequest(request);
|
|
|
|
request = """
|
|
POST /test HTTP/1.1\r
|
|
Transfer-Encoding: chunked\r
|
|
Content-Length: 7\r
|
|
\r
|
|
5\r
|
|
01234\r
|
|
5\r
|
|
56789\r
|
|
0\r\n\r\n""";
|
|
_testParseInvalidRequest(request);
|
|
}
|
|
|
|
void testParseInvalidResponse() {
|
|
String response;
|
|
|
|
response = "HTTP/1.1\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 \r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 200\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 OK\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 20A OK\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "200 OK\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1. 200 OK\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 200 O\rK\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 000 OK\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 999 Server Error\r\nContent-Length: 0\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 200 OK\r\nContent-Length: x\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = "HTTP/1.1 200 OK\r\nbadheader\r\n\r\n";
|
|
_testParseInvalidResponse(response);
|
|
|
|
response = """
|
|
HTTP/1.1 200 OK\r
|
|
Transfer-Encoding: chunked\r
|
|
\r
|
|
1A\r
|
|
01234567890123456789012345\r
|
|
1g\r
|
|
0123456789012345678901234567890\r
|
|
0\r\n\r\n""";
|
|
_testParseInvalidResponse(response);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
HttpParserTest.runAllTests();
|
|
}
|