286 lines
11 KiB
C++
286 lines
11 KiB
C++
#pragma once
|
|
|
|
#include <cstring>
|
|
#include <rublon/configuration.hpp>
|
|
#include <rublon/error.hpp>
|
|
#include <rublon/utils.hpp>
|
|
|
|
#include <tl/expected.hpp>
|
|
|
|
#include <libwebsockets.h>
|
|
#include <string>
|
|
|
|
namespace rublon {
|
|
// Struktura do obsługi połączenia WebSocket
|
|
class LWS {
|
|
std::reference_wrapper< const Configuration > _config;
|
|
|
|
const Configuration & conf() const noexcept {
|
|
return _config.get();
|
|
}
|
|
|
|
struct session_data {
|
|
const Request & request;
|
|
Response & response;
|
|
int status{};
|
|
bool done{false};
|
|
};
|
|
|
|
static int callback(struct lws * wsi, enum lws_callback_reasons reason, void * user, void * in, size_t len) {
|
|
auto * data = reinterpret_cast< session_data * >(user);
|
|
int n;
|
|
|
|
switch(reason) {
|
|
/* because we are protocols[0] ... */
|
|
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
|
|
log(LogLevel::Error, "CLIENT_CONNECTION_ERROR: %s\n", in ? ( char * ) in : "(null)");
|
|
if(data)
|
|
data->done = true;
|
|
lws_cancel_service(lws_get_context(wsi));
|
|
return 0;
|
|
|
|
case LWS_CALLBACK_CLOSED_CLIENT_HTTP:
|
|
if(data)
|
|
data->done = true;
|
|
lws_cancel_service(lws_get_context(wsi));
|
|
return 0;
|
|
|
|
/* ...callbacks related to receiving the result... */
|
|
|
|
case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: {
|
|
data->status = lws_http_client_http_response(wsi);
|
|
log(LogLevel::Info, "Connected with server response: %d\n", data->status);
|
|
|
|
char tmp[512];
|
|
|
|
auto copy_header = [&](const auto name) {
|
|
if (lws_hdr_custom_length(wsi, name.data(), name.size()) > 0 &&
|
|
lws_hdr_custom_copy(wsi, tmp, sizeof(tmp), name.data(), name.size()) > 0) {
|
|
data->response.headers[{name.data(), name.size()-1}] = tmp;
|
|
}
|
|
};
|
|
using namespace std::string_view_literals;
|
|
copy_header("x-ratelimit-limit:"sv);
|
|
copy_header("x-ratelimit-remaining:"sv);
|
|
copy_header("x-rublon-signature:"sv);
|
|
|
|
break;
|
|
}
|
|
|
|
case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: {
|
|
log(LogLevel::Info, "RECEIVE_CLIENT_HTTP_READ: read %d\n", ( int ) len);
|
|
if(data && in && len) {
|
|
data->response.body.append(static_cast< const char * >(in), len);
|
|
}
|
|
return 0; /* don't passthru */
|
|
}
|
|
|
|
case LWS_CALLBACK_RECEIVE_CLIENT_HTTP: {
|
|
// Drain the socket; lws will subsequently trigger READ callbacks with 'in/len'
|
|
char buf[LWS_PRE + 2048];
|
|
char * p = buf + LWS_PRE;
|
|
int n = ( int ) sizeof(buf) - LWS_PRE;
|
|
if(lws_http_client_read(wsi, &p, &n) < 0)
|
|
return -1;
|
|
return 0; /* don't passthru */
|
|
}
|
|
|
|
case LWS_CALLBACK_COMPLETED_CLIENT_HTTP:
|
|
log(LogLevel::Info, "LWS_CALLBACK_COMPLETED_CLIENT_HTTP\n");
|
|
if(data)
|
|
data->done = true;
|
|
lws_cancel_service(lws_get_context(wsi));
|
|
return 0;
|
|
|
|
case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: {
|
|
if(!lws_http_is_redirected_to_get(wsi)) {
|
|
unsigned char ** pp = ( unsigned char ** ) in;
|
|
unsigned char * end = (*pp) + len;
|
|
|
|
// Add user headers (ensure names include ':' e.g. "Content-Type:")
|
|
for(const auto & [name, value] : data->request.headers) {
|
|
if(lws_add_http_header_by_name(wsi,
|
|
( const unsigned char * ) name.c_str(),
|
|
( const unsigned char * ) value.c_str(),
|
|
( int ) value.size(),
|
|
pp,
|
|
end)) {
|
|
log(LogLevel::Error, "Header add error");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Ensure Content-Length if sending a body
|
|
if(!data->request.body.empty()) {
|
|
std::string cl = std::to_string(data->request.body.size());
|
|
if(lws_add_http_header_by_name(wsi,
|
|
( const unsigned char * ) "Content-Length:",
|
|
( const unsigned char * ) cl.c_str(),
|
|
( int ) cl.size(),
|
|
pp,
|
|
end)) {
|
|
log(LogLevel::Error, "Header Content-Length error");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Tell lws the body will follow
|
|
if(!data->request.body.empty())
|
|
lws_client_http_body_pending(wsi, 1);
|
|
|
|
lws_callback_on_writable(wsi);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
case LWS_CALLBACK_CLIENT_HTTP_WRITEABLE:
|
|
// according to doc, there is no need for LWS_PRE when using HTTP_WRITE
|
|
n = lws_write(wsi, ( unsigned char * ) data->request.body.c_str(), data->request.body.size(), LWS_WRITE_HTTP_FINAL);
|
|
if(n < 0)
|
|
return -1;
|
|
/* we only had one thing to send, so inform lws we are donefi
|
|
* if we had more to send, call lws_callback_on_writable(wsi);
|
|
* and just return 0 from callback. On having sent the last
|
|
* part, call the below api instead.*/
|
|
lws_client_http_body_pending(wsi, 0);
|
|
return 0;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return lws_callback_http_dummy(wsi, reason, user, in, len);
|
|
};
|
|
|
|
static inline lws_protocols protocols[] = {{
|
|
"http-client", // const char *name
|
|
LWS::callback, // lws_callback_function *
|
|
0, // size_t per_session_data_size
|
|
0, // size_t rx_buffer_size
|
|
0, // unsigned int id
|
|
0, // void *user
|
|
0 // size_t tx_packet_size
|
|
},
|
|
{nullptr, nullptr, 0, 0, 0, nullptr, 0}};
|
|
|
|
public:
|
|
lws_context_creation_info info{};
|
|
lws_context * _context{nullptr};
|
|
|
|
LWS(const Configuration & config) : _config{config} {
|
|
std::memset(&info, 0, sizeof(info));
|
|
|
|
info.port = CONTEXT_PORT_NO_LISTEN;
|
|
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
|
|
info.protocols = protocols;
|
|
|
|
_context = lws_create_context(&info);
|
|
assert(_context != nullptr);
|
|
|
|
auto lws_log_emit = [](int level, const char * line) {
|
|
LogLevel rlevel{LLL_DEBUG};
|
|
if(level == LLL_ERR)
|
|
rlevel = LogLevel::Error;
|
|
if(level == LLL_WARN)
|
|
rlevel = LogLevel::Warning;
|
|
if(level == LLL_INFO)
|
|
rlevel = LogLevel::Info;
|
|
if(level == LLL_NOTICE)
|
|
rlevel = LogLevel::Debug;
|
|
if(level == LLL_DEBUG)
|
|
rlevel = LogLevel::Debug;
|
|
|
|
log(rlevel, "libwesockets: %s", line);
|
|
};
|
|
|
|
// if(_config.get().logging) {
|
|
lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO | LLL_DEBUG | LLL_USER | LLL_HEADER | LLL_CLIENT, lws_log_emit);
|
|
// } else {
|
|
// lws_set_log_level(LLL_ERR | LLL_WARN, lws_log_emit);
|
|
// }
|
|
}
|
|
|
|
LWS(const LWS &) = delete;
|
|
LWS(LWS &&) = delete;
|
|
|
|
LWS & operator=(const LWS &) = delete;
|
|
LWS & operator=(LWS &&) = delete;
|
|
|
|
tl::expected< std::reference_wrapper< Response >, ConnectionError >
|
|
request(std::string_view uri, const Request & request, Response & response) const {
|
|
using namespace memory::literals;
|
|
memory::Monotonic_8k_Resource memoryResource;
|
|
|
|
// --- Parse URL ---
|
|
std::pmr::string protocol = "https";
|
|
std::pmr::string host{&memoryResource};
|
|
std::pmr::string path{"/", &memoryResource};
|
|
std::string_view sv_uri(uri);
|
|
|
|
if(auto pos = sv_uri.find("://"); pos != std::string_view::npos) {
|
|
protocol = std::string(sv_uri.substr(0, pos));
|
|
sv_uri.remove_prefix(pos + 3);
|
|
}
|
|
|
|
auto slash_pos = sv_uri.find('/');
|
|
if(slash_pos != std::string_view::npos) {
|
|
host = std::pmr::string(sv_uri.substr(0, slash_pos), &memoryResource);
|
|
path = std::pmr::string(sv_uri.substr(slash_pos), &memoryResource);
|
|
} else {
|
|
host = std::pmr::string(sv_uri, &memoryResource);
|
|
}
|
|
|
|
const int use_ssl = (protocol == "https") ? LCCSCF_USE_SSL : 0;
|
|
|
|
// --- Callback funkcji obsługującej klienta ---
|
|
session_data client_data{request, response};
|
|
|
|
lws_client_connect_info ccinfo = {};
|
|
ccinfo.alpn = "http/1.1";
|
|
ccinfo.context = _context;
|
|
ccinfo.address = host.c_str();
|
|
ccinfo.port = (protocol == "https") ? 443 : 80;
|
|
ccinfo.path = path.c_str();
|
|
ccinfo.ssl_connection = use_ssl;
|
|
ccinfo.host = host.c_str();
|
|
ccinfo.origin = host.c_str();
|
|
ccinfo.method = "POST";
|
|
ccinfo.protocol = protocols[0].name;
|
|
ccinfo.userdata = &client_data;
|
|
ccinfo.ietf_version_or_minus_one = -1;
|
|
ccinfo.pwsi = nullptr;
|
|
|
|
struct lws * wsi = lws_client_connect_via_info(&ccinfo);
|
|
if(!wsi) {
|
|
lws_context_destroy(_context);
|
|
return tl::unexpected{ConnectionError{ConnectionError::Timeout, 0}};
|
|
}
|
|
|
|
// --- Pętla serwisu ---
|
|
while(lws_service(_context, 1000) >= 0) {
|
|
if(!response.body.empty())
|
|
break; // odebrano dane
|
|
}
|
|
const auto http_code = client_data.status;
|
|
if(http_code >= 500) {
|
|
log(LogLevel::Error, "%s response with code %d ", "CURL", http_code);
|
|
return tl::unexpected{ConnectionError{ConnectionError::HttpError, http_code}};
|
|
}
|
|
|
|
for(const auto & [name, value] : response.headers) {
|
|
log(LogLevel::Debug, "Header %s:%s", name.c_str(), value.c_str());
|
|
}
|
|
log(LogLevel::Debug, "Body %s", response.body.c_str());
|
|
|
|
return response;
|
|
}
|
|
|
|
~LWS() {
|
|
if(_context) {
|
|
lws_context_destroy(_context);
|
|
_context = nullptr;
|
|
}
|
|
}
|
|
};
|
|
} // namespace rublon
|