# -*- 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 NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+') 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): 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 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 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 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 scheme 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.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"")