๐Ÿ‘ a fluffy Gleam web server
23

Configure Feed

Select the types of activity you want to include in your feed.

improve headers decoding & encoding

vshakitskiy (Mar 25, 2026, 11:36 AM +0300) 41b0d58e 25a0d3d5

+107 -43
+6
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## Unreleased 4 + 5 + - Sanitize CRLF sequences in outgoing HTTP response headers. 6 + - Eliminate `string.lowercase` in a codebase: validate and lowercase header field names in a single pass, validate and lowercase important protocol header values (like `transfer-encoding`, `connection`, `upgrade` and more) at parse time. 7 + - Include validation of trailer header field names and values during chunked body parsing. 8 + 3 9 # v3.0.5 - 15.03.2026 4 10 5 11 - Remove all usage of `string.inspect` as it is an anti-pattern for logging.
+15 -6
src/ewe/internal/encoder.gleam
··· 4 4 import gleam/list 5 5 6 6 /// Encodes an HTTP response into bytes. 7 - /// 7 + /// 8 8 pub fn encode_response( 9 9 response: response.Response(BitArray), 10 10 ) -> bytes_tree.BytesTree { ··· 15 15 } 16 16 17 17 /// Encodes the HTTP status line and headers of an HTTP response. 18 - /// 18 + /// 19 19 pub fn encode_response_partially( 20 20 response: response.Response(a), 21 21 ) -> bytes_tree.BytesTree { ··· 25 25 } 26 26 27 27 /// Encodes the HTTP status line. 28 - /// 28 + /// 29 29 fn encode_status_line(status: Int) -> BitArray { 30 30 let status_name = status_to_bit_array(status) 31 31 let status = int.to_string(status) ··· 34 34 } 35 35 36 36 /// Encodes HTTP headers into bytes. 37 - /// 37 + /// 38 38 fn encode_headers(headers: List(#(String, String))) -> BitArray { 39 39 let headers = 40 40 list.fold(headers, <<>>, fn(acc, headers) { 41 41 let #(key, value) = headers 42 42 43 - <<acc:bits, key:utf8, ": ", value:utf8, "\r\n">> 43 + << 44 + acc:bits, 45 + sanitize_header_value(key):utf8, 46 + ": ", 47 + sanitize_header_value(value):utf8, 48 + "\r\n", 49 + >> 44 50 }) 45 51 46 52 <<headers:bits, "\r\n">> 47 53 } 48 54 55 + @external(erlang, "ewe_ffi", "sanitize_header_value") 56 + fn sanitize_header_value(value: String) -> String 57 + 49 58 /// Maps HTTP status codes to their text descriptions. 50 - /// 59 + /// 51 60 fn status_to_bit_array(status: Int) -> BitArray { 52 61 case status { 53 62 100 -> <<"Continue">>
+27 -35
src/ewe/internal/http1.gleam
··· 202 202 203 203 case connection, upgrade, settings { 204 204 Ok(connection), Ok("h2c"), Ok(settings) -> { 205 - let is_upgrade = 206 - string.contains(string.lowercase(connection), "upgrade") 205 + let is_upgrade = string.contains(connection, "upgrade") 207 206 208 207 case is_upgrade { 209 208 True -> Ok(Http2Upgrade(Upgrade(req:, settings:))) ··· 248 247 Ok(Packet(HttpHeader(idx, field, value), rest)) -> { 249 248 use field <- try(case decoder.formatted_field_by_idx(idx) { 250 249 Ok(field) -> Ok(field) 251 - Error(Nil) -> { 252 - bit_array.to_string(field) 253 - |> result.map(string.lowercase) 254 - |> replace_error(InvalidHeaders) 255 - } 250 + Error(Nil) -> validate_lowercase_field(field) 256 251 }) 257 252 258 - use value <- try( 259 - validate_field_value(value) |> replace_error(InvalidHeaders), 260 - ) 253 + use value <- try(case field { 254 + "transfer-encoding" | "connection" | "upgrade" | "expect" | "trailer" -> 255 + validate_lowercase_field_value(value) 256 + _ -> validate_field_value(value) 257 + }) 261 258 262 259 let new_buffer = Buffer(rest, 0) 263 260 ··· 302 299 } 303 300 } 304 301 302 + @external(erlang, "ewe_ffi", "validate_lowercase_field") 303 + fn validate_lowercase_field(field: BitArray) -> Result(String, ParseError) 304 + 305 305 @external(erlang, "ewe_ffi", "validate_field_value") 306 - fn validate_field_value(value: BitArray) -> Result(String, Nil) 306 + fn validate_field_value(value: BitArray) -> Result(String, ParseError) 307 + 308 + @external(erlang, "ewe_ffi", "validate_lowercase_field_value") 309 + fn validate_lowercase_field_value(value: BitArray) -> Result(String, ParseError) 307 310 308 311 /// Inserts a header into the headers dictionary. 309 312 /// ··· 356 359 let transport = req.body.transport 357 360 let socket = req.body.socket 358 361 359 - let transfer_encoding = 360 - request.get_header(req, "transfer-encoding") 361 - |> result.map(string.lowercase) 362 - 363 - case transfer_encoding { 362 + case request.get_header(req, "transfer-encoding") { 364 363 Ok("chunked") -> { 365 364 use #(body, rest_buffer) <- try(read_chunked_body( 366 365 transport, ··· 379 378 trailer 380 379 |> string.split(",") 381 380 |> list.fold(set.new(), fn(set, field) { 382 - set.insert(set, string.trim(field) |> string.lowercase()) 381 + set.insert(set, string.trim(field)) 383 382 }) 384 383 385 384 Ok(handle_trailers(req, set, rest_buffer)) ··· 502 501 Ok(Packet(HttpHeader(idx, field, value), header_rest)) -> { 503 502 let field_name = case decoder.formatted_field_by_idx(idx) { 504 503 Ok(field_name) -> Ok(field_name) 505 - Error(Nil) -> { 506 - bit_array.to_string(field) 507 - |> result.map(string.lowercase) 508 - } 504 + Error(Nil) -> validate_lowercase_field(field) 509 505 } 510 506 511 507 case field_name { 512 508 Ok(field_name) -> { 513 509 case set.contains(set, field_name) && is_allowed_trailer(field_name) { 514 510 True -> { 515 - case bit_array.to_string(value) { 511 + case validate_field_value(value) { 516 512 Ok(value) -> { 517 513 request.set_header(req, field_name, value) 518 514 |> handle_trailers(set, Buffer(header_rest, 0)) 519 515 } 520 - Error(Nil) -> handle_trailers(req, set, Buffer(header_rest, 0)) 516 + Error(_) -> handle_trailers(req, set, Buffer(header_rest, 0)) 521 517 } 522 518 } 523 519 False -> handle_trailers(req, set, Buffer(header_rest, 0)) 524 520 } 525 521 } 526 - Error(Nil) -> handle_trailers(req, set, Buffer(header_rest, 0)) 522 + Error(_) -> handle_trailers(req, set, Buffer(header_rest, 0)) 527 523 } 528 524 } 529 525 _ -> req ··· 759 755 760 756 let is_upgrade = 761 757 request.get_header(req, "connection") 762 - |> result.map(fn(connection) { 763 - string.lowercase(connection) |> string.contains("upgrade") 764 - }) 758 + |> result.map(string.contains(_, "upgrade")) 765 759 766 760 use _ <- try(case is_upgrade { 767 761 Ok(True) -> Ok(Nil) ··· 769 763 Error(_) -> Error(MissingConnectionHeader) 770 764 }) 771 765 772 - use _ <- try( 773 - case request.get_header(req, "upgrade") |> result.map(string.lowercase) { 774 - Ok("websocket") -> Ok(Nil) 775 - Ok(_) -> Error(InvalidUpgradeHeader) 776 - Error(_) -> Error(MissingUpgradeHeader) 777 - }, 778 - ) 766 + use _ <- try(case request.get_header(req, "upgrade") { 767 + Ok("websocket") -> Ok(Nil) 768 + Ok(_) -> Error(InvalidUpgradeHeader) 769 + Error(_) -> Error(MissingUpgradeHeader) 770 + }) 779 771 780 772 use <- bool.guard( 781 773 request.get_header(req, "sec-websocket-version") == Error(Nil), ··· 881 873 let expect = 882 874 req.headers 883 875 |> list.find(fn(tupple) { 884 - tupple.0 == "expect" && string.lowercase(tupple.1) == "100-continue" 876 + tupple.0 == "expect" && tupple.1 == "100-continue" 885 877 }) 886 878 887 879 case expect {
+59 -2
src/ewe_ffi.erl
··· 1 1 -module(ewe_ffi). 2 2 3 3 -export([close_file/1, decode_packet/3, init_clock_storage/0, lookup_http_date/0, now/0, 4 - now_microseconds/0, open_file/1, set_http_date/1, validate_field_value/1, 4 + now_microseconds/0, open_file/1, set_http_date/1, validate_lowercase_field/1, 5 + validate_field_value/1, validate_lowercase_field_value/1, sanitize_header_value/1, 5 6 coerce_tcp_message/1, parse_path/1]). 6 7 7 8 % Socket ··· 48 49 {ok, {maps:get(path, Uri), Query}} 49 50 end. 50 51 52 + validate_lowercase_field(<<>>) -> 53 + {error, invalid_headers}; 54 + validate_lowercase_field(Value) -> 55 + validate_lowercase_field(Value, <<>>). 56 + 57 + validate_lowercase_field(<<>>, Acc) -> 58 + {ok, Acc}; 59 + validate_lowercase_field(<<C, Rest/bits>>, Acc) when C >= $A, C =< $Z -> 60 + validate_lowercase_field(Rest, <<Acc/binary, (C + 32)>>); 61 + validate_lowercase_field(<<C, Rest/bits>>, Acc) 62 + when C >= $a, C =< $z; 63 + C >= $0, C =< $9; 64 + C =:= $!; 65 + C =:= $#; 66 + C =:= $$; 67 + C =:= $%; 68 + C =:= $&; 69 + C =:= $'; 70 + C =:= $*; 71 + C =:= $+; 72 + C =:= $-; 73 + C =:= $.; 74 + C =:= $^; 75 + C =:= $_; 76 + C =:= $`; 77 + C =:= $|; 78 + C =:= $~ -> 79 + validate_lowercase_field(Rest, <<Acc/binary, C>>); 80 + validate_lowercase_field(_, _) -> 81 + {error, invalid_headers}. 82 + 51 83 validate_field_value(Value) -> 52 84 case do_validate_field_value(Value) of 53 85 true -> 54 86 {ok, Value}; 55 87 false -> 56 - {error, nil} 88 + {error, invalid_headers} 57 89 end. 58 90 59 91 % HTTP field values can contain: ··· 73 105 _ -> 74 106 false 75 107 end. 108 + 109 + validate_lowercase_field_value(Value) -> 110 + do_validate_lowercase_field_value(Value, <<>>). 111 + 112 + do_validate_lowercase_field_value(<<>>, Acc) -> 113 + {ok, Acc}; 114 + do_validate_lowercase_field_value(<<C, Rest/bits>>, Acc) when C >= $A, C =< $Z -> 115 + do_validate_lowercase_field_value(Rest, <<Acc/binary, (C + 32)>>); 116 + do_validate_lowercase_field_value(<<C, Rest/bits>>, Acc) 117 + when C =:= 16#09 118 + orelse C >= 16#20 andalso C =< 16#7E 119 + orelse C >= 16#80 andalso C =< 16#FF -> 120 + do_validate_lowercase_field_value(Rest, <<Acc/binary, C>>); 121 + do_validate_lowercase_field_value(_, _) -> 122 + {error, invalid_headers}. 123 + 124 + sanitize_header_value(Value) -> 125 + sanitize_header_value(Value, <<>>). 126 + 127 + sanitize_header_value(<<>>, Acc) -> 128 + Acc; 129 + sanitize_header_value(<<C, Rest/bitstring>>, Acc) when C =:= 16#0D; C =:= 16#0A -> 130 + sanitize_header_value(Rest, Acc); 131 + sanitize_header_value(<<C, Rest/bitstring>>, Acc) -> 132 + sanitize_header_value(Rest, <<Acc/binary, C>>). 76 133 77 134 % CLOCK 78 135 % -----------------------------------------------------------------------------