8#include <core/Base64.hpp>
14#include <system_error>
18#include "tcp_socket.hpp"
20namespace morph::net::detail {
23inline constexpr std::string_view kWebSocketGuid =
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
28inline std::string computeAcceptKey(std::string_view clientKey) {
29 std::string concatenated{clientKey};
30 concatenated += kWebSocketGuid;
31 auto digest = sha1Digest(concatenated);
32 return ::core::base64::encode(digest.begin(), digest.end());
37inline std::string generateClientKey() {
38 static thread_local std::mt19937 gen{std::random_device{}()};
39 std::uniform_int_distribution<int> dist{0, 255};
40 std::array<std::uint8_t, 16> raw{};
42 b =
static_cast<std::uint8_t
>(dist(gen));
44 return ::core::base64::encode(raw.begin(), raw.end());
52 std::uint16_t port{0};
63inline ParsedWsUrl parseWsUrl(std::string_view url) {
64 constexpr std::string_view kWsScheme =
"ws://";
65 constexpr std::string_view kWssScheme =
"wss://";
66 if (url.substr(0, kWssScheme.size()) == kWssScheme) {
67 throw std::runtime_error(
68 "parseWsUrl: wss:// is not supported by morph::net (plaintext ws:// only; TLS is future work)");
70 if (url.substr(0, kWsScheme.size()) != kWsScheme) {
71 throw std::runtime_error(
"parseWsUrl: URL must start with ws://");
73 std::string_view rest = url.substr(kWsScheme.size());
74 auto slashPos = rest.find(
'/');
75 std::string_view hostPort = (slashPos == std::string_view::npos) ? rest : rest.substr(0, slashPos);
76 std::string path = (slashPos == std::string_view::npos) ?
"/" : std::string(rest.substr(slashPos));
77 auto colonPos = hostPort.rfind(
':');
78 if (colonPos == std::string_view::npos || colonPos == 0) {
79 throw std::runtime_error(
"parseWsUrl: URL must include an explicit host and port (e.g. ws://host:1234)");
81 std::string
host{hostPort.substr(0, colonPos)};
82 std::string_view portStr = hostPort.substr(colonPos + 1);
83 if (portStr.empty()) {
84 throw std::runtime_error(
"parseWsUrl: invalid port in URL");
87 for (
char ch : portStr) {
88 if (ch <
'0' || ch >
'9') {
89 throw std::runtime_error(
"parseWsUrl: invalid port in URL");
91 portVal = (portVal * 10) + (ch -
'0');
92 if (portVal > 65535) {
93 throw std::runtime_error(
"parseWsUrl: port out of range");
97 throw std::runtime_error(
"parseWsUrl: invalid port in URL");
99 return ParsedWsUrl{std::move(host),
static_cast<std::uint16_t
>(portVal), std::move(path)};
106inline std::string buildClientHandshakeRequest(
const ParsedWsUrl& url, std::string_view key) {
108 req +=
"GET " + url.path +
" HTTP/1.1\r\n";
111 req +=
"Host: " + url.host +
":" + std::to_string(
static_cast<unsigned>(url.port)) +
"\r\n";
112 req +=
"Upgrade: websocket\r\n";
113 req +=
"Connection: Upgrade\r\n";
114 req +=
"Sec-WebSocket-Key: " + std::string(key) +
"\r\n";
115 req +=
"Sec-WebSocket-Version: 13\r\n";
123inline std::string buildServerHandshakeResponse(std::string_view clientKey) {
125 resp +=
"HTTP/1.1 101 Switching Protocols\r\n";
126 resp +=
"Upgrade: websocket\r\n";
127 resp +=
"Connection: Upgrade\r\n";
128 resp +=
"Sec-WebSocket-Accept: " + computeAcceptKey(clientKey) +
"\r\n";
133namespace ws_handshake_impl {
135inline std::string toLowerAscii(std::string_view s) {
137 for (
char& c : out) {
138 if (c >=
'A' && c <=
'Z') {
139 c =
static_cast<char>(c -
'A' +
'a');
145inline std::vector<std::string_view> splitLines(std::string_view raw) {
146 std::vector<std::string_view> lines;
147 std::size_t start = 0;
148 while (start < raw.size()) {
149 auto pos = raw.find(
"\r\n", start);
150 if (pos == std::string_view::npos) {
151 lines.push_back(raw.substr(start));
154 lines.push_back(raw.substr(start, pos - start));
160inline std::string_view trimLeadingSpace(std::string_view s) {
162 while (i < s.size() && s[i] ==
' ') {
171struct ClientHandshakeRequest {
185inline ClientHandshakeRequest parseClientHandshakeRequest(std::string_view headerBlock) {
186 auto lines = ws_handshake_impl::splitLines(headerBlock);
188 throw std::runtime_error(
"parseClientHandshakeRequest: empty request");
190 std::string_view requestLine = lines.front();
191 if (requestLine.substr(0, 4) !=
"GET ") {
192 throw std::runtime_error(
"parseClientHandshakeRequest: expected a GET request line");
194 std::size_t
const pathStart = 4;
195 std::size_t
const pathEnd = requestLine.find(
' ', pathStart);
196 if (pathEnd == std::string_view::npos) {
197 throw std::runtime_error(
"parseClientHandshakeRequest: malformed request line");
199 std::string path{requestLine.substr(pathStart, pathEnd - pathStart)};
202 bool hasUpgrade =
false;
203 for (std::size_t i = 1; i < lines.size(); ++i) {
204 std::string_view line = lines[i];
205 auto colon = line.find(
':');
206 if (colon == std::string_view::npos) {
209 std::string name = ws_handshake_impl::toLowerAscii(line.substr(0, colon));
210 std::string_view value = ws_handshake_impl::trimLeadingSpace(line.substr(colon + 1));
211 if (name ==
"sec-websocket-key") {
212 key = std::string(value);
213 }
else if (name ==
"upgrade") {
218 throw std::runtime_error(
"parseClientHandshakeRequest: missing Sec-WebSocket-Key header");
221 throw std::runtime_error(
"parseClientHandshakeRequest: missing Upgrade header");
223 return ClientHandshakeRequest{std::move(key), std::move(path)};
233inline void verifyServerHandshakeResponse(std::string_view headerBlock, std::string_view clientKey) {
234 auto lines = ws_handshake_impl::splitLines(headerBlock);
236 throw std::runtime_error(
"verifyServerHandshakeResponse: empty response");
238 std::string_view statusLine = lines.front();
239 if (statusLine.find(
" 101 ") == std::string_view::npos) {
240 throw std::runtime_error(
"verifyServerHandshakeResponse: server did not return 101 Switching Protocols: " +
241 std::string(statusLine));
244 for (std::size_t i = 1; i < lines.size(); ++i) {
245 std::string_view line = lines[i];
246 auto colon = line.find(
':');
247 if (colon == std::string_view::npos) {
250 std::string name = ws_handshake_impl::toLowerAscii(line.substr(0, colon));
251 if (name ==
"sec-websocket-accept") {
252 accept = std::string(ws_handshake_impl::trimLeadingSpace(line.substr(colon + 1)));
255 std::string expected = computeAcceptKey(clientKey);
256 if (accept != expected) {
257 throw std::runtime_error(
"verifyServerHandshakeResponse: Sec-WebSocket-Accept mismatch");
262struct HandshakeReadResult {
267 std::string leftover;
277inline void waitReadableUntil(
const TcpSocket& socket, std::chrono::steady_clock::time_point deadline) {
278 auto const result = pollUntil(socket.nativeHandle(), POLLIN, deadline);
279 if (result.outcome == PollOutcome::kTimedOut) {
280 throw std::runtime_error(
"readHttpHeaderBlock: handshake timed out");
282 if (result.outcome == PollOutcome::kFailed) {
283 throw std::runtime_error(
"readHttpHeaderBlock: poll failed: " + std::system_category().message(result.error));
305inline HandshakeReadResult readHttpHeaderBlock(TcpSocket& socket,
306 std::chrono::milliseconds timeout = std::chrono::milliseconds{0}) {
309 bool const bounded = timeout.count() > 0;
310 auto const deadline = std::chrono::steady_clock::now() + timeout;
312 auto pos = buf.find(
"\r\n\r\n");
313 if (pos != std::string::npos) {
314 HandshakeReadResult result;
315 result.header = buf.substr(0, pos);
316 result.leftover = buf.substr(pos + 4);
319 if (buf.size() > 64u * 1024u) {
320 throw std::runtime_error(
"readHttpHeaderBlock: header exceeds maximum size (64 KiB)");
323 waitReadableUntil(socket, deadline);
325 std::size_t got = socket.recvSome(chunk,
sizeof(chunk));
327 throw std::runtime_error(
"readHttpHeaderBlock: peer closed connection during handshake");
329 buf.append(chunk, got);
340inline std::string performClientHandshake(TcpSocket& socket,
const ParsedWsUrl& url) {
341 std::string key = generateClientKey();
342 std::string
request = buildClientHandshakeRequest(url, key);
344 HandshakeReadResult
const result = readHttpHeaderBlock(socket);
345 verifyServerHandshakeResponse(result.header, key);
346 return result.leftover;
357inline std::string performServerHandshake(TcpSocket& socket,
358 std::chrono::milliseconds handshakeTimeout = std::chrono::milliseconds{0}) {
359 HandshakeReadResult
const result = readHttpHeaderBlock(socket, handshakeTimeout);
360 ClientHandshakeRequest req = parseClientHandshakeRequest(result.header);
361 std::string response = buildServerHandshakeResponse(req.key);
362 socket.sendAll(response.data(), response.size());
363 return result.leftover;
Command host(std::string const &name, AnyHandle const &args={})
host: a command the application registered with the client.
void request(Store< ViewState, Msg > &store, bridge::BridgeHandler< Model, Sharing > &handler, async::CallbackScope const &scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg)
Executes action and sends its result, or its failure, to store as one message.