From b8c474a2a81c5c62b3368eb5127d22a822259a63 Mon Sep 17 00:00:00 2001 From: pascal betz Date: Tue, 20 Apr 2021 13:19:42 +0200 Subject: [PATCH 1/3] Improve parsing of HTTP_HOST header IPV6 Host was not properly parsed. https://github.com/puma/puma/issues/2584 --- lib/puma/request.rb | 2 +- test/test_puma_server.rb | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/puma/request.rb b/lib/puma/request.rb index 66c9884cca..88cfc92728 100644 --- a/lib/puma/request.rb +++ b/lib/puma/request.rb @@ -231,7 +231,7 @@ def fetch_status_code(status) # def normalize_env(env, client) if host = env[HTTP_HOST] - if colon = host.index(":") + if colon = host.index(/:\d+$/) env[SERVER_NAME] = host[0, colon] env[SERVER_PORT] = host[colon+1, host.bytesize] else diff --git a/test/test_puma_server.rb b/test/test_puma_server.rb index 5e591171bf..de20bbf2ee 100644 --- a/test/test_puma_server.rb +++ b/test/test_puma_server.rb @@ -53,6 +53,36 @@ def new_connection TCPSocket.new(@host, @port).tap {|sock| @ios << sock} end + def test_normalize_host_header_missing + server_run app: ->(env) do + [200, {}, [env["SERVER_NAME"], "\n", env["SERVER_PORT"]]] + end + + data = send_http_and_read "GET / HTTP/1.0\r\n\r\n" + assert_equal "localhost\n80", data.split("\r\n").last + end + + def test_normalize_host_header_ipv4 + server_run app: ->(env) do + [200, {}, [env["SERVER_NAME"], "\n", env["SERVER_PORT"]]] + end + + data = send_http_and_read "GET / HTTP/1.0\r\nHost: 123.123.123.123:456\r\n\r\n" + assert_equal "123.123.123.123\n456", data.split("\r\n").last + end + + def test_normalize_host_header_ipv6 + server_run app: ->(env) do + [200, {}, [env["SERVER_NAME"], "\n", env["SERVER_PORT"]]] + end + + data = send_http_and_read "GET / HTTP/1.0\r\nHost: ::ffff:127.0.0.1:9292\r\n\r\n" + assert_equal "::ffff:127.0.0.1\n9292", data.split("\r\n").last + + data = send_http_and_read "GET / HTTP/1.0\r\nHost: ::1:9292\r\n\r\n" + assert_equal "::1\n9292", data.split("\r\n").last + end + def test_proper_stringio_body data = nil From a0b7a168c63bce7ecdfbb0c7793645ee73f46fed Mon Sep 17 00:00:00 2001 From: pascal betz Date: Thu, 22 Apr 2021 07:47:44 +0200 Subject: [PATCH 2/3] Extracted Regex to constant --- lib/puma/const.rb | 3 +++ lib/puma/request.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/puma/const.rb b/lib/puma/const.rb index a0744673fe..36ce7732ea 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -247,5 +247,8 @@ module Const # Banned keys of response header BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze + + # Index of the colon separating host/port in the HTTP_HOST header for IPV4 and IPV6 + COLON_INDEX_REGEX = /:\d+\z/.freeze end end diff --git a/lib/puma/request.rb b/lib/puma/request.rb index 88cfc92728..a27f316cb3 100644 --- a/lib/puma/request.rb +++ b/lib/puma/request.rb @@ -231,7 +231,7 @@ def fetch_status_code(status) # def normalize_env(env, client) if host = env[HTTP_HOST] - if colon = host.index(/:\d+$/) + if colon = host.index(COLON_INDEX_REGEX) env[SERVER_NAME] = host[0, colon] env[SERVER_PORT] = host[colon+1, host.bytesize] else From b8bb4b42438a6229294666386589c025a0ac989a Mon Sep 17 00:00:00 2001 From: pascal betz Date: Thu, 22 Apr 2021 19:21:45 +0200 Subject: [PATCH 3/3] Incorporate feedback IPV6 are bracketed but contain colons so I needed to adapt the logic to separate host/port. --- lib/puma/const.rb | 3 --- lib/puma/request.rb | 6 +++++- test/test_puma_server.rb | 26 ++++++++++++++++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/puma/const.rb b/lib/puma/const.rb index 36ce7732ea..a0744673fe 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -247,8 +247,5 @@ module Const # Banned keys of response header BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze - - # Index of the colon separating host/port in the HTTP_HOST header for IPV4 and IPV6 - COLON_INDEX_REGEX = /:\d+\z/.freeze end end diff --git a/lib/puma/request.rb b/lib/puma/request.rb index a27f316cb3..08b0354819 100644 --- a/lib/puma/request.rb +++ b/lib/puma/request.rb @@ -231,7 +231,11 @@ def fetch_status_code(status) # def normalize_env(env, client) if host = env[HTTP_HOST] - if colon = host.index(COLON_INDEX_REGEX) + # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port. + if colon = host.rindex("]:") # IPV6 with port + env[SERVER_NAME] = host[0, colon+1] + env[SERVER_PORT] = host[colon+2, host.bytesize] + elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port env[SERVER_NAME] = host[0, colon] env[SERVER_PORT] = host[colon+1, host.bytesize] else diff --git a/test/test_puma_server.rb b/test/test_puma_server.rb index d3da0db61e..c64ab16099 100644 --- a/test/test_puma_server.rb +++ b/test/test_puma_server.rb @@ -62,6 +62,18 @@ def test_normalize_host_header_missing assert_equal "localhost\n80", data.split("\r\n").last end + def test_normalize_host_header_hostname + server_run app: ->(env) do + [200, {}, [env["SERVER_NAME"], "\n", env["SERVER_PORT"]]] + end + + data = send_http_and_read "GET / HTTP/1.0\r\nHost: example.com:456\r\n\r\n" + assert_equal "example.com\n456", data.split("\r\n").last + + data = send_http_and_read "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n" + assert_equal "example.com\n80", data.split("\r\n").last + end + def test_normalize_host_header_ipv4 server_run app: ->(env) do [200, {}, [env["SERVER_NAME"], "\n", env["SERVER_PORT"]]] @@ -69,6 +81,9 @@ def test_normalize_host_header_ipv4 data = send_http_and_read "GET / HTTP/1.0\r\nHost: 123.123.123.123:456\r\n\r\n" assert_equal "123.123.123.123\n456", data.split("\r\n").last + + data = send_http_and_read "GET / HTTP/1.0\r\nHost: 123.123.123.123\r\n\r\n" + assert_equal "123.123.123.123\n80", data.split("\r\n").last end def test_normalize_host_header_ipv6 @@ -76,11 +91,14 @@ def test_normalize_host_header_ipv6 [200, {}, [env["SERVER_NAME"], "\n", env["SERVER_PORT"]]] end - data = send_http_and_read "GET / HTTP/1.0\r\nHost: ::ffff:127.0.0.1:9292\r\n\r\n" - assert_equal "::ffff:127.0.0.1\n9292", data.split("\r\n").last + data = send_http_and_read "GET / HTTP/1.0\r\nHost: [::ffff:127.0.0.1]:9292\r\n\r\n" + assert_equal "[::ffff:127.0.0.1]\n9292", data.split("\r\n").last + + data = send_http_and_read "GET / HTTP/1.0\r\nHost: [::1]:9292\r\n\r\n" + assert_equal "[::1]\n9292", data.split("\r\n").last - data = send_http_and_read "GET / HTTP/1.0\r\nHost: ::1:9292\r\n\r\n" - assert_equal "::1\n9292", data.split("\r\n").last + data = send_http_and_read "GET / HTTP/1.0\r\nHost: [::1]\r\n\r\n" + assert_equal "[::1]\n80", data.split("\r\n").last end def test_proper_stringio_body