Richard's August Update
Aug 10, 2022Beast and HTTP Redirect
Some months ago, I was asked how to handle HTTP redirect responses in beast. Characteristically, I took a moment to model how I would do that in my head, waved my hands and kind of explained it and that was that.
Then more recently, someone else asked how beast websockets would handle a redirect response when performing a websocket handshake. Now I’m pretty sure that websocket clients have no requirement at all to follow redirects. I believe the WebSocket specification does not allow for such things, but I thought it would be an interesting exercise to cover the topic and provide a working code example and cover it in a blog post.
There are a few reasons I decided to do this:
- Redirects are going to be important for any client-side framework written on Beast.
- There are a few new features in Asio which I thought it would be interesting to showcase.
Code Repositiory
The code for this blog can be found here.
I have tested it on Fedora 36 and GCC-12. The code requires at least boost-1.80.0.beta1, because it takes advantage of
the new change in Asio, which allows the deferred object returned by the asio::deferred
completion token to be
directly co_await
ed. This provides a significant improvement in performance for operations that don’t need the full
functionality of the asio::awaitable<>
type.
Handling a Redirect - General Case
Redirects can be followed with the following generalised algorithm:
set redirect count to 0
while not connected, no egregious errors and redirect limit has not been exceeded:
crack URL
resolve the FQDN of the host specified in the URL
connect to the host
if URL indicates HTTPS:
negotiate TLS
endif
send http request (upgrade request for websocket)
await response
if response is 200ish:
exit success
elseif response is redirect:
increment redirect count
update URL with data found in Location header field
continue
else
exit error
endif
endwhile
Handling a Redirect in C++ with Beast
It turns out that the entire process can be handled in one coroutine. Now remember that an HTTP connection can redirect to an HTTPS connection. So the “connection” type returned from a coroutine that creates a connection, having taken into account any redirects, must handle both transport types.
It’s worth mentioning at this point that if you’re writing for a modern Linux kernel, TLS is now supported natively by the berkley sockets interface. This means that programs need no longer generate one code path for SSL and one for TCP. If this is interesting to you, there is some documentation here. When I get a moment I will create a modified copy of this program that uses Kernel TLS. However, for now, we do it the old-fashioned portable way.
A connection abstraction
First we define some useful types for our variant implementation
…provide TLS and SSL constructors…
…provide access to the underlying (optional) SSL and TCP streams…
… provide functions that return awaitables for the high level functions we will need…
…and finally the implementation details…
The implementation of the various member functions are then all defined in terms of visit
, e.g.:
Note that this function is not actually a coroutine. Since it doesn’t maintain any state during the async operation,
the function can simply return the awaitable
to the calling coroutine. This saves the creation of a coroutine frame
when we don’t need it.
The interface and implementation for this class can be found in websocket_connection.[ch]pp
in the git repo linked
above.
Moveable ssl::stream
?
You may have noticed something in this constructor:
I have std::move
‘d the ssl stream into the WebSocket stream. Until a few versions ago, asio ssl streams were not
moveable, which caused all kinds of issues when wanting to, for example, upgrade an SSL stream connection to a secure
websocket stream.
The Beast library has two workarounds for this:
- Beast provides its own version of ssl::stream, and
beast::websocket::stream
has a specialisation defined which holds a reference to a stream.
These are probably now un-necessary and could arguably be deprecated.
The algorithm in C++20
This part of the code builds a unique pointer to an initialised websocket_connection
object, initialised with either
an SSL stream or a TCP stream as indicated by the result of cracking the URL. For brevity I have used a regex to crack
the URL, but you should check out Vinnie Falco’s new Boost.URL candidate library here.
Vinnie will be looking for reviewers during this library’s submission to Boost later this month, so do keep an eye out
in the Boost mailing list.
Here we are awaiting a connect operation with the result of awaiting a resolve operation. Note the use of
asio::experimental::deferred
. deferred
is quite a versatile completion token which can be used to:
- return an lightweight awaitable, as demonstrated here,
- return a function object which may be later called multiple times with another completion handler; effectively creating a curried initiation,
- be supplied with a completion handler up front in order to create a deferred sequence of chained asynchronous operations; allowing simple composed operations to be built quickly and easily.
In the case that the endpoint we are connecting to is secure, we must do the SSL/TLS handshake:
The function try_handshake simply initiates the form of websocket handshake operation which preserves the http response returned from the server. We will need this in case the websocket connection response is actually a redirect.
And here is the code that handles the actual redirect. Note that in this simplistic implementation, I am replacing the
URL with the Location
field in the web server’s response. In reality, the returned URL could be a relative URL which
would need to be merged into the original URL. Boost.URL handles this nicely.
Once that library is available I’ll upgrade this example.
So with that written, all we need to do is write a simple coroutine to connect, chat and disconnect in order to test:
A Simple Http/WebSocket Server
In order to test this code, I put together a super-simple web server, which is included in the repo and run as part of the demo program.
This web server runs two coroutines, each with its own acceptor. One is the acceptor for HTTP/WS connections and the other is for HTTPS/WSS connections. Of course I could have used beast’s flex helper to auto-deduce WS/WSS on the same port, but I wanted to keep the implementation as simple as possible.
The HTTP server is very simple. All it does is redirect the caller to the same Target
on the WSS server:
The WSS server is minutely more complex. It looks for a URL of the form /websocket-(\d+)(/.*)?
where group 1 is the
“index number” of the request. If the index number is 0, the websocket request is accepted and we head off into a chat
coroutine for the remainder of the connection. If it is non-zero, then the index is decremented, the URL is
reconstructed with the new index, and the redirect response is sent back.
So if for example you requested http://some-server/websocket-2/bar
, you would be redirected along the following path:
https://some-server/websocket-2/bar
(first http to https transition)https://some-server/websocket-1/bar
https://some-server/websocket-0/bar
(websocket handshake accepted on this URL)
Here’s the code:
The run_echo_server
coroutine is about as simple as it gets. Note the use of deferred
as a completion token in order
to create the lightweight awaitable type.
An Example of Cancellation
The server is trivial, but there is one little feature I wanted to demonstrate.
The purpose of the demo is:
- spin up a web server
- connect to the web server a few times and have a chat with it
- exit the program
This then leaves the issue of causing the web server to shut down so as to release its ownership of the underlying
io_context run operation. i.e. if the io_context doesn’t run out of work, the call to io_context::run()
won’t return.
I have taken advantage of the fact that when coroutines are spawned with an associated cancellation slot, the cancellation slot tree propagates down through all child coroutines and asio operations.
So it becomes as simple as:
Define a cancellation signal:
Run the server, passing in the cancellation signal’s slot:
When the client code has completed, it simply needs to cause the signal to emit:
We emit the signal regardless of whether the client ended in an error or not - we want to stop the server in either case
Within the server, we spawn the internal coroutines bound to the cancellation slot. This will cause the slot to
propagate the signal into the subordinate coroutines, causing whatever they are doing to complete with an
operation_aborted
error.
awaitable_operators
makes dealing with parallel coroutines extremely simple.
Here we are creating an outer coroutine which represents the simultaneous execution of the two inner coroutines,
http_server
and wss_server
. The completion token of this outer coroutine is bound to the supplied cancellation slot.
When this slot is invoked, it will propagate the signal into the two subordinate coroutines.
Final output
Here is an example of the output generated by this program, tracking the various redirects and correct shutdown of all IO operations.
Final Note
I have of course cut many corners in this demonstration. The error handling is a bit ropey and I haven’t considered timeouts, connection re-use, etc.
But hopefully this will be useful to anyone reading.
Until next time.
All Posts by This Author
- 08/10/2022 Richard's August Update
- 10/10/2021 Richard's October Update
- 05/30/2021 Richard's May 2021 Update
- 04/30/2021 Richard's April Update
- 03/30/2021 Richard's February/March Update
- 01/31/2021 Richard's January Update
- 01/01/2021 Richard's New Year Update - Reusable HTTP Connections
- 12/22/2020 Richard's November/December Update
- 10/31/2020 Richard's October Update
- 09/30/2020 Richard's September Update
- 09/01/2020 Richard's August Update
- 08/01/2020 Richard's July Update
- 07/01/2020 Richard's May/June Update
- 04/30/2020 Richard's April Update
- 03/31/2020 Richard's March Update
- 02/29/2020 Richard's February Update
- 01/31/2020 Richard's January Update
- View All Posts...