LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
http.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_HTTP_H
6#define LINE_UTIL_HTTP_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * Minimal HTTP/1.1 client, enough to talk to a line-*-rest service.
12 *
13 * The port needs this for api/sym, whose only backend is the line-sage-rest
14 * service: the JAR reaches it through HttpURLConnection, MATLAB through
15 * webwrite and Python through requests, and there is no equivalent in the C++
16 * standard library. Rather than take a dependency on libcurl or Boost.Beast --
17 * the first is a link-time obligation for every binary in the tree, the second
18 * pulls in Asio and its thread requirements -- this speaks the protocol
19 * directly over a POSIX socket. What it supports is exactly what the service
20 * uses: GET, POST of a JSON body, identity and chunked transfer encodings,
21 * connect and read timeouts.
22 *
23 * NO TLS. https:// is REFUSED BY NAME rather than silently downgraded to http.
24 * That matters because the symbolic backend is normally a container on
25 * localhost, where plaintext is correct, and a caller pointing LINE_SAGE_URL at
26 * a remote https endpoint must be told that this client cannot authenticate it
27 * rather than have the request fail obscurely or, worse, travel in the clear.
28 *
29 * Redirects are NOT followed and cookies are not kept: the REST protocol has
30 * neither, and following a redirect silently would let a 302 turn a POST into a
31 * GET and lose the request body.
32 */
33
34#include <cctype>
35#include <cerrno>
36#include <cstddef>
37#include <cstdlib>
38#include <cstring>
39#include <string>
40
41#include <fcntl.h>
42#include <netdb.h>
43#include <poll.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 http {
52
53/** Transport-level failure: unresolvable host, refused connection, timeout. */
54class HttpError : public Error {
55public:
56 explicit HttpError(const std::string& what) : Error(what) {}
57};
58
59/** An HTTP response, with the body already de-chunked. */
60struct Response {
61 int status = 0; ///< HTTP status code
62 std::string body; ///< Response body, decoded
63};
64
65/** The pieces of an http:// URL this client needs. */
66struct Url {
67 std::string host;
68 int port = 80;
69 std::string path = "/";
70};
71
72/**
73 * Splits an http:// URL. Supports host, host:port and [v6addr]:port forms.
74 *
75 * @param url the URL
76 * @return its host, port and path
77 */
78inline Url parse_url(const std::string& url) {
79 if (url.compare(0, 8, "https://") == 0)
80 throw UnsupportedError(
81 "line::http: https is not supported by this client, which has no TLS; point the "
82 "service URL at an http:// endpoint (a local container is the usual case) or "
83 "terminate TLS in front of it");
84 if (url.compare(0, 7, "http://") != 0)
85 throw InputError("line::http: the URL must start with http://, got '" + url + "'");
86
87 const std::string rest = url.substr(7);
88 const std::size_t slash = rest.find('/');
89 const std::string authority = slash == std::string::npos ? rest : rest.substr(0, slash);
90 Url u;
91 u.path = slash == std::string::npos ? "/" : rest.substr(slash);
92 if (u.path.empty()) u.path = "/";
93
94 if (!authority.empty() && authority[0] == '[') {
95 const std::size_t close = authority.find(']');
96 if (close == std::string::npos)
97 throw InputError("line::http: unterminated IPv6 literal in '" + url + "'");
98 u.host = authority.substr(1, close - 1);
99 if (close + 1 < authority.size() && authority[close + 1] == ':')
100 u.port = std::atoi(authority.c_str() + close + 2);
101 } else {
102 const std::size_t colon = authority.rfind(':');
103 if (colon == std::string::npos) {
104 u.host = authority;
105 } else {
106 u.host = authority.substr(0, colon);
107 u.port = std::atoi(authority.c_str() + colon + 1);
108 }
109 }
110 if (u.host.empty()) throw InputError("line::http: no host in '" + url + "'");
111 if (u.port <= 0 || u.port > 65535)
112 throw InputError("line::http: port out of range in '" + url + "'");
113 return u;
114}
115
116namespace detail {
117
118/** RAII holder so every early return closes the socket. */
119class Socket {
120public:
121 explicit Socket(int fd = -1) : fd_(fd) {}
122 ~Socket() {
123 if (fd_ >= 0) ::close(fd_);
124 }
125 Socket(const Socket&) = delete;
126 Socket& operator=(const Socket&) = delete;
127 int fd() const { return fd_; }
128
129private:
130 int fd_;
131};
132
133/**
134 * Connects with a bounded wait. The connect timeout is enforced by hand through
135 * a nonblocking connect plus poll(): SO_SNDTIMEO does NOT bound connect() on
136 * Linux, so relying on it would let an unreachable host hang for the kernel's
137 * own SYN retry budget, over two minutes.
138 */
139inline int connect_socket(const std::string& host, int port, int connectMillis) {
140 struct addrinfo hints;
141 std::memset(&hints, 0, sizeof(hints));
142 hints.ai_family = AF_UNSPEC;
143 hints.ai_socktype = SOCK_STREAM;
144
145 struct addrinfo* res = nullptr;
146 const std::string portStr = std::to_string(port);
147 if (::getaddrinfo(host.c_str(), portStr.c_str(), &hints, &res) != 0 || res == nullptr)
148 throw HttpError("line::http: cannot resolve " + host + ":" + portStr);
149
150 int connected = -1;
151 std::string lastError = "connection refused";
152 for (struct addrinfo* ai = res; ai != nullptr && connected < 0; ai = ai->ai_next) {
153 const int fd = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
154 if (fd < 0) continue;
155 const int flags = ::fcntl(fd, F_GETFL, 0);
156 ::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
157
158 int rc = ::connect(fd, ai->ai_addr, ai->ai_addrlen);
159 if (rc != 0 && errno == EINPROGRESS) {
160 struct pollfd pfd;
161 pfd.fd = fd;
162 pfd.events = POLLOUT;
163 pfd.revents = 0;
164 const int pr = ::poll(&pfd, 1, connectMillis > 0 ? connectMillis : -1);
165 if (pr > 0) {
166 int soerr = 0;
167 socklen_t len = sizeof(soerr);
168 if (::getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &len) == 0 && soerr == 0)
169 rc = 0;
170 else
171 lastError = std::strerror(soerr);
172 } else if (pr == 0) {
173 lastError = "connect timed out";
174 rc = -1;
175 } else {
176 lastError = std::strerror(errno);
177 rc = -1;
178 }
179 } else if (rc != 0) {
180 lastError = std::strerror(errno);
181 }
182
183 if (rc == 0) {
184 ::fcntl(fd, F_SETFL, flags);
185 connected = fd;
186 } else {
187 ::close(fd);
188 }
189 }
190 ::freeaddrinfo(res);
191 if (connected < 0)
192 throw HttpError("line::http: cannot connect to " + host + ":" + portStr + " (" +
193 lastError + ")");
194 return connected;
195}
196
197/** Applies a receive and send timeout to an established socket. */
198inline void set_io_timeout(int fd, int millis) {
199 if (millis <= 0) return;
200 struct timeval tv;
201 tv.tv_sec = millis / 1000;
202 tv.tv_usec = (millis % 1000) * 1000;
203 ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
204 ::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
205}
206
207inline void send_all(int fd, const std::string& data) {
208 std::size_t sent = 0;
209 while (sent < data.size()) {
210#ifdef MSG_NOSIGNAL
211 const ssize_t k = ::send(fd, data.data() + sent, data.size() - sent, MSG_NOSIGNAL);
212#else
213 const ssize_t k = ::send(fd, data.data() + sent, data.size() - sent, 0);
214#endif
215 if (k > 0) {
216 sent += static_cast<std::size_t>(k);
217 continue;
218 }
219 if (k < 0 && (errno == EINTR)) continue;
220 throw HttpError(std::string("line::http: write failed (") + std::strerror(errno) + ")");
221 }
222}
223
224/** Appends one read to buf. Returns false at end of stream. */
225inline bool read_more(int fd, std::string& buf) {
226 char chunk[8192];
227 while (true) {
228 const ssize_t k = ::recv(fd, chunk, sizeof(chunk), 0);
229 if (k > 0) {
230 buf.append(chunk, static_cast<std::size_t>(k));
231 return true;
232 }
233 if (k == 0) return false;
234 if (errno == EINTR) continue;
235 if (errno == EAGAIN || errno == EWOULDBLOCK)
236 throw HttpError("line::http: read timed out");
237 throw HttpError(std::string("line::http: read failed (") + std::strerror(errno) + ")");
238 }
239}
240
241/** Case-insensitive header lookup over the raw header block. */
242inline std::string header_value(const std::string& headers, const std::string& name) {
243 std::string lowerHeaders(headers);
244 for (std::size_t i = 0; i < lowerHeaders.size(); ++i)
245 lowerHeaders[i] = static_cast<char>(std::tolower(lowerHeaders[i]));
246 std::string key = "\r\n" + name + ":";
247 for (std::size_t i = 0; i < key.size(); ++i)
248 key[i] = static_cast<char>(std::tolower(key[i]));
249 const std::size_t at = lowerHeaders.find(key);
250 if (at == std::string::npos) return std::string();
251 const std::size_t from = at + key.size();
252 const std::size_t eol = headers.find("\r\n", from);
253 std::string v = headers.substr(from, eol == std::string::npos ? std::string::npos : eol - from);
254 const std::size_t b = v.find_first_not_of(" \t");
255 const std::size_t e = v.find_last_not_of(" \t");
256 return b == std::string::npos ? std::string() : v.substr(b, e - b + 1);
257}
258
259/** Reads the status line, the headers and the body, de-chunking if needed. */
260inline Response read_response(int fd) {
261 std::string buf;
262 std::size_t headerEnd = std::string::npos;
263 while ((headerEnd = buf.find("\r\n\r\n")) == std::string::npos) {
264 if (!read_more(fd, buf)) break;
265 }
266 if (headerEnd == std::string::npos)
267 throw HttpError("line::http: the server closed the connection before sending headers");
268
269 // the leading CRLF lets header_value() match the first header too
270 const std::string headers = "\r\n" + buf.substr(0, headerEnd + 2);
271 std::string body = buf.substr(headerEnd + 4);
272
273 Response r;
274 const std::size_t sp = buf.find(' ');
275 if (sp == std::string::npos) throw HttpError("line::http: malformed status line");
276 r.status = std::atoi(buf.c_str() + sp + 1);
277
278 const std::string encoding = header_value(headers, "transfer-encoding");
279 const std::string length = header_value(headers, "content-length");
280 if (encoding.find("chunked") != std::string::npos) {
281 std::string decoded;
282 std::size_t at = 0;
283 while (true) {
284 std::size_t eol;
285 while ((eol = body.find("\r\n", at)) == std::string::npos) {
286 if (!read_more(fd, body))
287 throw HttpError("line::http: truncated chunked body");
288 }
289 const std::size_t size =
290 static_cast<std::size_t>(std::strtoul(body.c_str() + at, nullptr, 16));
291 at = eol + 2;
292 if (size == 0) break;
293 while (body.size() < at + size + 2) {
294 if (!read_more(fd, body))
295 throw HttpError("line::http: truncated chunked body");
296 }
297 decoded.append(body, at, size);
298 at += size + 2;
299 }
300 r.body = decoded;
301 } else if (!length.empty()) {
302 const std::size_t want = static_cast<std::size_t>(std::strtoul(length.c_str(), nullptr, 10));
303 while (body.size() < want) {
304 if (!read_more(fd, body)) break;
305 }
306 r.body = body.substr(0, want < body.size() ? want : body.size());
307 } else {
308 while (read_more(fd, body)) {
309 }
310 r.body = body;
311 }
312 return r;
313}
314
315/** Sends one request on a fresh connection and reads the whole response. */
316inline Response request(const std::string& method, const std::string& url, const std::string& body,
317 const std::string& contentType, int timeoutMillis) {
318 const Url u = parse_url(url);
319 const int connectMillis =
320 timeoutMillis > 0 && timeoutMillis < 30000 ? timeoutMillis : 30000;
321 Socket sock(connect_socket(u.host, u.port, connectMillis));
322 set_io_timeout(sock.fd(), timeoutMillis);
323
324 std::string req = method + " " + u.path + " HTTP/1.1\r\n";
325 req += "Host: " + u.host + ":" + std::to_string(u.port) + "\r\n";
326 req += "User-Agent: line-cpp\r\n";
327 req += "Accept: application/json\r\n";
328 req += "Connection: close\r\n";
329 if (!body.empty()) {
330 req += "Content-Type: " + contentType + "\r\n";
331 req += "Content-Length: " + std::to_string(body.size()) + "\r\n";
332 }
333 req += "\r\n";
334 req += body;
335
336 send_all(sock.fd(), req);
337 return read_response(sock.fd());
338}
339
340} // namespace detail
341
342/**
343 * GET a URL.
344 *
345 * @param url the absolute http:// URL
346 * @param timeoutMillis read timeout in milliseconds, 0 for none
347 * @return the response
348 */
349inline Response get(const std::string& url, int timeoutMillis) {
350 return detail::request("GET", url, std::string(), std::string(), timeoutMillis);
351}
352
353/**
354 * POST a JSON document.
355 *
356 * @param url the absolute http:// URL
357 * @param json the request body
358 * @param timeoutMillis read timeout in milliseconds, 0 for none
359 * @return the response
360 */
361inline Response post_json(const std::string& url, const std::string& json, int timeoutMillis) {
362 return detail::request("POST", url, json, "application/json; charset=utf-8", timeoutMillis);
363}
364
365} // namespace http
366} // namespace line
367
368#endif // LINE_UTIL_HTTP_H
Error(const std::string &what)
Definition error.h:33
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
HttpError(const std::string &what)
Definition http.h:56
The exception types the port throws.
Response post_json(const std::string &url, const std::string &json, int timeoutMillis)
POST a JSON document.
Definition http.h:361
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Definition http.h:349
Url parse_url(const std::string &url)
Splits an http:// URL.
Definition http.h:78
An HTTP response, with the body already de-chunked.
Definition http.h:60
std::string body
Response body, decoded.
Definition http.h:62
int status
HTTP status code.
Definition http.h:61
The pieces of an http:// URL this client needs.
Definition http.h:66
std::string host
Definition http.h:67
std::string path
Definition http.h:69