Skip to content

Commit 1707e6f

Browse files
committed
http/h1_stream: parse request-target out into scheme/authority/path
- absolute-form host overrides host header - CONNECT requests should have a host header - OPTIONS requests in absolute-form without a path should have path '*'
1 parent 952f45c commit 1707e6f

File tree

3 files changed

+125
-22
lines changed

3 files changed

+125
-22
lines changed

http/h1_stream.lua

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ local ce = require "cqueues.errno"
55
local new_fifo = require "fifo"
66
local lpeg = require "lpeg"
77
local http_patts = require "lpeg_patterns.http"
8+
local uri_patts = require "lpeg_patterns.uri"
89
local new_headers = require "http.headers".new
910
local reason_phrases = require "http.h1_reason_phrases"
1011
local stream_common = require "http.stream_common"
@@ -22,6 +23,8 @@ local Connection = lpeg.Ct(http_patts.Connection) * EOF
2223
local Content_Encoding = lpeg.Ct(http_patts.Content_Encoding) * EOF
2324
local Transfer_Encoding = lpeg.Ct(http_patts.Transfer_Encoding) * EOF
2425
local TE = lpeg.Ct(http_patts.TE) * EOF
26+
local absolute_form = uri_patts.absolute_uri * EOF
27+
local authority_form = uri_patts.authority * EOF
2528

2629
local function has(list, val)
2730
if list then
@@ -249,6 +252,41 @@ function stream_methods:step(timeout)
249252
return true
250253
end
251254

255+
-- should return scheme, authority, path
256+
local function parse_target(path)
257+
if path:sub(1, 1) == "/" or path == "*" then
258+
-- 'origin-form' or 'asterisk-form'
259+
-- early exit for common case
260+
return nil, nil, path
261+
end
262+
263+
local absolute_uri = absolute_form:match(path)
264+
if absolute_uri then
265+
-- don't want normalised form of authority or path
266+
local authority
267+
if absolute_uri.host then
268+
authority, path = path:match("://([^/]*)(.*)")
269+
if path == "" then
270+
path = nil
271+
end
272+
else
273+
-- authority is nil
274+
-- path should be nil if there are no characters.
275+
path = path:match(":(.+)")
276+
end
277+
return absolute_uri.scheme, authority, path
278+
end
279+
280+
if authority_form:match(path) then
281+
-- don't want normalised form of authority
282+
-- `path` *is* the authority
283+
return nil, path, nil
284+
end
285+
286+
-- other...
287+
return nil, nil, path
288+
end
289+
252290
-- read_headers may be called more than once for a stream
253291
-- e.g. for 100 Continue
254292
-- this function *should never throw* under normal operation
@@ -281,12 +319,43 @@ function stream_methods:read_headers(timeout)
281319
self.peer_version = httpversion
282320
headers = new_headers()
283321
headers:append(":method", method)
284-
if method == "CONNECT" then
285-
headers:append(":authority", target)
286-
else
287-
headers:append(":path", target)
322+
local scheme, authority, path = parse_target(target)
323+
if authority then
324+
-- RFC 7230 Section 5.4
325+
-- When a proxy receives a request with an absolute-form of
326+
-- request-target, the proxy MUST ignore the received Host header field
327+
-- (if any) and instead replace it with the host information of the
328+
-- request-target.
329+
headers:append(":authority", authority)
330+
end
331+
-- RFC 7230 Section 5.5
332+
-- If the request-target is in absolute-form, the effective request URI
333+
-- is the same as the request-target. Otherwise, the effective request
334+
-- URI is constructed as follows:
335+
if not scheme then
336+
-- If the server's configuration (or outbound gateway) provides a
337+
-- fixed URI scheme, that scheme is used for the effective request
338+
-- URI. Otherwise, if the request is received over a TLS-secured TCP
339+
-- connection, the effective request URI's scheme is "https"; if not,
340+
-- the scheme is "http".
341+
if self:checktls() then
342+
scheme = "https"
343+
else
344+
scheme = "http"
345+
end
346+
end
347+
if path then
348+
headers:append(":path", path)
349+
elseif method == "OPTIONS" then
350+
-- RFC 7230 Section 5.3.4
351+
-- If a proxy receives an OPTIONS request with an absolute-form of
352+
-- request-target in which the URI has an empty path and no query
353+
-- component, then the last proxy on the request chain MUST send a
354+
-- request-target of "*" when it forwards the request to the indicated
355+
-- origin server.
356+
headers:append(":path", "*")
288357
end
289-
headers:append(":scheme", self:checktls() and "https" or "http")
358+
headers:append(":scheme", scheme)
290359
self:set_state("open")
291360
else -- client
292361
-- Make sure we're at front of connection pipeline
@@ -342,9 +411,17 @@ function stream_methods:read_headers(timeout)
342411
end
343412
k = k:lower() -- normalise to lower case
344413
if k == "host" and not is_trailers then
345-
k = ":authority"
414+
-- RFC 7230 Section 5.4
415+
-- When a proxy receives a request with an absolute-form of
416+
-- request-target, the proxy MUST ignore the received Host header field
417+
-- (if any) and instead replace it with the host information of the
418+
-- request-target.
419+
if not headers:has(":authority") then
420+
headers:append(":authority", v)
421+
end
422+
else
423+
headers:append(k, v)
346424
end
347-
headers:append(k, v)
348425
end
349426

