I am reading an HTTP request from the network that I want to use in a handshake with a remote websocket server. The websocket::stream::async_handshake
API does not provide a way to specify the request that should be sent as part of the handshake.
Instead, it suggests using a decorator for the request that beast generates itself. When I use the decorator to modify the beast
request headers, I get an error in the handshake: "The WebSocket handshake Sec-WebSocket-Accept field is invalid".
It looks like boost::beast::websocket::stream
generates its own Sec-WebSocket-Key
value and does not take into account the Sec-WebSocket-Key
header that I passed via the decorator.
How can I solve this problem? Here is the part of the code where async_handshake
is executed (please pay attention to comments):
// request variable points to http request which should be used in websocket handshake
// this request upgrades HTTP to WebSocket
ws_.set_option(boost::beast::websocket::stream_base::decorator(
[request](boost::beast::websocket::request_type& req)
{
// copy all headers from received request to handshake request (including Sec-WebSocket-Key)
for (auto& field : *request)
{
req.set(field.name(), field.value());
}
}));
ws_.async_handshake((*request)["Host"], request->target(),
[this, request](boost::beast::error_code ec)
{
// here is error (i think because Sec-WebSocket-Key is wrong)
if (ec)
{
std::cout << ec.message() << std::endl;
}
});
To the comments, a simple implementation of application-level WS proxy.
Note how it uses the following "embracing" order:
It protects the "protected" headers because they MUST NOT be relayed. It also adds some courtesy headers both ways to indicate the source of the proxied peer (X-Real-Ip
and Server
headers).
Note that it does not currently address SSL, extensions, control frames and/or message fragmentation. This is fine for application level proxying: Perhaps you expressly want different SSL/extensions/ping/etc. on the upstream than the downstream. These are possible reasons to interpose a proxy in the first place.
#include <boost/asio.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <boost/beast.hpp>
#include <iostream>
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
namespace websocket = beast::websocket;
using net::awaitable;
using net::ip::tcp;
using WS = websocket::stream<tcp::socket>;
template <typename From, typename To, bool IsRequest = To::is_request::value>
static void mirror_headers(From const& from, To& to) {
using http::field;
auto inject = [&](auto const& h) {
auto k = h.name_string();
auto v = h.value();
if (to.count(k)) {
if (v != to.at(k)) {
std::cout << "Warning: overriding header " << k << "\n"
<< " - old: " << to.at(k) << "\n"
<< " - new: " << v << std::endl;
}
} else {
std::cout << "Adding header " << k << ": " << v << std::endl;
}
to.set(k, v);
};
for (auto const& h : from.base()) {
if (auto known_field = h.name(); //
(!IsRequest || known_field != field::host) && //
known_field != field::sec_websocket_key && //
known_field != field::sec_websocket_accept) //
{
inject(h);
} else {
std::cout << "Skipping protected header " << known_field << std::endl;
}
}
}
awaitable<void> session(tcp::socket s) {
auto ex = co_await net::this_coro::executor;
std::cout << "Session " << s.remote_endpoint() << " started\n";
// #1 read downstream upgrade request
beast::flat_buffer buf;
websocket::request_type down_req;
co_await async_read(s, buf, down_req);
assert(websocket::is_upgrade(down_req)); // assuming pure WebSocket traffic
assert(buf.size() == 0); // not supporting pipelined requests on upgrade
// adding courtesy headers to downstream request
down_req.set("X-Real-IP", s.remote_endpoint().address().to_string());
// #2 connect upstream
WS downstream(std::move(s));
WS upstream(ex);
co_await upstream.next_layer().async_connect({{}, 8989});
// #3 send decorated upgrade request to upstream
upstream.set_option(websocket::stream_base::decorator([&down_req](websocket::request_type& req) {
std::cout << " ---- Decorating upgrade request" << std::endl;
mirror_headers(down_req, req);
}));
websocket::response_type up_res;
co_await upstream.async_handshake(up_res, "localhost:8989", "/upstream");
// adding courtesy headers to upstream response
if (up_res.count(http::field::server)==0)
up_res.set(http::field::server, "localhost:8989");
// #4 accept downstream upgrade, decorating response with upstream headers
downstream.set_option(websocket::stream_base::decorator([&up_res](websocket::response_type& res) {
std::cout << " ---- Decorating upgrade response" << std::endl;
mirror_headers(up_res, res);
}));
co_await downstream.async_accept(down_req);
std::cout << "Proxying " << downstream.next_layer().remote_endpoint() << " <-> "
<< upstream.next_layer().remote_endpoint() << std::endl;
auto half_duplex = [&](WS& source, WS& sink) -> awaitable<void> {
for (beast::flat_buffer buffer;; buffer.clear()) {
co_await source.async_read_some(buffer, sink.write_buffer_bytes());
sink.binary(source.got_binary());
co_await sink.async_write_some(source.is_message_done(), buffer.cdata());
}
};
using namespace net::experimental::awaitable_operators;
co_await (half_duplex(downstream, upstream) && //
half_duplex(upstream, downstream));
}
awaitable<void> listen(uint16_t port) {
auto ex = co_await net::this_coro::executor;
for (tcp::acceptor acc(ex, {{}, port});;)
co_spawn(ex, session(co_await acc.async_accept()), net::detached);
}
int main() try {
net::io_context ioc;
co_spawn(ioc, listen(7878), net::detached);
ioc.run();
} catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << '\n';
}
With my usual local demo:
It looks like boost::beast::websocket::stream generates its own Sec-WebSocket-Key value and does not take into account the Sec-WebSocket-Key header that I passed via the decorator.
The opposite. If you supply Sec-WebSocket-Key it will be wrong.
That's because Sec-WebSocket-Key depends on the entire upgrade request. It is not for security, and if you pass a value from a different request it will be incorrect for the actual request.
Read here for the role of Sec-WebSocket-Key
in Websocket protocol: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-WebSocket-Key
The value of the key is computed using an algorithm defined in the WebSocket specification, so this does not provide security. Instead, it helps to prevent non-WebSocket clients from inadvertently, or through misuse, requesting a WebSocket connection.
Note that e.g. browsers do not allow you to set the header manually:
This header is automatically added by user agents when a script opens a WebSocket; it cannot be added using the fetch() or XMLHttpRequest.setRequestHeader() methods.
It seems that you had the wrong expectation about this header.