Skip to content

Commit a4751bf

Browse files
authored
Trailers are ignored by default. (#91)
- Also introduce rich support for `Header::Digest`, `Header::ServerTiming`, `Header::TE`, `Header::Trailer` and `Header::TransferEncoding`.
1 parent 36e1345 commit a4751bf

File tree

24 files changed

+1502
-11
lines changed

24 files changed

+1502
-11
lines changed

guides/headers/readme.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Headers
2+
3+
This guide explains how to work with HTTP headers using `protocol-http`.
4+
5+
## Core Concepts
6+
7+
`protocol-http` provides several core concepts for working with HTTP headers:
8+
9+
- A {ruby Protocol::HTTP::Headers} class which represents a collection of HTTP headers with built-in security and policy features.
10+
- Header-specific classes like {ruby Protocol::HTTP::Header::Accept} and {ruby Protocol::HTTP::Header::Authorization} which provide specialized parsing and formatting.
11+
- Trailer security validation to prevent HTTP request smuggling attacks.
12+
13+
## Usage
14+
15+
The {Protocol::HTTP::Headers} class provides a comprehensive interface for creating and manipulating HTTP headers:
16+
17+
```ruby
18+
require 'protocol/http'
19+
20+
headers = Protocol::HTTP::Headers.new
21+
headers.add('content-type', 'text/html')
22+
headers.add('set-cookie', 'session=abc123')
23+
24+
# Access headers
25+
content_type = headers['content-type'] # => "text/html"
26+
27+
# Check if header exists
28+
headers.include?('content-type') # => true
29+
```
30+
31+
### Header Policies
32+
33+
Different header types have different behaviors for merging, validation, and trailer handling:
34+
35+
```ruby
36+
# Some headers can be specified multiple times
37+
headers.add('set-cookie', 'first=value1')
38+
headers.add('set-cookie', 'second=value2')
39+
40+
# Others are singletons and will raise errors if duplicated
41+
headers.add('content-length', '100')
42+
# headers.add('content-length', '200') # Would raise DuplicateHeaderError
43+
```
44+
45+
### Structured Headers
46+
47+
Some headers have specialized classes for parsing and formatting:
48+
49+
```ruby
50+
# Accept header with media ranges
51+
accept = Protocol::HTTP::Header::Accept.new('text/html,application/json;q=0.9')
52+
media_ranges = accept.media_ranges
53+
54+
# Authorization header
55+
auth = Protocol::HTTP::Header::Authorization.basic('username', 'password')
56+
# => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
57+
```
58+
59+
### Trailer Security
60+
61+
HTTP trailers are headers that appear after the message body. For security reasons, only certain headers are allowed in trailers:
62+
63+
```ruby
64+
# Working with trailers
65+
headers = Protocol::HTTP::Headers.new([
66+
['content-type', 'text/html'],
67+
['content-length', '1000']
68+
])
69+
70+
# Start trailer section
71+
headers.trailer!
72+
73+
# These will be allowed (safe metadata)
74+
headers.add('etag', '"12345"')
75+
headers.add('date', Time.now.httpdate)
76+
77+
# These will be silently ignored for security
78+
headers.add('authorization', 'Bearer token') # Ignored - credential leakage risk
79+
headers.add('connection', 'close') # Ignored - hop-by-hop header
80+
```
81+
82+
The trailer security system prevents HTTP request smuggling by restricting which headers can appear in trailers:
83+
84+
**Allowed headers** (return `true` for `trailer?`):
85+
- `date` - Response generation timestamps.
86+
- `digest` - Content integrity verification.
87+
- `etag` - Cache validation tags.
88+
- `server-timing` - Performance metrics.
89+
90+
**Forbidden headers** (return `false` for `trailer?`):
91+
- `authorization` - Prevents credential leakage.
92+
- `connection`, `te`, `transfer-encoding` - Hop-by-hop headers that control connection behavior.
93+
- `cookie`, `set-cookie` - State information needed during initial processing.
94+
- `accept` - Content negotiation must occur before response generation.

guides/links.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ getting-started:
22
order: 1
33
message-body:
44
order: 2
5-
middleware:
5+
headers:
66
order: 3
7-
hypertext-references:
7+
middleware:
88
order: 4
9-
url-parsing:
9+
hypertext-references:
1010
order: 5
11-
streaming:
11+
url-parsing:
1212
order: 6
13+
streaming:
14+
order: 7
1315
design-overview:
1416
order: 10

lib/protocol/http/header/accept.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ def to_s
9292
join(",")
9393
end
9494

95+
# Whether this header is acceptable in HTTP trailers.
96+
# @returns [Boolean] `false`, as Accept headers are used for response content negotiation.
97+
def self.trailer?
98+
false
99+
end
100+
95101
# Parse the `accept` header.
96102
#
97103
# @returns [Array(Charset)] the list of content types and their associated parameters.

lib/protocol/http/header/authorization.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ def self.basic(username, password)
3434
"Basic #{strict_base64_encoded}"
3535
)
3636
end
37+
38+
# Whether this header is acceptable in HTTP trailers.
39+
# @returns [Boolean] `false`, as authorization headers are used for request authentication.
40+
def self.trailer?
41+
false
42+
end
3743
end
3844
end
3945
end

