Skip to content

Commit

Permalink
Merge pull request #66 from brian-mcnamara/rfc7230-5.3.3
Browse files Browse the repository at this point in the history
Implement RFC-7230 section 5.3.3
  • Loading branch information
renatoathaydes committed May 10, 2023
2 parents d51e4ce + f0a770d commit ff052e4
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion rawhttp-core/src/main/java/rawhttp/core/RawHttp.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
29 changes: 28 additions & 1 deletion rawhttp-core/src/main/java/rawhttp/core/RawHttpOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -34,6 +35,7 @@ private RawHttpOptions(boolean insertHostHeaderIfMissing,
boolean ignoreLeadingEmptyLine,
boolean allowIllegalStartLineCharacters,
boolean allowComments,
boolean allowIllegalConnectAuthority,
HttpHeadersOptions httpHeadersOptions,
HttpBodyEncodingRegistry encodingRegistry) {
this.insertHostHeaderIfMissing = insertHostHeaderIfMissing;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -299,6 +310,22 @@ public Builder allowComments() {
return this;
}

/**
* Allows bypassing requirements for CONNECT requests which require host and port to be sent
* <p>
* RFC-7230 section 5.3.3 defines CONNECT requests must:
* "A client sending a CONNECT request MUST send the authority form of
* request-target (<a href="https://datatracker.ietf.org/doc/html/rfc7230#section-5.3">Section 5.3 of RFC7230</a>)"
* <p>
* 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.
*
Expand Down Expand Up @@ -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 {
Expand Down
52 changes: 42 additions & 10 deletions rawhttp-core/src/main/java/rawhttp/core/RequestLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* This constructor does not validate the method name. If validation is required,
* use the {@link HttpMetadataParser#parseRequestLine(java.io.InputStream)} method.
Expand All @@ -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}.
* <p>
* 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;
}

/**
Expand Down Expand Up @@ -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(' ');
Expand Down
18 changes: 18 additions & 0 deletions rawhttp-core/src/test/kotlin/rawhttp/core/RequestLineTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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
}
}
}

}

0 comments on commit ff052e4

Please sign in to comment.