Skip to content

Commit b96398f

Browse files
committed
Initial allow by default trailers implementation.
1 parent 36e1345 commit b96398f

File tree

18 files changed

+661
-6
lines changed

18 files changed

+661
-6
lines changed

lib/protocol/http/error.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,17 @@ 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
2941
end
3042
end

lib/protocol/http/header/accept.rb

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

95+
def self.trailer_forbidden?
96+
false
97+
end
98+
9599
# Parse the `accept` header.
96100
#
97101
# @returns [Array(Charset)] the list of content types and their associated parameters.

lib/protocol/http/header/authorization.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ def self.basic(username, password)
3434
"Basic #{strict_base64_encoded}"
3535
)
3636
end
37+
38+
def self.trailer_forbidden?
39+
true
40+
end
3741
end
3842
end
3943
end

lib/protocol/http/header/connection.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ def close?
5050
def upgrade?
5151
self.include?(UPGRADE)
5252
end
53+
54+
def self.trailer_forbidden?
55+
true
56+
end
5357
end
5458
end
5559
end

lib/protocol/http/header/cookie.rb

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

2424
cookies.map{|cookie| [cookie.name, cookie]}.to_h
2525
end
26+
27+
def self.trailer_forbidden?
28+
true
29+
end
2630
end
2731

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

lib/protocol/http/header/date.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def << value
2525
def to_time
2626
::Time.parse(self)
2727
end
28+
29+
def self.trailer_forbidden?
30+
false
31+
end
2832
end
2933
end
3034
end

lib/protocol/http/header/etag.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def << value
2525
def weak?
2626
self.start_with?("W/")
2727
end
28+
29+
def self.trailer_forbidden?
30+
false
31+
end
2832
end
2933
end
3034
end

lib/protocol/http/header/multiple.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def initialize(value)
2525
def to_s
2626
join("\n")
2727
end
28+
29+
def self.trailer_forbidden?
30+
false
31+
end
2832
end
2933
end
3034
end

lib/protocol/http/header/split.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ def to_s
4040
join(",")
4141
end
4242

43+
def self.trailer_forbidden?
44+
false
45+
end
46+
4347
protected
4448

4549
def reverse_find(&block)

lib/protocol/http/header/te.rb

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 `te` header indicates the transfer encodings the client is willing to accept.
14+
#
15+
# The `te` header allows a client to indicate which transfer encodings it can handle, and in what order of preference using quality factors.
16+
class TE < Split
17+
ParseError = Class.new(Error)
18+
19+
# Transfer encoding token pattern
20+
TOKEN = /[!#$%&'*+\-.0-9A-Z^_`a-z|~]+/
21+
22+
# Quality value pattern (0.0 to 1.0)
23+
QVALUE = /0(\.[0-9]{0,3})?|1(\.[0]{0,3})?/
24+
25+
# Pattern for parsing transfer encoding with optional quality factor
26+
TRANSFER_CODING = /\A(?<name>#{TOKEN})(\s*;\s*q=(?<q>#{QVALUE}))?\z/
27+
28+
# The `chunked` transfer encoding
29+
CHUNKED = "chunked"
30+
31+
# The `gzip` transfer encoding
32+
GZIP = "gzip"
33+
34+
# The `deflate` transfer encoding
35+
DEFLATE = "deflate"
36+
37+
# The `compress` transfer encoding
38+
COMPRESS = "compress"
39+
40+
# The `identity` transfer encoding
41+
IDENTITY = "identity"
42+
43+
# The `trailers` pseudo-encoding indicates willingness to accept trailer fields
44+
TRAILERS = "trailers"
45+
46+
# A single transfer coding entry with optional quality factor
47+
TransferCoding = Struct.new(:name, :q) do
48+
def quality_factor
49+
(q || 1.0).to_f
50+
end
51+
52+
def <=> other
53+
other.quality_factor <=> self.quality_factor
54+
end
55+
56+
def to_s
57+
if q && q != 1.0
58+
"#{name};q=#{q}"
59+
else
60+
name.to_s
61+
end
62+
end
63+
end
64+
65+
# Initializes the TE header with the given value. The value is split into distinct entries and converted to lowercase for normalization.
66+
#
67+
# @parameter value [String | Nil] the raw header value containing transfer encodings separated by commas.
68+
def initialize(value = nil)
69+
super(value&.downcase)
70+
end
71+
72+
# Adds one or more comma-separated values to the TE header. The values are converted to lowercase for normalization.
73+
#
74+
# @parameter value [String] the value or values to add, separated by commas.
75+
def << value
76+
super(value.downcase)
77+
end
78+
79+
# Parse the `te` header value into a list of transfer codings with quality factors.
80+
#
81+
# @returns [Array(TransferCoding)] the list of transfer codings and their associated quality factors.
82+
def transfer_codings
83+
self.map do |value|
84+
if match = value.match(TRANSFER_CODING)
85+
TransferCoding.new(match[:name], match[:q])
86+
else
87+
raise ParseError.new("Could not parse transfer coding: #{value.inspect}")
88+
end
89+
end
90+
end
91+
92+
# @returns [Boolean] whether the `chunked` encoding is accepted.
93+
def chunked?
94+
self.any? {|value| value.start_with?(CHUNKED)}
95+
end
96+
97+
# @returns [Boolean] whether the `gzip` encoding is accepted.
98+
def gzip?
99+
self.any? {|value| value.start_with?(GZIP)}
100+
end
101+
102+
# @returns [Boolean] whether the `deflate` encoding is accepted.
103+
def deflate?
104+
self.any? {|value| value.start_with?(DEFLATE)}
105+
end
106+
107+
# @returns [Boolean] whether the `compress` encoding is accepted.
108+
def compress?
109+
self.any? {|value| value.start_with?(COMPRESS)}
110+
end
111+
112+
# @returns [Boolean] whether the `identity` encoding is accepted.
113+
def identity?
114+
self.any? {|value| value.start_with?(IDENTITY)}
115+
end
116+
117+
# @returns [Boolean] whether trailers are accepted.
118+
def trailers?
119+
self.any? {|value| value.start_with?(TRAILERS)}
120+
end
121+
122+
def self.trailer_forbidden?
123+
true
124+
end
125+
end
126+
end
127+
end
128+
end

0 commit comments

Comments
 (0)