• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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