LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
websocket.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_UTIL_WEBSOCKET_H
6#define LINE_UTIL_WEBSOCKET_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * Minimal RFC 6455 WebSocket server, enough to serve `LineWebSocketServer`'s
12 * protocol.
13 *
14 * The JAR runs `java -jar jline.jar -p <port>` as a solve server: a client
15 * opens a WebSocket, sends ONE text message whose first line is the CSV
16 * argument list and whose remainder is the model document, and receives the
17 * CLI's output as one text message before the connection closes. This is the
18 * same protocol, spoken directly over a POSIX socket.
19 *
20 * NO DEPENDENCY, for `http.h`'s reason: java-websocket is a link-time
21 * obligation in the JAR and Boost.Beast would pull Asio and its thread
22 * requirements into every binary in this tree, to speak a framing that fits in
23 * two hundred lines. What is implemented is exactly what the protocol uses --
24 * the opening handshake, masked client text frames of any length,
25 * continuation frames, ping/pong and close.
26 *
27 * NO TLS, as `http.h` has none: the server binds a port for a local client, and
28 * a caller who needs an authenticated channel must front it with a proxy rather
29 * than believe this one provides it.
30 *
31 * ONE CONNECTION AT A TIME, deliberately. A solve is CPU-bound and the JAR's
32 * server serves one message per connection and closes; accepting concurrently
33 * would let two solves contend for the same cores and report timings neither
34 * would produce alone.
35 */
36
37#include <cstdint>
38#include <cstring>
39#include <string>
40#include <vector>
41
42#include <arpa/inet.h>
43#include <netinet/in.h>
44#include <sys/socket.h>
45#include <sys/types.h>
46#include <unistd.h>
47
48#include "line/util/error.h"
49
50namespace line {
51namespace ws {
52
53/** Transport-level failure: the port is taken, the peer vanished, a bad frame. */
54class WsError : public Error {
55 public:
56 explicit WsError(const std::string& what) : Error(what) {}
57};
58
59namespace detail {
60
61/** SHA-1 of a byte string, RFC 3174; the handshake's only cryptographic need. */
62inline std::string sha1(const std::string& msg) {
63 std::uint32_t h[5] = {0x67452301u, 0xEFCDAB89u, 0x98BADCFEu, 0x10325476u, 0xC3D2E1F0u};
64 std::string data = msg;
65 const std::uint64_t bitlen = static_cast<std::uint64_t>(data.size()) * 8ull;
66 data.push_back(static_cast<char>(0x80));
67 while (data.size() % 64 != 56) data.push_back('\0');
68 for (int i = 7; i >= 0; --i)
69 data.push_back(static_cast<char>((bitlen >> (i * 8)) & 0xFF));
70
71 for (std::size_t off = 0; off < data.size(); off += 64) {
72 std::uint32_t w[80];
73 for (int i = 0; i < 16; ++i) {
74 const unsigned char* p =
75 reinterpret_cast<const unsigned char*>(data.data() + off + i * 4);
76 w[i] = (std::uint32_t(p[0]) << 24) | (std::uint32_t(p[1]) << 16) |
77 (std::uint32_t(p[2]) << 8) | std::uint32_t(p[3]);
78 }
79 for (int i = 16; i < 80; ++i) {
80 const std::uint32_t v = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
81 w[i] = (v << 1) | (v >> 31);
82 }
83 std::uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
84 for (int i = 0; i < 80; ++i) {
85 std::uint32_t f = 0, k = 0;
86 if (i < 20) {
87 f = (b & c) | ((~b) & d);
88 k = 0x5A827999u;
89 } else if (i < 40) {
90 f = b ^ c ^ d;
91 k = 0x6ED9EBA1u;
92 } else if (i < 60) {
93 f = (b & c) | (b & d) | (c & d);
94 k = 0x8F1BBCDCu;
95 } else {
96 f = b ^ c ^ d;
97 k = 0xCA62C1D6u;
98 }
99 const std::uint32_t tmp = ((a << 5) | (a >> 27)) + f + e + k + w[i];
100 e = d;
101 d = c;
102 c = (b << 30) | (b >> 2);
103 b = a;
104 a = tmp;
105 }
106 h[0] += a;
107 h[1] += b;
108 h[2] += c;
109 h[3] += d;
110 h[4] += e;
111 }
112 std::string out(20, '\0');
113 for (int i = 0; i < 5; ++i)
114 for (int j = 0; j < 4; ++j)
115 out[i * 4 + j] = static_cast<char>((h[i] >> ((3 - j) * 8)) & 0xFF);
116 return out;
117}
118
119inline std::string base64(const std::string& in) {
120 static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
121 std::string out;
122 std::size_t i = 0;
123 while (i + 2 < in.size()) {
124 const unsigned v = (static_cast<unsigned char>(in[i]) << 16) |
125 (static_cast<unsigned char>(in[i + 1]) << 8) |
126 static_cast<unsigned char>(in[i + 2]);
127 out.push_back(tbl[(v >> 18) & 63]);
128 out.push_back(tbl[(v >> 12) & 63]);
129 out.push_back(tbl[(v >> 6) & 63]);
130 out.push_back(tbl[v & 63]);
131 i += 3;
132 }
133 if (i + 1 == in.size()) {
134 const unsigned v = static_cast<unsigned char>(in[i]) << 16;
135 out.push_back(tbl[(v >> 18) & 63]);
136 out.push_back(tbl[(v >> 12) & 63]);
137 out += "==";
138 } else if (i + 2 == in.size()) {
139 const unsigned v = (static_cast<unsigned char>(in[i]) << 16) |
140 (static_cast<unsigned char>(in[i + 1]) << 8);
141 out.push_back(tbl[(v >> 18) & 63]);
142 out.push_back(tbl[(v >> 12) & 63]);
143 out.push_back(tbl[(v >> 6) & 63]);
144 out.push_back('=');
145 }
146 return out;
147}
148
149inline bool send_all(int fd, const char* p, std::size_t n) {
150 while (n) {
151 const ssize_t k = ::send(fd, p, n, MSG_NOSIGNAL);
152 if (k <= 0) return false;
153 p += k;
154 n -= static_cast<std::size_t>(k);
155 }
156 return true;
157}
158
159inline bool recv_exact(int fd, char* p, std::size_t n) {
160 while (n) {
161 const ssize_t k = ::recv(fd, p, n, 0);
162 if (k <= 0) return false;
163 p += k;
164 n -= static_cast<std::size_t>(k);
165 }
166 return true;
167}
168
169/** Case-insensitive header lookup over a raw request head. */
170inline std::string header(const std::string& head, const std::string& key) {
171 std::string lower = head, lkey = key;
172 for (std::size_t i = 0; i < lower.size(); ++i)
173 lower[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(lower[i])));
174 for (std::size_t i = 0; i < lkey.size(); ++i)
175 lkey[i] = static_cast<char>(std::tolower(static_cast<unsigned char>(lkey[i])));
176 std::size_t at = lower.find("\n" + lkey + ":");
177 if (at == std::string::npos) return std::string();
178 at += lkey.size() + 2;
179 const std::size_t end = head.find('\n', at);
180 std::string v = head.substr(at, end == std::string::npos ? std::string::npos : end - at);
181 while (!v.empty() && (v[0] == ' ' || v[0] == '\t')) v.erase(v.begin());
182 while (!v.empty() && (v.back() == '\r' || v.back() == ' ')) v.pop_back();
183 return v;
184}
185
186} // namespace detail
187
188/** A listening socket; one connection is served at a time. */
189class Server {
190 public:
191 explicit Server(int port) {
192 fd_ = ::socket(AF_INET, SOCK_STREAM, 0);
193 if (fd_ < 0) throw WsError("cannot create a listening socket");
194 int on = 1;
195 ::setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
196 sockaddr_in a;
197 std::memset(&a, 0, sizeof(a));
198 a.sin_family = AF_INET;
199 a.sin_addr.s_addr = htonl(INADDR_ANY);
200 a.sin_port = htons(static_cast<uint16_t>(port));
201 if (::bind(fd_, reinterpret_cast<sockaddr*>(&a), sizeof(a)) != 0) {
202 ::close(fd_);
203 throw WsError("cannot bind port " + std::to_string(port) +
204 "; another process is probably listening on it");
205 }
206 if (::listen(fd_, 4) != 0) {
207 ::close(fd_);
208 throw WsError("cannot listen on port " + std::to_string(port));
209 }
210 }
212 if (fd_ >= 0) ::close(fd_);
213 }
214 Server(const Server&) = delete;
215 Server& operator=(const Server&) = delete;
216
217 /**
218 * Accept one connection, complete the handshake, read ONE text message and
219 * hand it to `serve`; send what `serve` returns and close.
220 *
221 * @return false when the peer failed before a message arrived, which is a
222 * dropped client and not a reason to stop the server.
223 */
224 template <class Fn>
225 bool serve_one(Fn serve) {
226 const int c = ::accept(fd_, nullptr, nullptr);
227 if (c < 0) return false;
228 const bool ok = handshake(c) && exchange(c, serve);
229 ::close(c);
230 return ok;
231 }
232
233 private:
234 int fd_ = -1;
235
236 static bool handshake(int c) {
237 std::string head;
238 char buf[1024];
239 while (head.find("\r\n\r\n") == std::string::npos) {
240 const ssize_t k = ::recv(c, buf, sizeof(buf), 0);
241 if (k <= 0) return false;
242 head.append(buf, static_cast<std::size_t>(k));
243 if (head.size() > 65536) return false; // not a handshake
244 }
245 const std::string key = detail::header(head, "Sec-WebSocket-Key");
246 if (key.empty()) return false;
247 // The RFC's fixed GUID; the accept token is base64(sha1(key + GUID)).
248 const std::string accept = detail::base64(
249 detail::sha1(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
250 const std::string resp = "HTTP/1.1 101 Switching Protocols\r\n"
251 "Upgrade: websocket\r\n"
252 "Connection: Upgrade\r\n"
253 "Sec-WebSocket-Accept: " + accept + "\r\n\r\n";
254 return detail::send_all(c, resp.data(), resp.size());
255 }
256
257 /** Read one complete text message, possibly fragmented. */
258 static bool read_message(int c, std::string& out) {
259 out.clear();
260 for (;;) {
261 unsigned char h[2];
262 if (!detail::recv_exact(c, reinterpret_cast<char*>(h), 2)) return false;
263 const bool fin = (h[0] & 0x80) != 0;
264 const int opcode = h[0] & 0x0F;
265 const bool masked = (h[1] & 0x80) != 0;
266 std::uint64_t len = h[1] & 0x7F;
267 if (len == 126) {
268 unsigned char e[2];
269 if (!detail::recv_exact(c, reinterpret_cast<char*>(e), 2)) return false;
270 len = (std::uint64_t(e[0]) << 8) | e[1];
271 } else if (len == 127) {
272 unsigned char e[8];
273 if (!detail::recv_exact(c, reinterpret_cast<char*>(e), 8)) return false;
274 len = 0;
275 for (int i = 0; i < 8; ++i) len = (len << 8) | e[i];
276 }
277 // A client frame MUST be masked (RFC 6455 5.1); an unmasked one is
278 // a protocol error, not a frame to decode as if the mask were zero.
279 unsigned char mask[4] = {0, 0, 0, 0};
280 if (masked && !detail::recv_exact(c, reinterpret_cast<char*>(mask), 4)) return false;
281 if (!masked) return false;
282 if (len > (1ull << 30)) return false; // a model document, not a stream
283 std::string payload(static_cast<std::size_t>(len), '\0');
284 if (len && !detail::recv_exact(c, &payload[0], payload.size())) return false;
285 for (std::size_t i = 0; i < payload.size(); ++i)
286 payload[i] = static_cast<char>(payload[i] ^ mask[i % 4]);
287
288 if (opcode == 0x8) return false; // close
289 if (opcode == 0x9) { // ping -> pong, then keep reading
290 std::string pong;
291 pong.push_back(static_cast<char>(0x8A));
292 pong.push_back(static_cast<char>(payload.size() & 0x7F));
293 pong += payload;
294 if (!detail::send_all(c, pong.data(), pong.size())) return false;
295 continue;
296 }
297 if (opcode == 0xA) continue; // pong
298 out += payload;
299 if (fin) return true;
300 }
301 }
302
303 static bool send_text(int c, const std::string& msg) {
304 std::string f;
305 f.push_back(static_cast<char>(0x81)); // FIN + text
306 if (msg.size() < 126) {
307 f.push_back(static_cast<char>(msg.size()));
308 } else if (msg.size() <= 0xFFFF) {
309 f.push_back(static_cast<char>(126));
310 f.push_back(static_cast<char>((msg.size() >> 8) & 0xFF));
311 f.push_back(static_cast<char>(msg.size() & 0xFF));
312 } else {
313 f.push_back(static_cast<char>(127));
314 for (int i = 7; i >= 0; --i)
315 f.push_back(static_cast<char>((static_cast<std::uint64_t>(msg.size()) >> (i * 8)) &
316 0xFF));
317 }
318 f += msg;
319 if (!detail::send_all(c, f.data(), f.size())) return false;
320 // A clean close, so the client sees the message as complete rather than
321 // as a connection that dropped mid-answer.
322 const char close_frame[2] = {static_cast<char>(0x88), 0};
323 detail::send_all(c, close_frame, 2);
324 return true;
325 }
326
327 template <class Fn>
328 static bool exchange(int c, Fn serve) {
329 std::string msg;
330 if (!read_message(c, msg)) return false;
331 return send_text(c, serve(msg));
332 }
333};
334
335} // namespace ws
336} // namespace line
337
338#endif // LINE_UTIL_WEBSOCKET_H
Error(const std::string &what)
Definition error.h:33
Server & operator=(const Server &)=delete
bool serve_one(Fn serve)
Accept one connection, complete the handshake, read ONE text message and hand it to serve; send what ...
Definition websocket.h:225
Server(const Server &)=delete
Server(int port)
Definition websocket.h:191
WsError(const std::string &what)
Definition websocket.h:56
The exception types the port throws.