123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- # -*- 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.http.message import HEADER_RE
- from gunicorn.http.errors import InvalidHeader, InvalidHeaderName
- from gunicorn import SERVER_SOFTWARE, SERVER
- import gunicorn.util as util
- # 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,
- "wsgi.input_terminated": True,
- "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, str):
- environ['REMOTE_ADDR'] = client
- elif isinstance(client, bytes):
- environ['REMOTE_ADDR'] = client.decode()
- 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, str):
- 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'] = util.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
- 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:
- util.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, str):
- raise TypeError('%r is not a string' % name)
- if HEADER_RE.search(name):
- raise InvalidHeaderName('%r' % name)
- if not isinstance(value, str):
- raise TypeError('%r is not a string' % value)
- if HEADER_VALUE_RE.search(value):
- raise InvalidHeader('%r' % value)
- value = 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, "latin-1"))
- self.headers_sent = True
- def write(self, arg):
- self.send_headers()
- if not isinstance(arg, bytes):
- 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
- 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
- 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'))
- self.sock.sendfile(respiter.filelike, count=nbytes)
- 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"")
|