util.http: Implement parser for RFC 7239 Forwarded header
authorKim Alvefur <zash@zash.se>
Sat, 03 Jun 2023 16:15:52 +0200
changeset 13128 f15e23840780
parent 13127 dee26e4cfb2b
child 13129 90394be5e6a5
util.http: Implement parser for RFC 7239 Forwarded header Standardized and structured replacement for the X-Forwarded-For, X-Forwarded-Proto set of headers. Notably, this allows per-hop protocol information, unlike X-Forwarded-Proto which is always a single value for some reason.
doc/doap.xml
spec/util_http_spec.lua
util/http.lua
--- a/doc/doap.xml	Thu Jun 01 14:33:57 2023 +0200
+++ b/doc/doap.xml	Sat Jun 03 16:15:52 2023 +0200
@@ -56,6 +56,7 @@
     <implements rdf:resource="https://www.rfc-editor.org/info/rfc6455"/>
     <implements rdf:resource="https://www.rfc-editor.org/info/rfc6901"/>
     <implements rdf:resource="https://www.rfc-editor.org/info/rfc7233"/>
+    <implements rdf:resource="https://www.rfc-editor.org/info/rfc7239"/>
     <implements rdf:resource="https://www.rfc-editor.org/info/rfc7301"/>
     <implements rdf:resource="https://www.rfc-editor.org/info/rfc7395"/>
     <implements rdf:resource="https://www.rfc-editor.org/info/rfc7590"/>
--- a/spec/util_http_spec.lua	Thu Jun 01 14:33:57 2023 +0200
+++ b/spec/util_http_spec.lua	Sat Jun 03 16:15:52 2023 +0200
@@ -108,4 +108,25 @@
 			assert.is_(http.contains_token("fo o", "foo"));
 		end);
 	end);
+
+do
+	describe("parse_forwarded", function()
+		it("works", function()
+			assert.same({ { ["for"] = "[2001:db8:cafe::17]:4711" } }, http.parse_forwarded('For="[2001:db8:cafe::17]:4711"'), "case insensitive");
+
+			assert.same({ { ["for"] = "192.0.2.60"; proto = "http"; by = "203.0.113.43" } }, http.parse_forwarded('for=192.0.2.60;proto=http;by=203.0.113.43'),
+				"separated by semicolon");
+
+			assert.same({ { ["for"] = "192.0.2.43" }; { ["for"] = "198.51.100.17" } }, http.parse_forwarded('for=192.0.2.43, for=198.51.100.17'),
+				"Values from multiple proxy servers can be appended using a comma");
+
+		end)
+		it("rejects quoted quotes", function ()
+			assert.falsy(http.parse_forwarded('foo="bar\"bar'), "quoted quotes");
+		end)
+		pending("deals with quoted quotes", function ()
+			assert.same({ { foo = 'bar"baz' } }, http.parse_forwarded('foo="bar\"bar'), "quoted quotes");
+		end)
+	end)
+end
 end);
--- a/util/http.lua	Thu Jun 01 14:33:57 2023 +0200
+++ b/util/http.lua	Sat Jun 03 16:15:52 2023 +0200
@@ -69,9 +69,42 @@
 	return path;
 end
 
+--- Parse the RFC 7239 Forwarded header into array of key-value pairs.
+local function parse_forwarded(forwarded)
+	if type(forwarded) ~= "string" then
+		return nil;
+	end
+
+	local fwd = {}; -- array
+	local cur = {}; -- map, to which we add the next key-value pair
+	for key, quoted, value, delim in forwarded:gmatch("(%w+)%s*=%s*(\"?)([^,;\"]+)%2%s*(.?)") do
+		-- FIXME quoted quotes like "foo\"bar"
+		-- unlikely when only dealing with IP addresses
+		if quoted == '"' then
+			value = value:gsub("\\(.)", "%1");
+		end
+
+		cur[key:lower()] = value;
+		if delim == "" or delim == "," then
+			t_insert(fwd, cur)
+			if delim == "" then
+				-- end of the string
+				break;
+			end
+			cur = {};
+		elseif delim ~= ";" then
+			-- misparsed
+			return false;
+		end
+	end
+
+	return fwd;
+end
+
 return {
 	urlencode = urlencode, urldecode = urldecode;
 	formencode = formencode, formdecode = formdecode;
 	contains_token = contains_token;
 	normalize_path = normalize_path;
+	parse_forwarded = parse_forwarded;
 };