|
| 1 | +From f3e7e0568b3c4cf9fa4bea79d5116e67ce76ad25 Mon Sep 17 00:00:00 2001 |
| 2 | +From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= <essen@ninenines.eu> |
| 3 | +Date: Thu, 4 Jun 2026 14:47:13 +0200 |
| 4 | +Subject: [PATCH] Add max_{header|trailer}_block_size HTTP/1.1 options |
| 5 | + |
| 6 | +Upstream Patch reference: https://github.com/ninenines/gun/commit/f3e7e0568b3c4cf9fa4bea79d5116e67ce76ad25.patch |
| 7 | +--- |
| 8 | + deps/gun/src/gun.erl | 2 ++ |
| 9 | + deps/gun/src/gun_http.erl | 40 +++++++++++++++++++++++++++++++++------ |
| 10 | + 2 files changed, 36 insertions(+), 6 deletions(-) |
| 11 | + |
| 12 | +diff --git a/deps/gun/src/gun.erl b/deps/gun/src/gun.erl |
| 13 | +index 1659ee3..b2e2196 100644 |
| 14 | +--- a/deps/gun/src/gun.erl |
| 15 | ++++ b/deps/gun/src/gun.erl |
| 16 | +@@ -144,6 +144,8 @@ |
| 17 | + |
| 18 | + -type http_opts() :: #{ |
| 19 | + keepalive => timeout(), |
| 20 | ++ max_header_block_size => non_neg_integer(), |
| 21 | ++ max_trailer_block_size => non_neg_integer(), |
| 22 | + transform_header_name => fun((binary()) -> binary()), |
| 23 | + version => 'HTTP/1.1' | 'HTTP/1.0' |
| 24 | + }. |
| 25 | +diff --git a/deps/gun/src/gun_http.erl b/deps/gun/src/gun_http.erl |
| 26 | +index abd4fc5..51f6f46 100644 |
| 27 | +--- a/deps/gun/src/gun_http.erl |
| 28 | ++++ b/deps/gun/src/gun_http.erl |
| 29 | +@@ -56,7 +56,9 @@ |
| 30 | + in = head :: io(), |
| 31 | + in_state = {0, 0} :: {non_neg_integer(), non_neg_integer()}, |
| 32 | + out = head :: io(), |
| 33 | +- transform_header_name :: fun((binary()) -> binary()) |
| 34 | ++ transform_header_name :: fun((binary()) -> binary()), |
| 35 | ++ max_header_block_size = 100000 :: non_neg_integer(), |
| 36 | ++ max_trailer_block_size = 10000 :: non_neg_integer() |
| 37 | + }). |
| 38 | + |
| 39 | + check_options(Opts) -> |
| 40 | +@@ -73,6 +75,10 @@ do_check_options([{keepalive, infinity}|Opts]) -> |
| 41 | + do_check_options(Opts); |
| 42 | + do_check_options([{keepalive, K}|Opts]) when is_integer(K), K > 0 -> |
| 43 | + do_check_options(Opts); |
| 44 | ++do_check_options([{max_header_block_size, M}|Opts]) when is_integer(M), M > 0 -> |
| 45 | ++ do_check_options(Opts); |
| 46 | ++do_check_options([{max_trailer_block_size, M}|Opts]) when is_integer(M), M > 0 -> |
| 47 | ++ do_check_options(Opts); |
| 48 | + do_check_options([{transform_header_name, F}|Opts]) when is_function(F) -> |
| 49 | + do_check_options(Opts); |
| 50 | + do_check_options([{version, V}|Opts]) when V =:= 'HTTP/1.1'; V =:= 'HTTP/1.0' -> |
| 51 | +@@ -86,8 +92,11 @@ init(Owner, Socket, Transport, Opts) -> |
| 52 | + Version = maps:get(version, Opts, 'HTTP/1.1'), |
| 53 | + Handlers = maps:get(content_handlers, Opts, [gun_data_h]), |
| 54 | + TransformHeaderName = maps:get(transform_header_name, Opts, fun (N) -> N end), |
| 55 | ++ MaxHeaderBlockSize = maps:get(max_header_block_size, Opts, 100000), |
| 56 | ++ MaxTrailerBlockSize = maps:get(max_trailer_block_size, Opts, 10000), |
| 57 | + #http_state{owner=Owner, socket=Socket, transport=Transport, version=Version, |
| 58 | +- content_handlers=Handlers, transform_header_name=TransformHeaderName}. |
| 59 | ++ content_handlers=Handlers, transform_header_name=TransformHeaderName, |
| 60 | ++ max_header_block_size=MaxHeaderBlockSize, max_trailer_block_size=MaxTrailerBlockSize}. |
| 61 | + |
| 62 | + %% Stop looping when we got no more data. |
| 63 | + handle(<<>>, State) -> |
| 64 | +@@ -96,10 +105,20 @@ handle(<<>>, State) -> |
| 65 | + handle(_, #http_state{streams=[]}) -> |
| 66 | + close; |
| 67 | + %% Wait for the full response headers before trying to parse them. |
| 68 | +-handle(Data, State=#http_state{in=head, buffer=Buffer}) -> |
| 69 | ++handle(Data, State=#http_state{in=head, buffer=Buffer, max_header_block_size=MaxHeaderBlockSize, |
| 70 | ++ streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}) -> |
| 71 | + Data2 = << Buffer/binary, Data/binary >>, |
| 72 | + case binary:match(Data2, <<"\r\n\r\n">>) of |
| 73 | +- nomatch -> {state, State#http_state{buffer=Data2}}; |
| 74 | ++ nomatch -> |
| 75 | ++ case byte_size(Data2) > MaxHeaderBlockSize of |
| 76 | ++ true -> |
| 77 | ++ Reason = {connection_error, limit_reached, |
| 78 | ++ "The response header block is too large."}, |
| 79 | ++ gun:reply(ReplyTo, {gun_error, self(), Reason}), |
| 80 | ++ {error, Reason}; |
| 81 | ++ false -> |
| 82 | ++ {state, State#http_state{buffer=Data2}} |
| 83 | ++ end; |
| 84 | + {_, _} -> handle_head(Data2, State#http_state{buffer= <<>>}) |
| 85 | + end; |
| 86 | + %% Everything sent to the socket until it closes is part of the response body. |
| 87 | +@@ -156,11 +175,20 @@ handle(Data, State=#http_state{in=body_chunked, in_state=InState, |
| 88 | + close |
| 89 | + end |
| 90 | + end; |
| 91 | +-handle(Data, State=#http_state{in=body_trailer, buffer=Buffer, connection=Conn, |
| 92 | ++handle(Data, State=#http_state{in=body_trailer, buffer=Buffer, connection=Conn, max_trailer_block_size=MaxTrailerBlockSize, |
| 93 | + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}) -> |
| 94 | + Data2 = << Buffer/binary, Data/binary >>, |
| 95 | + case binary:match(Data2, <<"\r\n\r\n">>) of |
| 96 | +- nomatch -> {state, State#http_state{buffer=Data2}}; |
| 97 | ++ nomatch -> |
| 98 | ++ case byte_size(Data2) > MaxTrailerBlockSize of |
| 99 | ++ true -> |
| 100 | ++ Reason = {connection_error, limit_reached, |
| 101 | ++ "The response trailer block is too large."}, |
| 102 | ++ gun:reply(ReplyTo, {gun_error, self(), Reason}), |
| 103 | ++ {error, Reason}; |
| 104 | ++ false -> |
| 105 | ++ {state, State#http_state{buffer=Data2}} |
| 106 | ++ end; |
| 107 | + {_, _} -> |
| 108 | + {Trailers, Rest} = cow_http:parse_headers(Data2), |
| 109 | + %% @todo We probably want to pass this to gun_content_handler? |
| 110 | +-- |
| 111 | +2.43.0 |
| 112 | + |
0 commit comments