120 lines
4.5 KiB
Python
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()
|