lib/protocol/http/header/connection.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ def close?
5050
def upgrade?
5151
self.include?(UPGRADE)
5252
end
53+
54+
# Whether this header is acceptable in HTTP trailers.
55+
# Connection headers control the current connection and must not appear in trailers.
56+
# @returns [Boolean] `false`, as connection headers are hop-by-hop and forbidden in trailers.
57+
def self.trailer?
58+
false
59+
end
5360
end
5461
end
5562
end

lib/protocol/http/header/cookie.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ def to_h
2323

2424
cookies.map{|cookie| [cookie.name, cookie]}.to_h
2525
end
26+
27+
# Whether this header is acceptable in HTTP trailers.
28+
# Cookie headers should not appear in trailers as they contain state information needed early in processing.
29+
# @returns [Boolean] `false`, as cookie headers are needed during initial request processing.
30+
def self.trailer?
31+
false
32+
end
2633
end
2734

2835
# The `set-cookie` header sends cookies from the server to the user agent.

lib/protocol/http/header/date.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ def << value
2525
def to_time
2626
::Time.parse(self)
2727
end
28+
29+
# Whether this header is acceptable in HTTP trailers.
30+
# Date headers can safely appear in trailers as they provide metadata about response generation.
31+
# @returns [Boolean] `true`, as date headers are metadata that can be computed after response generation.
32+
def self.trailer?
33+
true
34+
end
2835
end
2936
end
3037
end

lib/protocol/http/header/digest.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require_relative "split"
7+
require_relative "quoted_string"
8+
require_relative "../error"
9+
10+
module Protocol
11+
module HTTP
12+
module Header
13+
# The `digest` header provides a digest of the message body for integrity verification.
14+
#
15+
# This header allows servers to send cryptographic hashes of the response body, enabling clients to verify data integrity. Multiple digest algorithms can be specified, and the header is particularly useful as a trailer since the digest can only be computed after the entire message body is available.
16+
#
17+
# ## Examples
18+
#
19+
# ```ruby
20+
# digest = Digest.new("sha-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=")
21+
# digest << "md5=9bb58f26192e4ba00f01e2e7b136bbd8"
22+
# puts digest.to_s
23+
# # => "sha-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=, md5=9bb58f26192e4ba00f01e2e7b136bbd8"
24+
# ```
25+
class Digest < Split
26+
ParseError = Class.new(Error)
27+
28+
# https://tools.ietf.org/html/rfc3230#section-4.3.2
29+
ENTRY = /\A(?<algorithm>[a-zA-Z0-9][a-zA-Z0-9\-]*)\s*=\s*(?<value>.*)\z/
30+
31+
# A single digest entry in the Digest header.
32+
Entry = Struct.new(:algorithm, :value) do
33+
# Create a new digest entry.
34+
#
35+
# @parameter algorithm [String] the digest algorithm (e.g., "sha-256", "md5").
36+
# @parameter value [String] the base64-encoded or hex-encoded digest value.
37+
def initialize(algorithm, value)
38+
super(algorithm.downcase, value)
39+
end
40+
41+
# Convert the entry to its string representation.
42+
#
43+
# @returns [String] the formatted digest string.
44+
def to_s
45+
"#{algorithm}=#{value}"
46+
end
47+
end
48+
49+
# Parse the `digest` header value into a list of digest entries.
50+
#
51+
# @returns [Array(Entry)] the list of digest entries with their algorithms and values.
52+
def entries
53+
self.map do |value|
54+
if match = value.match(ENTRY)
55+
Entry.new(match[:algorithm], match[:value])
56+
else
57+
raise ParseError.new("Could not parse digest value: #{value.inspect}")
58+
end
59+
end
60+
end
61+
62+
# Whether this header is acceptable in HTTP trailers.
63+
# @returns [Boolean] `true`, as digest headers contain integrity hashes that can only be calculated after the entire message body is available.
64+
def self.trailer?
65+
true
66+
end
67+
end
68+
end
69+
end
70+
end

lib/protocol/http/header/etag.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ def << value
2525
def weak?
2626
self.start_with?("W/")
2727
end
28+
29+
# Whether this header is acceptable in HTTP trailers.
30+
# ETag headers can safely appear in trailers as they provide cache validation metadata.
31+
# @returns [Boolean] `true`, as ETag headers are metadata that can be computed after response generation.
32+
def self.trailer?
33+
true
34+
end
2835
end
2936
end
3037
end

lib/protocol/http/header/multiple.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ def initialize(value)
2525
def to_s
2626
join("\n")
2727
end
28+
29+
# Whether this header is acceptable in HTTP trailers.
30+
# This is a base class for headers with multiple values, default is to disallow in trailers.
31+
# @returns [Boolean] `false`, as most multiple-value headers should not appear in trailers by default.
32+
def self.trailer?
33+
false
34+
end
2835
end
2936
end
3037
end

0 commit comments

Comments
 (0)