#pragma once #ifndef HTTPREQUEST_HPP #define HTTPREQUEST_HPP #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) || defined(__CYGWIN__) # pragma push_macro("WIN32_LEAN_AND_MEAN") # pragma push_macro("NOMINMAX") # ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN # endif // WIN32_LEAN_AND_MEAN # ifndef NOMINMAX # define NOMINMAX # endif // NOMINMAX # include # if _WIN32_WINNT < _WIN32_WINNT_WINXP extern "C" char* _strdup(const char* strSource); # define strdup _strdup # include # endif // _WIN32_WINNT < _WIN32_WINNT_WINXP # include # pragma pop_macro("WIN32_LEAN_AND_MEAN") # pragma pop_macro("NOMINMAX") #else # include # include # include # include # include # include # include # include #endif // defined(_WIN32) || defined(__CYGWIN__) namespace http { class RequestError final : public std::logic_error { public: explicit RequestError(const char* str) : std::logic_error{ str } {} explicit RequestError(const std::string& str) : std::logic_error{ str } {} }; class ResponseError final : public std::runtime_error { public: explicit ResponseError(const char* str) : std::runtime_error{ str } {} explicit ResponseError(const std::string& str) : std::runtime_error{ str } {} }; enum class InternetProtocol : std::uint8_t { V4, V6 }; inline namespace detail { #if defined(_WIN32) || defined(__CYGWIN__) class WinSock final { public: WinSock() { WSADATA wsaData; const auto error = WSAStartup(MAKEWORD(2, 2), &wsaData); if (error != 0) throw std::system_error{ error, std::system_category(), "WSAStartup failed" }; if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { WSACleanup(); throw std::runtime_error{ "Invalid WinSock version" }; } started = true; } ~WinSock() { if (started) WSACleanup(); } WinSock(WinSock&& other) noexcept : started{ other.started } { other.started = false; } WinSock& operator=(WinSock&& other) noexcept { if (&other == this) return *this; if (started) WSACleanup(); started = other.started; other.started = false; return *this; } private: bool started = false; }; #endif // defined(_WIN32) || defined(__CYGWIN__) inline int getLastError() noexcept { #if defined(_WIN32) || defined(__CYGWIN__) return WSAGetLastError(); #else return errno; #endif // defined(_WIN32) || defined(__CYGWIN__) } constexpr int getAddressFamily(const InternetProtocol internetProtocol) { return (internetProtocol == InternetProtocol::V4) ? AF_INET : (internetProtocol == InternetProtocol::V6) ? AF_INET6 : throw RequestError{ "Unsupported protocol" }; } class Socket final { public: #if defined(_WIN32) || defined(__CYGWIN__) using Type = SOCKET; static constexpr Type invalid = INVALID_SOCKET; #else using Type = int; static constexpr Type invalid = -1; #endif // defined(_WIN32) || defined(__CYGWIN__) explicit Socket(const InternetProtocol internetProtocol) : endpoint{ socket(getAddressFamily(internetProtocol), SOCK_STREAM, IPPROTO_TCP) } { if (endpoint == invalid) throw std::system_error{ getLastError(), std::system_category(), "Failed to create socket" }; #if defined(_WIN32) || defined(__CYGWIN__) ULONG mode = 1; if (ioctlsocket(endpoint, FIONBIO, &mode) != 0) { close(); throw std::system_error{ WSAGetLastError(), std::system_category(), "Failed to get socket flags" }; } #else const auto flags = fcntl(endpoint, F_GETFL); if (flags == -1) { close(); throw std::system_error{ errno, std::system_category(), "Failed to get socket flags" }; } if (fcntl(endpoint, F_SETFL, flags | O_NONBLOCK) == -1) { close(); throw std::system_error{ errno, std::system_category(), "Failed to set socket flags" }; } #endif // defined(_WIN32) || defined(__CYGWIN__) #ifdef __APPLE__ const int value = 1; if (setsockopt(endpoint, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(value)) == -1) { close(); throw std::system_error{ errno, std::system_category(), "Failed to set socket option" }; } #endif // __APPLE__ } ~Socket() { if (endpoint != invalid) close(); } Socket(Socket&& other) noexcept : endpoint{ other.endpoint } { other.endpoint = invalid; } Socket& operator=(Socket&& other) noexcept { if (&other == this) return *this; if (endpoint != invalid) close(); endpoint = other.endpoint; other.endpoint = invalid; return *this; } void connect(const struct sockaddr* address, const socklen_t addressSize, const std::int64_t timeout) { #if defined(_WIN32) || defined(__CYGWIN__) auto result = ::connect(endpoint, address, addressSize); while (result == -1 && WSAGetLastError() == WSAEINTR) result = ::connect(endpoint, address, addressSize); if (result == -1) { if (WSAGetLastError() == WSAEWOULDBLOCK) { select(SelectType::write, timeout); char socketErrorPointer[sizeof(int)]; socklen_t optionLength = sizeof(socketErrorPointer); if (getsockopt(endpoint, SOL_SOCKET, SO_ERROR, socketErrorPointer, &optionLength) == -1) throw std::system_error{ WSAGetLastError(), std::system_category(), "Failed to get socket option" }; int socketError; std::memcpy(&socketError, socketErrorPointer, sizeof(socketErrorPointer)); if (socketError != 0) throw std::system_error{ socketError, std::system_category(), "Failed to connect" }; } else throw std::system_error{ WSAGetLastError(), std::system_category(), "Failed to connect" }; } #else auto result = ::connect(endpoint, address, addressSize); while (result == -1 && errno == EINTR) result = ::connect(endpoint, address, addressSize); if (result == -1) { if (errno == EINPROGRESS) { select(SelectType::write, timeout); int socketError; socklen_t optionLength = sizeof(socketError); if (getsockopt(endpoint, SOL_SOCKET, SO_ERROR, &socketError, &optionLength) == -1) throw std::system_error{ errno, std::system_category(), "Failed to get socket option" }; if (socketError != 0) throw std::system_error{ socketError, std::system_category(), "Failed to connect" }; } else throw std::system_error{ errno, std::system_category(), "Failed to connect" }; } #endif // defined(_WIN32) || defined(__CYGWIN__) } std::size_t send(const void* buffer, const std::size_t length, const std::int64_t timeout) { select(SelectType::write, timeout); #if defined(_WIN32) || defined(__CYGWIN__) auto result = ::send(endpoint, reinterpret_cast(buffer), static_cast(length), 0); while (result == -1 && WSAGetLastError() == WSAEINTR) result = ::send(endpoint, reinterpret_cast(buffer), static_cast(length), 0); if (result == -1) throw std::system_error{ WSAGetLastError(), std::system_category(), "Failed to send data" }; #else auto result = ::send(endpoint, reinterpret_cast(buffer), length, noSignal); while (result == -1 && errno == EINTR) result = ::send(endpoint, reinterpret_cast(buffer), length, noSignal); if (result == -1) throw std::system_error{ errno, std::system_category(), "Failed to send data" }; #endif // defined(_WIN32) || defined(__CYGWIN__) return static_cast(result); } std::size_t recv(void* buffer, const std::size_t length, const std::int64_t timeout) { select(SelectType::read, timeout); #if defined(_WIN32) || defined(__CYGWIN__) auto result = ::recv(endpoint, reinterpret_cast(buffer), static_cast(length), 0); while (result == -1 && WSAGetLastError() == WSAEINTR) result = ::recv(endpoint, reinterpret_cast(buffer), static_cast(length), 0); if (result == -1) throw std::system_error{ WSAGetLastError(), std::system_category(), "Failed to read data" }; #else auto result = ::recv(endpoint, reinterpret_cast(buffer), length, noSignal); while (result == -1 && errno == EINTR) result = ::recv(endpoint, reinterpret_cast(buffer), length, noSignal); if (result == -1) throw std::system_error{ errno, std::system_category(), "Failed to read data" }; #endif // defined(_WIN32) || defined(__CYGWIN__) return static_cast(result); } private: enum class SelectType { read, write }; void select(const SelectType type, const std::int64_t timeout) { fd_set descriptorSet; FD_ZERO(&descriptorSet); FD_SET(endpoint, &descriptorSet); #if defined(_WIN32) || defined(__CYGWIN__) TIMEVAL selectTimeout{ static_cast(timeout / 1000), static_cast((timeout % 1000) * 1000) }; auto count = ::select(0, (type == SelectType::read) ? &descriptorSet : nullptr, (type == SelectType::write) ? &descriptorSet : nullptr, nullptr, (timeout >= 0) ? &selectTimeout : nullptr); while (count == -1 && WSAGetLastError() == WSAEINTR) count = ::select(0, (type == SelectType::read) ? &descriptorSet : nullptr, (type == SelectType::write) ? &descriptorSet : nullptr, nullptr, (timeout >= 0) ? &selectTimeout : nullptr); if (count == -1) throw std::system_error{ WSAGetLastError(), std::system_category(), "Failed to select socket" }; else if (count == 0) throw ResponseError{ "Request timed out" }; #else timeval selectTimeout{ static_cast(timeout / 1000), static_cast((timeout % 1000) * 1000) }; auto count = ::select(endpoint + 1, (type == SelectType::read) ? &descriptorSet : nullptr, (type == SelectType::write) ? &descriptorSet : nullptr, nullptr, (timeout >= 0) ? &selectTimeout : nullptr); while (count == -1 && errno == EINTR) count = ::select(endpoint + 1, (type == SelectType::read) ? &descriptorSet : nullptr, (type == SelectType::write) ? &descriptorSet : nullptr, nullptr, (timeout >= 0) ? &selectTimeout : nullptr); if (count == -1) throw std::system_error{ errno, std::system_category(), "Failed to select socket" }; else if (count == 0) throw ResponseError{ "Request timed out" }; #endif // defined(_WIN32) || defined(__CYGWIN__) } void close() noexcept { #if defined(_WIN32) || defined(__CYGWIN__) closesocket(endpoint); #else ::close(endpoint); #endif // defined(_WIN32) || defined(__CYGWIN__) } #if defined(__unix__) && !defined(__APPLE__) && !defined(__CYGWIN__) static constexpr int noSignal = MSG_NOSIGNAL; #else static constexpr int noSignal = 0; #endif // defined(__unix__) && !defined(__APPLE__) Type endpoint = invalid; }; inline bool isWhitespaceChar(const char c) noexcept { // RFC 7230, 3.2.3. Whitespace return c == ' ' || c == '\t'; }; inline bool isDigitChar(const char c) noexcept { // RFC 5234, Appendix B.1. Core Rules return c >= '0' && c <= '9'; } inline bool isAlphaChar(const char c) noexcept { // RFC 5234, Appendix B.1. Core Rules return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } inline bool isTokenChar(const char c) noexcept { // RFC 7230, 3.2.6. Field Value Components return c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~' || isDigitChar(c) || isAlphaChar(c); }; // RFC 5234, B.1. Core Rules inline bool isVisibleChar(const char c) noexcept { return c >= 0x21 && c <= 0x7E; } // RFC 7230, Appendix B. Collected ABNF inline bool isObsTextChar(const char c) noexcept { return static_cast(c) >= 0x80 && static_cast(c) <= 0xFF; } template Iterator skipWhitespaces(const Iterator begin, const Iterator end) { auto i = begin; for (i = begin; i != end; ++i) if (!isWhitespaceChar(*i)) break; return i; } // RFC 7230, 3.2.6. Field Value Components template std::pair parseToken(const Iterator begin, const Iterator end) { std::string result; auto i = begin; for (; i != end && isTokenChar(*i); ++i) result.push_back(*i); if (result.empty()) throw ResponseError{ "Invalid token" }; return std::make_pair(i, std::move(result)); } template std::pair parseFieldValue(const Iterator begin, const Iterator end) { std::string result; auto i = begin; for (; i != end && (isVisibleChar(*i) || isWhitespaceChar(*i) || isObsTextChar(*i)); ++i) result.push_back(*i); return std::make_pair(i, std::move(result)); } } struct Response final { // RFC 7231, 6. Response Status Codes enum Status { Continue = 100, SwitchingProtocol = 101, Processing = 102, EarlyHints = 103, Ok = 200, Created = 201, Accepted = 202, NonAuthoritativeInformation = 203, NoContent = 204, ResetContent = 205, PartialContent = 206, MultiStatus = 207, AlreadyReported = 208, ImUsed = 226, MultipleChoice = 300, MovedPermanently = 301, Found = 302, SeeOther = 303, NotModified = 304, UseProxy = 305, TemporaryRedirect = 307, PermanentRedirect = 308, BadRequest = 400, Unauthorized = 401, PaymentRequired = 402, Forbidden = 403, NotFound = 404, MethodNotAllowed = 405, NotAcceptable = 406, ProxyAuthenticationRequired = 407, RequestTimeout = 408, Conflict = 409, Gone = 410, LengthRequired = 411, PreconditionFailed = 412, PayloadTooLarge = 413, UriTooLong = 414, UnsupportedMediaType = 415, RangeNotSatisfiable = 416, ExpectationFailed = 417, MisdirectedRequest = 421, UnprocessableEntity = 422, Locked = 423, FailedDependency = 424, TooEarly = 425, UpgradeRequired = 426, PreconditionRequired = 428, TooManyRequests = 429, RequestHeaderFieldsTooLarge = 431, UnavailableForLegalReasons = 451, InternalServerError = 500, NotImplemented = 501, BadGateway = 502, ServiceUnavailable = 503, GatewayTimeout = 504, HttpVersionNotSupported = 505, VariantAlsoNegotiates = 506, InsufficientStorage = 507, LoopDetected = 508, NotExtended = 510, NetworkAuthenticationRequired = 511 }; int status = 0; std::string description; std::vector headers; std::vector body; }; class Request final { public: explicit Request(const std::string& url, const InternetProtocol protocol = InternetProtocol::V4) : internetProtocol{ protocol } { const auto schemeEndPosition = url.find("://"); if (schemeEndPosition != std::string::npos) { scheme = url.substr(0, schemeEndPosition); path = url.substr(schemeEndPosition + 3); } else { scheme = "http"; path = url; } const auto fragmentPosition = path.find('#'); // remove the fragment part if (fragmentPosition != std::string::npos) path.resize(fragmentPosition); const auto pathPosition = path.find('/'); if (pathPosition == std::string::npos) { host = path; path = "/"; } else { host = path.substr(0, pathPosition); path = path.substr(pathPosition); } const auto portPosition = host.find(':'); if (portPosition != std::string::npos) { domain = host.substr(0, portPosition); port = host.substr(portPosition + 1); } else { domain = host; port = "80"; } } Response send(const std::string& method = "GET", const std::string& body = "", const std::vector& headers = {}, const std::chrono::milliseconds timeout = std::chrono::milliseconds{ -1 }) { return send(method, std::vector(body.begin(), body.end()), headers, timeout); } Response send(const std::string& method, const std::vector& body, const std::vector& headers, const std::chrono::milliseconds timeout = std::chrono::milliseconds{ -1 }) { const auto stopTime = std::chrono::steady_clock::now() + timeout; if (scheme != "http") throw RequestError{ "Only HTTP scheme is supported" }; addrinfo hints = {}; hints.ai_family = getAddressFamily(internetProtocol); hints.ai_socktype = SOCK_STREAM; addrinfo* info; if (getaddrinfo(domain.c_str(), port.c_str(), &hints, &info) != 0) throw std::system_error{ getLastError(), std::system_category(), "Failed to get address info of " + domain }; const std::unique_ptr addressInfo{ info, freeaddrinfo }; // RFC 7230, 3.1.1. Request Line std::string headerData = method + " " + path + " HTTP/1.1\r\n"; for (const auto& header : headers) headerData += header + "\r\n"; // RFC 7230, 3.2. Header Fields headerData += "Host: " + host + "\r\n" "Content-Length: " + std::to_string(body.size()) + "\r\n" "\r\n"; std::vector requestData(headerData.begin(), headerData.end()); requestData.insert(requestData.end(), body.begin(), body.end()); Socket socket{ internetProtocol }; const auto getRemainingMilliseconds = [](const std::chrono::steady_clock::time_point time) noexcept -> std::int64_t { const auto now = std::chrono::steady_clock::now(); const auto remainingTime = std::chrono::duration_cast(time - now); return (remainingTime.count() > 0) ? remainingTime.count() : 0; }; // take the first address from the list socket.connect(addressInfo->ai_addr, static_cast(addressInfo->ai_addrlen), (timeout.count() >= 0) ? getRemainingMilliseconds(stopTime) : -1); auto remaining = requestData.size(); auto sendData = requestData.data(); // send the request while (remaining > 0) { const auto size = socket.send(sendData, remaining, (timeout.count() >= 0) ? getRemainingMilliseconds(stopTime) : -1); remaining -= size; sendData += size; } std::array tempBuffer; constexpr std::array crlf = { '\r', '\n' }; Response response; std::vector responseData; enum class State { parsingStatusLine, parsingHeaders, parsingBody } state = State::parsingStatusLine; bool contentLengthReceived = false; std::size_t contentLength = 0; bool chunkedResponse = false; std::size_t expectedChunkSize = 0; bool removeCrlfAfterChunk = false; // read the response for (;;) { const auto size = socket.recv(tempBuffer.data(), tempBuffer.size(), (timeout.count() >= 0) ? getRemainingMilliseconds(stopTime) : -1); if (size == 0) // disconnected return response; responseData.insert(responseData.end(), tempBuffer.begin(), tempBuffer.begin() + size); if (state != State::parsingBody) for (;;) { // RFC 7230, 3. Message Format const auto i = std::search(responseData.begin(), responseData.end(), crlf.begin(), crlf.end()); // didn't find a newline if (i == responseData.end()) break; const std::string line(responseData.begin(), i); responseData.erase(responseData.begin(), i + 2); // empty line indicates the end of the header section (RFC 7230, 2.1. Client/Server Messaging) if (line.empty()) { state = State::parsingBody; break; } else if (state == State::parsingStatusLine) // RFC 7230, 3.1.2. Status Line { state = State::parsingHeaders; const auto httpEndIterator = std::find(line.begin(), line.end(), ' '); if (httpEndIterator != line.end()) { const auto statusStartIterator = httpEndIterator + 1; const auto statusEndIterator = std::find(statusStartIterator, line.end(), ' '); const std::string status{ statusStartIterator, statusEndIterator }; response.status = std::stoi(status); if (statusEndIterator != line.end()) { const auto descriptionStartIterator = statusEndIterator + 1; response.description = std::string{ descriptionStartIterator, line.end() }; } } } else if (state == State::parsingHeaders) // RFC 7230, 3.2. Header Fields { response.headers.push_back(line); auto headerIterator = line.cbegin(); const auto tokenResult = parseToken(headerIterator, line.cend()); headerIterator = tokenResult.first; auto headerName = std::move(tokenResult.second); const auto toLower = [](const char c) noexcept { return (c >= 'A' && c <= 'Z') ? c - ('A' - 'a') : c; }; std::transform(headerName.begin(), headerName.end(), headerName.begin(), toLower); if (headerIterator == line.cend()) throw ResponseError{ "Invalid header" }; if (*headerIterator++ != ':') throw ResponseError{ "Invalid header" }; headerIterator = skipWhitespaces(headerIterator, line.cend()); const auto valueResult = parseFieldValue(headerIterator, line.cend()); headerIterator = valueResult.first; auto headerValue = std::move(valueResult.second); headerIterator = skipWhitespaces(headerIterator, line.cend()); if (headerIterator != line.cend()) throw ResponseError{ "Invalid header" }; if (headerName == "content-length") { contentLength = std::stoul(headerValue); contentLengthReceived = true; response.body.reserve(contentLength); } else if (headerName == "transfer-encoding") { if (headerValue == "chunked") chunkedResponse = true; else throw ResponseError{ "Unsupported transfer encoding: " + headerValue }; } } } if (state == State::parsingBody) { // Content-Length must be ignored if Transfer-Encoding is received (RFC 7230, 3.2. Content-Length) if (chunkedResponse) { for (;;) { if (expectedChunkSize > 0) { const auto toWrite = (std::min)(expectedChunkSize, responseData.size()); response.body.insert(response.body.end(), responseData.begin(), responseData.begin() + static_cast(toWrite)); responseData.erase(responseData.begin(), responseData.begin() + static_cast(toWrite)); expectedChunkSize -= toWrite; if (expectedChunkSize == 0) removeCrlfAfterChunk = true; if (responseData.empty()) break; } else { if (removeCrlfAfterChunk) { if (responseData.size() < 2) break; if (!std::equal(crlf.begin(), crlf.end(), responseData.begin())) throw ResponseError{ "Invalid chunk" }; removeCrlfAfterChunk = false; responseData.erase(responseData.begin(), responseData.begin() + 2); } const auto i = std::search(responseData.begin(), responseData.end(), crlf.begin(), crlf.end()); if (i == responseData.end()) break; const std::string line(responseData.begin(), i); responseData.erase(responseData.begin(), i + 2); expectedChunkSize = std::stoul(line, nullptr, 16); if (expectedChunkSize == 0) return response; } } } else { response.body.insert(response.body.end(), responseData.begin(), responseData.end()); responseData.clear(); // got the whole content if (contentLengthReceived && response.body.size() >= contentLength) return response; } } } return response; } private: #if defined(_WIN32) || defined(__CYGWIN__) WinSock winSock; #endif // defined(_WIN32) || defined(__CYGWIN__) InternetProtocol internetProtocol; std::string scheme; std::string host; std::string domain; std::string port; std::string path; }; } #endif // HTTPREQUEST_HPP