/*
Copyright (C) 2025
Matthias P. Braendli, matthias.braendli@mpb.li
http://www.opendigitalradio.org
*/
/*
This file is part of the ODR-mmbTools.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include "webserver.h"
#include
#include "Log.h"
using namespace std;
static const char* http_ok = "HTTP/1.0 200 OK\r\n";
static const char* http_404 = "HTTP/1.0 404 Not Found\r\n";
/* unused:
static const char* http_400 = "HTTP/1.0 400 Bad Request\r\n";
static const char* http_405 = "HTTP/1.0 405 Method Not Allowed\r\n";
static const char* http_500 = "HTTP/1.0 500 Internal Server Error\r\n";
static const char* http_503 = "HTTP/1.0 503 Service Unavailable\r\n";
static const char* http_contenttype_data = "Content-Type: application/octet-stream\r\n";
static const char* http_contenttype_html = "Content-Type: text/html; charset=utf-8\r\n";
*/
static const char* http_contenttype_text = "Content-Type: text/plain\r\n";
static const char* http_contenttype_json = "Content-Type: application/json; charset=utf-8\r\n";
static const char* http_nocache = "Cache-Control: no-cache\r\n";
WebServer::WebServer(std::string listen_ip, uint16_t port, const std::string& index_content)
: index_content(index_content)
{
server_socket.listen(port, listen_ip);
handler_thread = thread(&WebServer::serve, this);
}
WebServer::~WebServer()
{
running = false;
if (handler_thread.joinable()) {
handler_thread.join();
}
server_socket.close();
}
void WebServer::update_stats_json(const std::string& new_stats_json)
{
unique_lock lock(data_mutex);
stats_json = new_stats_json;
}
void WebServer::serve()
{
deque > running_connections;
while (running) {
auto sock = server_socket.accept(1000);
if (sock.valid()) {
running_connections.push_back(async(launch::async,
&WebServer::dispatch_client, this, std::move(sock)));
deque > still_running_connections;
for (auto& fut : running_connections) {
if (fut.valid()) {
switch (fut.wait_for(chrono::milliseconds(1))) {
case future_status::deferred:
case future_status::timeout:
still_running_connections.push_back(std::move(fut));
break;
case future_status::ready:
fut.get();
break;
}
}
}
running_connections = std::move(still_running_connections);
}
}
etiLog.level(info) << "WebServer draining connections";
while (running_connections.size() > 0) {
deque > still_running_connections;
for (auto& fut : running_connections) {
if (fut.valid()) {
switch (fut.wait_for(chrono::milliseconds(1))) {
case future_status::deferred:
case future_status::timeout:
still_running_connections.push_back(std::move(fut));
break;
case future_status::ready:
fut.get();
break;
}
}
}
running_connections = std::move(still_running_connections);
}
}
static string recv_line(Socket::TCPSocket& s) {
string line;
bool cr_seen = false;
while (true) {
char c = 0;
ssize_t ret = s.recv(&c, 1, 0);
if (ret == 0) {
return "";
}
else if (ret == -1) {
string errstr = strerror(errno);
etiLog.level(error) << "recv error " << errstr;
return "";
}
line += c;
if (c == '\r') {
cr_seen = true;
}
else if (cr_seen and c == '\n') {
return line;
}
}
}
static vector recv_exactly(Socket::TCPSocket& s, size_t num_bytes)
{
vector buf(num_bytes);
size_t rx = 0;
while (rx < num_bytes) {
const size_t remain = num_bytes - rx;
ssize_t ret = s.recv(buf.data() + rx, remain, 0);
if (ret == 0) {
break;
}
else if (ret == -1) {
string errstr = strerror(errno);
etiLog.level(error) << "recv error " << errstr;
return {};
}
else {
rx += ret;
}
}
return buf;
}
static vector split(const string& str, char c = ' ')
{
const char *s = str.data();
vector result;
do {
const char *begin = s;
while (*s != c && *s)
s++;
result.push_back(string(begin, s));
} while (0 != *s++);
return result;
}
struct http_request_t {
bool valid = false;
bool is_get = false;
bool is_post = false;
string url;
map headers;
string post_data;
};
static http_request_t parse_http_headers(Socket::TCPSocket& s) {
http_request_t r;
const auto first_line = recv_line(s);
const auto request_type = split(first_line);
if (request_type.size() != 3) {
return r;
}
else if (request_type[0] == "GET") {
r.is_get = true;
}
else if (request_type[0] == "POST") {
r.is_post = true;
}
else {
return r;
}
r.url = request_type[1];
while (true) {
string header_line = recv_line(s);
if (header_line == "\r\n") {
break;
}
const auto header = split(header_line, ':');
if (header.size() == 2) {
r.headers.emplace(header[0], header[1]);
}
}
if (r.is_post) {
constexpr auto CONTENT_LENGTH = "Content-Length";
if (r.headers.count(CONTENT_LENGTH) == 1) {
try {
const int content_length = stoi(r.headers[CONTENT_LENGTH]);
if (content_length > 1024 * 1024) {
etiLog.level(warn) << "Unreasonable POST Content-Length: " << content_length;
return r;
}
const auto buf = recv_exactly(s, content_length);
r.post_data = string(buf.begin(), buf.end());
}
catch (const invalid_argument&) {
etiLog.level(warn) << "Cannot parse POST Content-Length: " << r.headers[CONTENT_LENGTH];
return r;
}
catch (const out_of_range&) {
etiLog.level(warn) << "Cannot represent POST Content-Length: " << r.headers[CONTENT_LENGTH];
return r;
}
}
}
r.valid = true;
return r;
}
static bool send_http_response(
Socket::TCPSocket& s,
const string& statuscode,
const string& data,
const string& content_type = http_contenttype_text)
{
string headers = statuscode;
headers += content_type;
headers += http_nocache;
headers += "\r\n";
headers += data;
ssize_t ret = s.send(headers.data(), headers.size(), MSG_NOSIGNAL);
if (ret == -1) {
etiLog.level(warn) << "Failed to send response " << statuscode << " " << data;
}
return ret != -1;
}
bool WebServer::dispatch_client(Socket::TCPSocket&& sock)
{
try {
Socket::TCPSocket s(std::move(sock));
bool success = false;
if (not s.valid()) {
etiLog.level(error) << "socket in dispatcher not valid!";
return false;
}
const auto req = parse_http_headers(s);
if (not req.valid) {
return false;
}
if (req.is_get) {
if (req.url == "/") {
success = send_index(s);
}
else if (req.url == "/stats.json") {
success = send_stats(s);
}
}
else if (req.is_post) {
if (req.url == "/rc") {
//success = handle_rc(s, req.post_data);
}
else {
etiLog.level(warn) << "Could not understand POST request " << req.url;
}
}
else {
throw logic_error("valid req is neither GET nor POST!");
}
if (not success) {
send_http_response(s, http_404, "Could not understand request.\r\n");
}
return success;
}
catch (const std::exception& e)
{
return false;
}
}
bool WebServer::send_index(Socket::TCPSocket& s)
{
if (not send_http_response(s, http_ok, "", http_contenttype_text)) {
return false;
}
ssize_t ret = s.send(index_content.c_str(), index_content.size(), MSG_NOSIGNAL);
if (ret == -1) {
etiLog.level(warn) << "Failed to send index";
return false;
}
return true;
}
bool WebServer::send_stats(Socket::TCPSocket& s)
{
if (not send_http_response(s, http_ok, "", http_contenttype_json)) {
return false;
}
std::string jsonstr = "{ }";
{
unique_lock lock(data_mutex);
if (not stats_json.empty()) {
jsonstr = stats_json;
}
}
ssize_t ret = s.send(jsonstr.c_str(), jsonstr.size(), MSG_NOSIGNAL);
if (ret == -1) {
etiLog.level(warn) << "Failed to send index";
return false;
}
return true;
}