From 727e658662b221c4446f141e55bb1059345095e9 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Thu, 8 Jan 2026 09:05:58 +0100 Subject: Add missing lib/webserver.{h,cpp} --- lib/webserver.cpp | 360 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 lib/webserver.cpp (limited to 'lib/webserver.cpp') diff --git a/lib/webserver.cpp b/lib/webserver.cpp new file mode 100644 index 0000000..df66bd9 --- /dev/null +++ b/lib/webserver.cpp @@ -0,0 +1,360 @@ +/* + 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; +} -- cgit v1.2.3