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 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).
|
||||||
|
"""
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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],
|
||||||
|
|||||||
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