79 if (url.compare(0, 8,
"https://") == 0)
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 +
"'");
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);
91 u.
path = slash == std::string::npos ?
"/" : rest.substr(slash);
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);
102 const std::size_t colon = authority.rfind(
':');
103 if (colon == std::string::npos) {
106 u.
host = authority.substr(0, colon);
107 u.
port = std::atoi(authority.c_str() + colon + 1);
110 if (u.
host.empty())
throw InputError(
"line::http: no host in '" + url +
"'");
112 throw InputError(
"line::http: port out of range in '" + url +
"'");
121 explicit Socket(
int fd = -1) : fd_(fd) {}
123 if (fd_ >= 0) ::close(fd_);
125 Socket(
const Socket&) =
delete;
126 Socket& operator=(
const Socket&) =
delete;
127 int fd()
const {
return fd_; }
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;
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);
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);
158 int rc = ::connect(fd, ai->ai_addr, ai->ai_addrlen);
159 if (rc != 0 && errno == EINPROGRESS) {
162 pfd.events = POLLOUT;
164 const int pr = ::poll(&pfd, 1, connectMillis > 0 ? connectMillis : -1);
167 socklen_t len =
sizeof(soerr);
168 if (::getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &len) == 0 && soerr == 0)
171 lastError = std::strerror(soerr);
172 }
else if (pr == 0) {
173 lastError =
"connect timed out";
176 lastError = std::strerror(errno);
179 }
else if (rc != 0) {
180 lastError = std::strerror(errno);
184 ::fcntl(fd, F_SETFL, flags);
192 throw HttpError(
"line::http: cannot connect to " + host +
":" + portStr +
" (" +
198inline void set_io_timeout(
int fd,
int millis) {
199 if (millis <= 0)
return;
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));
207inline void send_all(
int fd,
const std::string& data) {
208 std::size_t sent = 0;
209 while (sent < data.size()) {
211 const ssize_t k = ::send(fd, data.data() + sent, data.size() - sent, MSG_NOSIGNAL);
213 const ssize_t k = ::send(fd, data.data() + sent, data.size() - sent, 0);
216 sent +=
static_cast<std::size_t
>(k);
219 if (k < 0 && (errno == EINTR))
continue;
220 throw HttpError(std::string(
"line::http: write failed (") + std::strerror(errno) +
")");
225inline bool read_more(
int fd, std::string& buf) {
228 const ssize_t k = ::recv(fd, chunk,
sizeof(chunk), 0);
230 buf.append(chunk,
static_cast<std::size_t
>(k));
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) +
")");
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);
260inline Response read_response(
int fd) {
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;
266 if (headerEnd == std::string::npos)
267 throw HttpError(
"line::http: the server closed the connection before sending headers");
270 const std::string headers =
"\r\n" + buf.substr(0, headerEnd + 2);
271 std::string body = buf.substr(headerEnd + 4);
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);
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) {
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");
289 const std::size_t size =
290 static_cast<std::size_t
>(std::strtoul(body.c_str() + at,
nullptr, 16));
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");
297 decoded.append(body, at, size);
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;
306 r.body = body.substr(0, want < body.size() ? want : body.size());
308 while (read_more(fd, body)) {
316inline Response request(
const std::string& method,
const std::string& url,
const std::string& body,
317 const std::string& contentType,
int timeoutMillis) {
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);
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";
330 req +=
"Content-Type: " + contentType +
"\r\n";
331 req +=
"Content-Length: " + std::to_string(body.size()) +
"\r\n";
336 send_all(sock.fd(), req);
337 return read_response(sock.fd());
350 return detail::request(
"GET", url, std::string(), std::string(), timeoutMillis);
362 return detail::request(
"POST", url, json,
"application/json; charset=utf-8", timeoutMillis);