add http client

This commit is contained in:
2024-01-07 00:24:56 +08:00
parent 49070e1e60
commit 2515ccdb54
8 changed files with 642 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ option(ENABLE_ICONV "Use libiconv to convert encoding" ON)
option(ENABLE_STANDALONE "Build utils standalone" OFF)
option(ENABLE_CXX17 "Enable C++ 17" OFF)
option(INSTALL_DEP_FILES "Install a file with dependences." OFF)
option(ENABLE_SSL "Enable SSL" OFF)
if (ENABLE_STANDALONE)
project(utils)
@@ -23,6 +24,11 @@ if (ENABLE_ICONV)
find_package(Iconv)
endif()
if (ENABLE_SSL)
find_package(OpenSSL 3.0.0 REQUIRED)
set(HAVE_OPENSSL 1)
endif()
if (Iconv_FOUND)
set(HAVE_ICONV 1)
endif()
@@ -82,6 +88,7 @@ set(SOURCE_FILE
c_linked_list.cpp
file_reader.c
urlparse.cpp
http_client.cpp
)
set(SOURCE_FILE_HEADERS
cfileop.h
@@ -101,6 +108,7 @@ set(SOURCE_FILE_HEADERS
c_linked_list.h
file_reader.h
urlparse.h
http_client.h
)
add_library(utils STATIC ${SOURCE_FILE} ${SOURCE_FILE_HEADERS})
@@ -118,6 +126,12 @@ if (INSTALL_DEP_FILES)
target_link_libraries(utils shell32)
endif()
endif()
if (WIN32)
target_link_libraries(utils Ws2_32)
endif()
if (ENABLE_SSL)
target_link_libraries(utils OpenSSL::SSL OpenSSL::Crypto)
endif()
if (ENABLE_CXX17)
target_compile_features(utils PRIVATE cxx_std_17)
endif()

View File

@@ -92,6 +92,21 @@ int cstr_tolowercase(const char* str, size_t input_len, char** output) {
return 1;
}
int cstr_touppercase(const char* str, size_t input_len, char** output) {
if (!str || !output) return 0;
if (!input_len) input_len = strlen(str);
if (input_len == (size_t)-1) return 0;
char* tmp = malloc(input_len + 1);
if (!tmp) return 0;
size_t i = 0;
for (; i < input_len; i++) {
tmp[i] = toupper(str[i]);
}
tmp[input_len] = 0;
*output = tmp;
return 1;
}
uint32_t cstr_read_uint32(const uint8_t* bytes, int big) {
if (!bytes) return 0;
return big ? (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3] : bytes[0] + (bytes[1] << 8) + (bytes[2] << 16) + (bytes[3] << 24);

View File

@@ -27,6 +27,14 @@ int cstr_is_integer(const char* str, int allow_sign);
* @return 1 if successed otherwise 0.
*/
int cstr_tolowercase(const char* str, size_t input_len, char** output);
/**
* @brief Convert string to uppercase
* @param str Origin string
* @param input_len The length of origin string. If is 0, strlen will be called to calculate length.
* @param output Output string. Need free memory by calling free.
* @return 1 if successed otherwise 0.
*/
int cstr_touppercase(const char* str, size_t input_len, char** output);
/**
* @brief Convert bytes to uint32
* @param bytes Bytes (at least 4 bytes)

423
http_client.cpp Normal file
View 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;
}
}

162
http_client.h Normal file
View File

@@ -0,0 +1,162 @@
#ifndef _UTIL_HTTP_CLIENT_H
#define _UTIL_HTTP_CLIENT_H
#include <stddef.h>
#include <string>
#include <map>
#include <memory>
#include <vector>
#include "cstr_util.h"
#include "utils_config.h"
#if _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#endif
#if HAVE_OPENSSL
#include "openssl/ssl.h"
#endif
struct HeaderNameCompare {
bool operator()(const std::string& a, const std::string& b) const {
return cstr_stricmp(a.c_str(), b.c_str()) < 0;
}
};
typedef std::map<std::string, std::string, HeaderNameCompare> HeaderMap;
std::string decodeURIComponent(std::string str);
class HttpClientOptions {
public:
bool https = false;
};
class HttpBody {
public:
virtual size_t pullData(char* buf, size_t len) = 0;
virtual bool isFinished() = 0;
virtual std::string contentType() = 0;
virtual bool hasLength() = 0;
virtual size_t length() = 0;
};
class HttpFullBody: public HttpBody {
public:
virtual size_t pullData(char* buf, size_t len);
virtual bool isFinished();
virtual bool hasLength();
virtual std::string contentType();
virtual size_t length();
protected:
virtual std::string body() = 0;
private:
void pull();
std::string buff;
bool pulled;
size_t len;
};
class QueryData: public HttpFullBody {
public:
QueryData();
QueryData(std::string data);
std::string contentType();
std::map<std::string, std::vector<std::string>> data;
std::string toQuery();
void append(std::string key, std::string value);
void set(std::string key, std::string value);
protected:
std::string body();
};
class AIException: std::exception {
public:
AIException(int code);
const char* what();
private:
int code;
};
class SocketError: std::exception {
public:
SocketError();
const char* what();
private:
int code;
std::string message;
};
class Socket {
public:
Socket(std::string host, std::string protocol);
~Socket();
void connect();
size_t send(const char* data, size_t len, int flags = 0);
template<size_t T>
size_t send(const char(&data)[T], int flags = 0) {
return this->send(data, T, flags);
}
size_t send(std::string data, int flags = 0);
size_t recv(char* data, size_t len, int flags = 0);
template<size_t T>
size_t recv(char(&data)[T], int flags = 0) {
return this->recv(data, T, flags);
}
std::string recv(size_t len, int flags = 0);
void close();
private:
#if HAVE_OPENSSL
SSL_CTX* ssl_ctx = nullptr;
BIO* web = nullptr;
SSL* ssl = nullptr;
#endif
std::string host;
std::string protocol;
int socket = -1;
bool closed = false;
addrinfo* addr = nullptr;
};
class Request {
public:
Request(std::string host, std::string path, std::string method, HeaderMap headers, HttpClientOptions options);
~Request();
void send();
HeaderMap headers;
HttpClientOptions options;
void setBody(HttpBody* body) {
if (this->body != nullptr) delete this->body;
this->body = body;
}
private:
HttpBody* body = nullptr;
std::string host;
std::string path;
std::string method;
};
class Response {
public:
Response(Request req, Socket socket);
private:
Request req;
Socket socket;
};
class HttpClient {
public:
HttpClient(std::string host);
Request request(std::string path, std::string method);
HttpClientOptions options;
HeaderMap headers;
private:
std::string host;
};
#endif

View File

@@ -18,6 +18,18 @@ bool str_util::tolowercase(std::string ori, std::string& result) {
}
}
bool str_util::touppercase(std::string ori, std::string& result) {
char* tmp = nullptr;
auto re = cstr_touppercase(ori.c_str(), ori.length(), &tmp);
if (re) {
result = std::string(tmp, ori.length());
free(tmp);
return true;
} else {
return false;
}
}
std::string str_util::str_replace(std::string input, std::string pattern, std::string new_content) {
auto loc = input.find(pattern, 0);
auto len = pattern.length();

View File

@@ -12,6 +12,13 @@ namespace str_util {
* @return true if successed.
*/
bool tolowercase(std::string ori, std::string& result);
/**
* @brief Convert string to uppercase
* @param ori Origin string
* @param result Output string.
* @return true if successed.
*/
bool touppercase(std::string ori, std::string& result);
/**
* @brief Replace all pattern to new_content
* @param input Input string

View File

@@ -17,3 +17,4 @@
#cmakedefine HAVE__STRNICMP @HAVE__STRNICMP@
#cmakedefine HAVE_STRNCASECMP @HAVE_STRNCASECMP@
#cmakedefine HAVE_FCLOSEALL @HAVE_FCLOSEALL@
#cmakedefine HAVE_OPENSSL @HAVE_OPENSSL@