nghttp2/python/wsgi.py

120 lines
4.5 KiB
Python

# nghttp2 - HTTP/2.0 C Library
# Copyright (c) 2013 Tatsuhiro Tsujikawa
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import io
import sys
from urllib.parse import urlparse
import nghttp2
def _dance_decode(b):
# TODO faster than looping through and mod-128'ing all unicode points?
return b.decode('utf-8').encode('latin1').decode('latin1')
class WSGIContainer(nghttp2.BaseRequestHandler):
_BASE_ENVIRON = {
'wsgi.version': (1,0),
'wsgi.url_scheme': 'http', # FIXME
'wsgi.multithread': True, # TODO I think?
'wsgi.multiprocess': False, # TODO no idea
'wsgi.run_once': True, # TODO now I'm just guessing
'wsgi.errors': sys.stderr, # TODO will work for testing - is this even used by any frameworks?
}
def __init__(self, app, *args, **kwargs):
super(WSGIContainer, self).__init__(*args, **kwargs)
self.app = app
self.chunks = []
def on_data(self, chunk):
self.chunks.append(chunk)
def on_request_done(self):
environ = WSGIContainer._BASE_ENVIRON.copy()
parsed = urlparse(self.path)
environ['wsgi.input'] = io.BytesIO(b''.join(self.chunks))
for name, value in self.headers:
mangled_name = b'HTTP_' + name.replace(b'-', b'_').upper()
environ[_dance_decode(mangled_name)] = _dance_decode(value)
environ.update(dict(
REQUEST_METHOD=_dance_decode(self.method),
# TODO SCRIPT_NAME? like APPLICATION_ROOT in Flask...
PATH_INFO=_dance_decode(parsed.path),
QUERY_STRING=_dance_decode(parsed.query),
CONTENT_TYPE=environ.get('HTTP_CONTENT_TYPE', ''),
CONTENT_LENGTH=environ.get('HTTP_CONTENT_LENGTH', ''),
SERVER_NAME=_dance_decode(self.host),
SERVER_PORT='', # FIXME probably requires changes in nghttp2
SERVER_PROTOCOL='HTTP/2.0',
))
response_status = [None]
response_headers = [None]
response_chunks = []
def start_response(status, headers, exc_info=None):
if response_status[0] is not None:
raise AssertionError('Response already started')
exc_info = None # avoid dangling circular ref - TODO is this necessary? borrowed from snippet in WSGI spec
response_status[0] = status
response_headers[0] = headers
# TODO handle exc_info
return lambda chunk: response_chunks.append(chunk)
# TODO technically, this breaks the WSGI spec by buffering the status,
# headers, and body until all are completely output from the app before
# writing the response, but it looks like nghttp2 doesn't support any
# other way for now
# TODO disallow yielding/returning before start_response is called
response_chunks.extend(self.app(environ, start_response))
response_body = b''.join(response_chunks)
# TODO automatically set content-length if not provided
self.send_response(
status=response_status[0],
headers=response_headers[0],
body=response_body,
)
def wsgi_app(app):
return lambda *args, **kwargs: WSGIContainer(app, *args, **kwargs)
if __name__ == '__main__':
import ssl
from werkzeug.testapp import test_app
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
ssl_ctx.options = ssl.OP_ALL | ssl.OP_NO_SSLv2
ssl_ctx.load_cert_chain('server.crt', 'server.key')
server = nghttp2.HTTP2Server(('127.0.0.1', 8443), wsgi_app(test_app),
ssl=ssl_ctx)
server.serve_forever()