78 if (url.compare(0, 8,
"https://") == 0)
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 +
"'");
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);
90 u.
path = slash == std::string::npos ?
"/" : rest.substr(slash);
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);
101 const std::size_t colon = authority.rfind(
':');
102 if (colon == std::string::npos) {
105 u.
host = authority.substr(0, colon);
106 u.
port = std::atoi(authority.c_str() + colon + 1);
109 if (u.
host.empty())
throw InputError(
"line::http: no host in '" + url +
"'");
111 throw InputError(
"line::http: port out of range in '" + url +
"'");
120 explicit Socket(
int fd = -1) : fd_(fd) {}
122 if (fd_ >= 0) ::close(fd_);
124 Socket(
const Socket&) =
delete;
125 Socket& operator=(
const Socket&) =
delete;
126 int fd()
const {
return fd_; }
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;
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);
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);
157 int rc = ::connect(fd, ai->ai_addr, ai->ai_addrlen);
158 if (rc != 0 && errno == EINPROGRESS) {
161 pfd.events = POLLOUT;
163 const int pr = ::poll(&pfd, 1, connectMillis > 0 ? connectMillis : -1);
166 socklen_t len =
sizeof(soerr);
167 if (::getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &len) == 0 && soerr == 0)
170 lastError = std::strerror(soerr);
171 }
else if (pr == 0) {
172 lastError =
"connect timed out";
175 lastError = std::strerror(errno);
178 }
else if (rc != 0) {
179 lastError = std::strerror(errno);
183 ::fcntl(fd, F_SETFL, flags);
191 throw HttpError(
"line::http: cannot connect to " + host +
":" + portStr +
" (" +
197inline void set_io_timeout(
int fd,
int millis) {
198 if (millis <= 0)
return;
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));
206inline void send_all(
int fd,
const std::string& data) {
207 std::size_t sent = 0;
208 while (sent < data.size()) {
210 const ssize_t k = ::send(fd, data.data() + sent, data.size() - sent, MSG_NOSIGNAL);
212 const ssize_t k = ::send(fd, data.data() + sent, data.size() - sent, 0);
215 sent +=
static_cast<std::size_t
>(k);
218 if (k < 0 && (errno == EINTR))
continue;
219 throw HttpError(std::string(
"line::http: write failed (") + std::strerror(errno) +
")");
224inline bool read_more(
int fd, std::string& buf) {
227 const ssize_t k = ::recv(fd, chunk,
sizeof(chunk), 0);
229 buf.append(chunk,
static_cast<std::size_t
>(k));
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) +
")");
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);
259inline Response read_response(
int fd) {
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;
265 if (headerEnd == std::string::npos)
266 throw HttpError(
"line::http: the server closed the connection before sending headers");
269 const std::string headers =
"\r\n" + buf.substr(0, headerEnd + 2);
270 std::string body = buf.substr(headerEnd + 4);
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);
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) {
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");
288 const std::size_t size =
289 static_cast<std::size_t
>(std::strtoul(body.c_str() + at,
nullptr, 16));
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");
296 decoded.append(body, at, size);
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;
305 r.body = body.substr(0, want < body.size() ? want : body.size());
307 while (read_more(fd, body)) {
315inline Response request(
const std::string& method,
const std::string& url,
const std::string& body,
316 const std::string& contentType,
int timeoutMillis) {
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);
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";
329 req +=
"Content-Type: " + contentType +
"\r\n";
330 req +=
"Content-Length: " + std::to_string(body.size()) +
"\r\n";
335 send_all(sock.fd(), req);
336 return read_response(sock.fd());
349 return detail::request(
"GET", url, std::string(), std::string(), timeoutMillis);
361 return detail::request(
"POST", url, json,
"application/json; charset=utf-8", timeoutMillis);