/gunicorn/http/wsgi.py
Python | 422 lines | 388 code | 27 blank | 7 comment | 16 complexity | ccdacd3700a7b410ef8ef8ce14703690 MD5 | raw file
- # -*- 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.six import unquote_to_wsgi_str, string_types, binary_type, reraise
- from gunicorn import SERVER_SOFTWARE
- import gunicorn.six as six
- 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
- NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')
- 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 WSGIErrorsWraper(io.RawIOBase):
- def __init__(self, cfg):
- 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": WSGIErrorsWraper(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
- url_scheme = "https" if cfg.is_ssl else "http"
- script_name = os.environ.get("SCRIPT_NAME", "")
- # set secure_headers
- secure_headers = cfg.secure_scheme_headers
- if client and not isinstance(client, string_types):
- if ('*' not in cfg.forwarded_allow_ips
- and client[0] not in cfg.forwarded_allow_ips):
- secure_headers = {}
- # add the headers tot 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 secure_headers and (hdr_name in secure_headers and
- hdr_value == secure_headers[hdr_name]):
- url_scheme = "https"
- 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 schejeme
- environ['wsgi.url_scheme'] = url_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 and host is not None:
- server = host.split(':')
- if len(server) == 1:
- if url_scheme == "http":
- server.append(80),
- elif url_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.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:
- assert isinstance(name, string_types), "%r is not a string" % name
- 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.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))
- self.headers_sent = True
- def write(self, arg):
- self.send_headers()
- assert isinstance(arg, binary_type), "%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 sendfile_all(self, fileno, sockno, offset, nbytes):
- # Send file in at most 1GB blocks as some operating
- # systems can have problems with sending files in blocks
- # over 2GB.
- BLKSIZE = 0x3FFFFFFF
- if nbytes > BLKSIZE:
- for m in range(0, nbytes, BLKSIZE):
- self.sendfile_all(fileno, sockno, offset, min(nbytes, BLKSIZE))
- offset += BLKSIZE
- nbytes -= BLKSIZE
- else:
- sent = 0
- sent += sendfile(sockno, fileno, offset + sent, nbytes - sent)
- while sent != nbytes:
- sent += sendfile(sockno, fileno, offset + sent, nbytes - sent)
- def sendfile_use_send(self, fileno, fo_offset, nbytes):
- # send file in blocks of 8182 bytes
- BLKSIZE = 8192
- sent = 0
- while sent != nbytes:
- data = os.read(fileno, BLKSIZE)
- if not data:
- break
- sent += len(data)
- if sent > nbytes:
- data = data[:nbytes - sent]
- util.write(self.sock, data, self.chunked)
- def write_file(self, respiter):
- if sendfile is not None and util.is_fileobject(respiter.filelike):
- # sometimes the fileno isn't a callable
- if six.callable(respiter.filelike.fileno):
- fileno = respiter.filelike.fileno()
- else:
- fileno = respiter.filelike.fileno
- fd_offset = os.lseek(fileno, 0, os.SEEK_CUR)
- fo_offset = respiter.filelike.tell()
- nbytes = max(os.fstat(fileno).st_size - fo_offset, 0)
- if self.response_length:
- nbytes = min(nbytes, self.response_length)
- if nbytes == 0:
- return
- self.send_headers()
- if self.cfg.is_ssl:
- self.sendfile_use_send(fileno, fo_offset, nbytes)
- else:
- if self.is_chunked():
- chunk_size = "%X\r\n" % nbytes
- self.sock.sendall(chunk_size.encode('utf-8'))
- self.sendfile_all(fileno, self.sock.fileno(), fo_offset, nbytes)
- if self.is_chunked():
- self.sock.sendall(b"\r\n")
- os.lseek(fileno, fd_offset, os.SEEK_SET)
- else:
- 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"")