From 56c19add3013948e6c97eadb99966ef955250170 Mon Sep 17 00:00:00 2001 From: Brian McNamara Date: Fri, 5 May 2023 07:42:35 -0700 Subject: [PATCH 1/7] Implement RFC-7230 section 5.3.3 --- .../main/java/rawhttp/core/RequestLine.java | 31 +++++++++++++------ .../kotlin/rawhttp/core/RequestLineTest.kt | 18 +++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java index ec7b5568..3e15a126 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java @@ -88,16 +88,29 @@ private void writeTo(OutputStream outputStream, boolean newLine) throws IOExcept outputStream.write(method.getBytes(StandardCharsets.US_ASCII)); outputStream.write(' '); - String path = uri.getRawPath(); - if (path == null || path.isEmpty()) { - outputStream.write('/'); + //RFC-7230 section 5.3.3 + if ("CONNECT".equalsIgnoreCase(method)) { + String host = uri.getHost(); + int port = uri.getPort(); + assert host != null : "Host is missing from RequestLine uri"; + + outputStream.write(host.getBytes(StandardCharsets.US_ASCII)); + if (port != -1) { + outputStream.write(':'); + outputStream.write(Integer.toString(port).getBytes(StandardCharsets.US_ASCII)); + } } else { - outputStream.write(path.getBytes(StandardCharsets.US_ASCII)); - } - String query = uri.getRawQuery(); - if (query != null && !query.isEmpty()) { - outputStream.write('?'); - outputStream.write(query.getBytes(StandardCharsets.US_ASCII)); + String path = uri.getRawPath(); + if (path == null || path.isEmpty()) { + outputStream.write('/'); + } else { + outputStream.write(path.getBytes(StandardCharsets.US_ASCII)); + } + String query = uri.getRawQuery(); + if (query != null && !query.isEmpty()) { + outputStream.write('?'); + outputStream.write(query.getBytes(StandardCharsets.US_ASCII)); + } } outputStream.write(' '); diff --git a/rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt b/rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt index 1c21b10d..7222d89c 100644 --- a/rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt +++ b/rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt @@ -9,6 +9,7 @@ import io.kotest.data.table import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test import rawhttp.core.errors.InvalidHttpRequest +import java.net.URI class RequestLineTest { @@ -193,4 +194,21 @@ class RequestLineTest { } } + @Test + fun testConnectSetsAuthorityFormTarget() { + val table = table(headers("method", "uri", "version", "expected request line"), + row("CONNECT", URI("http://example.com"), HttpVersion.HTTP_1_1, "CONNECT example.com HTTP/1.1"), + row("CONNECT", URI("http://example.com:80"), HttpVersion.HTTP_1_1, "CONNECT example.com:80 HTTP/1.1"), + row("CONNECT", URI("http://user:pass@example.com:80"), HttpVersion.HTTP_1_1, "CONNECT example.com:80 HTTP/1.1"), + row("CONNECT", URI("http://example.com/somePath"), HttpVersion.HTTP_1_1, "CONNECT example.com HTTP/1.1"), + row("CONNECT", URI("http://example.com:80"), HttpVersion.HTTP_1_1, "CONNECT example.com:80 HTTP/1.1"), + row("CONNECT", URI("http://www.example.com:80"), HttpVersion.HTTP_1_1, "CONNECT www.example.com:80 HTTP/1.1"), + ) + forAll(table) { method, uri, version, expectedRequest -> + RequestLine(method, uri, version).run { + toString() shouldBe expectedRequest + } + } + } + } \ No newline at end of file From b1ec83ff6d77ec5f9d591f0d65a697f7ae54b892 Mon Sep 17 00:00:00 2001 From: Brian McNamara Date: Fri, 5 May 2023 12:47:35 -0700 Subject: [PATCH 2/7] More validations --- .../java/rawhttp/core/HttpMetadataParser.java | 2 +- .../src/main/java/rawhttp/core/RawHttp.java | 13 ++++++-- .../java/rawhttp/core/RawHttpOptions.java | 31 ++++++++++++++++++- .../main/java/rawhttp/core/RequestLine.java | 25 ++++++++++++--- 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/rawhttp-core/src/main/java/rawhttp/core/HttpMetadataParser.java b/rawhttp-core/src/main/java/rawhttp/core/HttpMetadataParser.java index e8972735..f584509f 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/HttpMetadataParser.java +++ b/rawhttp-core/src/main/java/rawhttp/core/HttpMetadataParser.java @@ -178,7 +178,7 @@ private RequestLine buildRequestLine(String requestLine) { URI uri = parseUri(uriPart); - return new RequestLine(method, uri, httpVersion); + return new RequestLine(method, uri, httpVersion, options); } /** diff --git a/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java b/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java index cfaff0c3..59f2c49c 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java @@ -141,7 +141,7 @@ public RawHttpRequest parseRequest(InputStream inputStream, RawHttpHeaders headers = modifiableHeaders.build(); - @Nullable BodyReader bodyReader = requestHasBody(headers) + @Nullable BodyReader bodyReader = requestHasBody(headers, requestLine) ? createBodyReader(inputStream, requestLine, headers) : null; @@ -276,7 +276,14 @@ public FramedBody getFramedBody(StartLine startLine, RawHttpHeaders headers) { * @param headers HTTP request's headers * @return true if the headers indicate the request should have a body, false otherwise */ - public static boolean requestHasBody(RawHttpHeaders headers) { + public static boolean requestHasBody(RawHttpHeaders headers, RequestLine requestLine) { + // As per RFC-7231 section 4.3.6: + // A payload within a CONNECT request message has no defined semantics; + // sending a payload body on a CONNECT request might cause some existing + // implementations to reject the request. + if ("CONNECT".equalsIgnoreCase(requestLine.getMethod())) { + return false; + } // The presence of a message body in a request is signaled by a // Content-Length or Transfer-Encoding header field. Request message // framing is independent of method semantics, even if the method does @@ -316,7 +323,7 @@ public static boolean responseHasBody(StatusLine statusLine, } if (requestLine.getMethod().equalsIgnoreCase("CONNECT") && startsWith(2, statusLine.getStatusCode())) { - return false; // CONNECT successful means start tunelling + return false; // CONNECT successful means start tunnelling } } diff --git a/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java b/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java index 7fe29b0f..91bff76d 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java @@ -25,6 +25,7 @@ public class RawHttpOptions { private final boolean ignoreLeadingEmptyLine; private final boolean allowIllegalStartLineCharacters; private final boolean allowComments; + private final boolean allowIllegalConnectAuthority; private final HttpHeadersOptions httpHeadersOptions; private final HttpBodyEncodingRegistry encodingRegistry; @@ -34,6 +35,7 @@ private RawHttpOptions(boolean insertHostHeaderIfMissing, boolean ignoreLeadingEmptyLine, boolean allowIllegalStartLineCharacters, boolean allowComments, + boolean allowIllegalConnectAuthority, HttpHeadersOptions httpHeadersOptions, HttpBodyEncodingRegistry encodingRegistry) { this.insertHostHeaderIfMissing = insertHostHeaderIfMissing; @@ -42,6 +44,7 @@ private RawHttpOptions(boolean insertHostHeaderIfMissing, this.ignoreLeadingEmptyLine = ignoreLeadingEmptyLine; this.allowIllegalStartLineCharacters = allowIllegalStartLineCharacters; this.allowComments = allowComments; + this.allowIllegalConnectAuthority = allowIllegalConnectAuthority; this.httpHeadersOptions = httpHeadersOptions; this.encodingRegistry = encodingRegistry; } @@ -115,6 +118,13 @@ public boolean allowComments() { return allowComments; } + /** + * @return whether or not to bypass illegal CONNECT request authority validation + */ + public boolean allowIllegalConnectAuthority() { + return allowIllegalConnectAuthority; + } + /** * @return options for parsing HTTP headers */ @@ -197,6 +207,7 @@ public static final class Builder { private boolean insertHttpVersionIfMissing = true; private boolean allowIllegalStartLineCharacters = false; private boolean allowComments = false; + private boolean allowIllegalConnectAuthority = false; private HttpHeadersOptionsBuilder httpHeadersOptionsBuilder = new HttpHeadersOptionsBuilder(); private HttpBodyEncodingRegistry encodingRegistry; @@ -299,6 +310,24 @@ public Builder allowComments() { return this; } + /** + * Allows bypassing validation logic for CONNECT request authority requirements + *

+ * RFC-7230 section 5.3.3 defines CONNECT requests must: + * + * A client sending a CONNECT request MUST send the authority form of + * request-target ([Section 5.3 of RFC7230]) + * + * + * Setting this will allow bypassing the validations and allow nullable host and or port + * + * @return this + */ + public Builder allowIllegalConnectAuthority() { + this.allowIllegalConnectAuthority = true; + return this; + } + /** * Get a builder of {@link HttpHeadersOptions} to use with this object. * @@ -333,7 +362,7 @@ public RawHttpOptions build() { return new RawHttpOptions(insertHostHeaderIfMissing, insertHttpVersionIfMissing, allowNewLineWithoutReturn, ignoreLeadingEmptyLine, allowIllegalStartLineCharacters, allowComments, - httpHeadersOptionsBuilder.getOptions(), registry); + allowIllegalConnectAuthority, httpHeadersOptionsBuilder.getOptions(), registry); } public class HttpHeadersOptionsBuilder { diff --git a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java index 3e15a126..9c819ea8 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java @@ -15,6 +15,7 @@ public class RequestLine implements StartLine { private final String method; private final URI uri; private final HttpVersion httpVersion; + private final RawHttpOptions options; /** * Create a new {@link RequestLine}. @@ -27,9 +28,14 @@ public class RequestLine implements StartLine { * @param httpVersion HTTP version of the message */ public RequestLine(String method, URI uri, HttpVersion httpVersion) { + this(method, uri, httpVersion, RawHttpOptions.defaultInstance()); + } + + public RequestLine(String method, URI uri, HttpVersion httpVersion, RawHttpOptions options) { this.method = method; this.uri = uri; this.httpVersion = httpVersion; + this.options = options; } /** @@ -92,12 +98,21 @@ private void writeTo(OutputStream outputStream, boolean newLine) throws IOExcept if ("CONNECT".equalsIgnoreCase(method)) { String host = uri.getHost(); int port = uri.getPort(); - assert host != null : "Host is missing from RequestLine uri"; - outputStream.write(host.getBytes(StandardCharsets.US_ASCII)); - if (port != -1) { - outputStream.write(':'); - outputStream.write(Integer.toString(port).getBytes(StandardCharsets.US_ASCII)); + if (!options.allowIllegalStartLineCharacters()) { + if (host == null) { + throw new IllegalArgumentException("URI host can not be null when CONNECT method is used"); + } else if (port == -1) { + throw new IllegalArgumentException("URI port must be defined when CONNECT method is used"); + } + } + + if (host != null) { + outputStream.write(host.getBytes(StandardCharsets.US_ASCII)); + if (port != -1) { + outputStream.write(':'); + outputStream.write(Integer.toString(port).getBytes(StandardCharsets.US_ASCII)); + } } } else { String path = uri.getRawPath(); From d7312dd4ee25580f67f2b312de6e60c1fb050e9c Mon Sep 17 00:00:00 2001 From: Brian McNamara Date: Fri, 5 May 2023 13:00:34 -0700 Subject: [PATCH 3/7] More comments --- .../src/main/java/rawhttp/core/RequestLine.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java index 9c819ea8..4efc6b66 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java @@ -18,7 +18,7 @@ public class RequestLine implements StartLine { private final RawHttpOptions options; /** - * Create a new {@link RequestLine}. + * Create a new {@link RequestLine} using the default options. *

* This constructor does not validate the method name. If validation is required, * use the {@link HttpMetadataParser#parseRequestLine(java.io.InputStream)} method. @@ -31,6 +31,17 @@ public RequestLine(String method, URI uri, HttpVersion httpVersion) { this(method, uri, httpVersion, RawHttpOptions.defaultInstance()); } + /** + * Create a new {@link RequestLine}. + *

+ * This constructor does not validate the method name. If validation is required, + * use the {@link HttpMetadataParser#parseRequestLine(java.io.InputStream)} method. + * + * @param method name of the HTTP method + * @param uri URI of the request target + * @param httpVersion HTTP version of the message + * @param options RawHttp configuration options + */ public RequestLine(String method, URI uri, HttpVersion httpVersion, RawHttpOptions options) { this.method = method; this.uri = uri; From 3d779a3e7799f3c26b2cfd400831d76545574f6f Mon Sep 17 00:00:00 2001 From: Brian McNamara Date: Mon, 8 May 2023 16:52:40 -0700 Subject: [PATCH 4/7] Simplify Request line based on review --- .../src/main/java/rawhttp/core/RawHttp.java | 7 ------ .../main/java/rawhttp/core/RequestLine.java | 23 +++++++------------ 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java b/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java index 59f2c49c..851570d7 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java @@ -277,13 +277,6 @@ public FramedBody getFramedBody(StartLine startLine, RawHttpHeaders headers) { * @return true if the headers indicate the request should have a body, false otherwise */ public static boolean requestHasBody(RawHttpHeaders headers, RequestLine requestLine) { - // As per RFC-7231 section 4.3.6: - // A payload within a CONNECT request message has no defined semantics; - // sending a payload body on a CONNECT request might cause some existing - // implementations to reject the request. - if ("CONNECT".equalsIgnoreCase(requestLine.getMethod())) { - return false; - } // The presence of a message body in a request is signaled by a // Content-Length or Transfer-Encoding header field. Request message // framing is independent of method semantics, even if the method does diff --git a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java index 4efc6b66..a8a3cdc2 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java @@ -106,25 +106,18 @@ private void writeTo(OutputStream outputStream, boolean newLine) throws IOExcept outputStream.write(' '); //RFC-7230 section 5.3.3 - if ("CONNECT".equalsIgnoreCase(method)) { + if (!options.allowIllegalConnectAuthority() && "CONNECT".equalsIgnoreCase(method)) { String host = uri.getHost(); int port = uri.getPort(); - if (!options.allowIllegalStartLineCharacters()) { - if (host == null) { - throw new IllegalArgumentException("URI host can not be null when CONNECT method is used"); - } else if (port == -1) { - throw new IllegalArgumentException("URI port must be defined when CONNECT method is used"); - } - } - - if (host != null) { - outputStream.write(host.getBytes(StandardCharsets.US_ASCII)); - if (port != -1) { - outputStream.write(':'); - outputStream.write(Integer.toString(port).getBytes(StandardCharsets.US_ASCII)); - } + if (host == null) { + throw new IllegalArgumentException("URI host can not be null when CONNECT method is used"); + } else if (port < 1) { + throw new IllegalArgumentException("URI port must be defined when CONNECT method is used"); } + outputStream.write(host.getBytes(StandardCharsets.US_ASCII)); + outputStream.write(':'); + outputStream.write(Integer.toString(port).getBytes(StandardCharsets.US_ASCII)); } else { String path = uri.getRawPath(); if (path == null || path.isEmpty()) { From 05d17594c878ce2fd2970e67347e5eeda8da856a Mon Sep 17 00:00:00 2001 From: Brian McNamara Date: Mon, 8 May 2023 16:58:46 -0700 Subject: [PATCH 5/7] Update comments --- rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java | 6 +++--- rawhttp-core/src/main/java/rawhttp/core/RequestLine.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java b/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java index 91bff76d..1d9e185e 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java @@ -119,7 +119,7 @@ public boolean allowComments() { } /** - * @return whether or not to bypass illegal CONNECT request authority validation + * @return bypasses the CONNECT requirement that a host and port are used in the request line */ public boolean allowIllegalConnectAuthority() { return allowIllegalConnectAuthority; @@ -311,7 +311,7 @@ public Builder allowComments() { } /** - * Allows bypassing validation logic for CONNECT request authority requirements + * Allows bypassing requirements for CONNECT requests which require host and port to be sent *

* RFC-7230 section 5.3.3 defines CONNECT requests must: * @@ -319,7 +319,7 @@ public Builder allowComments() { * request-target ([Section 5.3 of RFC7230]) * * - * Setting this will allow bypassing the validations and allow nullable host and or port + * Setting this will bypass this requirement and fall back to non-CONNECT request line requirements * * @return this */ diff --git a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java index a8a3cdc2..5bca40e8 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java @@ -113,7 +113,7 @@ private void writeTo(OutputStream outputStream, boolean newLine) throws IOExcept if (host == null) { throw new IllegalArgumentException("URI host can not be null when CONNECT method is used"); } else if (port < 1) { - throw new IllegalArgumentException("URI port must be defined when CONNECT method is used"); + throw new IllegalArgumentException("URI port must be defined and valid when CONNECT method is used"); } outputStream.write(host.getBytes(StandardCharsets.US_ASCII)); outputStream.write(':'); From 0ce68f072a9859eb6d8fc9183517d529ffb48fd2 Mon Sep 17 00:00:00 2001 From: Brian McNamara Date: Tue, 9 May 2023 08:32:29 -0700 Subject: [PATCH 6/7] Remove quote from javadoc --- rawhttp-core/src/main/java/rawhttp/core/RawHttp.java | 4 ++-- .../src/main/java/rawhttp/core/RawHttpOptions.java | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java b/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java index 851570d7..c5d63aed 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java @@ -141,7 +141,7 @@ public RawHttpRequest parseRequest(InputStream inputStream, RawHttpHeaders headers = modifiableHeaders.build(); - @Nullable BodyReader bodyReader = requestHasBody(headers, requestLine) + @Nullable BodyReader bodyReader = requestHasBody(headers) ? createBodyReader(inputStream, requestLine, headers) : null; @@ -276,7 +276,7 @@ public FramedBody getFramedBody(StartLine startLine, RawHttpHeaders headers) { * @param headers HTTP request's headers * @return true if the headers indicate the request should have a body, false otherwise */ - public static boolean requestHasBody(RawHttpHeaders headers, RequestLine requestLine) { + public static boolean requestHasBody(RawHttpHeaders headers) { // The presence of a message body in a request is signaled by a // Content-Length or Transfer-Encoding header field. Request message // framing is independent of method semantics, even if the method does diff --git a/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java b/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java index 1d9e185e..9bab1c64 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java @@ -314,11 +314,9 @@ public Builder allowComments() { * Allows bypassing requirements for CONNECT requests which require host and port to be sent *

* RFC-7230 section 5.3.3 defines CONNECT requests must: - * - * A client sending a CONNECT request MUST send the authority form of - * request-target ([Section 5.3 of RFC7230]) - * - * + * "A client sending a CONNECT request MUST send the authority form of + * request-target (Section 5.3 of RFC7230)" + *

* Setting this will bypass this requirement and fall back to non-CONNECT request line requirements * * @return this From f0a770de1448dd0006816c05878afc35d745cc62 Mon Sep 17 00:00:00 2001 From: Brian McNamara Date: Tue, 9 May 2023 09:50:43 -0700 Subject: [PATCH 7/7] Test fixes --- rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt b/rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt index 7222d89c..00bfae14 100644 --- a/rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt +++ b/rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt @@ -197,10 +197,10 @@ class RequestLineTest { @Test fun testConnectSetsAuthorityFormTarget() { val table = table(headers("method", "uri", "version", "expected request line"), - row("CONNECT", URI("http://example.com"), HttpVersion.HTTP_1_1, "CONNECT example.com HTTP/1.1"), + row("CONNECT", URI("http://example.com:8080"), HttpVersion.HTTP_1_1, "CONNECT example.com:8080 HTTP/1.1"), row("CONNECT", URI("http://example.com:80"), HttpVersion.HTTP_1_1, "CONNECT example.com:80 HTTP/1.1"), row("CONNECT", URI("http://user:pass@example.com:80"), HttpVersion.HTTP_1_1, "CONNECT example.com:80 HTTP/1.1"), - row("CONNECT", URI("http://example.com/somePath"), HttpVersion.HTTP_1_1, "CONNECT example.com HTTP/1.1"), + row("CONNECT", URI("http://example.com:8080/somePath"), HttpVersion.HTTP_1_1, "CONNECT example.com:8080 HTTP/1.1"), row("CONNECT", URI("http://example.com:80"), HttpVersion.HTTP_1_1, "CONNECT example.com:80 HTTP/1.1"), row("CONNECT", URI("http://www.example.com:80"), HttpVersion.HTTP_1_1, "CONNECT www.example.com:80 HTTP/1.1"), )