mirror of
https://github.com/Sneed-Group/Poodletooth-iLand
synced 2024-12-27 05:32:41 -06:00
658 lines
22 KiB
Python
658 lines
22 KiB
Python
# Copyright (c) 2005-2009, eventlet contributors
|
|
# Copyright (c) 2009-2011, gevent contributors
|
|
|
|
import errno
|
|
import sys
|
|
import time
|
|
import traceback
|
|
import mimetools
|
|
from datetime import datetime
|
|
from urllib import unquote
|
|
|
|
from gevent import socket
|
|
import gevent
|
|
from gevent.server import StreamServer
|
|
from gevent.hub import GreenletExit
|
|
|
|
|
|
__all__ = ['WSGIHandler', 'WSGIServer']
|
|
|
|
|
|
MAX_REQUEST_LINE = 8192
|
|
# 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\nConnection: 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 format_date_time(timestamp):
|
|
year, month, day, hh, mm, ss, wd, _y, _z = time.gmtime(timestamp)
|
|
return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (_WEEKDAYNAME[wd], day, _MONTHNAME[month], year, hh, mm, ss)
|
|
|
|
|
|
class Input(object):
|
|
|
|
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 %s" % (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
|
|
|
|
|
|
class WSGIHandler(object):
|
|
protocol_version = 'HTTP/1.1'
|
|
MessageClass = mimetools.Message
|
|
|
|
def __init__(self, socket, address, server, rfile=None):
|
|
self.socket = socket
|
|
self.client_address = address
|
|
self.server = server
|
|
if rfile is None:
|
|
self.rfile = socket.makefile('rb', -1)
|
|
else:
|
|
self.rfile = rfile
|
|
|
|
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:
|
|
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:
|
|
# read out request data to prevent error: [Errno 104] Connection reset by peer
|
|
try:
|
|
self.socket._sock.recv(16384)
|
|
finally:
|
|
self.socket._sock.close() # do not rely on garbage collection
|
|
self.socket.close()
|
|
except socket.error:
|
|
pass
|
|
self.__dict__.pop('socket', None)
|
|
self.__dict__.pop('rfile', None)
|
|
|
|
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(".")) # "HTTP/"
|
|
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()
|
|
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
|
|
elif len(words) == 2:
|
|
self.command, self.path = words
|
|
if self.command != "GET":
|
|
self.log_error('Expected GET method: %r', raw_requestline)
|
|
return
|
|
self.request_version = "HTTP/0.9"
|
|
# QQQ I'm pretty sure we can drop support for HTTP/0.9
|
|
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":
|
|
conntype = self.headers.get("Connection", "").lower()
|
|
if conntype == "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):
|
|
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:
|
|
ex = sys.exc_info()[1]
|
|
if not isinstance(ex, ValueError):
|
|
traceback.print_exc()
|
|
self.log_error('Invalid request: %s', str(ex) or ex.__class__.__name__)
|
|
return ('400', _BAD_REQUEST_RESPONSE)
|
|
|
|
self.environ = self.get_environ()
|
|
self.application = self.server.application
|
|
try:
|
|
self.handle_one_response()
|
|
except socket.error:
|
|
ex = sys.exc_info()[1]
|
|
# Broken pipe, connection reset by peer
|
|
if ex.args[0] in (errno.EPIPE, errno.ECONNRESET):
|
|
sys.exc_clear()
|
|
return
|
|
else:
|
|
raise
|
|
|
|
if self.close_connection:
|
|
return
|
|
|
|
if self.rfile.closed:
|
|
return
|
|
|
|
return True # 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, 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)
|
|
|
|
if sys.version_info[:2] >= (2, 6):
|
|
|
|
def _write_with_headers(self, data):
|
|
towrite = bytearray()
|
|
self.headers_sent = True
|
|
self.finalize_headers()
|
|
|
|
towrite.extend('HTTP/1.1 %s\r\n' % self.status)
|
|
for header in self.response_headers:
|
|
towrite.extend('%s: %s\r\n' % header)
|
|
|
|
towrite.extend('\r\n')
|
|
if data:
|
|
if self.response_use_chunked:
|
|
## Write the chunked encoding
|
|
towrite.extend("%x\r\n%s\r\n" % (len(data), data))
|
|
else:
|
|
towrite.extend(data)
|
|
self._sendall(towrite)
|
|
|
|
else:
|
|
# Python 2.5 does not have bytearray
|
|
|
|
def _write_with_headers(self, data):
|
|
towrite = []
|
|
self.headers_sent = True
|
|
self.finalize_headers()
|
|
|
|
towrite.append('HTTP/1.1 %s\r\n' % self.status)
|
|
for header in self.response_headers:
|
|
towrite.append('%s: %s\r\n' % header)
|
|
|
|
towrite.append('\r\n')
|
|
if data:
|
|
if self.response_use_chunked:
|
|
## Write the chunked encoding
|
|
towrite.append("%x\r\n%s\r\n" % (len(data), data))
|
|
else:
|
|
towrite.append(data)
|
|
self._sendall(''.join(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
|
|
raise 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 = self.server.log
|
|
if log:
|
|
log.write(self.format_request() + '\n')
|
|
|
|
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:
|
|
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(StreamServer):
|
|
"""A WSGI server based on :class:`StreamServer` that supports HTTPS."""
|
|
|
|
handler_class = WSGIHandler
|
|
base_env = {'GATEWAY_INTERFACE': 'CGI/1.1',
|
|
'SERVER_SOFTWARE': 'gevent/%d.%d Python/%d.%d' % (gevent.version_info[:2] + sys.version_info[:2]),
|
|
'SCRIPT_NAME': '',
|
|
'wsgi.version': (1, 0),
|
|
'wsgi.multithread': False,
|
|
'wsgi.multiprocess': False,
|
|
'wsgi.run_once': False}
|
|
|
|
def __init__(self, listener, application=None, backlog=None, spawn='default', log='default', handler_class=None,
|
|
environ=None, **ssl_args):
|
|
StreamServer.__init__(self, listener, backlog=backlog, spawn=spawn, **ssl_args)
|
|
if application is not None:
|
|
self.application = application
|
|
if handler_class is not None:
|
|
self.handler_class = handler_class
|
|
if log == 'default':
|
|
self.log = sys.stderr
|
|
else:
|
|
self.log = log
|
|
self.set_environ(environ)
|
|
self.set_max_accept()
|
|
|
|
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()
|
|
if self.ssl_enabled:
|
|
self.environ['wsgi.url_scheme'] = 'https'
|
|
else:
|
|
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 set_max_accept(self):
|
|
if self.environ.get('wsgi.multiprocess'):
|
|
self.max_accept = 1
|
|
|
|
def get_environ(self):
|
|
return self.environ.copy()
|
|
|
|
def init_socket(self):
|
|
StreamServer.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(self, socket, address):
|
|
handler = self.handler_class(socket, address, self)
|
|
handler.handle()
|