| // 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(); |
| } |