mirror of
https://github.com/lifegpc/c-utils.git
synced 2026-06-06 05:08:45 +08:00
add http client
This commit is contained in:
423
http_client.cpp
Normal file
423
http_client.cpp
Normal file
@@ -0,0 +1,423 @@
|
||||
#include "http_client.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "err.h"
|
||||
#include "str_util.h"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#if _WIN32
|
||||
static bool inited = false;
|
||||
static WSADATA wsaData = { 0 };
|
||||
#endif
|
||||
|
||||
#if HAVE_OPENSSL
|
||||
static bool ssl_inited = false;
|
||||
#endif
|
||||
|
||||
AIException::AIException(int code) {
|
||||
this->code = code;
|
||||
}
|
||||
|
||||
const char* AIException::what() {
|
||||
#if _WIN32
|
||||
return gai_strerrorA(this->code);
|
||||
#else
|
||||
return gai_strerror(this->code);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string decodeURIComponent(std::string str) {
|
||||
std::string re;
|
||||
for (size_t i = 0; i < str.length(); i++) {
|
||||
if (str[i] == '%') {
|
||||
if (i + 2 >= str.length()) {
|
||||
throw std::runtime_error("Invalid Percent-encoding");
|
||||
}
|
||||
char buf[3] = { str[i + 1], str[i + 2], 0 };
|
||||
int c = strtol(buf, nullptr, 16);
|
||||
re += (char)c;
|
||||
i += 2;
|
||||
} else {
|
||||
re += str[i];
|
||||
}
|
||||
}
|
||||
return re;
|
||||
}
|
||||
|
||||
std::string encodeURIComponent(std::string str) {
|
||||
std::string re;
|
||||
for (size_t i = 0; i < str.length(); i++) {
|
||||
if (str[i] == ' ') {
|
||||
re += '+';
|
||||
} else if (str[i] == '-' || str[i] == '_' || str[i] == '.' || str[i] == '~' ||
|
||||
(str[i] >= 'a' && str[i] <= 'z') ||
|
||||
(str[i] >= 'A' && str[i] <= 'Z') ||
|
||||
(str[i] >= '0' && str[i] <= '9')) {
|
||||
re += str[i];
|
||||
} else {
|
||||
char buf[4] = { '%', 0 };
|
||||
snprintf(buf + 1, 3, "%02X", (unsigned char)str[i]);
|
||||
re += buf;
|
||||
}
|
||||
}
|
||||
return re;
|
||||
}
|
||||
|
||||
SocketError::SocketError() {
|
||||
#if _WIN32
|
||||
this->code = WSAGetLastError();
|
||||
#else
|
||||
this->code = errno;
|
||||
#endif
|
||||
#if _WIN32
|
||||
if (!err::get_winerror(this->message, this->code)) {
|
||||
#else
|
||||
if (!err::get_errno_message(this->message, this->code)) {
|
||||
#endif
|
||||
char buf[32];
|
||||
snprintf(buf, 32, "Unknown error %d", this->code);
|
||||
this->message = std::string(buf);
|
||||
}
|
||||
}
|
||||
|
||||
const char* SocketError::what() {
|
||||
return this->message.c_str();
|
||||
}
|
||||
|
||||
Socket::Socket(std::string host, std::string protocol) {
|
||||
this->host = host;
|
||||
this->protocol = protocol;
|
||||
#if HAVE_OPENSSL
|
||||
if (protocol == "https") {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
addrinfo hints = { 0 };
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_protocol = IPPROTO_TCP;
|
||||
int re = getaddrinfo(host.c_str(), protocol.c_str(), &hints, &this->addr);
|
||||
if (re) {
|
||||
throw AIException(re);
|
||||
}
|
||||
}
|
||||
|
||||
Socket::~Socket() {
|
||||
#if HAVE_OPENSSL
|
||||
if (this->ssl_ctx) {
|
||||
SSL_CTX_free(this->ssl_ctx);
|
||||
this->ssl_ctx = nullptr;
|
||||
}
|
||||
if (this->web) {
|
||||
BIO_free_all(this->web);
|
||||
this->web = nullptr;
|
||||
}
|
||||
#endif
|
||||
this->close();
|
||||
if (this->addr) {
|
||||
freeaddrinfo(this->addr);
|
||||
this->addr = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Socket::connect() {
|
||||
#if HAVE_OPENSSL
|
||||
if (this->protocol == "https") {
|
||||
const SSL_METHOD* method = SSLv23_client_method();
|
||||
if (!method) {
|
||||
throw std::runtime_error("SSLv23_client_method failed");
|
||||
}
|
||||
this->ssl_ctx = SSL_CTX_new(method);
|
||||
if (!this->ssl_ctx) {
|
||||
throw std::runtime_error("SSL_CTX_new failed");
|
||||
}
|
||||
const long flags = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION;
|
||||
SSL_CTX_set_options(this->ssl_ctx, flags);
|
||||
this->web = BIO_new_ssl_connect(this->ssl_ctx);
|
||||
if (!this->web) {
|
||||
throw std::runtime_error("BIO_new_ssl_connect failed");
|
||||
}
|
||||
int res =BIO_set_conn_hostname(this->web, this->host.c_str());
|
||||
if (res == -1) {
|
||||
throw std::runtime_error("BIO_set_conn_hostname failed");
|
||||
}
|
||||
BIO_get_ssl(this->web, &this->ssl);
|
||||
if (!this->ssl) {
|
||||
throw std::runtime_error("BIO_get_ssl failed");
|
||||
}
|
||||
res = SSL_set_tlsext_host_name(this->ssl, this->host.c_str());
|
||||
if (res == -1) {
|
||||
throw std::runtime_error("SSL_set_tlsext_host_name failed");
|
||||
}
|
||||
res = BIO_do_connect(this->web);
|
||||
if (res != 1) {
|
||||
throw std::runtime_error("BIO_do_connect failed");
|
||||
}
|
||||
res = BIO_do_handshake(this->web);
|
||||
if (res != 1) {
|
||||
throw std::runtime_error("BIO_do_handshake failed");
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (this->socket != -1 || !this->addr) {
|
||||
return;
|
||||
}
|
||||
this->socket = ::socket(this->addr->ai_family, this->addr->ai_socktype, this->addr->ai_protocol);
|
||||
#if _WIN32
|
||||
if (this->socket == INVALID_SOCKET) {
|
||||
#else
|
||||
if (this->socket == -1) {
|
||||
#endif
|
||||
throw SocketError();
|
||||
}
|
||||
int re = ::connect(this->socket, this->addr->ai_addr, this->addr->ai_addrlen);
|
||||
#if _WIN32
|
||||
if (re == SOCKET_ERROR) {
|
||||
#else
|
||||
if (re == -1) {
|
||||
#endif
|
||||
throw SocketError();
|
||||
}
|
||||
}
|
||||
|
||||
size_t Socket::send(const char* data, size_t len, int flags) {
|
||||
#if HAVE_OPENSSL
|
||||
if (this->protocol == "https") {
|
||||
int sented = BIO_write(this->web, data, (int)len);
|
||||
if (sented <= 0) {
|
||||
throw std::runtime_error("BIO_write failed");
|
||||
}
|
||||
return sented;
|
||||
}
|
||||
#endif
|
||||
if (this->socket == -1) {
|
||||
throw std::runtime_error("Socket not connected");
|
||||
}
|
||||
#if _WIN32
|
||||
int sented = ::send(this->socket, data, (int)len, flags);
|
||||
if (sented == SOCKET_ERROR) {
|
||||
#else
|
||||
ssize_t sented = ::send(this->socket, data, len, flags);
|
||||
if (sented == -1) {
|
||||
#endif
|
||||
throw SocketError();
|
||||
}
|
||||
return sented;
|
||||
}
|
||||
|
||||
size_t Socket::send(std::string data, int flags) {
|
||||
return this->send(data.c_str(), data.length(), flags);
|
||||
}
|
||||
|
||||
size_t Socket::recv(char* data, size_t len, int flags) {
|
||||
#if HAVE_OPENSSL
|
||||
if (this->protocol == "https") {
|
||||
int recved = BIO_read(this->web, data, (int)len);
|
||||
if (recved <= 0) {
|
||||
throw std::runtime_error("BIO_read failed");
|
||||
}
|
||||
return recved;
|
||||
}
|
||||
#endif
|
||||
if (this->socket == -1) {
|
||||
throw std::runtime_error("Socket not connected");
|
||||
}
|
||||
#if _WIN32
|
||||
int recved = ::recv(this->socket, data, (int)len, flags);
|
||||
if (recved == SOCKET_ERROR) {
|
||||
#else
|
||||
ssize_t recved = ::recv(this->socket, data, len, flags);
|
||||
if (recved == -1) {
|
||||
#endif
|
||||
throw SocketError();
|
||||
}
|
||||
return recved;
|
||||
}
|
||||
|
||||
std::string Socket::recv(size_t len, int flags) {
|
||||
char* buf = new char[len];
|
||||
try {
|
||||
size_t recved = this->recv(buf, len, flags);
|
||||
std::string re(buf, recved);
|
||||
delete[] buf;
|
||||
return re;
|
||||
} catch (std::exception& e) {
|
||||
delete[] buf;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
void Socket::close() {
|
||||
if (this->socket != -1 && !closed) {
|
||||
#if _WIN32
|
||||
closesocket(this->socket);
|
||||
#else
|
||||
::close(this->socket);
|
||||
#endif
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
Request::Request(std::string host, std::string path, std::string method, HeaderMap headers, HttpClientOptions options) {
|
||||
this->host = host;
|
||||
this->path = path;
|
||||
this->method = method;
|
||||
this->headers = headers;
|
||||
this->options = options;
|
||||
}
|
||||
|
||||
void Request::send() {
|
||||
std::string data;
|
||||
data += this->method + " " + this->path + " HTTP/1.1\r\n";
|
||||
auto hasBody = !this->body || !this->body->isFinished();
|
||||
if (hasBody) {
|
||||
auto type = this->body->contentType();
|
||||
if (!type.empty()) {
|
||||
this->headers["Content-Type"] = type;
|
||||
}
|
||||
if (this->body->hasLength()) {
|
||||
this->headers["Content-Length"] = std::to_string(this->body->length());
|
||||
}
|
||||
}
|
||||
for (auto& header : this->headers) {
|
||||
data += header.first + ": " + header.second + "\r\n";
|
||||
}
|
||||
data += "\r\n";
|
||||
printf("%s", data.c_str());
|
||||
#if HAVE_OPENSSL
|
||||
Socket socket(this->host, this->options.https ? "https" : "http");
|
||||
#else
|
||||
Socket socket(this->host, "http");
|
||||
#endif
|
||||
socket.connect();
|
||||
socket.send(data);
|
||||
if (hasBody) {
|
||||
while (!this->body->isFinished()) {
|
||||
char buf[1024];
|
||||
size_t len = this->body->pullData(buf, 1024);
|
||||
socket.send(buf, len, 0);
|
||||
printf("%s", std::string(buf, len).c_str());
|
||||
}
|
||||
socket.send("\r\n");
|
||||
}
|
||||
auto s = socket.recv(10240);
|
||||
printf("%s\n", s.c_str());
|
||||
}
|
||||
|
||||
HttpClient::HttpClient(std::string host) {
|
||||
this->host = host;
|
||||
#if _WIN32
|
||||
if (!inited) {
|
||||
WSAStartup(MAKEWORD(2, 2), &wsaData);
|
||||
inited = true;
|
||||
}
|
||||
#endif
|
||||
#if HAVE_OPENSSL
|
||||
if (!ssl_inited) {
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
SSL_library_init();
|
||||
#else
|
||||
SSL_load_error_strings();
|
||||
#endif
|
||||
ssl_inited = true;
|
||||
}
|
||||
#endif
|
||||
this->headers["Host"] = host;
|
||||
this->headers["User-Agent"] = "simple-http-client";
|
||||
this->headers["Accept"] = "*/*";
|
||||
}
|
||||
|
||||
Request HttpClient::request(std::string path, std::string method) {
|
||||
return Request(this->host, path, method, this->headers, this->options);
|
||||
}
|
||||
|
||||
void HttpFullBody::pull() {
|
||||
if (!this->pulled) {
|
||||
this->buff = this->body();
|
||||
this->len = this->buff.length();
|
||||
this->pulled = true;
|
||||
}
|
||||
}
|
||||
|
||||
size_t HttpFullBody::pullData(char* buf, size_t len) {
|
||||
this->pull();
|
||||
size_t rlen = std::min(this->buff.length(), len);
|
||||
memcpy(buf, this->buff.c_str(), rlen);
|
||||
this->buff = this->buff.substr(rlen);
|
||||
return rlen;
|
||||
}
|
||||
|
||||
bool HttpFullBody::isFinished() {
|
||||
this->pull();
|
||||
return this->buff.length() == 0;
|
||||
}
|
||||
|
||||
bool HttpFullBody::hasLength() {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string HttpFullBody::contentType() {
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t HttpFullBody::length() {
|
||||
this->pull();
|
||||
return this->len;
|
||||
}
|
||||
|
||||
QueryData::QueryData() {}
|
||||
|
||||
QueryData::QueryData(std::string data) {
|
||||
auto list = str_util::str_split(data, "&");
|
||||
for (auto& item : list) {
|
||||
auto kv = str_util::str_splitv(item, "=", 2);
|
||||
auto k = decodeURIComponent(kv[0]);
|
||||
auto v = decodeURIComponent(kv[1]);
|
||||
this->append(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
std::string QueryData::contentType() {
|
||||
return "application/x-www-form-urlencoded";
|
||||
}
|
||||
|
||||
std::string QueryData::toQuery() {
|
||||
std::string re;
|
||||
for (auto& item : this->data) {
|
||||
for (auto& v : item.second) {
|
||||
if (re.length() > 0) {
|
||||
re += "&";
|
||||
}
|
||||
re += encodeURIComponent(item.first) + "=" + encodeURIComponent(v);
|
||||
}
|
||||
}
|
||||
return re;
|
||||
}
|
||||
|
||||
void QueryData::append(std::string key, std::string value) {
|
||||
if (this->data.find(key) != this->data.end()) {
|
||||
this->data[key].push_back(value);
|
||||
} else {
|
||||
this->data[key] = std::vector<std::string>({ value });
|
||||
}
|
||||
}
|
||||
|
||||
void QueryData::set(std::string key, std::string value) {
|
||||
this->data[key] = std::vector<std::string>({ value });
|
||||
}
|
||||
|
||||
std::string QueryData::body() {
|
||||
return this->toQuery();
|
||||
}
|
||||
|
||||
Request::~Request() {
|
||||
if (this->body) {
|
||||
delete this->body;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user