rublon-ssh/PAM/ssh/include/rublon/ws_http.hpp
2025-09-23 14:32:15 +02:00

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