-
Notifications
You must be signed in to change notification settings - Fork 50
fix: handle a ':path' with an asterisk as the RFC required #981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -351,7 +351,8 @@ private[http] final class UriParser( | |
| // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 | ||
| def `http2-path-pseudo-header` = rule( | ||
| `absolute-path` ~ optional('?' ~ rawQueryString) // origin-form | ||
| ) // TODO: asterisk-form | ||
| | '*' ~ run(setPath(Path.Empty)) ~ run { _rawQueryString = None } // asterisk-form (for OPTIONS requests) | ||
| ) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pseudo-header field MUST NOT be empty for "http" or "https"
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a test for this behavior? We need to demonstrate how users can deal with these requests. |
||
|
|
||
| /** | ||
| * @return path and percent-encoded query string. When in in 'relaxed' mode, characters not permitted by https://tools.ietf.org/html/rfc3986#section-3.4 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,7 +141,9 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp | |
| // Insert the Foo header so it occurs before at least one pseudo-header | ||
| val (before, after) = pseudoHeaders.splitAt(insertPoint) | ||
| val modified = before ++ Vector("Foo" -> "bar") ++ after | ||
| parseExpectProtocolError(modified) | ||
| val ex = parseExpectProtocolError(modified) | ||
| ex.getMessage should | ||
| ===(s"Malformed request: Pseudo-header field '${after.head._1}' must not appear after a regular header") | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -152,24 +154,29 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp | |
|
|
||
| "not accept connection-specific headers" in { | ||
| // Add Connection header to indicate that Foo is a connection-specific header | ||
| parseExpectProtocolError(Vector( | ||
| val ex = parseExpectProtocolError(Vector( | ||
| ":method" -> "GET", | ||
| ":scheme" -> "https", | ||
| ":path" -> "/", | ||
| "Connection" -> "foo", | ||
| "Foo" -> "bar")) | ||
| "Foo" -> "bar" | ||
| )) | ||
| ex.getMessage should ===("Malformed request: Header 'Connection' must not be used with HTTP/2") | ||
| } | ||
|
|
||
| "not accept TE with other values than 'trailers'" in { | ||
|
|
||
| // The only exception to this is the TE header field, which MAY be | ||
| // present in an HTTP/2 request; when it is, it MUST NOT contain any | ||
| // value other than "trailers". | ||
| parseExpectProtocolError(Vector( | ||
| val ex = parseExpectProtocolError(Vector( | ||
| ":method" -> "GET", | ||
| ":scheme" -> "https", | ||
| ":path" -> "/", | ||
| "TE" -> "chunked")) | ||
| "TE" -> "chunked" | ||
| )) | ||
| ex.getMessage should | ||
| ===("Malformed request: Header 'TE' must not contain value other than 'trailers', value was 'chunked") | ||
|
|
||
| } | ||
|
|
||
|
|
@@ -442,13 +449,15 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp | |
| // ... A request in asterisk form includes the | ||
| // value '*' for the ":path" pseudo-header field. | ||
|
|
||
| "handle a ':path' with an asterisk" in pendingUntilFixed { | ||
| "handle a ':path' with an asterisk" in { | ||
| val request: HttpRequest = parseExpectOk( | ||
| keyValuePairs = Vector( | ||
| ":method" -> "OPTIONS", | ||
| ":scheme" -> "http", | ||
| ":path" -> "*")) | ||
| request.uri.toString should ===("*") // FIXME: Compare in a better way | ||
| ":path" -> "*" | ||
| )) | ||
| request.uri.path.toString should ===("") | ||
| request.uri.rawQueryString should ===(None) | ||
| } | ||
|
|
||
| // [The ":path"] pseudo-header field MUST NOT be empty for "http" or "https" | ||
|
|
@@ -457,11 +466,13 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp | |
| "reject empty ':path' pseudo-headers for http and https" in { | ||
| val schemes = Seq("http", "https") | ||
| forAll(schemes) { (scheme: String) => | ||
| parseExpectProtocolError( | ||
| val ex = parseExpectProtocolError( | ||
| keyValuePairs = Vector( | ||
| ":method" -> "POST", | ||
| ":scheme" -> scheme, | ||
| ":path" -> "")) | ||
| ":path" -> "" | ||
| )) | ||
| ex.getMessage should ===("Malformed request: Pseudo-header ':path' must not be empty") | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -477,7 +488,76 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp | |
| // a CONNECT request (Section 8.3). An HTTP request that omits | ||
| // mandatory pseudo-header fields is malformed | ||
|
|
||
| // [assume CONNECT not supported] | ||
| // 8.3. HTTP/2 CONNECT Method | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we support Since we make the logic more strict, it could still be ok to fix this here but we should make sure we don't mask an error that reports that CONNECT is not supported with a different parsing error now. |
||
|
|
||
| // The CONNECT method can be used to convert the HTTP/2 connection into | ||
| // a tunnel for a non-HTTP/2 protocol. The CONNECT method is identified | ||
| // by the ":method" pseudo-header field having a value of "CONNECT". | ||
| // | ||
| // A CONNECT request that does not conform to the following restrictions | ||
| // is malformed (Section 8.1.2.6): | ||
| // | ||
| // * The ":scheme" and ":path" pseudo-header fields MUST NOT be included | ||
| // | ||
| // * The ":authority" pseudo-header field contains the host and port to | ||
| // connect to (equivalent to the authority-form of the request-target | ||
| // of CONNECT requests, see [RFC7230], Section 5.3.3). | ||
|
|
||
| "handle CONNECT requests correctly" should { | ||
|
|
||
| "accept valid CONNECT requests without :scheme and :path" in { | ||
| val request: HttpRequest = parseExpectOk( | ||
| keyValuePairs = Vector( | ||
| ":method" -> "CONNECT", | ||
| ":authority" -> "example.com:443" | ||
| )) | ||
| request.method should ===(HttpMethods.CONNECT) | ||
| request.uri.authority.host.toString should ===("example.com") | ||
| request.uri.authority.port should ===(443) | ||
| } | ||
|
|
||
| "reject CONNECT requests with :path pseudo-header" in { | ||
| val ex = parseExpectProtocolError( | ||
| keyValuePairs = Vector( | ||
| ":method" -> "CONNECT", | ||
| ":authority" -> "example.com:443", | ||
| ":path" -> "/" | ||
| )) | ||
| ex.getMessage should ===("Malformed request: Pseudo-header ':path' must not be included in CONNECT request") | ||
| } | ||
|
|
||
| "reject CONNECT requests with :scheme pseudo-header" in { | ||
| val ex = parseExpectProtocolError( | ||
| keyValuePairs = Vector( | ||
| ":method" -> "CONNECT", | ||
| ":authority" -> "example.com:443", | ||
| ":scheme" -> "https" | ||
| )) | ||
| ex.getMessage should ===("Malformed request: Pseudo-header ':scheme' must not be included in CONNECT request") | ||
| } | ||
|
|
||
| "reject CONNECT requests with both :scheme and :path pseudo-headers" in { | ||
| val ex = parseExpectProtocolError( | ||
| keyValuePairs = Vector( | ||
| ":method" -> "CONNECT", | ||
| ":authority" -> "example.com:443", | ||
| ":scheme" -> "https", | ||
| ":path" -> "/" | ||
| )) | ||
| // Should fail on :path first since it's checked before :scheme in the header parsing order | ||
| ex.getMessage should ===("Malformed request: Pseudo-header ':path' must not be included in CONNECT request") | ||
| } | ||
|
|
||
| "reject CONNECT requests with empty :path" in { | ||
| val ex = parseExpectProtocolError( | ||
| keyValuePairs = Vector( | ||
| ":method" -> "CONNECT", | ||
| ":authority" -> "example.com:443", | ||
| ":path" -> "" | ||
| )) | ||
| ex.getMessage should ===("Malformed request: Pseudo-header ':path' must not be empty") | ||
| } | ||
| } | ||
|
|
||
| "reject requests without a mandatory pseudo-headers" in { | ||
| val mandatoryPseudoHeaders = Seq(":method", ":scheme", ":path") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ":path" pseudo-header field includes the path and query parts
of the target URI (the "path-absolute" production and optionally a
'?' character followed by the "query" production (see Sections 3.3
and 3.4 of [RFC3986]). A request in asterisk form includes the
value '*' for the ":path" pseudo-header field.
Belshe, et al. Standards Track [Page 55]
RFC 7540 HTTP/2 May 2015
All HTTP/2 requests MUST include exactly one valid value for the
":method", ":scheme", and ":path" pseudo-header fields, unless it is
a CONNECT request (Section 8.3). An HTTP request that omits
mandatory pseudo-header fields is malformed (Section 8.1.2.6).
HTTP/2 does not define a way to carry the version identifier that is
included in the HTTP/1.1 request line.