diff --git a/docs/source/asyncio-example.rst b/docs/source/asyncio-example.rst index d3afbfd05..2509c225a 100644 --- a/docs/source/asyncio-example.rst +++ b/docs/source/asyncio-example.rst @@ -14,4 +14,13 @@ This example demonstrates some basic asyncio techniques. :encoding: utf-8 +You can use ``cert.crt`` and ``cert.key`` files provided within the repository +or generate your own certificates using `OpenSSL`_: + +.. code-block:: console + + $ openssl req -x509 -newkey rsa:2048 -keyout cert.key -out cert.crt -days 365 -nodes + + .. _asyncio: https://docs.python.org/3/library/asyncio.html +.. _OpenSSL: https://openssl-library.org/source/index.html \ No newline at end of file diff --git a/docs/source/basic-usage.rst b/docs/source/basic-usage.rst index 13d2d8b5e..22cfd4e0c 100644 --- a/docs/source/basic-usage.rst +++ b/docs/source/basic-usage.rst @@ -156,7 +156,7 @@ This is not a socket tutorial, so we're not going to dive too deeply into how this works. If you want more detail about sockets, there are lots of good tutorials on the web that you should investigate. -When you want to listen for incoming connections, the you need to *bind* an +When you want to listen for incoming connections, you need to *bind* an address first. So let's do that. Try setting up your file to look like this: .. code-block:: python @@ -213,20 +213,23 @@ connection object and start handing it data. For now, let's just see what happens as we feed it data. To make HTTP/2 connections, we need a tool that knows how to speak HTTP/2. -Most versions of curl in the wild don't, so let's install a Python tool. In -your Python environment, run ``pip install hyper``. This will install a Python -command-line HTTP/2 tool called ``hyper``. To confirm that it works, try -running this command and verifying that the output looks similar to the one -shown below: +You can simply use `curl`_ or any other client with HTTP/2 support like +`httpx`_. To confirm that it works, try running this command and verifying that +the output looks similar to the one shown below: .. code-block:: console - $ hyper GET https://nghttp2.org/httpbin/get + $ curl --http2 https://nghttp2.org/httpbin/get {'args': {}, 'headers': {'Host': 'nghttp2.org'}, 'origin': '10.0.0.2', 'url': 'https://nghttp2.org/httpbin/get'} + +To use it with our server though, you will need to invoke it with a different +``--http2-prior-knowledge`` flag as we are going to serve over the insecure +connection. + Assuming it works, you're now ready to start sending HTTP/2 data. Back in our ``h2server.py`` script, we're going to want to start handling data. @@ -290,15 +293,16 @@ function. Your ``h2server.py`` should end up looking a like this: handle(sock.accept()[0]) Running that in one shell, in your other shell you can run -``hyper --h2 GET http://localhost:8080/``. That shell should hang, and you -should then see the following output from your ``h2server.py`` shell: +``curl -v --http2-prior-knowledge http://localhost:8080/`` command. +That shell should hang, and you should then see the following output from your +``h2server.py`` shell: .. code-block:: console $ python h2server.py [] -You'll then need to kill ``hyper`` and ``h2server.py`` with Ctrl+C. Feel free +You'll then need to kill ``curl`` and ``h2server.py`` with Ctrl+C. Feel free to do this a few times, to see how things behave. So, what did we see here? When the connection was opened, we used the @@ -307,15 +311,15 @@ socket, in a loop. We then passed that data to the connection object, which returned us a single event object: :class:`RemoteSettingsChanged `. -But what we didn't see was anything else. So it seems like all ``hyper`` did -was change its settings, but nothing else. If you look at the other ``hyper`` +But what we didn't see was anything else. So it seems like all ``curl`` did +was change its settings, but nothing else. If you look at the other ``curl`` window, you'll notice that it hangs for a while and then eventually fails with a socket timeout. It was waiting for something: what? Well, it turns out that at the start of a connection, both sides need to send a bit of data, called "the HTTP/2 preamble". We don't need to get into too much detail here, but basically both sides need to send a single block of HTTP/2 -data that tells the other side what their settings are. ``hyper`` did that, +data that tells the other side what their settings are. ``curl`` did that, but we didn't. Let's do that next. @@ -388,9 +392,10 @@ Your ``h2server.py`` script should now look like this: With this change made, rerun your ``h2server.py`` script and hit it with the -same ``hyper`` command: ``hyper --h2 GET http://localhost:8080/``. The -``hyper`` command still hangs, but this time we get a bit more output from our -``h2server.py`` script: +same ``curl`` command: +``curl -v --http2-prior-knowledge http://localhost:8080/``. +The ``curl`` command still hangs, but this time we get a bit more output from +our ``h2server.py`` script: .. code-block:: console @@ -410,17 +415,17 @@ Finally, even more data that triggers *two* events: :class:`RequestReceived ` and :class:`StreamEnded `. -So, what's happening is that ``hyper`` is telling us about its settings, +So, what's happening is that ``curl`` is telling us about its settings, acknowledging ours, and then sending us a request. Then it ends a *stream*, which is a HTTP/2 communications channel that holds a request and response pair. A stream isn't done until it's either *reset* or both sides *close* it: in this sense it's bi-directional. So what the ``StreamEnded`` event tells us -is that ``hyper`` is closing its half of the stream: it won't send us any more +is that ``curl`` is closing its half of the stream: it won't send us any more data on that stream. That means the request is done. -So why is ``hyper`` hanging? Well, we haven't sent a response yet: let's do +So why is ``curl`` hanging? Well, we haven't sent a response yet: let's do that. @@ -489,7 +494,7 @@ one exception is headers: h2 will automatically encode those into UTF-8. The last thing to note is that on our call to ``send_data``, we set ``end_stream`` to ``True``. This tells h2 (and the remote peer) that we're done with sending data: the response is over. Because we know that -``hyper`` will have ended its side of the stream, when we end ours the stream +``curl`` will have ended its side of the stream, when we end ours the stream will be totally done with. We're nearly ready to go with this: we just need to plumb this function in. @@ -581,9 +586,9 @@ With these changes, your ``h2server.py`` file should look like this: while True: handle(sock.accept()[0]) -Alright. Let's run this, and then run our ``hyper`` command again. +Alright. Let's run this, and then run our ``curl`` command again. -This time, nothing is printed from our server, and the ``hyper`` side prints +This time, nothing is printed from our server, and the ``curl`` side prints ``it works!``. Success! Try running it a few more times, and we can see that not only does it work the first time, it works the other times too! @@ -692,15 +697,15 @@ file, which should now look like this: while True: handle(sock.accept()[0]) -Now, execute ``h2server.py`` and then point ``hyper`` at it again. You should -see something like the following output from ``hyper``: +Now, execute ``h2server.py`` and then point ``curl`` at it again. You should +see something like the following output from ``curl``: .. code-block:: console - $ hyper --h2 GET http://localhost:8080/ + $ curl -v --http2-prior-knowledge http://localhost:8080/ {":scheme": "http", ":authority": "localhost", ":method": "GET", ":path": "/"} -Here you can see the HTTP/2 request 'special headers' that ``hyper`` sends. +Here you can see the HTTP/2 request 'special headers' that ``curl`` sends. These are similar to the ``:status`` header we have to send on our response: they encode important parts of the HTTP request in a clearly-defined way. If you were writing a client stack using h2, you'd need to make sure you @@ -744,3 +749,5 @@ it, there are a few directions you could investigate: .. _get your private key here: https://raw.githubusercontent.com/python-hyper/h2/master/examples/twisted/server.key .. _PyOpenSSL: http://pyopenssl.readthedocs.org/ .. _Eventlet example: https://github.com/python-hyper/h2/blob/master/examples/eventlet/eventlet-server.py +.. _curl: https://curl.se/docs/http2.html +.. _httpx: https://www.python-httpx.org/ \ No newline at end of file diff --git a/docs/source/wsgi-example.rst b/docs/source/wsgi-example.rst index 5dedd2709..6039326a0 100644 --- a/docs/source/wsgi-example.rst +++ b/docs/source/wsgi-example.rst @@ -20,4 +20,12 @@ The main advantages of this example are: :encoding: utf-8 +You can use ``cert.crt`` and ``cert.key`` files provided within the repository +or generate your own certificates using `OpenSSL`_: + +.. code-block:: console + + $ openssl req -x509 -newkey rsa:2048 -keyout cert.key -out cert.crt -days 365 -nodes + .. _asyncio: https://docs.python.org/3/library/asyncio.html +.. _OpenSSL: https://openssl-library.org/source/index.html diff --git a/examples/asyncio/asyncio-server.py b/examples/asyncio/asyncio-server.py index 5c41f2bbe..4f288bef3 100644 --- a/examples/asyncio/asyncio-server.py +++ b/examples/asyncio/asyncio-server.py @@ -208,8 +208,8 @@ def window_updated(self, stream_id, delta): loop.run_forever() except KeyboardInterrupt: pass - -# Close the server -server.close() -loop.run_until_complete(server.wait_closed()) -loop.close() +finally: + # Close the server + server.close() + loop.run_until_complete(server.wait_closed()) + loop.close() diff --git a/examples/asyncio/wsgi-server.py b/examples/asyncio/wsgi-server.py index 1680765bd..11105cbb9 100644 --- a/examples/asyncio/wsgi-server.py +++ b/examples/asyncio/wsgi-server.py @@ -193,10 +193,9 @@ def window_opened(self, event): for data in self._flow_controlled_data.values(): self._stream_data.put_nowait(data) - self._flow_controlled_data = {} + self._flow_controlled_data.clear() - @asyncio.coroutine - def sending_loop(self): + async def sending_loop(self): """ A call that loops forever, attempting to send data. This sending loop contains most of the flow-control smarts of this class: it pulls data @@ -216,7 +215,7 @@ def sending_loop(self): This coroutine explicitly *does not end*. """ while True: - stream_id, data, event = yield from self._stream_data.get() + stream_id, data, event = await self._stream_data.get() # If this stream got reset, just drop the data on the floor. Note # that we need to reset the event here to make sure that @@ -327,7 +326,7 @@ def reset_stream(self, event): data. """ if event.stream_id in self._flow_controlled_data: - del self._flow_controlled_data + del self._flow_controlled_data[event.stream_id] self._reset_streams.add(event.stream_id) self.end_stream(event) @@ -534,23 +533,9 @@ def readline(self, hint=None): def readlines(self, hint=None): """ Called by the WSGI application to read several lines of data. - - This method is really pretty stupid. It rigorously observes the - ``hint`` parameter, and quite happily returns the input split into - lines. """ - # This method is *crazy inefficient*, but it's also a pretty stupid - # method to call. data = self.read(hint) - lines = data.split(b'\n') - - # Split removes the newline character, but we want it, so put it back. - lines = [line + b'\n' for line in lines] - - # Except if the last character was a newline character we now have an - # extra line that is just a newline: pull that out. - if lines[-1] == b'\n': - lines = lines[:-1] + lines = data.splitlines(keepends=True) return lines def start_response(self, status, response_headers, exc_info=None): @@ -688,41 +673,41 @@ def _build_environ_dict(headers, stream): version you'd want to fix it. """ header_dict = dict(headers) - path = header_dict.pop(u':path') + path = header_dict.pop(':path') try: - path, query = path.split(u'?', 1) + path, query = path.split('?', 1) except ValueError: - query = u"" - server_name = header_dict.pop(u':authority') + query = "" + server_name = header_dict.pop(':authority') try: - server_name, port = server_name.split(u':', 1) - except ValueError as e: + server_name, port = server_name.split(':', 1) + except ValueError: port = "8443" environ = { - u'REQUEST_METHOD': header_dict.pop(u':method'), - u'SCRIPT_NAME': u'', - u'PATH_INFO': path, - u'QUERY_STRING': query, - u'SERVER_NAME': server_name, - u'SERVER_PORT': port, - u'SERVER_PROTOCOL': u'HTTP/2', - u'HTTPS': u"on", - u'SSL_PROTOCOL': u'TLSv1.2', - u'wsgi.version': (1, 0), - u'wsgi.url_scheme': header_dict.pop(u':scheme'), - u'wsgi.input': stream, - u'wsgi.errors': sys.stderr, - u'wsgi.multithread': True, - u'wsgi.multiprocess': False, - u'wsgi.run_once': False, + 'REQUEST_METHOD': header_dict.pop(':method'), + 'SCRIPT_NAME': '', + 'PATH_INFO': path, + 'QUERY_STRING': query, + 'SERVER_NAME': server_name, + 'SERVER_PORT': port, + 'SERVER_PROTOCOL': 'HTTP/2', + 'HTTPS': "on", + 'SSL_PROTOCOL': 'TLSv1.2', + 'wsgi.version': (1, 0), + 'wsgi.url_scheme': header_dict.pop(':scheme'), + 'wsgi.input': stream, + 'wsgi.errors': sys.stderr, + 'wsgi.multithread': True, + 'wsgi.multiprocess': False, + 'wsgi.run_once': False, } - if u'content-type' in header_dict: - environ[u'CONTENT_TYPE'] = header_dict[u'content-type'] - if u'content-length' in header_dict: - environ[u'CONTENT_LENGTH'] = header_dict[u'content-length'] + if 'content-type' in header_dict: + environ['CONTENT_TYPE'] = header_dict.pop('content-type') + if 'content-length' in header_dict: + environ['CONTENT_LENGTH'] = header_dict.pop('content-length') for name, value in header_dict.items(): - environ[u'HTTP_' + name.upper()] = value + environ['HTTP_' + name.upper()] = value return environ @@ -753,8 +738,8 @@ def _build_environ_dict(headers, stream): loop.run_forever() except KeyboardInterrupt: pass - -# Close the server -server.close() -loop.run_until_complete(server.wait_closed()) -loop.close() +finally: + # Close the server + server.close() + loop.run_until_complete(server.wait_closed()) + loop.close() diff --git a/tox.ini b/tox.ini index 11f5ad2d1..eaf0a4360 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,7 @@ commands = flake8 src/ test/ [testenv:docs] deps = - sphinx>=4.0.2,<5 + sphinx>=5.0.2,<6 allowlist_externals = make changedir = {toxinidir}/docs commands =