You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
412 lines
13 KiB
412 lines
13 KiB
7 years ago
|
# -*- coding: utf-8 -
|
||
|
#
|
||
|
# This file is part of gunicorn released under the MIT license.
|
||
|
# See the NOTICE for more information.
|
||
|
|
||
|
import io
|
||
|
import logging
|
||
|
import os
|
||
|
import re
|
||
|
import sys
|
||
|
|
||
|
from gunicorn._compat import unquote_to_wsgi_str
|
||
|
from gunicorn.http.message import HEADER_RE
|
||
|
from gunicorn.http.errors import InvalidHeader, InvalidHeaderName
|
||
|
from gunicorn.six import string_types, binary_type, reraise
|
||
|
from gunicorn import SERVER_SOFTWARE
|
||
|
import gunicorn.util as util
|
||
|
|
||
|
try:
|
||
|
# Python 3.3 has os.sendfile().
|
||
|
from os import sendfile
|
||
|
except ImportError:
|
||
|
try:
|
||
|
from ._sendfile import sendfile
|
||
|
except ImportError:
|
||
|
sendfile = None
|
||
|
|
||
|
# Send files in at most 1GB blocks as some operating systems can have problems
|
||
|
# with sending files in blocks over 2GB.
|
||
|
BLKSIZE = 0x3FFFFFFF
|
||
|
|
||
|
HEADER_VALUE_RE = re.compile(r'[\x00-\x1F\x7F]')
|
||
|
|
||
|
log = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
class FileWrapper(object):
|
||
|
|
||
|
def __init__(self, filelike, blksize=8192):
|
||
|
self.filelike = filelike
|
||
|
self.blksize = blksize
|
||
|
if hasattr(filelike, 'close'):
|
||
|
self.close = filelike.close
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
data = self.filelike.read(self.blksize)
|
||
|
if data:
|
||
|
return data
|
||
|
raise IndexError
|
||
|
|
||
|
|
||
|
class WSGIErrorsWrapper(io.RawIOBase):
|
||
|
|
||
|
def __init__(self, cfg):
|
||
|
# There is no public __init__ method for RawIOBase so
|
||
|
# we don't need to call super() in the __init__ method.
|
||
|
# pylint: disable=super-init-not-called
|
||
|
errorlog = logging.getLogger("gunicorn.error")
|
||
|
handlers = errorlog.handlers
|
||
|
self.streams = []
|
||
|
|
||
|
if cfg.errorlog == "-":
|
||
|
self.streams.append(sys.stderr)
|
||
|
handlers = handlers[1:]
|
||
|
|
||
|
for h in handlers:
|
||
|
if hasattr(h, "stream"):
|
||
|
self.streams.append(h.stream)
|
||
|
|
||
|
def write(self, data):
|
||
|
for stream in self.streams:
|
||
|
try:
|
||
|
stream.write(data)
|
||
|
except UnicodeError:
|
||
|
stream.write(data.encode("UTF-8"))
|
||
|
stream.flush()
|
||
|
|
||
|
|
||
|
def base_environ(cfg):
|
||
|
return {
|
||
|
"wsgi.errors": WSGIErrorsWrapper(cfg),
|
||
|
"wsgi.version": (1, 0),
|
||
|
"wsgi.multithread": False,
|
||
|
"wsgi.multiprocess": (cfg.workers > 1),
|
||
|
"wsgi.run_once": False,
|
||
|
"wsgi.file_wrapper": FileWrapper,
|
||
|
"SERVER_SOFTWARE": SERVER_SOFTWARE,
|
||
|
}
|
||
|
|
||
|
|
||
|
def default_environ(req, sock, cfg):
|
||
|
env = base_environ(cfg)
|
||
|
env.update({
|
||
|
"wsgi.input": req.body,
|
||
|
"gunicorn.socket": sock,
|
||
|
"REQUEST_METHOD": req.method,
|
||
|
"QUERY_STRING": req.query,
|
||
|
"RAW_URI": req.uri,
|
||
|
"SERVER_PROTOCOL": "HTTP/%s" % ".".join([str(v) for v in req.version])
|
||
|
})
|
||
|
return env
|
||
|
|
||
|
|
||
|
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, cfg)
|
||
|
|
||
|
# set initial environ
|
||
|
environ = default_environ(req, sock, cfg)
|
||
|
|
||
|
# default variables
|
||
|
host = None
|
||
|
script_name = os.environ.get("SCRIPT_NAME", "")
|
||
|
|
||
|
# add the headers to the environ
|
||
|
for hdr_name, hdr_value in req.headers:
|
||
|
if hdr_name == "EXPECT":
|
||
|
# handle expect
|
||
|
if hdr_value.lower() == "100-continue":
|
||
|
sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
|
||
|
elif hdr_name == 'HOST':
|
||
|
host = hdr_value
|
||
|
elif hdr_name == "SCRIPT_NAME":
|
||
|
script_name = hdr_value
|
||
|
elif hdr_name == "CONTENT-TYPE":
|
||
|
environ['CONTENT_TYPE'] = hdr_value
|
||
|
continue
|
||
|
elif hdr_name == "CONTENT-LENGTH":
|
||
|
environ['CONTENT_LENGTH'] = hdr_value
|
||
|
continue
|
||
|
|
||
|
key = 'HTTP_' + hdr_name.replace('-', '_')
|
||
|
if key in environ:
|
||
|
hdr_value = "%s,%s" % (environ[key], hdr_value)
|
||
|
environ[key] = hdr_value
|
||
|
|
||
|
# set the url scheme
|
||
|
environ['wsgi.url_scheme'] = req.scheme
|
||
|
|
||
|
# set the REMOTE_* keys in environ
|
||
|
# authors should be aware that REMOTE_HOST and REMOTE_ADDR
|
||
|
# may not qualify the remote addr:
|
||
|
# http://www.ietf.org/rfc/rfc3875
|
||
|
if isinstance(client, string_types):
|
||
|
environ['REMOTE_ADDR'] = client
|
||
|
elif isinstance(client, binary_type):
|
||
|
environ['REMOTE_ADDR'] = str(client)
|
||
|
else:
|
||
|
environ['REMOTE_ADDR'] = client[0]
|
||
|
environ['REMOTE_PORT'] = str(client[1])
|
||
|
|
||
|
# handle the SERVER_*
|
||
|
# Normally only the application should use the Host header but since the
|
||
|
# WSGI spec doesn't support unix sockets, we are using it to create
|
||
|
# viable SERVER_* if possible.
|
||
|
if isinstance(server, string_types):
|
||
|
server = server.split(":")
|
||
|
if len(server) == 1:
|
||
|
# unix socket
|
||
|
if host:
|
||
|
server = host.split(':')
|
||
|
if len(server) == 1:
|
||
|
if req.scheme == "http":
|
||
|
server.append(80)
|
||
|
elif req.scheme == "https":
|
||
|
server.append(443)
|
||
|
else:
|
||
|
server.append('')
|
||
|
else:
|
||
|
# no host header given which means that we are not behind a
|
||
|
# proxy, so append an empty port.
|
||
|
server.append('')
|
||
|
environ['SERVER_NAME'] = server[0]
|
||
|
environ['SERVER_PORT'] = str(server[1])
|
||
|
|
||
|
# set the path and script name
|
||
|
path_info = req.path
|
||
|
if script_name:
|
||
|
path_info = path_info.split(script_name, 1)[1]
|
||
|
environ['PATH_INFO'] = unquote_to_wsgi_str(path_info)
|
||
|
environ['SCRIPT_NAME'] = script_name
|
||
|
|
||
|
# override the environ with the correct remote and server address if
|
||
|
# we are behind a proxy using the proxy protocol.
|
||
|
environ.update(proxy_environ(req))
|
||
|
return resp, environ
|
||
|
|
||
|
|
||
|
class Response(object):
|
||
|
|
||
|
def __init__(self, req, sock, cfg):
|
||
|
self.req = req
|
||
|
self.sock = sock
|
||
|
self.version = SERVER_SOFTWARE
|
||
|
self.status = None
|
||
|
self.chunked = False
|
||
|
self.must_close = False
|
||
|
self.headers = []
|
||
|
self.headers_sent = False
|
||
|
self.response_length = None
|
||
|
self.sent = 0
|
||
|
self.upgrade = False
|
||
|
self.cfg = cfg
|
||
|
|
||
|
def force_close(self):
|
||
|
self.must_close = True
|
||
|
|
||
|
def should_close(self):
|
||
|
if self.must_close or self.req.should_close():
|
||
|
return True
|
||
|
if self.response_length is not None or self.chunked:
|
||
|
return False
|
||
|
if self.req.method == 'HEAD':
|
||
|
return False
|
||
|
if self.status_code < 200 or self.status_code in (204, 304):
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def start_response(self, status, headers, exc_info=None):
|
||
|
if exc_info:
|
||
|
try:
|
||
|
if self.status and self.headers_sent:
|
||
|
reraise(exc_info[0], exc_info[1], exc_info[2])
|
||
|
finally:
|
||
|
exc_info = None
|
||
|
elif self.status is not None:
|
||
|
raise AssertionError("Response headers already set!")
|
||
|
|
||
|
self.status = status
|
||
|
|
||
|
# get the status code from the response here so we can use it to check
|
||
|
# the need for the connection header later without parsing the string
|
||
|
# each time.
|
||
|
try:
|
||
|
self.status_code = int(self.status.split()[0])
|
||
|
except ValueError:
|
||
|
self.status_code = None
|
||
|
|
||
|
self.process_headers(headers)
|
||
|
self.chunked = self.is_chunked()
|
||
|
return self.write
|
||
|
|
||
|
def process_headers(self, headers):
|
||
|
for name, value in headers:
|
||
|
if not isinstance(name, string_types):
|
||
|
raise TypeError('%r is not a string' % name)
|
||
|
|
||
|
if HEADER_RE.search(name):
|
||
|
raise InvalidHeaderName('%r' % name)
|
||
|
|
||
|
if HEADER_VALUE_RE.search(value):
|
||
|
raise InvalidHeader('%r' % value)
|
||
|
|
||
|
value = str(value).strip()
|
||
|
lname = name.lower().strip()
|
||
|
if lname == "content-length":
|
||
|
self.response_length = int(value)
|
||
|
elif util.is_hoppish(name):
|
||
|
if lname == "connection":
|
||
|
# handle websocket
|
||
|
if value.lower().strip() == "upgrade":
|
||
|
self.upgrade = True
|
||
|
elif lname == "upgrade":
|
||
|
if value.lower().strip() == "websocket":
|
||
|
self.headers.append((name.strip(), value))
|
||
|
|
||
|
# ignore hopbyhop headers
|
||
|
continue
|
||
|
self.headers.append((name.strip(), value))
|
||
|
|
||
|
def is_chunked(self):
|
||
|
# Only use chunked responses when the client is
|
||
|
# speaking HTTP/1.1 or newer and there was
|
||
|
# no Content-Length header set.
|
||
|
if self.response_length is not None:
|
||
|
return False
|
||
|
elif self.req.version <= (1, 0):
|
||
|
return False
|
||
|
elif self.req.method == 'HEAD':
|
||
|
# Responses to a HEAD request MUST NOT contain a response body.
|
||
|
return False
|
||
|
elif self.status_code in (204, 304):
|
||
|
# Do not use chunked responses when the response is guaranteed to
|
||
|
# not have a response body.
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def default_headers(self):
|
||
|
# set the connection header
|
||
|
if self.upgrade:
|
||
|
connection = "upgrade"
|
||
|
elif self.should_close():
|
||
|
connection = "close"
|
||
|
else:
|
||
|
connection = "keep-alive"
|
||
|
|
||
|
headers = [
|
||
|
"HTTP/%s.%s %s\r\n" % (self.req.version[0],
|
||
|
self.req.version[1], self.status),
|
||
|
"Server: %s\r\n" % self.version,
|
||
|
"Date: %s\r\n" % util.http_date(),
|
||
|
"Connection: %s\r\n" % connection
|
||
|
]
|
||
|
if self.chunked:
|
||
|
headers.append("Transfer-Encoding: chunked\r\n")
|
||
|
return headers
|
||
|
|
||
|
def send_headers(self):
|
||
|
if self.headers_sent:
|
||
|
return
|
||
|
tosend = self.default_headers()
|
||
|
tosend.extend(["%s: %s\r\n" % (k, v) for k, v in self.headers])
|
||
|
|
||
|
header_str = "%s\r\n" % "".join(tosend)
|
||
|
util.write(self.sock, util.to_bytestring(header_str, "ascii"))
|
||
|
self.headers_sent = True
|
||
|
|
||
|
def write(self, arg):
|
||
|
self.send_headers()
|
||
|
if not isinstance(arg, binary_type):
|
||
|
raise TypeError('%r is not a byte' % arg)
|
||
|
arglen = len(arg)
|
||
|
tosend = arglen
|
||
|
if self.response_length is not None:
|
||
|
if self.sent >= self.response_length:
|
||
|
# Never write more than self.response_length bytes
|
||
|
return
|
||
|
|
||
|
tosend = min(self.response_length - self.sent, tosend)
|
||
|
if tosend < arglen:
|
||
|
arg = arg[:tosend]
|
||
|
|
||
|
# Sending an empty chunk signals the end of the
|
||
|
# response and prematurely closes the response
|
||
|
if self.chunked and tosend == 0:
|
||
|
return
|
||
|
|
||
|
self.sent += tosend
|
||
|
util.write(self.sock, arg, self.chunked)
|
||
|
|
||
|
def can_sendfile(self):
|
||
|
return self.cfg.sendfile is not False and sendfile is not None
|
||
|
|
||
|
def sendfile(self, respiter):
|
||
|
if self.cfg.is_ssl or not self.can_sendfile():
|
||
|
return False
|
||
|
|
||
|
if not util.has_fileno(respiter.filelike):
|
||
|
return False
|
||
|
|
||
|
fileno = respiter.filelike.fileno()
|
||
|
try:
|
||
|
offset = os.lseek(fileno, 0, os.SEEK_CUR)
|
||
|
if self.response_length is None:
|
||
|
filesize = os.fstat(fileno).st_size
|
||
|
|
||
|
# The file may be special and sendfile will fail.
|
||
|
# It may also be zero-length, but that is okay.
|
||
|
if filesize == 0:
|
||
|
return False
|
||
|
|
||
|
nbytes = filesize - offset
|
||
|
else:
|
||
|
nbytes = self.response_length
|
||
|
except (OSError, io.UnsupportedOperation):
|
||
|
return False
|
||
|
|
||
|
self.send_headers()
|
||
|
|
||
|
if self.is_chunked():
|
||
|
chunk_size = "%X\r\n" % nbytes
|
||
|
self.sock.sendall(chunk_size.encode('utf-8'))
|
||
|
|
||
|
sockno = self.sock.fileno()
|
||
|
sent = 0
|
||
|
|
||
|
while sent != nbytes:
|
||
|
count = min(nbytes - sent, BLKSIZE)
|
||
|
sent += sendfile(sockno, fileno, offset + sent, count)
|
||
|
|
||
|
if self.is_chunked():
|
||
|
self.sock.sendall(b"\r\n")
|
||
|
|
||
|
os.lseek(fileno, offset, os.SEEK_SET)
|
||
|
|
||
|
return True
|
||
|
|
||
|
def write_file(self, respiter):
|
||
|
if not self.sendfile(respiter):
|
||
|
for item in respiter:
|
||
|
self.write(item)
|
||
|
|
||
|
def close(self):
|
||
|
if not self.headers_sent:
|
||
|
self.send_headers()
|
||
|
if self.chunked:
|
||
|
util.write_chunk(self.sock, b"")
|