diff --git a/rawhttp-core/src/main/java/rawhttp/core/HttpMetadataParser.java b/rawhttp-core/src/main/java/rawhttp/core/HttpMetadataParser.java index e897273..f584509 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 cfaff0c..c5d63ae 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RawHttp.java @@ -316,7 +316,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 7fe29b0..9bab1c6 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 bypasses the CONNECT requirement that a host and port are used in the request line + */ + 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,22 @@ public Builder allowComments() { return this; } + /** + * 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)" + *

+ * Setting this will bypass this requirement and fall back to non-CONNECT request line requirements + * + * @return this + */ + public Builder allowIllegalConnectAuthority() { + this.allowIllegalConnectAuthority = true; + return this; + } + /** * Get a builder of {@link HttpHeadersOptions} to use with this object. * @@ -333,7 +360,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 ec7b556..5bca40e 100644 --- a/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java +++ b/rawhttp-core/src/main/java/rawhttp/core/RequestLine.java @@ -15,9 +15,10 @@ 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}. + * 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. @@ -27,9 +28,25 @@ 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()); + } + + /** + * 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; this.httpVersion = httpVersion; + this.options = options; } /** @@ -88,16 +105,31 @@ 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 (!options.allowIllegalConnectAuthority() && "CONNECT".equalsIgnoreCase(method)) { + String host = uri.getHost(); + int port = uri.getPort(); + + 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 and valid when CONNECT method is used"); + } + outputStream.write(host.getBytes(StandardCharsets.US_ASCII)); + 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 1c21b10..00bfae1 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: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: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"), + ) + forAll(table) { method, uri, version, expectedRequest -> + RequestLine(method, uri, version).run { + toString() shouldBe expectedRequest + } + } + } + } \ No newline at end of file