Skip to content
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

feat(ext): support non-canonical HTTP/1 reason phrases #2792

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

Copy link
Member

@acfoltzer acfoltzer commented Mar 23, 2022

Add a new extension type hyper::ext::ReasonPhrase gated by a new feature flag http1_reason_phrase. When enabled, store any non-canonical reason phrases in this extension when parsing responses, and write this reason phrase instead of the canonical reason phrase when emitting responses.

Reason phrases are a disused corner of the spec that implementations ought to treat as opaque blobs of bytes. Unfortunately, real-world traffic sometimes does depend on being able to inspect and manipulate them.

My main outstanding question is: should this new feature flag be included in full? Excluding it means this patch also has to modify the CI configuration in order to run the client and server tests.

Copy link
Member Author

@acfoltzer acfoltzer left a comment

So the tricky question is: is this worth a feature flag? Here are the performance numbers for the cargo bench suite (red is with the new feature disabled, green is with it enabled): https://gist.github.com/acfoltzer/c64e92ac95c390cb1c08624ecf61bfdf

It looks to me like roughly a wash, though there's a fair amount of noise in the sample. The problem is that none of the existing benches use the custom reason phrase, so it's hard to see the overall effect that existing users of "full" might get if they're already receiving responses with non-canonical phrases.

From an API cleanliness perspective I would prefer rolling this extension into the existing "http1" flag, but I also don't want to be introducing undue surprises for users of client who are not currently having to bear the cost of either the comparison against the canonical reason phrase or the allocation to store a custom one when sighted.

src/ext/h1_reason_phrase.rs Outdated Show resolved Hide resolved
src/ffi/http_types.rs Show resolved Hide resolved
#[cfg(feature = "http1_reason_phrase")]
let custom_reason_phrase = msg.head.extensions.get::<crate::ext::ReasonPhrase>();
#[cfg(not(feature = "http1_reason_phrase"))]
#[allow(non_upper_case_globals)]
const custom_reason_phrase: Option<()> = None;
Copy link
Member Author

@acfoltzer acfoltzer Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where it starts getting a bit messy. To reduce the overall cfgness of this code, I'm relying on const evaluation to ensure this does not have an effect on performance when the flag is not enabled.

@acfoltzer acfoltzer marked this pull request as ready for review Mar 23, 2022
Add a new extension type `hyper::ext::ReasonPhrase` gated by a new feature flag
`http1_reason_phrase`. When enabled, store any non-canonical reason phrases in this extension when
parsing responses, and write this reason phrase instead of the canonical reason phrase when emitting
responses.

Reason phrases are a disused corner of the spec that implementations ought to treat as opaque blobs
of bytes. Unfortunately, real-world traffic sometimes does depend on being able to inspect and
manipulate them.

My main outstanding question is: should this new feature flag be included in `full`? Excluding it
means this patch also has to modify the CI configuration in order to run the `client` and `server`
tests.
@acfoltzer acfoltzer force-pushed the acfoltzer/http1_reason_phrase branch from a99c3a7 to e7be2e1 Compare Mar 23, 2022
Copy link
Member

@seanmonstar seanmonstar left a comment

Very good start, I appreciate the thoroughness of the docs and tests 🤩

Cargo.toml Outdated
@@ -86,6 +86,9 @@ full = [
http1 = []
http2 = ["h2"]

# Support for non-canonical HTTP/1 reason phrases
http1_reason_phrase = ["http1"]
Copy link
Member

@seanmonstar seanmonstar Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, so let's talk about if we need a feature. My initial gut feeling is that it seems like such a small feature that it doesn't pull its weight, just include it. Especially if the benchmarks don't show a noticeable difference.

Copy link
Member Author

@acfoltzer acfoltzer Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be in favor of folding it into http1. I think it'd take some very unusual traffic (like maxing out reason phrase length on every response) to cause a noticeable difference. It also helps that the cost is incurred on responses rather than on requests which are usually more concerning as a DoS vector.

I'll go ahead and fold it into http1 but will keep it in a separate commit while this is under review.

Copy link
Member Author

@acfoltzer acfoltzer Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this change in d621c74, which led to a pleasant reduction in the amount of awkwardly cfged code

src/ext/h1_reason_phrase.rs Outdated Show resolved Hide resolved
src/ext/h1_reason_phrase.rs Outdated Show resolved Hide resolved
src/ext/h1_reason_phrase.rs Outdated Show resolved Hide resolved
}

/// Converts a static byte slice to a reason phrase.
pub const fn from_static(reason: &'static [u8]) -> Self {
Copy link
Member

@seanmonstar seanmonstar Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this and the other constructors, I think we probably need to verify there's no illegal bytes in here that would mess up the HTTP/1 message.

Copy link
Member Author

@acfoltzer acfoltzer Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, that's a really good catch. I'll convert some of the existing From impls to TryFrom, and expose an unsafe constructor for unchecked creation.

Copy link
Member

@seanmonstar seanmonstar Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the unchecked one needed yet? A from_static can likely just panic, similar to HeaderName::from_static. The verification can happen at compile time for most static strings.

Copy link
Member Author

@acfoltzer acfoltzer Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check when constructing at runtime, because the server role will happily splat out whatever bytes are there into the status line. If someone put non-spec characters into the byte string this would lead to Problems.

The unchecked one is there for the client role to save the validation step; it already knows the characters are valid since it made it through parsing.

Copy link
Member Author

@acfoltzer acfoltzer Mar 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented these interfaces in 699c441, added tests in 4ca1eee

acfoltzer added 3 commits Mar 23, 2022
Most notably, this adds runtime checking for validity of reason phrases, which is super important to
prevent invalid and dangerous characters from being emitted when writing responses. An `unsafe`
escape hatch is present for hyper itself to create reason phrases that have been parsed (and
therefore implicitly validated) by httparse.
Our discussion in the PR thread is leaning toward this option, so let's have it available as a
separate commit so we can decide one way or the other.
@acfoltzer acfoltzer requested a review from seanmonstar Mar 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants