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

Add the possibility to set the maximum number of header fields #549

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

Conversation

marinelli
Copy link
Contributor

This PR adds the possibility to configure the maximum number of HTTP header fields in a message.
This is similar to what it was done for configuring the maximum size of an HTTP header field (#514).

Copy link
Owner

@snoyberg snoyberg left a comment

Choose a reason for hiding this comment

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

Thanks! Two pieces of feedback:

  1. Can you update the version number in the cabal file too?
  2. It's slightly surprising that the new config value is a Maybe Int but it comes with a default of 100 and doesn't appear to have a way to set it to Nothing. Not a strong comment, but something more consistent might make sense.

@marinelli
Copy link
Contributor Author

Thanks! Two pieces of feedback:

Thanks for having a look :)

  1. Can you update the version number in the cabal file too?

done

  1. It's slightly surprising that the new config value is a Maybe Int but it comes with a default of 100 and doesn't appear to have a way to set it to Nothing. Not a strong comment, but something more consistent might make sense.

I used the same strategy of managerMaxHeaderLength, and for what I understood is the following:

  • if you want to use set a maximum value, use something like managerMaxNumberHeaders 123 defaultManagerSettings
  • if you want to set it to Nothing, import Network.HTTP.Client.Internal, and do something like: defaultManagerSettings { managerMaxNumberHeaders = Nothing }.

I think we introduced some extra complexity, not really required.
The MaxHeaderLength and the new MaxNumberHeaders are used in predicates where we compare those max values with a counter, we can already use a negative number to obtain the same behavior of configuring those parameters to Nothing, ie: managerMaxNumberHeaders (- 1) defaultManagerSettings.

The main purpose of configuring those parameters to Nothing would be for testing and debugging (I think), so we could just remove the Maybe, and have something like this: marinelli/http-client@set-max-headers...marinelli:http-client:use-ints.

If we would like to continue to use a Maybe value for those max settings in ManagerSettings, well, I would change it to a Maybe Word.

cc: @tfausak

@snoyberg
Copy link
Owner

I don't remember the reasons behind the max header length decisions. I'm fine with any approach. Even if we went over to Word, using 0 to mean "unlimited" would be reasonable too (since disallowing all headers wouldn't make sense).

@tfausak any opinion from you on which way to go?

@tfausak
Copy link
Contributor

tfausak commented Dec 13, 2024

I like the idea of removing Maybe. I also think that switching to Word makes sense, but that may require some annoying refactoring.

There may be no need for a special "unlimited" value. You could just set it to maxBound which is effectively unlimited. But also treating 0 as "unlimited" makes sense to me. You can't really do HTTP without any headers, so setting the limit to 0 would be useless.

@marinelli
Copy link
Contributor Author

marinelli commented Dec 13, 2024

I like the idea of removing Maybe. I also think that switching to Word makes sense, but that may require some annoying refactoring.

There may be no need for a special "unlimited" value. You could just set it to maxBound which is effectively unlimited. But also treating 0 as "unlimited" makes sense to me. You can't really do HTTP without any headers, so setting the limit to 0 would be useless.

I had a look here https://www.rfc-editor.org/rfc/rfc2616#page-31, and it seems that header fields can also be omitted, so 0 might be a proper value. Yeah, we could use maxBound, but I think also negative numbers are a valid option.
@tfausak I already refactored the code here [1], and I do not think that switching to Words will be so complicated.

We could also use data MaxValue = Unlimited | MaxValue Word :) but I would merge [1] instead of increasing complexity.

[1] marinelli/http-client@set-max-headers...marinelli:http-client:use-ints

@tfausak
Copy link
Contributor

tfausak commented Dec 13, 2024

Technically the RFC allows zero headers, but practically the Host header is always present for HTTP/1.1 requests. There's also this bit: https://www.rfc-editor.org/rfc/rfc2616#section-9

The Host request-header field (section 14.23) MUST accompany all HTTP/1.1 requests.

@marinelli
Copy link
Contributor Author

marinelli commented Dec 13, 2024

Technically the RFC allows zero headers, but practically the Host header is always present for HTTP/1.1 requests. There's also this bit: https://www.rfc-editor.org/rfc/rfc2616#section-9

The Host request-header field (section 14.23) MUST accompany all HTTP/1.1 requests.

I don't know, it's a bit confusing to me to assign to 0 the meaning of unlimited value, I could think to use 0 to test server implementations. Anyway, if we will find that using 0 with that meaning might be confusing, we can just change it in an other PR.

@marinelli
Copy link
Contributor Author

marinelli commented Dec 13, 2024

Technically the RFC allows zero headers, but practically the Host header is always present for HTTP/1.1 requests. There's also this bit: https://www.rfc-editor.org/rfc/rfc2616#section-9

The Host request-header field (section 14.23) MUST accompany all HTTP/1.1 requests.

I don't know, it's a bit confusing to me to assign to 0 the meaning of unlimited value, I could think to use 0 to test server implementations. Anyway, if we will find that using 0 with that meaning might be confusing, we can just change it in an other PR.

Here it is marinelli#1. It's based on this PR, and I can merge it here. I updated this PR. Let me know what you think.

@marinelli marinelli requested a review from snoyberg December 13, 2024 17:48
Copy link
Contributor

@tfausak tfausak left a comment

Choose a reason for hiding this comment

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

Looks great! I like removing the Maybe from both fields.

