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 ExternalError {
55public:
56 explicit HttpError(const std::string& what) : ExternalError(ErrorKind{"Http"}, 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 * @return its host, port and path
76 */
77inline Url parse_url(const std::string& url) {
78 if (url.compare(0, 8, "https://") == 0)
79 throw ExternalError(ErrorKind{"Http"},
80 "line::http: https is not supported by this client, which has no TLS; point the "
81 "service URL at an http:// endpoint (a local container is the usual case) or "
82 "terminate TLS in front of it");
83 if (url.compare(0, 7, "http://") != 0)
84 throw InputError("line::http: the URL must start with http://, got '" + url + "'");
85
86 const std::string rest = url.substr(7);
87 const std::size_t slash = rest.find('/');
88 const std::string authority = slash == std::string::npos ? rest : rest.substr(0, slash);
89 Url u;
90 u.path = slash == std::string::npos ? "/" : rest.substr(slash);
91 if (u.path.empty()) u.path = "/";
92
93 if (!authority.empty() && authority[0] == '[') {
94 const std::size_t close = authority.find(']');
95 if (close == std::string::npos)
96 throw InputError("line::http: unterminated IPv6 literal in '" + url + "'");
97 u.host = authority.substr(1, close - 1);
98 if (close + 1 < authority.size() && authority[close + 1] == ':')
99 u.port = std::atoi(authority.c_str() + close + 2);
100 } else {
101 const std::size_t colon = authority.rfind(':');
102 if (colon == std::string::npos) {
103 u.host = authority;
104 } else {
105 u.host = authority.substr(0, colon);
106 u.port = std::atoi(authority.c_str() + colon + 1);
107 }
108 }
109 if (u.host.empty()) throw InputError("line::http: no host in '" + url + "'");
110 if (u.port <= 0 || u.port > 65535)
111 throw InputError("line::http: port out of range in '" + url + "'");
112 return u;
113}
114
115namespace detail {
116
117/** RAII holder so every early return closes the socket. */
118class Socket {
119public:
120 explicit Socket(int fd = -1) : fd_(fd) {}
121 ~Socket() {
122 if (fd_ >= 0) ::close(fd_);
123 }
124 Socket(const Socket&) = delete;
125 Socket& operator=(const Socket&) = delete;
126 int fd() const { return fd_; }
127
128private:
129 int fd_;
130};
131
132/**
133 * Connects with a bounded wait. The connect timeout is enforced by hand through
134 * a nonblocking connect plus poll(): SO_SNDTIMEO does NOT bound connect() on
135 * Linux, so relying on it would let an unreachable host hang for the kernel's
136 * own SYN retry budget, over two minutes.
137 */
138inline int connect_socket(const std::string& host, int port, int connectMillis) {
139 struct addrinfo hints;
140 std::memset(&hints, 0, sizeof(hints));
141 hints.ai_family = AF_UNSPEC;
142 hints.ai_socktype = SOCK_STREAM;
143
144 struct addrinfo* res = nullptr;
145 const std::string portStr = std::to_string(port);
146 if (::getaddrinfo(host.c_str(), portStr.c_str(), &hints, &res) != 0 || res == nullptr)
147 throw HttpError("line::http: cannot resolve " + host + ":" + portStr);
148
149 int connected = -1;
150 std::string lastError = "connection refused";
151 for (struct addrinfo* ai = res; ai != nullptr && connected < 0; ai = ai->ai_next) {
152 const int fd = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
153 if (fd < 0) continue;
154 const int flags = ::fcntl(fd, F_GETFL, 0);
155 ::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
156
157 int rc = ::connect(fd, ai->ai_addr, ai->ai_addrlen);
158 if (rc != 0 && errno == EINPROGRESS) {
159 struct pollfd pfd;
160 pfd.fd = fd;
161 pfd.events = POLLOUT;
162 pfd.revents = 0;
163 const int pr = ::poll(&pfd, 1, connectMillis > 0 ? connectMillis : -1);
164 if (pr > 0) {
165 int soerr = 0;
166 socklen_t len = sizeof(soerr);
167 if (::getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &len) == 0 && soerr == 0)
168 rc = 0;
169 else
170 lastError = std::strerror(soerr);
171 } else if (pr == 0) {
172 lastError = "connect timed out";
173 rc = -1;
174 } else {
175 lastError = std::strerror(errno);
176 rc = -1;
177 }
178 } else if (rc != 0) {
179 lastError = std::strerror(errno);
180 }
181
182 if (rc == 0) {
183 ::fcntl(fd, F_SETFL, flags);
184 connected = fd;
185 } else {
186 ::close(fd);
187 }
188 }
189 ::freeaddrinfo(res);
190 if (connected < 0)
191 throw HttpError("line::http: cannot connect to " + host + ":" + portStr + " (" +
192 lastError + ")");
193 return connected;
194}
195
196/** Applies a receive and send timeout to an established socket. */
197inline void set_io_timeout(int fd, int millis) {
198 if (millis <= 0) return;
199 struct timeval tv;
200 tv.tv_sec = millis / 1000;
201 tv.tv_usec = (millis % 1000) * 1000;
202 ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
203 ::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
204}
205
206inline void send_all(int fd, const std::string& data) {
207 std::size_t sent = 0;
208 while (sent < data.size()) {
209#ifdef MSG_NOSIGNAL
210 const ssize_t k = ::send(fd, data.data() + sent, data.size() - sent, MSG_NOSIGNAL);
211#else
212 const ssize_t k = ::send(fd, data.data() + sent, data.size() - sent, 0);
213#endif
214 if (k > 0) {
215 sent += static_cast<std::size_t>(k);
216 continue;
217 }
218 if (k < 0 && (errno == EINTR)) continue;
219 throw HttpError(std::string("line::http: write failed (") + std::strerror(errno) + ")");
220 }
221}
222
223/** Appends one read to buf. Returns false at end of stream. */
224inline bool read_more(int fd, std::string& buf) {
225 char chunk[8192];
226 while (true) {
227 const ssize_t k = ::recv(fd, chunk, sizeof(chunk), 0);
228 if (k > 0) {
229 buf.append(chunk, static_cast<std::size_t>(k));
230 return true;
231 }
232 if (k == 0) return false;
233 if (errno == EINTR) continue;
234 if (errno == EAGAIN || errno == EWOULDBLOCK)
235 throw HttpError("line::http: read timed out");
236 throw HttpError(std::string("line::http: read failed (") + std::strerror(errno) + ")");
237 }
238}
239
240/** Case-insensitive header lookup over the raw header block. */
241inline std::string header_value(const std::string& headers, const std::string& name) {
242 std::string lowerHeaders(headers);
243 for (std::size_t i = 0; i < lowerHeaders.size(); ++i)
244 lowerHeaders[i] = static_cast<char>(std::tolower(lowerHeaders[i]));
245 std::string key = "\r\n" + name + ":";
246 for (std::size_t i = 0; i < key.size(); ++i)
247 key[i] = static_cast<char>(std::tolower(key[i]));
248 const std::size_t at = lowerHeaders.find(key);
249 if (at == std::string::npos) return std::string();
250 const std::size_t from = at + key.size();
251 const std::size_t eol = headers.find("\r\n", from);
252 std::string v = headers.substr(from, eol == std::string::npos ? std::string::npos : eol - from);
253 const std::size_t b = v.find_first_not_of(" \t");
254 const std::size_t e = v.find_last_not_of(" \t");
255 return b == std::string::npos ? std::string() : v.substr(b, e - b + 1);
256}
257
258/** Reads the status line, the headers and the body, de-chunking if needed. */
259inline Response read_response(int fd) {
260 std::string buf;
261 std::size_t headerEnd = std::string::npos;
262 while ((headerEnd = buf.find("\r\n\r\n")) == std::string::npos) {
263 if (!read_more(fd, buf)) break;
264 }
265 if (headerEnd == std::string::npos)
266 throw HttpError("line::http: the server closed the connection before sending headers");
267
268 // the leading CRLF lets header_value() match the first header too
269 const std::string headers = "\r\n" + buf.substr(0, headerEnd + 2);
270 std::string body = buf.substr(headerEnd + 4);
271
272 Response r;
273 const std::size_t sp = buf.find(' ');
274 if (sp == std::string::npos) throw HttpError("line::http: malformed status line");
275 r.status = std::atoi(buf.c_str() + sp + 1);
276
277 const std::string encoding = header_value(headers, "transfer-encoding");
278 const std::string length = header_value(headers, "content-length");
279 if (encoding.find("chunked") != std::string::npos) {
280 std::string decoded;
281 std::size_t at = 0;
282 while (true) {
283 std::size_t eol;
284 while ((eol = body.find("\r\n", at)) == std::string::npos) {
285 if (!read_more(fd, body))
286 throw HttpError("line::http: truncated chunked body");
287 }
288 const std::size_t size =
289 static_cast<std::size_t>(std::strtoul(body.c_str() + at, nullptr, 16));
290 at = eol + 2;
291 if (size == 0) break;
292 while (body.size() < at + size + 2) {
293 if (!read_more(fd, body))
294 throw HttpError("line::http: truncated chunked body");
295 }
296 decoded.append(body, at, size);
297 at += size + 2;
298 }
299 r.body = decoded;
300 } else if (!length.empty()) {
301 const std::size_t want = static_cast<std::size_t>(std::strtoul(length.c_str(), nullptr, 10));
302 while (body.size() < want) {
303 if (!read_more(fd, body)) break;
304 }
305 r.body = body.substr(0, want < body.size() ? want : body.size());
306 } else {
307 while (read_more(fd, body)) {
308 }
309 r.body = body;
310 }
311 return r;
312}
313
314/** Sends one request on a fresh connection and reads the whole response. */
315inline Response request(const std::string& method, const std::string& url, const std::string& body,
316 const std::string& contentType, int timeoutMillis) {
317 const Url u = parse_url(url);
318 const int connectMillis =
319 timeoutMillis > 0 && timeoutMillis < 30000 ? timeoutMillis : 30000;
320 Socket sock(connect_socket(u.host, u.port, connectMillis));
321 set_io_timeout(sock.fd(), timeoutMillis);
322
323 std::string req = method + " " + u.path + " HTTP/1.1\r\n";
324 req += "Host: " + u.host + ":" + std::to_string(u.port) + "\r\n";
325 req += "User-Agent: line-cpp\r\n";
326 req += "Accept: application/json\r\n";
327 req += "Connection: close\r\n";
328 if (!body.empty()) {
329 req += "Content-Type: " + contentType + "\r\n";
330 req += "Content-Length: " + std::to_string(body.size()) + "\r\n";
331 }
332 req += "\r\n";
333 req += body;
334
335 send_all(sock.fd(), req);
336 return read_response(sock.fd());
337}
338
339} // namespace detail
340
341/**
342 * GET a URL.
343 *
344 * @param url the absolute http:// URL
345 * @param timeoutMillis read timeout in milliseconds, 0 for none
346 * @return the response
347 */
348inline Response get(const std::string& url, int timeoutMillis) {
349 return detail::request("GET", url, std::string(), std::string(), timeoutMillis);
350}
351
352/**
353 * POST a JSON document.
354 *
355 * @param url the absolute http:// URL
356 * @param json the request body
357 * @param timeoutMillis read timeout in milliseconds, 0 for none
358 * @return the response
359 */
360inline Response post_json(const std::string& url, const std::string& json, int timeoutMillis) {
361 return detail::request("POST", url, json, "application/json; charset=utf-8", timeoutMillis);
362}
363
364} // namespace http
365} // namespace line
366
367#endif // LINE_UTIL_HTTP_H
ExternalError(const std::string &what)
Definition error.h:126
InputError(const std::string &what)
Definition error.h:105
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:360
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Definition http.h:348
Url parse_url(const std::string &url)
Splits an http:// URL.
Definition http.h:77
std::string name(const Element *el, const std::string &key="name")
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
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