diff --git a/lib/protocol/http1/connection.rb b/lib/protocol/http1/connection.rb index f09fc0e..52f221a 100644 --- a/lib/protocol/http1/connection.rb +++ b/lib/protocol/http1/connection.rb @@ -40,7 +40,7 @@ module HTTP1 OWS = /[ \t]*/ # A field value is any string of characters that does not contain a null character, CR, or LF. After reflecting on the RFCs and surveying real implementations, I came to the conclusion that the RFCs are too restrictive. Most servers only check for the presence of null bytes, and obviously CR/LF characters have semantic meaning in the parser. So, I decided to follow this defacto standard, even if I'm not entirely happy with it. FIELD_VALUE = /[^\0\r\n]+/.freeze - HEADER = /\A(#{FIELD_NAME}):#{OWS}(?:(#{FIELD_VALUE})#{OWS})?\z/.freeze + HEADER = /\A(#{FIELD_NAME}):#{OWS}(?:([^\0\r\n]*[^\0\r\n \t])#{OWS})?\z/.freeze VALID_FIELD_NAME = /\A#{FIELD_NAME}\z/.freeze VALID_FIELD_VALUE = /\A#{FIELD_VALUE}?\z/.freeze diff --git a/releases.md b/releases.md index db77727..833b7d2 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Strip trailing whitespace from parsed header and trailer values. + ## v0.41.0 - Map remote socket failures to `Protocol::HTTP::RemoteError` and prevent connection reuse. diff --git a/test/protocol/http1/body/chunked.rb b/test/protocol/http1/body/chunked.rb index 3d4580f..62423bf 100644 --- a/test/protocol/http1/body/chunked.rb +++ b/test/protocol/http1/body/chunked.rb @@ -128,6 +128,19 @@ end end + with "trailer with trailing whitespace" do + let(:postfix) {"ETag: abcd \t\r\n"} + + it "strips trailing whitespace from the value" do + headers.add("trailer", "etag") + + expect(body.read).to be == "Hello World" + expect(body.read).to be == nil + + expect(headers["etag"]).to be == "abcd" + end + end + with "incomplete trailer" do let(:buffer) {StringIO.new("0\r\n")} diff --git a/test/protocol/http1/connection.rb b/test/protocol/http1/connection.rb index 701ea88..f05a3fa 100644 --- a/test/protocol/http1/connection.rb +++ b/test/protocol/http1/connection.rb @@ -88,6 +88,15 @@ end end + with "#read_headers" do + it "strips optional whitespace around values" do + client.stream.write "X-A: a b \t\r\nX-B:\t\r\nX-C: c\v\r\n\r\n" + client.stream.close + + expect(server.read_headers).to be == [["X-A", "a b"], ["X-B", ""], ["X-C", "c\v"]] + end + end + with "#read_request" do it "reads request without body" do client.stream.write "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n" @@ -156,6 +165,15 @@ expect(server).to be(:persistent) end + it "reads request with chunked body when transfer-encoding has trailing whitespace" do + client.stream.write "GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked \r\n\r\nb\r\nHello World\r\n0\r\n\r\n" + client.stream.close + + authority, method, target, version, headers, body = server.read_request + + expect(body.join).to be == "Hello World" + end + it "reads request with CONNECT method" do client.stream.write "CONNECT localhost:443 HTTP/1.1\r\nHost: localhost\r\n\r\n" client.stream.close