1# nghttp2 - HTTP/2.0 C Library 2 3# Copyright (c) 2013 Tatsuhiro Tsujikawa 4 5# Permission is hereby granted, free of charge, to any person obtaining 6# a copy of this software and associated documentation files (the 7# "Software"), to deal in the Software without restriction, including 8# without limitation the rights to use, copy, modify, merge, publish, 9# distribute, sublicense, and/or sell copies of the Software, and to 10# permit persons to whom the Software is furnished to do so, subject to 11# the following conditions: 12 13# The above copyright notice and this permission notice shall be 14# included in all copies or substantial portions of the Software. 15 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23import io 24import sys 25from urllib.parse import urlparse 26 27import nghttp2 28 29def _dance_decode(b): 30 # TODO faster than looping through and mod-128'ing all unicode points? 31 return b.decode('utf-8').encode('latin1').decode('latin1') 32 33class WSGIContainer(nghttp2.BaseRequestHandler): 34 35 _BASE_ENVIRON = { 36 'wsgi.version': (1,0), 37 'wsgi.url_scheme': 'http', # FIXME 38 'wsgi.multithread': True, # TODO I think? 39 'wsgi.multiprocess': False, # TODO no idea 40 'wsgi.run_once': True, # TODO now I'm just guessing 41 'wsgi.errors': sys.stderr, # TODO will work for testing - is this even used by any frameworks? 42 } 43 44 def __init__(self, app, *args, **kwargs): 45 super(WSGIContainer, self).__init__(*args, **kwargs) 46 self.app = app 47 self.chunks = [] 48 49 def on_data(self, chunk): 50 self.chunks.append(chunk) 51 52 def on_request_done(self): 53 environ = WSGIContainer._BASE_ENVIRON.copy() 54 parsed = urlparse(self.path) 55 56 environ['wsgi.input'] = io.BytesIO(b''.join(self.chunks)) 57 58 for name, value in self.headers: 59 mangled_name = b'HTTP_' + name.replace(b'-', b'_').upper() 60 environ[_dance_decode(mangled_name)] = _dance_decode(value) 61 62 environ.update(dict( 63 REQUEST_METHOD=_dance_decode(self.method), 64 # TODO SCRIPT_NAME? like APPLICATION_ROOT in Flask... 65 PATH_INFO=_dance_decode(parsed.path), 66 QUERY_STRING=_dance_decode(parsed.query), 67 CONTENT_TYPE=environ.get('HTTP_CONTENT_TYPE', ''), 68 CONTENT_LENGTH=environ.get('HTTP_CONTENT_LENGTH', ''), 69 SERVER_NAME=_dance_decode(self.host), 70 SERVER_PORT='', # FIXME probably requires changes in nghttp2 71 SERVER_PROTOCOL='HTTP/2.0', 72 )) 73 74 response_status = [None] 75 response_headers = [None] 76 response_chunks = [] 77 78 def start_response(status, headers, exc_info=None): 79 if response_status[0] is not None: 80 raise AssertionError('Response already started') 81 exc_info = None # avoid dangling circular ref - TODO is this necessary? borrowed from snippet in WSGI spec 82 83 response_status[0] = status 84 response_headers[0] = headers 85 # TODO handle exc_info 86 87 return lambda chunk: response_chunks.append(chunk) 88 89 # TODO technically, this breaks the WSGI spec by buffering the status, 90 # headers, and body until all are completely output from the app before 91 # writing the response, but it looks like nghttp2 doesn't support any 92 # other way for now 93 94 # TODO disallow yielding/returning before start_response is called 95 response_chunks.extend(self.app(environ, start_response)) 96 response_body = b''.join(response_chunks) 97 98 # TODO automatically set content-length if not provided 99 self.send_response( 100 status=response_status[0], 101 headers=response_headers[0], 102 body=response_body, 103 ) 104 105def wsgi_app(app): 106 return lambda *args, **kwargs: WSGIContainer(app, *args, **kwargs) 107 108 109if __name__ == '__main__': 110 import ssl 111 from werkzeug.testapp import test_app 112 113 ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 114 ssl_ctx.options = ssl.OP_ALL | ssl.OP_NO_SSLv2 115 ssl_ctx.load_cert_chain('server.crt', 'server.key') 116 117 server = nghttp2.HTTP2Server(('127.0.0.1', 8443), wsgi_app(test_app), 118 ssl=ssl_ctx) 119 server.serve_forever() 120