mirror of
https://github.com/puma/puma.git
synced 2022-11-09 13:48:40 -05:00
add support for the PROXY protocol (v1 only) (#2654)
* add support for the PROXY protocol (v1 only) * slightly simplify test * address review feedback * move PROXY protocol parsing into its own method; avoid issue with short reads The HTTP parser requires that the buffer be non-empty when it's invoked, but if the entire buffer was the PROXY protocol, we may sometimes invoke it with an empty buffer. This causes the following error: Puma::HttpParserError: Requested start is after data buffer end. The fix? Any time we consume the entire buffer for the PROXY protocol, simply return `false` to indicate that we require more data.
This commit is contained in:
parent
bbe8adf258
commit
656f0f71da
5 changed files with 93 additions and 4 deletions
|
@ -56,6 +56,7 @@ module Puma
|
|||
@parser = HttpParser.new
|
||||
@parsed_bytes = 0
|
||||
@read_header = true
|
||||
@read_proxy = false
|
||||
@ready = false
|
||||
|
||||
@body = nil
|
||||
|
@ -71,6 +72,7 @@ module Puma
|
|||
@peerip = nil
|
||||
@listener = nil
|
||||
@remote_addr_header = nil
|
||||
@expect_proxy_proto = false
|
||||
|
||||
@body_remain = 0
|
||||
|
||||
|
@ -106,7 +108,7 @@ module Puma
|
|||
|
||||
# @!attribute [r] in_data_phase
|
||||
def in_data_phase
|
||||
!@read_header
|
||||
!(@read_header || @read_proxy)
|
||||
end
|
||||
|
||||
def set_timeout(val)
|
||||
|
@ -121,6 +123,7 @@ module Puma
|
|||
def reset(fast_check=true)
|
||||
@parser.reset
|
||||
@read_header = true
|
||||
@read_proxy = !!@expect_proxy_proto
|
||||
@env = @proto_env.dup
|
||||
@body = nil
|
||||
@tempfile = nil
|
||||
|
@ -131,6 +134,8 @@ module Puma
|
|||
@in_last_chunk = false
|
||||
|
||||
if @buffer
|
||||
return false unless try_to_parse_proxy_protocol
|
||||
|
||||
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
||||
|
||||
if @parser.finished?
|
||||
|
@ -161,8 +166,32 @@ module Puma
|
|||
end
|
||||
end
|
||||
|
||||
# If necessary, read the PROXY protocol from the buffer. Returns
|
||||
# false if more data is needed.
|
||||
def try_to_parse_proxy_protocol
|
||||
if @read_proxy
|
||||
if @expect_proxy_proto == :v1
|
||||
if @buffer.include? "\r\n"
|
||||
if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
|
||||
if md[1]
|
||||
@peerip = md[1].split(" ")[0]
|
||||
end
|
||||
@buffer = md.post_match
|
||||
end
|
||||
# if the buffer has a \r\n but doesn't have a PROXY protocol
|
||||
# request, this is just HTTP from a non-PROXY client; move on
|
||||
@read_proxy = false
|
||||
return @buffer.size > 0
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def try_to_finish
|
||||
return read_body unless @read_header
|
||||
return read_body if in_data_phase
|
||||
|
||||
begin
|
||||
data = @io.read_nonblock(CHUNK_SIZE)
|
||||
|
@ -187,6 +216,8 @@ module Puma
|
|||
@buffer = data
|
||||
end
|
||||
|
||||
return false unless try_to_parse_proxy_protocol
|
||||
|
||||
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
||||
|
||||
if @parser.finished?
|
||||
|
@ -243,6 +274,17 @@ module Puma
|
|||
@parsed_bytes == 0
|
||||
end
|
||||
|
||||
def expect_proxy_proto=(val)
|
||||
if val
|
||||
if @read_header
|
||||
@read_proxy = true
|
||||
end
|
||||
else
|
||||
@read_proxy = false
|
||||
end
|
||||
@expect_proxy_proto = val
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_body
|
||||
|
|
|
@ -247,5 +247,7 @@ module Puma
|
|||
|
||||
# Banned keys of response header
|
||||
BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
|
||||
|
||||
PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
|
||||
end
|
||||
end
|
||||
|
|
|
@ -818,7 +818,7 @@ module Puma
|
|||
# a kernel syscall is required which for very fast rack handlers
|
||||
# slows down the handling significantly.
|
||||
#
|
||||
# There are 4 possible values:
|
||||
# There are 5 possible values:
|
||||
#
|
||||
# 1. **:socket** (the default) - read the peername from the socket using the
|
||||
# syscall. This is the normal behavior.
|
||||
|
@ -828,7 +828,10 @@ module Puma
|
|||
# `set_remote_address header: "X-Real-IP"`.
|
||||
# Only the first word (as separated by spaces or comma) is used, allowing
|
||||
# headers such as X-Forwarded-For to be used as well.
|
||||
# 4. **\<Any string\>** - this allows you to hardcode remote address to any value
|
||||
# 4. **proxy_protocol: :v1**- set the remote address to the value read from the
|
||||
# HAproxy PROXY protocol, version 1. If the request does not have the PROXY
|
||||
# protocol attached to it, will fall back to :socket
|
||||
# 5. **\<Any string\>** - this allows you to hardcode remote address to any value
|
||||
# you wish. Because Puma never uses this field anyway, it's format is
|
||||
# entirely in your hands.
|
||||
#
|
||||
|
@ -846,6 +849,13 @@ module Puma
|
|||
if hdr = val[:header]
|
||||
@options[:remote_address] = :header
|
||||
@options[:remote_address_header] = "HTTP_" + hdr.upcase.tr("-", "_")
|
||||
elsif protocol_version = val[:proxy_protocol]
|
||||
@options[:remote_address] = :proxy_protocol
|
||||
protocol_version = protocol_version.downcase.to_sym
|
||||
unless [:v1].include?(protocol_version)
|
||||
raise "Invalid value for proxy_protocol - #{protocol_version.inspect}"
|
||||
end
|
||||
@options[:remote_address_proxy_protocol] = protocol_version
|
||||
else
|
||||
raise "Invalid value for set_remote_address - #{val.inspect}"
|
||||
end
|
||||
|
|
|
@ -323,6 +323,8 @@ module Puma
|
|||
remote_addr_value = @options[:remote_address_value]
|
||||
when :header
|
||||
remote_addr_header = @options[:remote_address_header]
|
||||
when :proxy_protocol
|
||||
remote_addr_proxy_protocol = @options[:remote_address_proxy_protocol]
|
||||
end
|
||||
|
||||
while @status == :run || (drain && shutting_down?)
|
||||
|
@ -348,6 +350,8 @@ module Puma
|
|||
client.peerip = remote_addr_value
|
||||
elsif remote_addr_header
|
||||
client.remote_addr_header = remote_addr_header
|
||||
elsif remote_addr_proxy_protocol
|
||||
client.expect_proxy_proto = remote_addr_proxy_protocol
|
||||
end
|
||||
pool << client
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ require_relative "helper"
|
|||
require "puma/events"
|
||||
require "net/http"
|
||||
require "nio"
|
||||
require "ipaddr"
|
||||
|
||||
class TestPumaServer < Minitest::Test
|
||||
parallelize_me! unless JRUBY_HEAD
|
||||
|
@ -48,6 +49,21 @@ class TestPumaServer < Minitest::Test
|
|||
new_connection << req
|
||||
end
|
||||
|
||||
def send_proxy_v1_http(req, remote_ip, multisend = false)
|
||||
addr = IPAddr.new(remote_ip)
|
||||
family = addr.ipv4? ? "TCP4" : "TCP6"
|
||||
target = addr.ipv4? ? "127.0.0.1" : "::1"
|
||||
conn = new_connection
|
||||
if multisend
|
||||
conn << "PROXY #{family} #{remote_ip} #{target} 10000 80\r\n"
|
||||
sleep 0.15
|
||||
conn << req
|
||||
else
|
||||
conn << ("PROXY #{family} #{remote_ip} #{target} 10000 80\r\n" + req)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def new_connection
|
||||
TCPSocket.new(@host, @port).tap {|sock| @ios << sock}
|
||||
end
|
||||
|
@ -1017,6 +1033,21 @@ EOF
|
|||
assert_match "X-header: first line\r\nX-header: second line\r\n", data
|
||||
end
|
||||
|
||||
def test_proxy_protocol
|
||||
server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env|
|
||||
[200, {}, [env["REMOTE_ADDR"]]]
|
||||
end
|
||||
|
||||
remote_addr = send_proxy_v1_http("GET / HTTP/1.0\r\n\r\n", "1.2.3.4").read.split("\r\n").last
|
||||
assert_equal '1.2.3.4', remote_addr
|
||||
|
||||
remote_addr = send_proxy_v1_http("GET / HTTP/1.0\r\n\r\n", "fd00::1").read.split("\r\n").last
|
||||
assert_equal 'fd00::1', remote_addr
|
||||
|
||||
remote_addr = send_proxy_v1_http("GET / HTTP/1.0\r\n\r\n", "fd00::1", true).read.split("\r\n").last
|
||||
assert_equal 'fd00::1', remote_addr
|
||||
end
|
||||
|
||||
# To comply with the Rack spec, we have to split header field values
|
||||
# containing newlines into multiple headers.
|
||||
def assert_does_not_allow_http_injection(app, opts = {})
|
||||
|
|
Loading…
Reference in a new issue