Skip to content

Commit 5fd9279

Browse files
samuel-williams-shopifyioquatix
authored andcommitted
Deny trailers by default.
1 parent b96398f commit 5fd9279

23 files changed

+847
-80
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Released under the MIT License.
5+
# Copyright, 2025, by Samuel Williams.
6+
7+
require_relative "../lib/protocol/http/headers"
8+
require "digest"
9+
10+
# Example: Using various headers suitable for trailers
11+
puts "HTTP Trailers - Suitable Headers Example"
12+
puts "=" * 50
13+
14+
# Create a new headers collection
15+
headers = Protocol::HTTP::Headers.new
16+
17+
# Add regular response headers
18+
headers.add("content-type", "application/json")
19+
headers.add("content-length", "2048")
20+
21+
# Enable trailers for headers that are calculated during response generation
22+
headers.trailer!
23+
24+
puts "Regular Headers:"
25+
headers.each do |key, value|
26+
next if headers.trailer? && headers.trailer.any? {|tk, _| tk == key}
27+
puts " #{key}: #{value}"
28+
end
29+
30+
puts "\nSimulating response generation and trailer calculation..."
31+
32+
# 1. Server-Timing - Performance metrics calculated during processing
33+
puts "\n1. Server-Timing Header:"
34+
server_timing = Protocol::HTTP::Header::ServerTiming.new
35+
server_timing << "db;dur=45.2;desc=\"Database query\""
36+
server_timing << "cache;dur=12.8;desc=\"Redis lookup\""
37+
server_timing << "render;dur=23.5;desc=\"JSON serialization\""
38+
39+
headers.add("server-timing", server_timing.to_s)
40+
puts " Added: #{server_timing}"
41+
42+
# 2. Digest - Content integrity calculated after body generation
43+
puts "\n2. Digest Header:"
44+
response_body = '{"message": "Hello, World!", "timestamp": "2025-09-19T06:18:21Z"}'
45+
sha256_digest = Digest::SHA256.base64digest(response_body)
46+
md5_digest = Digest::MD5.hexdigest(response_body)
47+
48+
digest = Protocol::HTTP::Header::Digest.new
49+
digest << "sha-256=#{sha256_digest}"
50+
digest << "md5=#{md5_digest}"
51+
52+
headers.add("digest", digest.to_s)
53+
puts " Added: #{digest}"
54+
puts " Response body: #{response_body}"
55+
56+
# 3. Custom Application Header - Application-specific metadata
57+
puts "\n3. Custom Application Header:"
58+
headers.add("x-processing-stats", "requests=1250, cache_hits=892, errors=0")
59+
puts " Added: x-processing-stats=requests=1250, cache_hits=892, errors=0"
60+
61+
# 4. Date - Response completion timestamp
62+
puts "\n4. Date Header:"
63+
completion_time = Time.now
64+
headers.add("date", completion_time.httpdate)
65+
puts " Added: #{completion_time.httpdate}"
66+
67+
# 5. ETag - Content-based entity tag (when calculated from response)
68+
puts "\n5. ETag Header:"
69+
etag_value = "\"#{Digest::SHA1.hexdigest(response_body)[0..15]}\""
70+
headers.add("etag", etag_value)
71+
puts " Added: #{etag_value}"
72+
73+
puts "\nFinal Trailer Headers (sent after response body):"
74+
puts "-" * 50
75+
headers.trailer do |key, value|
76+
puts " #{key}: #{value}"
77+
end
78+
79+
puts "\nWhy These Headers Are Perfect for Trailers:"
80+
puts "-" * 45
81+
puts "• Server-Timing: Performance metrics collected during processing"
82+
puts "• Digest: Content hashes calculated after body generation"
83+
puts "• Custom Headers: Application-specific metadata generated during response"
84+
puts "• Date: Completion timestamp when response finishes"
85+
puts "• ETag: Content-based tags when derived from response body"
86+
87+
puts "\nBenefits of Using Trailers:"
88+
puts "• No need to buffer entire response to calculate metadata"
89+
puts "• Streaming-friendly - can start sending body immediately"
90+
puts "• Perfect for large responses where metadata depends on content"
91+
puts "• Maintains HTTP semantics while enabling efficient processing"
92+
93+
# Demonstrate header integration and parsing
94+
puts "\nHeader Integration Examples:"
95+
puts "-" * 30
96+
97+
# Show that these headers work normally in the main header section too
98+
normal_headers = Protocol::HTTP::Headers.new
99+
normal_headers.add("server-timing", "total;dur=150.5")
100+
normal_headers.add("digest", "sha-256=abc123")
101+
normal_headers.add("x-cache-status", "hit")
102+
103+
puts "Normal headers (not trailers):"
104+
normal_headers.each do |key, value|
105+
puts " #{key}: #{value}"
106+
end
107+
108+
puts "\nParsing capabilities:"
109+
parsed_digest = Protocol::HTTP::Header::Digest.new("sha-256=#{sha256_digest}, md5=#{md5_digest}")
110+
entries = parsed_digest.entries
111+
puts "• Parsed digest entries: #{entries.size}"
112+
puts "• First algorithm: #{entries.first.algorithm}"
113+
puts "• Algorithms: #{entries.map(&:algorithm).join(', ')}"
114+
115+
parsed_timing = Protocol::HTTP::Header::ServerTiming.new("db;dur=25.4, cache;dur=8.2;desc=\"Redis hit\"")
116+
timing_metrics = parsed_timing.metrics
117+
puts "• Parsed timing metrics: #{timing_metrics.size}"
118+
puts "• First metric: #{timing_metrics.first.name} (#{timing_metrics.first.duration}ms)"

