import sys
import time
import traceback
import logging
from datetime import datetime
from greenlet import GreenletExit
from urllib.parse import unquote
import socket
from . import version_info, gyield
from .server import Server
from .exceptions import BROKEN_SOCK
from .support import reraise
log = logging.getLogger('guv.wsgi')
log.setLevel(logging.INFO)
DEFAULT_MAX_SIMULTANEOUS_REQUESTS = 1024
DEFAULT_MAX_HTTP_VERSION = 'HTTP/1.1'
MAX_REQUEST_LINE = 8192
MAX_HEADER_LINE = 8192
MAX_TOTAL_HEADER_SIZE = 65536
MINIMUM_CHUNK_SIZE = 4096
__all__ = ['serve', 'format_date_time']
# weekday and month names for HTTP date/time formatting; always English!
_weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
_monthname = [None, # dummy so we can use 1-based month numbers
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
_INTERNAL_ERROR_STATUS = '500 Internal Server Error'
_INTERNAL_ERROR_BODY = 'Internal Server Error'
_INTERNAL_ERROR_HEADERS = [('Content-Type', 'text/plain'),
('Connection', 'close'),
('Content-Length', str(len(_INTERNAL_ERROR_BODY)))]
_REQUEST_TOO_LONG_RESPONSE = "HTTP/1.1 414 Request URI Too Long\r\n" \
"Connection: close\r\nContent-length: 0\r\n\r\n"
_BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-length: 0\r\n\r\n"
_CONTINUE_RESPONSE = "HTTP/1.1 100 Continue\r\n\r\n"
def b(s):
return s.encode('latin-1')
class Input:
def __init__(self, rfile, content_length, socket=None, chunked_input=False):
self.rfile = rfile
self.content_length = content_length
self.socket = socket
self.position = 0
self.chunked_input = chunked_input
self.chunk_length = -1
def _discard(self):
if self.socket is None and \
(self.position < (self.content_length or 0) or self.chunked_input):
# read and discard body
while 1:
d = self.read(16384)
if not d:
break
def _send_100_continue(self):
if self.socket is not None:
self.socket.sendall(_CONTINUE_RESPONSE)
self.socket = None
def _do_read(self, length=None, use_readline=False):
if use_readline:
reader = self.rfile.readline
else:
reader = self.rfile.read
content_length = self.content_length
if content_length is None:
# Either Content-Length or "Transfer-Encoding: chunked" must be present in a request
# with a body if it was chunked, then this function would have not been called
return ''
self._send_100_continue()
left = content_length - self.position
if length is None:
length = left
elif length > left:
length = left
if not length:
return ''
read = reader(length)
self.position += len(read)
if len(read) < length:
if (use_readline and not read.endswith("\n")) or not use_readline:
raise IOError("unexpected end of file while reading request at position {}"
.format(self.position))
return read
def _chunked_read(self, length=None, use_readline=False):
rfile = self.rfile
self._send_100_continue()
if length == 0:
return ""
if length < 0:
length = None
if use_readline:
reader = self.rfile.readline
else:
reader = self.rfile.read
response = []
while self.chunk_length != 0:
maxreadlen = self.chunk_length - self.position
if length is not None and length < maxreadlen:
maxreadlen = length
if maxreadlen > 0:
data = reader(maxreadlen)
if not data:
self.chunk_length = 0
raise IOError("unexpected end of file while parsing chunked data")
datalen = len(data)
response.append(data)
self.position += datalen
if self.chunk_length == self.position:
rfile.readline()
if length is not None:
length -= datalen
if length == 0:
break
if use_readline and data[-1] == "\n":
break
else:
line = rfile.readline()
if not line.endswith("\n"):
self.chunk_length = 0
raise IOError("unexpected end of file while reading chunked data header")
self.chunk_length = int(line.split(";", 1)[0], 16)
self.position = 0
if self.chunk_length == 0:
rfile.readline()
return ''.join(response)
def read(self, length=None):
if self.chunked_input:
return self._chunked_read(length)
return self._do_read(length)
def readline(self, size=None):
if self.chunked_input:
return self._chunked_read(size, True)
else:
return self._do_read(size, use_readline=True)
def readlines(self, hint=None):
return list(self)
def __iter__(self):
return self
def next(self):
line = self.readline()
if not line:
raise StopIteration
return line
from http import client
class OldMessage(client.HTTPMessage):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.status = ''
def getheader(self, name, default=None):
return self.get(name, default)
@property
def headers(self):
for key, value in self._headers:
yield '%s: %s\r\n' % (key, value)
@property
def typeheader(self):
return self.get('content-type')
def headers_factory(fp, *args):
try:
ret = client.parse_headers(fp, _class=OldMessage)
except client.LineTooLong:
ret = OldMessage()
ret.status = 'Line too long'
return ret
class WSGIHandler:
protocol_version = 'HTTP/1.1'
def __init__(self, client_sock, address, server):
self.MessageClass = headers_factory
self.socket = client_sock
self.client_address = address
self.server = server
self.application = self.server.application
self.rfile = client_sock.makefile('rb', MINIMUM_CHUNK_SIZE)
# set up instance attributes
self.requestline = None
self.status = None
self.time_start = None
self.time_finish = None
self.headers = None
self.content_length = None
self.close_connection = None
self.response_length = None
self.environ = None
self.response_use_chunked = None
self.headers_sent = None
self.result = None
self.code = None
self.response_headers = None
self.provided_date = None
self.provided_content_length = None
def handle(self):
try:
while self.socket is not None:
self.time_start = time.time()
self.time_finish = 0
result = self.handle_one_request()
if result is None:
break
if result is True:
gyield()
continue
self.status, response_body = result
self.socket.sendall(response_body)
if self.time_finish == 0:
self.time_finish = time.time()
self.log_request()
break
finally:
if self.socket is not None:
try:
try:
# read out request data to prevent errno 104 Connection reset by peer
self.socket.recv(16384)
finally:
self.socket.close()
except socket.error:
pass
self.socket = None
self.rfile = None
return self.time_finish - self.time_start
def _check_http_version(self):
version = self.request_version
if not version.startswith('HTTP/'):
return False
version = tuple(int(x) for x in version[5:].split('.'))
if version[1] < 0 or version < (0, 9) or version >= (2, 0):
return False
return True
def read_request(self, raw_requestline):
self.requestline = raw_requestline.rstrip().decode()
words = self.requestline.split()
if len(words) == 3:
self.command, self.path, self.request_version = words
if not self._check_http_version():
self.log_error('Invalid http version: %r', raw_requestline)
return
else:
self.log_error('Invalid HTTP method: %r', raw_requestline)
return
self.headers = self.MessageClass(self.rfile, 0)
if self.headers.status:
self.log_error('Invalid headers status: %r', self.headers.status)
return
if self.headers.get("transfer-encoding", "").lower() == "chunked":
try:
del self.headers["content-length"]
except KeyError:
pass
content_length = self.headers.get("content-length")
if content_length is not None:
content_length = int(content_length)
if content_length < 0:
self.log_error('Invalid Content-Length: %r', content_length)
return
if content_length and self.command in ('HEAD', ):
self.log_error('Unexpected Content-Length')
return
self.content_length = content_length
if self.request_version == "HTTP/1.1":
conn_type = self.headers.get("Connection", "").lower()
if conn_type == "close":
self.close_connection = True
else:
self.close_connection = False
else:
self.close_connection = True
return True
def log_error(self, msg, *args):
try:
message = msg % args
except Exception:
traceback.print_exc()
message = '%r %r' % (msg, args)
try:
message = '%s: %s' % (self.socket, message)
except Exception:
pass
try:
sys.stderr.write(message + '\n')
except Exception:
traceback.print_exc()
def read_requestline(self):
return self.rfile.readline(MAX_REQUEST_LINE)
def handle_one_request(self):
"""Handle one request
:return: None if the connection should be closed; True if everything is ok and we can
proceed to read more requests; tuple[status code, message] if there is an HTTP error
:rtype: None or bool or tuple[int, str]
"""
if self.rfile.closed:
return
try:
self.requestline = self.read_requestline()
except socket.error:
# "Connection reset by peer" or other socket errors aren't interesting here
return
if not self.requestline:
return
self.response_length = 0
if len(self.requestline) >= MAX_REQUEST_LINE:
return '414', _REQUEST_TOO_LONG_RESPONSE
try:
# for compatibility with older versions of pywsgi, we pass self.requestline as an
# argument there
if not self.read_request(self.requestline):
return '400', _BAD_REQUEST_RESPONSE
except Exception as ex:
traceback.print_exc()
log.error('Invalid request: {}'.format(str(ex) or ex.__class__.__name__))
return '400', _BAD_REQUEST_RESPONSE
self.environ = self.get_environ()
try:
self.handle_one_response()
except socket.error as ex:
if ex.args[0] in BROKEN_SOCK:
log.error(ex)
return
else:
raise
if self.close_connection:
return
if self.rfile.closed:
return
return True # everything is ok, read more requests
def finalize_headers(self):
if self.provided_date is None:
self.response_headers.append(('Date', format_date_time(time.time())))
if self.code not in (304, 204):
# the reply will include message-body; make sure we have either Content-Length or
# chunked
if self.provided_content_length is None:
if hasattr(self.result, '__len__'):
self.response_headers.append(
('Content-Length', str(sum(len(chunk) for chunk in self.result))))
else:
if self.request_version != 'HTTP/1.0':
self.response_use_chunked = True
self.response_headers.append(('Transfer-Encoding', 'chunked'))
def _sendall(self, data):
try:
self.socket.sendall(data)
except socket.error as ex:
self.status = 'socket error: %s' % ex
if self.code > 0:
self.code = -self.code
raise
self.response_length += len(data)
def _write(self, data):
if not data:
return
if self.response_use_chunked:
# write the chunked encoding
data = "%x\r\n%s\r\n" % (len(data), data)
self._sendall(data)
def write(self, data):
if self.code in (304, 204) and data:
raise AssertionError('The %s response must have no body' % self.code)
if self.headers_sent:
self._write(data)
else:
if not self.status:
raise AssertionError("The application did not call start_response()")
self._write_with_headers(data)
def _write_with_headers(self, data):
towrite = bytearray()
self.headers_sent = True
self.finalize_headers()
towrite.extend(b('HTTP/1.1 %s\r\n' % self.status))
for header in self.response_headers:
towrite.extend(b('%s: %s\r\n' % header))
towrite.extend(b('\r\n'))
if data:
if self.response_use_chunked:
# write the chunked encoding
towrite.extend(b("%x\r\n%s\r\n" % (len(data), data)))
else:
towrite.extend(data)
self._sendall(towrite)
def start_response(self, status, headers, exc_info=None):
if exc_info:
try:
if self.headers_sent:
# re-raise original exception if headers sent
reraise(exc_info[0], exc_info[1], exc_info[2])
finally:
# avoid dangling circular ref
exc_info = None
self.code = int(status.split(' ', 1)[0])
self.status = status
self.response_headers = headers
provided_connection = None
self.provided_date = None
self.provided_content_length = None
for header, value in headers:
header = header.lower()
if header == 'connection':
provided_connection = value
elif header == 'date':
self.provided_date = value
elif header == 'content-length':
self.provided_content_length = value
if self.request_version == 'HTTP/1.0' and provided_connection is None:
headers.append(('Connection', 'close'))
self.close_connection = True
elif provided_connection == 'close':
self.close_connection = True
if self.code in (304, 204):
if self.provided_content_length is not None and self.provided_content_length != '0':
msg = 'Invalid Content-Length for %s response: %r (must be absent or zero)' % (
self.code, self.provided_content_length)
raise AssertionError(msg)
return self.write
def log_request(self):
log.debug(self.format_request())
def format_request(self):
now = datetime.now().replace(microsecond=0)
length = self.response_length or '-'
if self.time_finish:
delta = '%.6f' % (self.time_finish - self.time_start)
else:
delta = '-'
client_address = self.client_address[0] if isinstance(self.client_address,
tuple) else self.client_address
return '%s [%s] "%s" -> %s %s %s' % (
client_address or '-', now, getattr(self, 'requestline', ''),
(getattr(self, 'status', None) or '000').split()[0], length, delta)
def process_result(self):
for data in self.result:
if data:
self.write(data)
if self.status and not self.headers_sent:
self.write('')
if self.response_use_chunked:
self.socket.sendall('0\r\n\r\n')
self.response_length += 5
def run_application(self):
self.result = self.application(self.environ, self.start_response)
self.process_result()
def handle_one_response(self):
self.time_start = time.time()
self.status = None
self.headers_sent = False
self.result = None
self.response_use_chunked = False
self.response_length = 0
try:
try:
self.run_application()
finally:
close = getattr(self.result, 'close', None)
if close is not None:
close()
self.wsgi_input._discard()
except Exception as e:
self.handle_error(*sys.exc_info())
finally:
self.time_finish = time.time()
self.log_request()
def handle_error(self, type, value, tb):
if not issubclass(type, GreenletExit):
self.server.loop.handle_error(self.environ, type, value, tb)
del tb
if self.response_length:
self.close_connection = True
else:
self.start_response(_INTERNAL_ERROR_STATUS, _INTERNAL_ERROR_HEADERS[:])
self.write(_INTERNAL_ERROR_BODY)
def _headers(self):
key = None
value = None
for header in self.headers.headers:
if key is not None and header[:1] in " \t":
value += header
continue
if key not in (None, 'CONTENT_TYPE', 'CONTENT_LENGTH'):
yield 'HTTP_' + key, value.strip()
key, value = header.split(':', 1)
key = key.replace('-', '_').upper()
if key not in (None, 'CONTENT_TYPE', 'CONTENT_LENGTH'):
yield 'HTTP_' + key, value.strip()
def get_environ(self):
env = self.server.get_environ()
env['REQUEST_METHOD'] = self.command
env['SCRIPT_NAME'] = ''
if '?' in self.path:
path, query = self.path.split('?', 1)
else:
path, query = self.path, ''
env['PATH_INFO'] = unquote(path)
env['QUERY_STRING'] = query
if self.headers.typeheader is not None:
env['CONTENT_TYPE'] = self.headers.typeheader
length = self.headers.getheader('content-length')
if length:
env['CONTENT_LENGTH'] = length
env['SERVER_PROTOCOL'] = self.request_version
client_address = self.client_address
if isinstance(client_address, tuple):
env['REMOTE_ADDR'] = str(client_address[0])
env['REMOTE_PORT'] = str(client_address[1])
for key, value in self._headers():
if key in env:
if 'COOKIE' in key:
env[key] += '; ' + value
else:
env[key] += ',' + value
else:
env[key] = value
if env.get('HTTP_EXPECT') == '100-continue':
socket = self.socket
else:
socket = None
chunked = env.get('HTTP_TRANSFER_ENCODING', '').lower() == 'chunked'
self.wsgi_input = Input(self.rfile, self.content_length, socket=socket,
chunked_input=chunked)
env['wsgi.input'] = self.wsgi_input
return env
class WSGIServer(Server):
#: :type: tuple
server_version = version_info[:2] + sys.version_info[:2]
base_env = {'GATEWAY_INTERFACE': 'CGI/1.1',
'SERVER_SOFTWARE': 'guv/%d.%d Python/%d.%d' % server_version,
'SCRIPT_NAME': '',
'wsgi.version': (1, 0),
'wsgi.multithread': False,
'wsgi.multiprocess': False,
'wsgi.run_once': False}
def __init__(self, server_sock, application=None, environ=None):
super().__init__(server_sock, self.handle_client)
self.application = application
self.set_environ(environ)
self.num_connections = 0
def set_environ(self, environ=None):
if environ is not None:
self.environ = environ
environ_update = getattr(self, 'environ', None)
self.environ = self.base_env.copy()
self.environ['wsgi.url_scheme'] = 'http'
if environ_update is not None:
self.environ.update(environ_update)
if self.environ.get('wsgi.errors') is None:
self.environ['wsgi.errors'] = sys.stderr
def get_environ(self):
return self.environ.copy()
def init_socket(self):
self.update_environ()
def update_environ(self):
address = self.address
if isinstance(address, tuple):
if 'SERVER_NAME' not in self.environ:
try:
name = socket.getfqdn(address[0])
except socket.error:
name = str(address[0])
self.environ['SERVER_NAME'] = name
self.environ.setdefault('SERVER_PORT', str(address[1]))
else:
self.environ.setdefault('SERVER_NAME', '')
self.environ.setdefault('SERVER_PORT', '')
def handle_client(self, client_sock, address):
self.num_connections += 1
# log.debug('Open fd: {0}, Current total number of connections: {1.num_connections}'
# .format(client_sock.fileno(), self))
handler = WSGIHandler(client_sock, address, self)
handler.handle()
# log.debug('Done with fd: {}'.format(client_sock.fileno()))
self.num_connections -= 1
[docs]def serve(server_sock, app, log_output=True):
"""Start up a WSGI server handling requests from the supplied server socket
This function loops forever. The *sock* object will be closed after server exits, but the
underlying file descriptor will remain open, so if you have a dup() of *sock*, it will remain
usable.
:param server_sock: server socket, must be already bound to a port and listening
:param app: WSGI application callable
"""
try:
host, port = server_sock.getsockname()[:2]
log.info('WSGI server starting up on {}:{}'.format(host, port))
wsgi_server = WSGIServer(server_sock, app)
wsgi_server.start()
except (KeyboardInterrupt, SystemExit):
log.debug('KeyboardInterrupt, exiting')
finally:
log.debug('WSGI server exited')
try:
server_sock.close()
except socket.error as e:
if e.args[0] not in BROKEN_SOCK:
traceback.print_exc()