Implantation proxy protocol

This commit is contained in:
Konstantin Kapustin 2012-09-07 18:51:42 +04:00 committed by benoitc
parent aa22115cfc
commit 70534acde8
14 changed files with 282 additions and 21 deletions

View File

@ -996,3 +996,38 @@ class WorkerExit(Setting):
The callable needs to accept two instance variables for the Arbiter and
the just-exited Worker.
"""
class ProxyProtocol(Setting):
name = "proxy_protocol"
section = "Server Mechanics"
cli = ["--proxy-protocol"]
validator = validate_bool
default = False
action = "store_true"
desc = """\
Enable detect PROXY protocol (PROXY mode).
Allow using Http and Proxy together. It's may be useful for work with
stunnel as https frondend and gunicorn as http server.
PROXY protocol: http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
Example for stunnel config::
[https]
protocol = proxy
accept = 443
connect = 80
cert = /etc/ssl/certs/stunnel.pem
key = /etc/ssl/certs/stunnel.key
"""
class ProxyAllowFrom(Setting):
name = "proxy_allow_ips"
section = "Server Mechanics"
cli = ["--proxy-allow-from"]
validator = validate_string_to_list
default = "127.0.0.1"
desc = """\
Front-end's IPs from which allowed accept proxy requests (comma separate).
"""

View File

@ -78,3 +78,21 @@ class LimitRequestHeaders(ParseException):
def __str__(self):
return self.msg
class InvalidProxyLine(ParseException):
def __init__(self, line):
self.line = line
self.code = 400
def __str__(self):
return "Invalid PROXY line: %s" % self.line
class ForbiddenProxyRequest(ParseException):
def __init__(self, host):
self.host = host
self.code = 403
def __str__(self):
return "Proxy request from %s not allowed" % self.host

View File

@ -5,16 +5,20 @@
import re
import urlparse
import socket
from errno import ENOTCONN
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from gunicorn.http.unreader import SocketUnreader
from gunicorn.http.body import ChunkedReader, LengthReader, EOFReader, Body
from gunicorn.http.errors import InvalidHeader, InvalidHeaderName, NoMoreData, \
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, \
LimitRequestLine, LimitRequestHeaders
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
MAX_REQUEST_LINE = 8190
MAX_HEADERS = 32768
@ -131,7 +135,7 @@ class Message(object):
class Request(Message):
def __init__(self, cfg, unreader):
def __init__(self, cfg, unreader, req_number=1):
self.methre = re.compile("[A-Z0-9$-_.]{3,20}")
self.versre = re.compile("HTTP/(\d+).(\d+)")
@ -146,6 +150,10 @@ class Request(Message):
if (self.limit_request_line < 0
or self.limit_request_line >= MAX_REQUEST_LINE):
self.limit_request_line = MAX_REQUEST_LINE
self.req_number = req_number
self.proxy_protocol_info = None
super(Request, self).__init__(cfg, unreader)
@ -161,27 +169,19 @@ class Request(Message):
buf = StringIO()
self.get_data(unreader, buf, stop=True)
# Request line
data = buf.getvalue()
while True:
idx = data.find("\r\n")
if idx >= 0:
# check if the request line is too large
if idx > self.limit_request_line > 0:
raise LimitRequestLine(idx, self.limit_request_line)
break
# get request line
line, rbuf = self.read_line(unreader, buf, self.limit_request_line)
# check if chunk is too large before read next chunk
if len(data) - 2 > self.limit_request_line and \
self.limit_request_line> 0 :
raise LimitRequestLine(len(data), self.limit_request_line)
# proxy protocol
if self.proxy_protocol(line):
# get next request line
buf = StringIO()
buf.write(rbuf)
line, rbuf = self.read_line(unreader, buf, self.limit_request_line)
self.get_data(unreader, buf)
data = buf.getvalue()
self.parse_request_line(data[:idx])
self.parse_request_line(line)
buf = StringIO()
buf.write(data[idx+2:]) # Skip \r\n
buf.write(rbuf)
# Headers
data = buf.getvalue()
@ -210,6 +210,103 @@ class Request(Message):
buf = StringIO()
return ret
def read_line(self, unreader, buf, limit=0):
data = buf.getvalue()
while True:
idx = data.find("\r\n")
if idx >= 0:
# check if the request line is too large
if idx > limit > 0:
raise LimitRequestLine(idx, limit)
break
self.get_data(unreader, buf)
data = buf.getvalue()
if len(data) - 2 > limit > 0:
raise LimitRequestLine(len(data), limit)
return (data[:idx], # request line,
data[idx + 2:]) # residue in the buffer, skip \r\n
def proxy_protocol(self, line):
"""\
Detect, check and parse proxy protocol.
:raises: ForbiddenProxyRequest, InvalidProxyLine.
:return: True for proxy protocol line else False
"""
if not self.cfg.proxy_protocol:
return False
if self.req_number != 1:
return False
if not line.startswith("PROXY"):
return False
self.proxy_protocol_access_check()
self.parse_proxy_protocol(line)
return True
def proxy_protocol_access_check(self):
# check in allow list
if isinstance(self.unreader, SocketUnreader):
try:
remote_host = self.unreader.sock.getpeername()[0]
except socket.error as e:
if e[0] == ENOTCONN:
raise ForbiddenProxyRequest("UNKNOW")
raise
if remote_host not in self.cfg.proxy_allow_ips:
raise ForbiddenProxyRequest(remote_host)
def parse_proxy_protocol(self, line):
bits = line.split()
if len(bits) != 6:
raise InvalidProxyLine(line)
# Extract data
proto = bits[1]
s_addr = bits[2]
d_addr = bits[3]
# Validation
if proto not in ["TCP4", "TCP6"]:
raise InvalidProxyLine("protocol '%s' not supported" % proto)
if proto == "TCP4":
try:
socket.inet_pton(socket.AF_INET, s_addr)
socket.inet_pton(socket.AF_INET, d_addr)
except socket.error:
raise InvalidProxyLine(line)
elif proto == "TCP6":
try:
socket.inet_pton(socket.AF_INET6, s_addr)
socket.inet_pton(socket.AF_INET6, d_addr)
except socket.error:
raise InvalidProxyLine(line)
try:
s_port = int(bits[4])
d_port = int(bits[5])
except ValueError:
raise InvalidProxyLine("invalid port %s" % line)
if not ((0 <= s_port <= 65535) and (0 <= d_port <= 65535)):
raise InvalidProxyLine("invalid port %s" % line)
# Set data
self.proxy_protocol_info = {
"proxy_protocol": proto,
"client_addr": s_addr,
"client_port": s_port,
"proxy_addr": d_addr,
"proxy_port": d_port
}
def parse_request_line(self, line):
bits = line.split(None, 2)
if len(bits) != 3:

View File

@ -16,6 +16,9 @@ class Parser(object):
self.unreader = IterUnreader(source)
self.mesg = None
# request counter (for keepalive connetions)
self.req_count = 0
def __iter__(self):
return self
@ -31,7 +34,8 @@ class Parser(object):
data = self.mesg.body.read(8192)
# Parse the next request
self.mesg = self.mesg_class(self.cfg, self.unreader)
self.req_count += 1
self.mesg = self.mesg_class(self.cfg, self.unreader, self.req_count)
if not self.mesg:
raise StopIteration()
return self.mesg

View File

@ -57,6 +57,19 @@ def default_environ(req, sock, cfg):
"SERVER_PROTOCOL": "HTTP/%s" % ".".join(map(str, req.version))
}
def proxy_environ(req):
info = req.proxy_protocol_info
if not info:
return {}
return {
"PROXY_PROTOCOL": info["proxy_protocol"],
"REMOTE_ADDR": info["client_addr"],
"REMOTE_PORT": str(info["client_port"]),
"PROXY_ADDR": info["proxy_addr"],
"PROXY_PORT": str(info["proxy_port"]),
}
def create(req, sock, client, server, cfg):
resp = Response(req, sock)
@ -149,6 +162,8 @@ def create(req, sock, client, server, cfg):
environ['PATH_INFO'] = unquote(path_info)
environ['SCRIPT_NAME'] = script_name
environ.update(proxy_environ(req))
return resp, environ
class Response(object):

View File

@ -16,6 +16,7 @@ from gunicorn.workers.workertmp import WorkerTmp
from gunicorn.http.errors import InvalidHeader, InvalidHeaderName, \
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, \
LimitRequestLine, LimitRequestHeaders
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
from gunicorn.http.wsgi import default_environ, Response
class Worker(object):
@ -131,7 +132,8 @@ class Worker(object):
addr = addr or ('', -1) # unix socket case
if isinstance(exc, (InvalidRequestLine, InvalidRequestMethod,
InvalidHTTPVersion, InvalidHeader, InvalidHeaderName,
LimitRequestLine, LimitRequestHeaders,)):
LimitRequestLine, LimitRequestHeaders,
InvalidProxyLine, ForbiddenProxyRequest,)):
status_int = 400
reason = "Bad Request"
@ -150,6 +152,12 @@ class Worker(object):
mesg = "<p>%s</p>" % str(exc)
elif isinstance(exc, LimitRequestHeaders):
mesg = "<p>Error parsing headers: '%s'</p>" % str(exc)
elif isinstance(exc, InvalidProxyLine):
mesg = "<p>'%s'</p>" % str(exc)
elif isinstance(exc, ForbiddenProxyRequest):
reason = "Forbidden"
mesg = "<p>Request forbidden</p>"
status_int = 403
self.log.debug("Invalid request from ip={ip}: {error}"\
"".format(ip=addr[0],

View File

@ -0,0 +1 @@
PROXY TCP4 192.168.0.1 192.16...\r\n

View File

@ -0,0 +1,7 @@
from gunicorn.config import Config
from gunicorn.http.errors import InvalidProxyLine
cfg = Config()
cfg.set("proxy_protocol", True)
request = InvalidProxyLine

View File

@ -0,0 +1 @@
PROXY TCP4 192.168.0.1 192.168.0.11 65iii 100000\r\n

View File

@ -0,0 +1,7 @@
from gunicorn.config import Config
from gunicorn.http.errors import InvalidProxyLine
cfg = Config()
cfg.set('proxy_protocol', True)
request = InvalidProxyLine

View File

@ -0,0 +1,7 @@
PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n
GET /stuff/here?foo=bar HTTP/1.0\r\n
Server: http://127.0.0.1:5984\r\n
Content-Type: application/json\r\n
Content-Length: 14\r\n
\r\n
{"nom": "nom"}

View File

@ -0,0 +1,16 @@
from gunicorn.config import Config
cfg = Config()
cfg.set('proxy_protocol', True)
request = {
"method": "GET",
"uri": uri("/stuff/here?foo=bar"),
"version": (1, 0),
"headers": [
("SERVER", "http://127.0.0.1:5984"),
("CONTENT-TYPE", "application/json"),
("CONTENT-LENGTH", "14")
],
"body": '{"nom": "nom"}'
}

View File

@ -0,0 +1,15 @@
PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n
GET /stuff/here?foo=bar HTTP/1.1\r\n
Server: http://127.0.0.1:5984\r\n
Content-Type: application/json\r\n
Content-Length: 14\r\n
Connection: keep-alive\r\n
\r\n
{"nom": "nom"}
POST /post_chunked_all_your_base HTTP/1.1\r\n
Transfer-Encoding: chunked\r\n
\r\n
1e\r\n
all your base are belong to us\r\n
0\r\n
\r\n

View File

@ -0,0 +1,30 @@
from gunicorn.config import Config
cfg = Config()
cfg.set("proxy_protocol", True)
req1 = {
"method": "GET",
"uri": uri("/stuff/here?foo=bar"),
"version": (1, 1),
"headers": [
("SERVER", "http://127.0.0.1:5984"),
("CONTENT-TYPE", "application/json"),
("CONTENT-LENGTH", "14"),
("CONNECTION", "keep-alive")
],
"body": '{"nom": "nom"}'
}
req2 = {
"method": "POST",
"uri": uri("/post_chunked_all_your_base"),
"version": (1, 1),
"headers": [
("TRANSFER-ENCODING", "chunked"),
],
"body": "all your base are belong to us"
}
request = [req1, req2]