case fmap unMaxHeaderLength mhl of
Nothing -> pure ()
Just n -> when (total' > n) $ throwHttp OverlongHeaders
when (total >= unMaxHeaderLength mhl && total /= 0) $ do
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to compare against the new total, not the old one. And it should be checking to see if the MaxHeaderLength is 0, not the total.

Suggested change
when (total >= unMaxHeaderLength mhl && total /= 0) $ do
let n = unMaxHeaderLength mhl
total' = total + fromIntegral (S.length bs)
when (total' >= n && n /= 0) $ do

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know why I made this change :)

Copy link
Contributor Author

@marinelli marinelli Dec 18, 2024

Choose a reason for hiding this comment

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

parseHeaders 100 _ = throwHttp OverlongHeaders
guardMaxNumberHeaders :: Word -> IO ()
guardMaxNumberHeaders count =
when (count >= unMaxNumberHeaders mnh && count /= 0) $ do
Copy link
Contributor

Choose a reason for hiding this comment

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

Same deal here:

Suggested change
when (count >= unMaxNumberHeaders mnh && count /= 0) $ do
let n = unMaxNumberHeaders mnh
in when (count >= n && n /= 0) $ do

Copy link
Contributor Author

@marinelli marinelli Dec 18, 2024

Choose a reason for hiding this comment

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

, managerMaxHeaderLength :: Maybe MaxHeaderLength
, managerMaxHeaderLength :: MaxHeaderLength
-- ^ Configure the maximum size, in bytes, of an HTTP header field.
-- Set it to 0 to remove this limit (eg: for debugging purposes).
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's necessary to use 0 as a sentinel value. maxBound :: Word32 is more than 4 GB, and maxBound :: Word64 is unfathomably large. In other words, using maxBound would effectively disable these checks anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, and this is why I just removed the n /= 0 condition from the previous predicates.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Owner

@snoyberg snoyberg left a comment

Choose a reason for hiding this comment

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

Let's avoid any backwards-incompatible changes to the library, including to the internal modules. We can implement new features using the preferred approach though.

@@ -322,9 +323,14 @@ managerSetProxy :: ProxyOverride -> ManagerSettings -> ManagerSettings
managerSetProxy po = managerSetInsecureProxy po . managerSetSecureProxy po

-- @since 0.7.17
managerSetMaxHeaderLength :: Int -> ManagerSettings -> ManagerSettings
managerSetMaxHeaderLength :: Word -> ManagerSettings -> ManagerSettings
Copy link
Owner

Choose a reason for hiding this comment

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

This is a breaking change, I do not want to include it. It's not worth the ecosystem churn for a small improvement in type correctness.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right, I actually thought about it, and I didn't change it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But... are you talking about managerSetMaxHeaderLength only? I would just revert to use Word for both managerSetMaxHeaderLength and managerSetMaxNumberHeaders, and also the newtypes used internally for distinguishing those values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I missed your main comment above. Personally I would avoid using Int in one place and Word in an other, it would be confusing to me: why one max value is an int an an other a word?
If we want to refine a bit the types of the interface, let's do that in a new version like 0.8.0, we could put there multiple breaking changes.

Copy link
Owner

Choose a reason for hiding this comment

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

I'm not in favor of a breaking change for this topic. I'll leave the choice to you: inconsistency in the API, or using Int in both places. My only stance is avoiding breaking changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the end I reverted to the first original implementation, in this way we have similar settings for managerMaxHeaderLength and managerMaxNumberHeaders (we could also rename it to managerMaxNumberHeaderFields). I prefer to have a more uniform interface, it improves readability.

--
-- @since 0.5.0
| TooManyHeaders
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The introduction of this error might be considered a breaking change, but it also simplified debugging the code.

Copy link
Owner

Choose a reason for hiding this comment

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

This is acceptable, we've documented that adding extra constructors to error types is something end users should expect.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I renamed it to TooManyHeaderFields, it's more clear.

@@ -261,6 +262,18 @@ main = do
let Just req1 = parseUrlThrow $ "http://127.0.0.1:" ++ show port
_ <- httpLbs req1 manager
return ()
it "too many header fields" $ tooManyHeaderFields $ \port -> do
Copy link
Contributor Author

@marinelli marinelli Dec 16, 2024

Choose a reason for hiding this comment

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

I just wrote those tests here, but we could move the entire DOS protection tests in the http-client package, we do not really need a data stream to raise the OverlongHeaders exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved the tests for TooManyHeaderFields in the http-client package. I tried to move also the ones for OverlongHeaders, without success. I didn't find the correct way to simulate it (something similar to what I did for TooManyHeaderFields).

@@ -324,7 +325,12 @@ managerSetProxy po = managerSetInsecureProxy po . managerSetSecureProxy po
-- @since 0.7.17
managerSetMaxHeaderLength :: Int -> ManagerSettings -> ManagerSettings
managerSetMaxHeaderLength l manager = manager
{ managerMaxHeaderLength = Just $ MaxHeaderLength l }
{ managerMaxHeaderLength = MaxHeaderLength l }
Copy link
Owner

Choose a reason for hiding this comment

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

This should also not change, yes it's an internal module, but there's no need to break end-user code for this.

--
-- @since 0.5.0
| TooManyHeaders
Copy link
Owner

Choose a reason for hiding this comment

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

This is acceptable, we've documented that adding extra constructors to error types is something end users should expect.

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.

3 participants