mirror of
https://github.com/frappe/gunicorn.git
synced 2026-01-14 11:09:11 +08:00
Implantation proxy protocol
This commit is contained in:
parent
aa22115cfc
commit
70534acde8
@ -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).
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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],
|
||||
|
||||
1
tests/requests/invalid/pp_01.http
Normal file
1
tests/requests/invalid/pp_01.http
Normal file
@ -0,0 +1 @@
|
||||
PROXY TCP4 192.168.0.1 192.16...\r\n
|
||||
7
tests/requests/invalid/pp_01.py
Normal file
7
tests/requests/invalid/pp_01.py
Normal 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
|
||||
1
tests/requests/invalid/pp_02.http
Normal file
1
tests/requests/invalid/pp_02.http
Normal file
@ -0,0 +1 @@
|
||||
PROXY TCP4 192.168.0.1 192.168.0.11 65iii 100000\r\n
|
||||
7
tests/requests/invalid/pp_02.py
Normal file
7
tests/requests/invalid/pp_02.py
Normal 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
|
||||
7
tests/requests/valid/pp_01.http
Normal file
7
tests/requests/valid/pp_01.http
Normal 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"}
|
||||
16
tests/requests/valid/pp_01.py
Normal file
16
tests/requests/valid/pp_01.py
Normal 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"}'
|
||||
}
|
||||
15
tests/requests/valid/pp_02.http
Normal file
15
tests/requests/valid/pp_02.http
Normal 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
|
||||
30
tests/requests/valid/pp_02.py
Normal file
30
tests/requests/valid/pp_02.py
Normal 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]
|
||||
Loading…
x
Reference in New Issue
Block a user