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 callable needs to accept two instance variables for the Arbiter and
the just-exited Worker. 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): def __str__(self):
return self.msg 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 re
import urlparse import urlparse
import socket
from errno import ENOTCONN
try: try:
from cStringIO import StringIO from cStringIO import StringIO
except ImportError: except ImportError:
from StringIO import StringIO from StringIO import StringIO
from gunicorn.http.unreader import SocketUnreader
from gunicorn.http.body import ChunkedReader, LengthReader, EOFReader, Body from gunicorn.http.body import ChunkedReader, LengthReader, EOFReader, Body
from gunicorn.http.errors import InvalidHeader, InvalidHeaderName, NoMoreData, \ from gunicorn.http.errors import InvalidHeader, InvalidHeaderName, NoMoreData, \
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, \ InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, \
LimitRequestLine, LimitRequestHeaders LimitRequestLine, LimitRequestHeaders
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
MAX_REQUEST_LINE = 8190 MAX_REQUEST_LINE = 8190
MAX_HEADERS = 32768 MAX_HEADERS = 32768
@ -131,7 +135,7 @@ class Message(object):
class Request(Message): 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.methre = re.compile("[A-Z0-9$-_.]{3,20}")
self.versre = re.compile("HTTP/(\d+).(\d+)") self.versre = re.compile("HTTP/(\d+).(\d+)")
@ -146,6 +150,10 @@ class Request(Message):
if (self.limit_request_line < 0 if (self.limit_request_line < 0
or self.limit_request_line >= MAX_REQUEST_LINE): or self.limit_request_line >= MAX_REQUEST_LINE):
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) super(Request, self).__init__(cfg, unreader)
@ -161,27 +169,19 @@ class Request(Message):
buf = StringIO() buf = StringIO()
self.get_data(unreader, buf, stop=True) self.get_data(unreader, buf, stop=True)
# Request line # get request line
data = buf.getvalue() line, rbuf = self.read_line(unreader, buf, self.limit_request_line)
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
# check if chunk is too large before read next chunk # proxy protocol
if len(data) - 2 > self.limit_request_line and \ if self.proxy_protocol(line):
self.limit_request_line> 0 : # get next request line
raise LimitRequestLine(len(data), self.limit_request_line) buf = StringIO()
buf.write(rbuf)
line, rbuf = self.read_line(unreader, buf, self.limit_request_line)
self.get_data(unreader, buf) self.parse_request_line(line)
data = buf.getvalue()
self.parse_request_line(data[:idx])
buf = StringIO() buf = StringIO()
buf.write(data[idx+2:]) # Skip \r\n buf.write(rbuf)
# Headers # Headers
data = buf.getvalue() data = buf.getvalue()
@ -210,6 +210,103 @@ class Request(Message):
buf = StringIO() buf = StringIO()
return ret 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): def parse_request_line(self, line):
bits = line.split(None, 2) bits = line.split(None, 2)
if len(bits) != 3: if len(bits) != 3:

View File

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

View File

@ -57,6 +57,19 @@ def default_environ(req, sock, cfg):
"SERVER_PROTOCOL": "HTTP/%s" % ".".join(map(str, req.version)) "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): def create(req, sock, client, server, cfg):
resp = Response(req, sock) resp = Response(req, sock)
@ -149,6 +162,8 @@ def create(req, sock, client, server, cfg):
environ['PATH_INFO'] = unquote(path_info) environ['PATH_INFO'] = unquote(path_info)
environ['SCRIPT_NAME'] = script_name environ['SCRIPT_NAME'] = script_name
environ.update(proxy_environ(req))
return resp, environ return resp, environ
class Response(object): class Response(object):

View File

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