// // HTTPRequest // #ifndef HTTPREQUEST_HPP #define HTTPREQUEST_HPP #include #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: using logic_error::logic_error; using logic_error::operator=; }; class ResponseError final: public std::runtime_error { public: using runtime_error::runtime_error; using runtime_error::operator=; }; enum class InternetProtocol: std::uint8_t { v4, v6 }; struct Uri final { std::string scheme; std::string user; std::string password; std::string host; std::string port; std::string path; std::string query; std::string fragment; }; struct Version final { uint16_t major; uint16_t minor; }; struct Status final { // RFC 7231, 6. Response Status Codes enum Code: std::uint16_t { 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 }; Version version; std::uint16_t code; std::string reason; }; using HeaderField = std::pair; using HeaderFields = std::vector; struct Response final { Status status; HeaderFields headerFields; std::vector body; }; inline namespace detail { #if defined(_WIN32) || defined(__CYGWIN__) namespace winsock { class ErrorCategory final: public std::error_category { public: const char* name() const noexcept override { return "Windows Sockets API"; } std::string message(const int condition) const override { switch (condition) { case WSA_INVALID_HANDLE: return "Specified event object handle is invalid"; case WSA_NOT_ENOUGH_MEMORY: return "Insufficient memory available"; case WSA_INVALID_PARAMETER: return "One or more parameters are invalid"; case WSA_OPERATION_ABORTED: return "Overlapped operation aborted"; case WSA_IO_INCOMPLETE: return "Overlapped I/O event object not in signaled state"; case WSA_IO_PENDING: return "Overlapped operations will complete later"; case WSAEINTR: return "Interrupted function call"; case WSAEBADF: return "File handle is not valid"; case WSAEACCES: return "Permission denied"; case WSAEFAULT: return "Bad address"; case WSAEINVAL: return "Invalid argument"; case WSAEMFILE: return "Too many open files"; case WSAEWOULDBLOCK: return "Resource temporarily unavailable"; case WSAEINPROGRESS: return "Operation now in progress"; case WSAEALREADY: return "Operation already in progress"; case WSAENOTSOCK: return "Socket operation on nonsocket"; case WSAEDESTADDRREQ: return "Destination address required"; case WSAEMSGSIZE: return "Message too long"; case WSAEPROTOTYPE: return "Protocol wrong type for socket"; case WSAENOPROTOOPT: return "Bad protocol option"; case WSAEPROTONOSUPPORT: return "Protocol not supported"; case WSAESOCKTNOSUPPORT: return "Socket type not supported"; case WSAEOPNOTSUPP: return "Operation not supported"; case WSAEPFNOSUPPORT: return "Protocol family not supported"; case WSAEAFNOSUPPORT: return "Address family not supported by protocol family"; case WSAEADDRINUSE: return "Address already in use"; case WSAEADDRNOTAVAIL: return "Cannot assign requested address"; case WSAENETDOWN: return "Network is down"; case WSAENETUNREACH: return "Network is unreachable"; case WSAENETRESET: return "Network dropped connection on reset"; case WSAECONNABORTED: return "Software caused connection abort"; case WSAECONNRESET: return "Connection reset by peer"; case WSAENOBUFS: return "No buffer space available"; case WSAEISCONN: return "Socket is already connected"; case WSAENOTCONN: return "Socket is not connected"; case WSAESHUTDOWN: return "Cannot send after socket shutdown"; case WSAETOOMANYREFS: return "Too many references"; case WSAETIMEDOUT: return "Connection timed out"; case WSAECONNREFUSED: return "Connection refused"; case WSAELOOP: return "Cannot translate name"; case WSAENAMETOOLONG: return "Name too long"; case WSAEHOSTDOWN: return "Host is down"; case WSAEHOSTUNREACH: return "No route to host"; case WSAENOTEMPTY: return "Directory not empty"; case WSAEPROCLIM: return "Too many processes"; case WSAEUSERS: return "User quota exceeded"; case WSAEDQUOT: return "Disk quota exceeded"; case WSAESTALE: return "Stale file handle reference"; case WSAEREMOTE: return "Item is remote"; case WSASYSNOTREADY: return "Network subsystem is unavailable"; case WSAVERNOTSUPPORTED: return "Winsock.dll version out of range"; case WSANOTINITIALISED: return "Successful WSAStartup not yet performed"; case WSAEDISCON: return "Graceful shutdown in progress"; case WSAENOMORE: return "No more results"; case WSAECANCELLED: return "Call has been canceled"; case WSAEINVALIDPROCTABLE: return "Procedure call table is invalid"; case WSAEINVALIDPROVIDER: return "Service provider is invalid"; case WSAEPROVIDERFAILEDINIT: return "Service provider failed to initialize"; case WSASYSCALLFAILURE: return "System call failure"; case WSASERVICE_NOT_FOUND: return "Service not found"; case WSATYPE_NOT_FOUND: return "Class type not found"; case WSA_E_NO_MORE: return "No more results"; case WSA_E_CANCELLED: return "Call was canceled"; case WSAEREFUSED: return "Database query was refused"; case WSAHOST_NOT_FOUND: return "Host not found"; case WSATRY_AGAIN: return "Nonauthoritative host not found"; case WSANO_RECOVERY: return "This is a nonrecoverable error"; case WSANO_DATA: return "Valid name, no data record of requested type"; case WSA_QOS_RECEIVERS: return "QoS receivers"; case WSA_QOS_SENDERS: return "QoS senders"; case WSA_QOS_NO_SENDERS: return "No QoS senders"; case WSA_QOS_NO_RECEIVERS: return "QoS no receivers"; case WSA_QOS_REQUEST_CONFIRMED: return "QoS request confirmed"; case WSA_QOS_ADMISSION_FAILURE: return "QoS admission error"; case WSA_QOS_POLICY_FAILURE: return "QoS policy failure"; case WSA_QOS_BAD_STYLE: return "QoS bad style"; case WSA_QOS_BAD_OBJECT: return "QoS bad object"; case WSA_QOS_TRAFFIC_CTRL_ERROR: return "QoS traffic control error"; case WSA_QOS_GENERIC_ERROR: return "QoS generic error"; case WSA_QOS_ESERVICETYPE: return "QoS service type error"; case WSA_QOS_EFLOWSPEC: return "QoS flowspec error"; case WSA_QOS_EPROVSPECBUF: return "Invalid QoS provider buffer"; case WSA_QOS_EFILTERSTYLE: return "Invalid QoS filter style"; case WSA_QOS_EFILTERTYPE: return "Invalid QoS filter type"; case WSA_QOS_EFILTERCOUNT: return "Incorrect QoS filter count"; case WSA_QOS_EOBJLENGTH: return "Invalid QoS object length"; case WSA_QOS_EFLOWCOUNT: return "Incorrect QoS flow count"; case WSA_QOS_EUNKOWNPSOBJ: return "Unrecognized QoS object"; case WSA_QOS_EPOLICYOBJ: return "Invalid QoS policy object"; case WSA_QOS_EFLOWDESC: return "Invalid QoS flow descriptor"; case WSA_QOS_EPSFLOWSPEC: return "Invalid QoS provider-specific flowspec"; case WSA_QOS_EPSFILTERSPEC: return "Invalid QoS provider-specific filterspec"; case WSA_QOS_ESDMODEOBJ: return "Invalid QoS shape discard mode object"; case WSA_QOS_ESHAPERATEOBJ: return "Invalid QoS shaping rate object"; case WSA_QOS_RESERVED_PETYPE: return "Reserved policy QoS element type"; default: return "Unknown error (" + std::to_string(condition) + ")"; } } }; inline const ErrorCategory errorCategory; class Api final { public: Api() { WSADATA wsaData; const auto error = WSAStartup(MAKEWORD(2, 2), &wsaData); if (error != 0) throw std::system_error{error, errorCategory, "WSAStartup failed"}; if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { WSACleanup(); throw std::runtime_error{"Invalid WinSock version"}; } started = true; } ~Api() { if (started) WSACleanup(); } Api(Api&& other) noexcept: started{std::exchange(other.started, false)} { } Api& operator=(Api&& other) noexcept { if (&other == this) return *this; if (started) WSACleanup(); started = std::exchange(other.started, false); return *this; } private: bool started = false; }; } #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) #if defined(_WIN32) || defined(__CYGWIN__) throw std::system_error{WSAGetLastError(), winsock::errorCategory, "Failed to create socket"}; #else throw std::system_error{errno, std::system_category(), "Failed to create socket"}; #endif // defined(_WIN32) || defined(__CYGWIN__) #if defined(_WIN32) || defined(__CYGWIN__) ULONG mode = 1; if (ioctlsocket(endpoint, FIONBIO, &mode) == SOCKET_ERROR) { close(); throw std::system_error{WSAGetLastError(), winsock::errorCategory, "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{std::exchange(other.endpoint, invalid)} { } Socket& operator=(Socket&& other) noexcept { if (&other == this) return *this; if (endpoint != invalid) close(); endpoint = std::exchange(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) == SOCKET_ERROR) throw std::system_error{WSAGetLastError(), winsock::errorCategory, "Failed to get socket option"}; int socketError; std::memcpy(&socketError, socketErrorPointer, sizeof(socketErrorPointer)); if (socketError != 0) throw std::system_error{socketError, winsock::errorCategory, "Failed to connect"}; } else throw std::system_error{WSAGetLastError(), winsock::errorCategory, "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 == SOCKET_ERROR && WSAGetLastError() == WSAEINTR) result = ::send(endpoint, reinterpret_cast(buffer), static_cast(length), 0); if (result == SOCKET_ERROR) throw std::system_error{WSAGetLastError(), winsock::errorCategory, "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 == SOCKET_ERROR && WSAGetLastError() == WSAEINTR) result = ::recv(endpoint, reinterpret_cast(buffer), static_cast(length), 0); if (result == SOCKET_ERROR) throw std::system_error{WSAGetLastError(), winsock::errorCategory, "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 == SOCKET_ERROR && WSAGetLastError() == WSAEINTR) count = ::select(0, (type == SelectType::read) ? &descriptorSet : nullptr, (type == SelectType::write) ? &descriptorSet : nullptr, nullptr, (timeout >= 0) ? &selectTimeout : nullptr); if (count == SOCKET_ERROR) throw std::system_error{WSAGetLastError(), winsock::errorCategory, "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 char toLower(const char c) noexcept { return (c >= 'A' && c <= 'Z') ? c - ('A' - 'a') : c; } template T toLower(const T& s) { T result = s; for (auto& c : result) c = toLower(c); return result; } // RFC 7230, 3.2.3. WhiteSpace template constexpr bool isWhiteSpaceChar(const C c) noexcept { return c == 0x20 || c == 0x09; // space or tab }; // RFC 5234, Appendix B.1. Core Rules template constexpr bool isDigitChar(const C c) noexcept { return c >= 0x30 && c <= 0x39; // 0 - 9 } // RFC 5234, Appendix B.1. Core Rules template constexpr bool isAlphaChar(const C c) noexcept { return (c >= 0x61 && c <= 0x7A) || // a - z (c >= 0x41 && c <= 0x5A); // A - Z } // RFC 7230, 3.2.6. Field Value Components template constexpr bool isTokenChar(const C c) noexcept { return c == 0x21 || // ! c == 0x23 || // # c == 0x24 || // $ c == 0x25 || // % c == 0x26 || // & c == 0x27 || // ' c == 0x2A || // * c == 0x2B || // + c == 0x2D || // - c == 0x2E || // . c == 0x5E || // ^ c == 0x5F || // _ c == 0x60 || // ` c == 0x7C || // | c == 0x7E || // ~ isDigitChar(c) || isAlphaChar(c); }; // RFC 5234, Appendix B.1. Core Rules template constexpr bool isVisibleChar(const C c) noexcept { return c >= 0x21 && c <= 0x7E; } // RFC 7230, Appendix B. Collected ABNF template constexpr bool isObsoleteTextChar(const C 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 5234, Appendix B.1. Core Rules template ::value>::type* = nullptr> constexpr T digitToUint(const C c) { // DIGIT return (c >= 0x30 && c <= 0x39) ? static_cast(c - 0x30) : // 0 - 9 throw ResponseError{"Invalid digit"}; } // RFC 5234, Appendix B.1. Core Rules template ::value>::type* = nullptr> constexpr T hexDigitToUint(const C c) { // HEXDIG return (c >= 0x30 && c <= 0x39) ? static_cast(c - 0x30) : // 0 - 9 (c >= 0x41 && c <= 0x46) ? static_cast(c - 0x41) + T(10) : // A - Z (c >= 0x61 && c <= 0x66) ? static_cast(c - 0x61) + T(10) : // a - z, some services send lower-case hex digits throw ResponseError{"Invalid hex digit"}; } // RFC 3986, 3. Syntax Components template Uri parseUri(const Iterator begin, const Iterator end) { Uri result; // RFC 3986, 3.1. Scheme auto i = begin; if (i == end || !isAlphaChar(*begin)) throw RequestError{"Invalid scheme"}; result.scheme.push_back(*i++); for (; i != end && (isAlphaChar(*i) || isDigitChar(*i) || *i == '+' || *i == '-' || *i == '.'); ++i) result.scheme.push_back(*i); if (i == end || *i++ != ':') throw RequestError{"Invalid scheme"}; if (i == end || *i++ != '/') throw RequestError{"Invalid scheme"}; if (i == end || *i++ != '/') throw RequestError{"Invalid scheme"}; // RFC 3986, 3.2. Authority std::string authority = std::string(i, end); // RFC 3986, 3.5. Fragment const auto fragmentPosition = authority.find('#'); if (fragmentPosition != std::string::npos) { result.fragment = authority.substr(fragmentPosition + 1); authority.resize(fragmentPosition); // remove the fragment part } // RFC 3986, 3.4. Query const auto queryPosition = authority.find('?'); if (queryPosition != std::string::npos) { result.query = authority.substr(queryPosition + 1); authority.resize(queryPosition); // remove the query part } // RFC 3986, 3.3. Path const auto pathPosition = authority.find('/'); if (pathPosition != std::string::npos) { // RFC 3986, 3.3. Path result.path = authority.substr(pathPosition); authority.resize(pathPosition); } else result.path = "/"; // RFC 3986, 3.2.1. User Information std::string userinfo; const auto hostPosition = authority.find('@'); if (hostPosition != std::string::npos) { userinfo = authority.substr(0, hostPosition); const auto passwordPosition = userinfo.find(':'); if (passwordPosition != std::string::npos) { result.user = userinfo.substr(0, passwordPosition); result.password = userinfo.substr(passwordPosition + 1); } else result.user = userinfo; result.host = authority.substr(hostPosition + 1); } else result.host = authority; // RFC 3986, 3.2.2. Host const auto portPosition = result.host.find(':'); if (portPosition != std::string::npos) { // RFC 3986, 3.2.3. Port result.port = result.host.substr(portPosition + 1); result.host.resize(portPosition); } return result; } // RFC 7230, 2.6. Protocol Versioning template std::pair parseVersion(const Iterator begin, const Iterator end) { auto i = begin; if (i == end || *i++ != 'H') throw ResponseError{"Invalid HTTP version"}; if (i == end || *i++ != 'T') throw ResponseError{"Invalid HTTP version"}; if (i == end || *i++ != 'T') throw ResponseError{"Invalid HTTP version"}; if (i == end || *i++ != 'P') throw ResponseError{"Invalid HTTP version"}; if (i == end || *i++ != '/') throw ResponseError{"Invalid HTTP version"}; if (i == end) throw ResponseError{"Invalid HTTP version"}; const auto majorVersion = digitToUint(*i++); if (i == end || *i++ != '.') throw ResponseError{"Invalid HTTP version"}; if (i == end) throw ResponseError{"Invalid HTTP version"}; const auto minorVersion = digitToUint(*i++); return {i, Version{majorVersion, minorVersion}}; } // RFC 7230, 3.1.2. Status Line template std::pair parseStatusCode(const Iterator begin, const Iterator end) { std::uint16_t result = 0; auto i = begin; while (i != end && isDigitChar(*i)) result = static_cast(result * 10U) + digitToUint(*i++); if (std::distance(begin, i) != 3) throw ResponseError{"Invalid status code"}; return {i, result}; } // RFC 7230, 3.1.2. Status Line template std::pair parseReasonPhrase(const Iterator begin, const Iterator end) { std::string result; auto i = begin; for (; i != end && (isWhiteSpaceChar(*i) || isVisibleChar(*i) || isObsoleteTextChar(*i)); ++i) result.push_back(static_cast(*i)); return {i, std::move(result)}; } // 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(static_cast(*i)); if (result.empty()) throw ResponseError{"Invalid token"}; return {i, std::move(result)}; } // RFC 7230, 3.2. Header Fields template std::pair parseFieldValue(const Iterator begin, const Iterator end) { std::string result; auto i = begin; for (; i != end && (isWhiteSpaceChar(*i) || isVisibleChar(*i) || isObsoleteTextChar(*i)); ++i) result.push_back(static_cast(*i)); // trim white spaces result.erase(std::find_if(result.rbegin(), result.rend(), [](const char c) noexcept { return !isWhiteSpaceChar(c); }).base(), result.end()); return {i, std::move(result)}; } // RFC 7230, 3.2. Header Fields template std::pair parseFieldContent(const Iterator begin, const Iterator end) { std::string result; auto i = begin; for (;;) { const auto fieldValueResult = parseFieldValue(i, end); i = fieldValueResult.first; result += fieldValueResult.second; // Handle obsolete fold as per RFC 7230, 3.2.4. Field Parsing // Obsolete folding is known as linear white space (LWS) in RFC 2616, 2.2 Basic Rules auto obsoleteFoldIterator = i; if (obsoleteFoldIterator == end || *obsoleteFoldIterator++ != '\r') break; if (obsoleteFoldIterator == end || *obsoleteFoldIterator++ != '\n') break; if (obsoleteFoldIterator == end || !isWhiteSpaceChar(*obsoleteFoldIterator++)) break; result.push_back(' '); i = obsoleteFoldIterator; } return {i, std::move(result)}; } // RFC 7230, 3.2. Header Fields template std::pair parseHeaderField(const Iterator begin, const Iterator end) { auto tokenResult = parseToken(begin, end); auto i = tokenResult.first; auto fieldName = toLower(tokenResult.second); if (i == end || *i++ != ':') throw ResponseError{"Invalid header"}; i = skipWhiteSpaces(i, end); auto valueResult = parseFieldContent(i, end); i = valueResult.first; auto fieldValue = std::move(valueResult.second); if (i == end || *i++ != '\r') throw ResponseError{"Invalid header"}; if (i == end || *i++ != '\n') throw ResponseError{"Invalid header"}; return {i, {std::move(fieldName), std::move(fieldValue)}}; } // RFC 7230, 3.1.2. Status Line template std::pair parseStatusLine(const Iterator begin, const Iterator end) { const auto versionResult = parseVersion(begin, end); auto i = versionResult.first; if (i == end || *i++ != ' ') throw ResponseError{"Invalid status line"}; const auto statusCodeResult = parseStatusCode(i, end); i = statusCodeResult.first; if (i == end || *i++ != ' ') throw ResponseError{"Invalid status line"}; auto reasonPhraseResult = parseReasonPhrase(i, end); i = reasonPhraseResult.first; if (i == end || *i++ != '\r') throw ResponseError{"Invalid status line"}; if (i == end || *i++ != '\n') throw ResponseError{"Invalid status line"}; return {i, Status{ versionResult.second, statusCodeResult.second, std::move(reasonPhraseResult.second) }}; } // RFC 7230, 4.1. Chunked Transfer Coding template ::value>::type* = nullptr> T stringToUint(const Iterator begin, const Iterator end) { T result = 0; for (auto i = begin; i != end; ++i) result = T(10U) * result + digitToUint(*i); return result; } template ::value>::type* = nullptr> T hexStringToUint(const Iterator begin, const Iterator end) { T result = 0; for (auto i = begin; i != end; ++i) result = T(16U) * result + hexDigitToUint(*i); return result; } // RFC 7230, 3.1.1. Request Line inline std::string encodeRequestLine(const std::string& method, const std::string& target) { return method + " " + target + " HTTP/1.1\r\n"; } // RFC 7230, 3.2. Header Fields inline std::string encodeHeaderFields(const HeaderFields& headerFields) { std::string result; for (const auto& headerField : headerFields) { if (headerField.first.empty()) throw RequestError{"Invalid header field name"}; for (const auto c : headerField.first) if (!isTokenChar(c)) throw RequestError{"Invalid header field name"}; for (const auto c : headerField.second) if (!isWhiteSpaceChar(c) && !isVisibleChar(c) && !isObsoleteTextChar(c)) throw RequestError{"Invalid header field value"}; result += headerField.first + ": " + headerField.second + "\r\n"; } return result; } // RFC 4648, 4. Base 64 Encoding template std::string encodeBase64(const Iterator begin, const Iterator end) { constexpr std::array chars{ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; std::string result; std::size_t c = 0; std::array charArray; for (auto i = begin; i != end; ++i) { charArray[c++] = static_cast(*i); if (c == 3) { result += chars[static_cast((charArray[0] & 0xFC) >> 2)]; result += chars[static_cast(((charArray[0] & 0x03) << 4) + ((charArray[1] & 0xF0) >> 4))]; result += chars[static_cast(((charArray[1] & 0x0F) << 2) + ((charArray[2] & 0xC0) >> 6))]; result += chars[static_cast(charArray[2] & 0x3f)]; c = 0; } } if (c) { result += chars[static_cast((charArray[0] & 0xFC) >> 2)]; if (c == 1) result += chars[static_cast((charArray[0] & 0x03) << 4)]; else // c == 2 { result += chars[static_cast(((charArray[0] & 0x03) << 4) + ((charArray[1] & 0xF0) >> 4))]; result += chars[static_cast((charArray[1] & 0x0F) << 2)]; } while (++c < 4) result += '='; // padding } return result; } inline std::vector encodeHtml(const Uri& uri, const std::string& method, const std::vector& body, HeaderFields headerFields) { if (uri.scheme != "http") throw RequestError{"Only HTTP scheme is supported"}; // RFC 7230, 5.3. Request Target const std::string requestTarget = uri.path + (uri.query.empty() ? "" : '?' + uri.query); // RFC 7230, 5.4. Host headerFields.push_back({"Host", uri.host}); // RFC 7230, 3.3.2. Content-Length headerFields.push_back({"Content-Length", std::to_string(body.size())}); // RFC 7617, 2. The 'Basic' Authentication Scheme if (!uri.user.empty() || !uri.password.empty()) { std::string userinfo = uri.user + ':' + uri.password; headerFields.push_back({"Authorization", "Basic " + encodeBase64(userinfo.begin(), userinfo.end())}); } const auto headerData = encodeRequestLine(method, requestTarget) + encodeHeaderFields(headerFields) + "\r\n"; std::vector result(headerData.begin(), headerData.end()); result.insert(result.end(), body.begin(), body.end()); return result; } } class Request final { public: explicit Request(const std::string& uriString, const InternetProtocol protocol = InternetProtocol::v4): internetProtocol{protocol}, uri{parseUri(uriString.begin(), uriString.end())} { } Response send(const std::string& method = "GET", const std::string& body = "", const HeaderFields& headerFields = {}, const std::chrono::milliseconds timeout = std::chrono::milliseconds{-1}) { return send(method, std::vector(body.begin(), body.end()), headerFields, timeout); } Response send(const std::string& method, const std::vector& body, const HeaderFields& headerFields = {}, const std::chrono::milliseconds timeout = std::chrono::milliseconds{-1}) { const auto stopTime = std::chrono::steady_clock::now() + timeout; if (uri.scheme != "http") throw RequestError{"Only HTTP scheme is supported"}; addrinfo hints = {}; hints.ai_family = getAddressFamily(internetProtocol); hints.ai_socktype = SOCK_STREAM; const char* port = uri.port.empty() ? "80" : uri.port.c_str(); addrinfo* info; if (getaddrinfo(uri.host.c_str(), port, &hints, &info) != 0) #if defined(_WIN32) || defined(__CYGWIN__) throw std::system_error{WSAGetLastError(), winsock::errorCategory, "Failed to get address info of " + uri.host}; #else throw std::system_error{errno, std::system_category(), "Failed to get address info of " + uri.host}; #endif // defined(_WIN32) || defined(__CYGWIN__) const std::unique_ptr addressInfo{info, freeaddrinfo}; const auto requestData = encodeHtml(uri, method, body, headerFields); 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'}; constexpr std::array headerEnd = {'\r', '\n', '\r', '\n'}; Response response; std::vector responseData; bool parsingBody = false; bool contentLengthReceived = false; std::size_t contentLength = 0U; bool chunkedResponse = false; std::size_t expectedChunkSize = 0U; 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 (!parsingBody) { // RFC 7230, 3. Message Format // Empty line indicates the end of the header section (RFC 7230, 2.1. Client/Server Messaging) const auto endIterator = std::search(responseData.cbegin(), responseData.cend(), headerEnd.cbegin(), headerEnd.cend()); if (endIterator == responseData.cend()) break; // two consecutive CRLFs not found const auto headerBeginIterator = responseData.cbegin(); const auto headerEndIterator = endIterator + 2; auto statusLineResult = parseStatusLine(headerBeginIterator, headerEndIterator); auto i = statusLineResult.first; response.status = std::move(statusLineResult.second); for (;;) { auto headerFieldResult = parseHeaderField(i, headerEndIterator); i = headerFieldResult.first; auto fieldName = std::move(headerFieldResult.second.first); auto fieldValue = std::move(headerFieldResult.second.second); if (fieldName == "transfer-encoding") { // RFC 7230, 3.3.1. Transfer-Encoding if (fieldValue == "chunked") chunkedResponse = true; else throw ResponseError{"Unsupported transfer encoding: " + fieldValue}; } else if (fieldName == "content-length") { // RFC 7230, 3.3.2. Content-Length contentLength = stringToUint(fieldValue.cbegin(), fieldValue.cend()); contentLengthReceived = true; response.body.reserve(contentLength); } response.headerFields.push_back({std::move(fieldName), std::move(fieldValue)}); if (i == headerEndIterator) break; } responseData.erase(responseData.cbegin(), headerEndIterator + 2); parsingBody = true; } if (parsingBody) { // Content-Length must be ignored if Transfer-Encoding is received (RFC 7230, 3.2. Content-Length) if (chunkedResponse) { // RFC 7230, 4.1. Chunked Transfer Coding 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; expectedChunkSize = detail::hexStringToUint(responseData.begin(), i); responseData.erase(responseData.begin(), i + 2); 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::Api winSock; #endif // defined(_WIN32) || defined(__CYGWIN__) InternetProtocol internetProtocol; Uri uri; }; } #endif // HTTPREQUEST_HPP