lib/protocol/http/error.rb

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,5 @@ def initialize(key)
2626
# @attribute [String] key The header key that was duplicated.
2727
attr :key
2828
end
29-
30-
class ForbiddenTrailerError < Error
31-
include BadRequest
32-
33-
# @parameter key [String] The header key that was forbidden in trailers.
34-
def initialize(key)
35-
super("#{key} is forbidden in trailers!")
36-
end
37-
38-
# @attribute [String] key The header key that was forbidden in trailers.
39-
attr :key
40-
end
4129
end
4230
end

lib/protocol/http/header/accept.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def to_s
9292
join(",")
9393
end
9494

95-
def self.trailer_forbidden?
95+
def self.trailer?
9696
false
9797
end
9898

lib/protocol/http/header/authorization.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ def self.basic(username, password)
3535
)
3636
end
3737

38-
def self.trailer_forbidden?
39-
true
38+
def self.trailer?
39+
false
4040
end
4141
end
4242
end

lib/protocol/http/header/connection.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ def upgrade?
5151
self.include?(UPGRADE)
5252
end
5353

54-
def self.trailer_forbidden?
55-
true
54+
def self.trailer?
55+
false
5656
end
5757
end
5858
end

lib/protocol/http/header/cookie.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def to_h
2424
cookies.map{|cookie| [cookie.name, cookie]}.to_h
2525
end
2626

27-
def self.trailer_forbidden?
28-
true
27+
def self.trailer?
28+
false
2929
end
3030
end
3131

lib/protocol/http/header/date.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def to_time
2626
::Time.parse(self)
2727
end
2828

29-
def self.trailer_forbidden?
30-
false
29+
def self.trailer?
30+
true
3131
end
3232
end
3333
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+
# Digest headers are perfect for use as trailers since they contain
63+
# 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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def weak?
2626
self.start_with?("W/")
2727
end
2828

29-
def self.trailer_forbidden?
30-
false
29+
def self.trailer?
30+
true
3131
end
3232
end
3333
end

lib/protocol/http/header/multiple.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def to_s
2626
join("\n")
2727
end
2828

29-
def self.trailer_forbidden?
29+
def self.trailer?
3030
false
3131
end
3232
end

0 commit comments

Comments
 (0)