mirror of
https://github.com/QIDITECH/QIDI_Q1_Pro.git
synced 2026-01-30 15:38:43 +03:00
1208 lines
45 KiB
C++
1208 lines
45 KiB
C++
//
|
|
// HTTPRequest
|
|
//
|
|
|
|
#ifndef HTTPREQUEST_HPP
|
|
#define HTTPREQUEST_HPP
|
|
|
|
#include <cctype>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <type_traits>
|
|
#include <vector>
|
|
|
|
#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 <winsock2.h>
|
|
# if _WIN32_WINNT < _WIN32_WINNT_WINXP
|
|
extern "C" char *_strdup(const char *strSource);
|
|
# define strdup _strdup
|
|
# include <wspiapi.h>
|
|
# endif // _WIN32_WINNT < _WIN32_WINNT_WINXP
|
|
# include <ws2tcpip.h>
|
|
# pragma pop_macro("WIN32_LEAN_AND_MEAN")
|
|
# pragma pop_macro("NOMINMAX")
|
|
#else
|
|
# include <errno.h>
|
|
# include <fcntl.h>
|
|
# include <netinet/in.h>
|
|
# include <netdb.h>
|
|
# include <sys/select.h>
|
|
# include <sys/socket.h>
|
|
# include <sys/types.h>
|
|
# include <unistd.h>
|
|
#endif // defined(_WIN32) || defined(__CYGWIN__)
|
|
|
|
namespace http
|
|
{
|
|
class RequestError final: public std::logic_error
|
|
{
|
|
public:
|
|
using std::logic_error::logic_error;
|
|
};
|
|
|
|
class ResponseError final: public std::runtime_error
|
|
{
|
|
public:
|
|
using std::runtime_error::runtime_error;
|
|
};
|
|
|
|
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 HttpVersion 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
|
|
};
|
|
|
|
HttpVersion httpVersion;
|
|
std::uint16_t code;
|
|
std::string reason;
|
|
};
|
|
|
|
using HeaderField = std::pair<std::string, std::string>;
|
|
using HeaderFields = std::vector<HeaderField>;
|
|
|
|
struct Response final
|
|
{
|
|
Status status;
|
|
HeaderFields headerFields;
|
|
std::vector<std::uint8_t> body;
|
|
};
|
|
|
|
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<const char*>(buffer),
|
|
static_cast<int>(length), 0);
|
|
|
|
while (result == -1 && WSAGetLastError() == WSAEINTR)
|
|
result = ::send(endpoint, reinterpret_cast<const char*>(buffer),
|
|
static_cast<int>(length), 0);
|
|
|
|
if (result == -1)
|
|
throw std::system_error{WSAGetLastError(), std::system_category(), "Failed to send data"};
|
|
#else
|
|
auto result = ::send(endpoint, reinterpret_cast<const char*>(buffer),
|
|
length, noSignal);
|
|
|
|
while (result == -1 && errno == EINTR)
|
|
result = ::send(endpoint, reinterpret_cast<const char*>(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<std::size_t>(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<char*>(buffer),
|
|
static_cast<int>(length), 0);
|
|
|
|
while (result == -1 && WSAGetLastError() == WSAEINTR)
|
|
result = ::recv(endpoint, reinterpret_cast<char*>(buffer),
|
|
static_cast<int>(length), 0);
|
|
|
|
if (result == -1)
|
|
throw std::system_error{WSAGetLastError(), std::system_category(), "Failed to read data"};
|
|
#else
|
|
auto result = ::recv(endpoint, reinterpret_cast<char*>(buffer),
|
|
length, noSignal);
|
|
|
|
while (result == -1 && errno == EINTR)
|
|
result = ::recv(endpoint, reinterpret_cast<char*>(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<std::size_t>(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<LONG>(timeout / 1000),
|
|
static_cast<LONG>((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<time_t>(timeout / 1000),
|
|
static_cast<suseconds_t>((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;
|
|
};
|
|
|
|
// RFC 7230, 3.2.3. WhiteSpace
|
|
template <typename C>
|
|
constexpr bool isWhiteSpaceChar(const C c) noexcept
|
|
{
|
|
return c == 0x20 || c == 0x09; // space or tab
|
|
};
|
|
|
|
// RFC 5234, Appendix B.1. Core Rules
|
|
template <typename C>
|
|
constexpr bool isDigitChar(const C c) noexcept
|
|
{
|
|
return c >= 0x30 && c <= 0x39; // 0 - 9
|
|
}
|
|
|
|
// RFC 5234, Appendix B.1. Core Rules
|
|
template <typename C>
|
|
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 <typename C>
|
|
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 <typename C>
|
|
constexpr bool isVisibleChar(const C c) noexcept
|
|
{
|
|
return c >= 0x21 && c <= 0x7E;
|
|
}
|
|
|
|
// RFC 7230, Appendix B. Collected ABNF
|
|
template <typename C>
|
|
constexpr bool isObsoleteTextChar(const C c) noexcept
|
|
{
|
|
return static_cast<unsigned char>(c) >= 0x80 &&
|
|
static_cast<unsigned char>(c) <= 0xFF;
|
|
}
|
|
|
|
template <class Iterator>
|
|
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 <typename T, typename C, typename std::enable_if<std::is_unsigned<T>::value>::type* = nullptr>
|
|
constexpr T digitToUint(const C c)
|
|
{
|
|
// DIGIT
|
|
return (c >= 0x30 && c <= 0x39) ? static_cast<T>(c - 0x30) : // 0 - 9
|
|
throw ResponseError{"Invalid digit"};
|
|
}
|
|
|
|
// RFC 5234, Appendix B.1. Core Rules
|
|
template <typename T, typename C, typename std::enable_if<std::is_unsigned<T>::value>::type* = nullptr>
|
|
constexpr T hexDigitToUint(const C c)
|
|
{
|
|
// HEXDIG
|
|
return (c >= 0x30 && c <= 0x39) ? static_cast<T>(c - 0x30) : // 0 - 9
|
|
(c >= 0x41 && c <= 0x46) ? static_cast<T>(c - 0x41) + T(10) : // A - Z
|
|
(c >= 0x61 && c <= 0x66) ? static_cast<T>(c - 0x61) + T(10) : // a - z, some services send lower-case hex digits
|
|
throw ResponseError{"Invalid hex digit"};
|
|
}
|
|
|
|
// RFC 3986, 3. Syntax Components
|
|
template <class Iterator>
|
|
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 <class Iterator>
|
|
std::pair<Iterator, HttpVersion> parseHttpVersion(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<std::uint16_t>(*i++);
|
|
|
|
if (i == end || *i++ != '.')
|
|
throw ResponseError{"Invalid HTTP version"};
|
|
|
|
if (i == end)
|
|
throw ResponseError{"Invalid HTTP version"};
|
|
|
|
const auto minorVersion = digitToUint<std::uint16_t>(*i++);
|
|
|
|
return {i, HttpVersion{majorVersion, minorVersion}};
|
|
}
|
|
|
|
// RFC 7230, 3.1.2. Status Line
|
|
template <class Iterator>
|
|
std::pair<Iterator, std::uint16_t> parseStatusCode(const Iterator begin, const Iterator end)
|
|
{
|
|
std::uint16_t result = 0;
|
|
|
|
auto i = begin;
|
|
while (i != end && isDigitChar(*i))
|
|
result = static_cast<std::uint16_t>(result * 10U) + digitToUint<std::uint16_t>(*i++);
|
|
|
|
if (std::distance(begin, i) != 3)
|
|
throw ResponseError{"Invalid status code"};
|
|
|
|
return {i, result};
|
|
}
|
|
|
|
// RFC 7230, 3.1.2. Status Line
|
|
template <class Iterator>
|
|
std::pair<Iterator, std::string> 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<char>(*i));
|
|
|
|
return {i, std::move(result)};
|
|
}
|
|
|
|
// RFC 7230, 3.2.6. Field Value Components
|
|
template <class Iterator>
|
|
std::pair<Iterator, std::string> parseToken(const Iterator begin, const Iterator end)
|
|
{
|
|
std::string result;
|
|
|
|
auto i = begin;
|
|
for (; i != end && isTokenChar(*i); ++i)
|
|
result.push_back(static_cast<char>(*i));
|
|
|
|
if (result.empty())
|
|
throw ResponseError{"Invalid token"};
|
|
|
|
return {i, std::move(result)};
|
|
}
|
|
|
|
// RFC 7230, 3.2. Header Fields
|
|
template <class Iterator>
|
|
std::pair<Iterator, std::string> 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<char>(*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 <class Iterator>
|
|
std::pair<Iterator, std::string> 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 <class Iterator>
|
|
std::pair<Iterator, HeaderField> parseHeaderField(const Iterator begin, const Iterator end)
|
|
{
|
|
auto tokenResult = parseToken(begin, end);
|
|
auto i = tokenResult.first;
|
|
auto fieldName = std::move(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 <class Iterator>
|
|
std::pair<Iterator, Status> parseStatusLine(const Iterator begin, const Iterator end)
|
|
{
|
|
const auto httpVersionResult = parseHttpVersion(begin, end);
|
|
auto i = httpVersionResult.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{
|
|
httpVersionResult.second,
|
|
statusCodeResult.second,
|
|
std::move(reasonPhraseResult.second)
|
|
}};
|
|
}
|
|
|
|
// RFC 7230, 4.1. Chunked Transfer Coding
|
|
template <typename T, class Iterator, typename std::enable_if<std::is_unsigned<T>::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<T>(*i);
|
|
|
|
return result;
|
|
}
|
|
|
|
template <typename T, class Iterator, typename std::enable_if<std::is_unsigned<T>::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<T>(*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 <class Iterator>
|
|
std::string encodeBase64(const Iterator begin, const Iterator end)
|
|
{
|
|
constexpr std::array<char, 64> 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<std::uint8_t, 3> charArray;
|
|
|
|
for (auto i = begin; i != end; ++i)
|
|
{
|
|
charArray[c++] = static_cast<std::uint8_t>(*i);
|
|
if (c == 3)
|
|
{
|
|
result += chars[static_cast<std::uint8_t>((charArray[0] & 0xFC) >> 2)];
|
|
result += chars[static_cast<std::uint8_t>(((charArray[0] & 0x03) << 4) + ((charArray[1] & 0xF0) >> 4))];
|
|
result += chars[static_cast<std::uint8_t>(((charArray[1] & 0x0F) << 2) + ((charArray[2] & 0xC0) >> 6))];
|
|
result += chars[static_cast<std::uint8_t>(charArray[2] & 0x3f)];
|
|
c = 0;
|
|
}
|
|
}
|
|
|
|
if (c)
|
|
{
|
|
result += chars[static_cast<std::uint8_t>((charArray[0] & 0xFC) >> 2)];
|
|
|
|
if (c == 1)
|
|
result += chars[static_cast<std::uint8_t>((charArray[0] & 0x03) << 4)];
|
|
else // c == 2
|
|
{
|
|
result += chars[static_cast<std::uint8_t>(((charArray[0] & 0x03) << 4) + ((charArray[1] & 0xF0) >> 4))];
|
|
result += chars[static_cast<std::uint8_t>((charArray[1] & 0x0F) << 2)];
|
|
}
|
|
|
|
while (++c < 4) result += '='; // padding
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
inline std::vector<std::uint8_t> encodeHtml(const Uri& uri,
|
|
const std::string& method,
|
|
const std::vector<uint8_t>& 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<uint8_t> 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<uint8_t>(body.begin(), body.end()),
|
|
headerFields,
|
|
timeout);
|
|
}
|
|
|
|
Response send(const std::string& method,
|
|
const std::vector<uint8_t>& 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)
|
|
throw std::system_error{getLastError(), std::system_category(), "Failed to get address info of " + uri.host};
|
|
|
|
const std::unique_ptr<addrinfo, decltype(&freeaddrinfo)> 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<std::chrono::milliseconds>(time - now);
|
|
return (remainingTime.count() > 0) ? remainingTime.count() : 0;
|
|
};
|
|
|
|
// take the first address from the list
|
|
socket.connect(addressInfo->ai_addr, static_cast<socklen_t>(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<std::uint8_t, 4096> tempBuffer;
|
|
constexpr std::array<std::uint8_t, 2> crlf = {'\r', '\n'};
|
|
constexpr std::array<std::uint8_t, 4> headerEnd = {'\r', '\n', '\r', '\n'};
|
|
Response response;
|
|
std::vector<std::uint8_t> 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);
|
|
const auto toLower = [](const char c) noexcept {
|
|
return (c >= 'A' && c <= 'Z') ? c - ('A' - 'a') : c;
|
|
};
|
|
std::transform(fieldName.begin(), fieldName.end(), fieldName.begin(), toLower);
|
|
|
|
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<std::size_t>(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<std::ptrdiff_t>(toWrite));
|
|
responseData.erase(responseData.begin(),
|
|
responseData.begin() + static_cast<std::ptrdiff_t>(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<std::size_t>(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 winSock;
|
|
#endif // defined(_WIN32) || defined(__CYGWIN__)
|
|
InternetProtocol internetProtocol;
|
|
Uri uri;
|
|
};
|
|
}
|
|
|
|
#endif // HTTPREQUEST_HPP
|