350427
do
@@ -742,13 +819,9 @@ function stream_methods:write_headers(headers, end_stream, timeout)
742819
return nil, err, errno
743820
end
744821
elseif name == ":authority" then
745-
-- for CONNECT requests, :authority is the path
746-
if self.req_method ~= "CONNECT" then
747-
-- otherwise it's the Host header
748-
local ok, err, errno = self.connection:write_header("host", value, 0)
749-
if not ok then
750-
return nil, err, errno
751-
end
822+
local ok, err, errno = self.connection:write_header("host", value, 0)
823+
if not ok then
824+
return nil, err, errno
752825
end
753826
end
754827
end

spec/h1_stream_spec.lua

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,33 @@ describe("http1 stream", function()
2525
assert.same("/", h:get(":path"))
2626
assert.same("bar", h:get("foo"))
2727
end)
28+
it("CONNECT requests should have an host header on the wire", function()
29+
local server, client = new_pair(1.1)
30+
local cq = cqueues.new()
31+
cq:wrap(function()
32+
local stream = client:new_stream()
33+
local req_headers = new_headers()
34+
req_headers:append(":method", "CONNECT")
35+
req_headers:append(":scheme", "http")
36+
req_headers:append(":authority", "myauthority:8888")
37+
assert(stream:write_headers(req_headers, true))
38+
stream:shutdown()
39+
end)
40+
cq:wrap(function()
41+
local method, path, httpversion = assert(server:read_request_line())
42+
assert.same("CONNECT", method)
43+
assert.same("myauthority:8888", path)
44+
assert.same(1.1, httpversion)
45+
local k, v = assert(server:read_header())
46+
assert.same("host", k)
47+
assert.same("myauthority:8888", v)
48+
server:shutdown()
49+
end)
50+
assert_loop(cq, TEST_TIMEOUT)
51+
assert.truthy(cq:empty())
52+
server:close()
53+
client:close()
54+
end)
2855
it("Writing to a shutdown connection returns EPIPE", function()
2956
local server, client = new_pair(1.1)
3057
local stream = client:new_stream()

spec/request_spec.lua

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -611,8 +611,9 @@ describe("http.request module", function()
611611
local h = assert(stream:get_headers())
612612
local _, host, port = stream:localname()
613613
local authority = http_util.to_authority(host, port, "http")
614+
assert.same("http", h:get(":scheme"))
614615
assert.same(authority, h:get ":authority")
615-
assert.same("http://" .. authority .. "/", h:get(":path"))
616+
assert.same("/", h:get(":path"))
616617
local resp_headers = new_headers()
617618
resp_headers:append(":status", "200")
618619
assert(stream:write_headers(resp_headers, false))
@@ -633,9 +634,9 @@ describe("http.request module", function()
633634
test(function(stream)
634635
local h = assert(stream:get_headers())
635636
local _, host, port = stream:localname()
636-
local authority = http_util.to_authority(host, port, "http")
637-
assert.same(authority, h:get ":authority")
638-
assert.same("http://" .. authority .. "/", h:get(":path"))
637+
assert.same("http", h:get(":scheme"))
638+
assert.same(http_util.to_authority(host, port, "http"), h:get ":authority")
639+
assert.same("/", h:get(":path"))
639640
local resp_headers = new_headers()
640641
resp_headers:append(":status", "200")
641642
assert(stream:write_headers(resp_headers, false))
@@ -658,7 +659,9 @@ describe("http.request module", function()
658659
local h = assert(stream:get_headers())
659660
assert.same("OPTIONS", h:get ":method")
660661
local _, host, port = stream:localname()
661-
assert.same("http://" .. http_util.to_authority(host, port, "http"), h:get(":path"))
662+
assert.same("http", h:get(":scheme"))
663+
assert.same(http_util.to_authority(host, port, "http"), h:get(":authority"))
664+
assert.same("*", h:get(":path"))
662665
stream:shutdown()
663666
end, function(req)
664667
req.headers:upsert(":method", "OPTIONS")
@@ -693,9 +696,9 @@ describe("http.request module", function()
693696
test(function(stream)
694697
local h = assert(stream:get_headers())
695698
local _, host, port = stream:localname()
696-
local authority = http_util.to_authority(host, port, "http")
697-
assert.same(authority, h:get ":authority")
698-
assert.same("http://" .. authority .. "/foo", h:get(":path"))
699+
assert.same("http", h:get(":scheme"))
700+
assert.same(http_util.to_authority(host, port, "http"), h:get ":authority")
701+
assert.same("/foo", h:get(":path"))
699702
stream:shutdown()
700703
end, function(req)
701704
req.proxy = {

0 commit comments

Comments
 (0)