Skip to content

Commit 8dee49e

Browse files
Fix header parsing, allowing for \t characters between VCHAR. (#44)
* Ensure regex are linear time.
1 parent b01fea1 commit 8dee49e

File tree

3 files changed

+152
-3
lines changed

3 files changed

+152
-3
lines changed

lib/protocol/http1/connection.rb

+6-3
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@ module HTTP1
3737

3838
# HTTP/1.x header parser:
3939
FIELD_NAME = TOKEN
40-
FIELD_VALUE = /[^\000-\037]*/.freeze
41-
HEADER = /\A(#{FIELD_NAME}):\s*(#{FIELD_VALUE})\s*\z/.freeze
40+
WS = /[ \t]/ # Whitespace.
41+
OWS = /#{WS}*/ # Optional whitespace.
42+
VCHAR = /[!-~]/ # Match visible characters from ASCII 33 to 126.
43+
FIELD_VALUE = /#{VCHAR}+(?:#{WS}+#{VCHAR}+)*/.freeze
44+
HEADER = /\A(#{FIELD_NAME}):#{OWS}(?:(#{FIELD_VALUE})#{OWS})?\z/.freeze
4245

4346
VALID_FIELD_NAME = /\A#{FIELD_NAME}\z/.freeze
4447
VALID_FIELD_VALUE = /\A#{FIELD_VALUE}\z/.freeze
@@ -484,7 +487,7 @@ def read_headers
484487
break if line.empty?
485488

486489
if match = line.match(HEADER)
487-
fields << [match[1], match[2]]
490+
fields << [match[1], match[2] || ""]
488491
else
489492
raise BadHeader, "Could not parse header: #{line.inspect}"
490493
end
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "protocol/http1/connection"
7+
require "connection_context"
8+
9+
describe Protocol::HTTP1::Connection do
10+
include_context ConnectionContext
11+
12+
let(:headers) {Array.new}
13+
14+
before do
15+
client.stream.write "GET / HTTP/1.1\r\nHost: localhost\r\n#{headers.join("\r\n")}\r\n\r\n"
16+
client.stream.close
17+
end
18+
19+
with "header that contains tab characters" do
20+
let(:headers) {[
21+
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) \t\t\tChrome/55.0.2883.95 Safari/537.36"
22+
]}
23+
24+
it "can parse the header" do
25+
authority, method, target, version, headers, body = server.read_request
26+
27+
expect(headers).to have_keys(
28+
"user-agent" => be == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) \t\t\tChrome/55.0.2883.95 Safari/537.36"
29+
)
30+
end
31+
end
32+
33+
with "header that contains obsolete folding whitespace" do
34+
let(:headers) {[
35+
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko)\n\tChrome/55.0.2883.95 Safari/537.36"
36+
]}
37+
38+
it "rejects the request" do
39+
expect do
40+
server.read_request
41+
end.to raise_exception(Protocol::HTTP1::BadHeader)
42+
end
43+
end
44+
45+
with "header that contains invalid characters" do
46+
let(:headers) {[
47+
"user-agent: Mozilla\x00Hacker Browser"
48+
]}
49+
50+
it "rejects the request" do
51+
expect do
52+
server.read_request
53+
end.to raise_exception(Protocol::HTTP1::BadHeader)
54+
end
55+
end
56+
57+
with "header that contains invalid high characters" do
58+
let(:headers) {[
59+
"user-agent: Mozilla\x7FHacker Browser"
60+
]}
61+
62+
it "rejects the request" do
63+
expect do
64+
server.read_request
65+
end.to raise_exception(Protocol::HTTP1::BadHeader)
66+
end
67+
end
68+
69+
with "header that has empty value" do
70+
let(:headers) {[
71+
"user-agent: "
72+
]}
73+
74+
it "can parse the header" do
75+
authority, method, target, version, headers, body = server.read_request
76+
77+
expect(headers).to have_keys(
78+
"user-agent" => be == ""
79+
)
80+
end
81+
end
82+
83+
with "header that has invalid name" do
84+
let(:headers) {[
85+
"invalid name: value"
86+
]}
87+
88+
it "rejects the request" do
89+
expect do
90+
server.read_request
91+
end.to raise_exception(Protocol::HTTP1::BadHeader)
92+
end
93+
end
94+
95+
with "header that has empty name" do
96+
let(:headers) {[
97+
": value"
98+
]}
99+
100+
it "rejects the request" do
101+
expect do
102+
server.read_request
103+
end.to raise_exception(Protocol::HTTP1::BadHeader)
104+
end
105+
end
106+
end

test/protocol/http1/parser.rb

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "protocol/http1/connection"
7+
8+
describe Protocol::HTTP1 do
9+
describe "REQUEST_LINE" do
10+
it "parses in linear time" do
11+
skip_unless_method_defined(:linear_time?, Regexp.singleton_class)
12+
13+
expect(Regexp).to be(:linear_time?, Protocol::HTTP1::REQUEST_LINE)
14+
end
15+
end
16+
17+
describe "HEADER" do
18+
it "parses in linear time" do
19+
skip_unless_method_defined(:linear_time?, Regexp.singleton_class)
20+
21+
expect(Regexp).to be(:linear_time?, Protocol::HTTP1::HEADER)
22+
end
23+
end
24+
25+
describe "VALID_FIELD_NAME" do
26+
it "parses in linear time" do
27+
skip_unless_method_defined(:linear_time?, Regexp.singleton_class)
28+
29+
expect(Regexp).to be(:linear_time?, Protocol::HTTP1::VALID_FIELD_NAME)
30+
end
31+
end
32+
33+
describe "VALID_FIELD_VALUE" do
34+
it "parses in linear time" do
35+
skip_unless_method_defined(:linear_time?, Regexp.singleton_class)
36+
37+
expect(Regexp).to be(:linear_time?, Protocol::HTTP1::VALID_FIELD_VALUE)
38+
end
39+
end
40+
end

0 commit comments

Comments
 (0)