Files
firmware/src/mesh/raspihttp/PiWebServer.cpp
T
Ben Meadors 0739d2a68c fix(raspihttp): memory safety and error-path leaks in PiWebServer (#11451)
- handleAPIv1ToRadio: the toRadio handler memcpy'd a fixed 512 bytes
  out of the request body regardless of its actual size. ulfius
  allocates binary_body at exactly binary_body_length bytes and leaves
  it NULL for a body-less PUT, so short bodies caused a heap overread
  and empty ones dereferenced NULL. The unclamped length (ulfius
  accepts up to 1024) was also handed to handleToRadio, reading past
  the 512-byte stack buffer. Clamp both directions, matching the ESP32
  ContentHandler behavior.

- callback_static_file: the stream-free callback only runs when
  ulfius_set_stream_response succeeds, so the failure branch leaked the
  FILE (and its fd) on every failed request. Close it and return 500.

- CheckSSLandLoad: free cert_pem before the missing-key return; the
  constructor retries after regenerating certs and the reload
  overwrote (leaked) the first buffer.

- generate_rsa_key / CreateSSLCertificate: free the EVP_PKEY_CTX,
  EVP_PKEY, and X509 on their error returns; previously only the
  success paths released them.
2026-08-12 19:12:32 -05:00

573 lines
20 KiB
C++

/*
Adds a WebServer and WebService callbacks to meshtastic via the Portduino/native target (Linux and
macOS). The WebServer & Webservices run in a real host thread beside the Portduino threading
emulation. It replaces the complete ESP32 Webserver libs including generation of SSL certificates,
because those libs use ESP-specific details that can't be emulated.
The WebServices adapt to the two major phoneapi functions "handleAPIv1FromRadio,handleAPIv1ToRadio"
The WebServer just adds basic support to deliver WebContent, so it can be used to
deliver the WebGui defined by the WebClient Project.
Steps to get it running:
Linux (apt):
1.) Add these libs to the compile and target machine:
sudo apt update && \
apt -y install openssl libssl-dev libsdl2-dev \
libulfius-dev liborcania-dev
macOS (Homebrew):
1.) Install prerequisites via Homebrew:
brew install ulfius openssl@3
The PlatformIO env (native-macos) picks up compiler/linker flags via
`pkg-config`. In particular, OpenSSL needs `pkg-config --cflags --libs openssl@3`
so both the Homebrew include path and linker flags are provided; ulfius and its
dependencies (liborcania, libyder) are also resolved via `pkg-config`.
2.) Configure the root directory of the web Content in the config.yaml file.
The following tags should be included and set at your needs
Example entry in the config.yaml
Webserver:
Port: 9001 # Port for Webserver & Webservices
RootPath: /home/marc/web # Root Dir of WebServer
3.) Checkout the web project
https://github.com/meshtastic/web.git
Build it and copy the content of the folder web/dist/* to the folder you did set as "RootPath"
!!!The WebServer should not be used as production system or exposed to the Internet. Its a raw basic version!!!
Author: Marc Philipp Hammermann
mail: marchammermann@googlemail.com
*/
// Mirrors the guard in PiWebServer.h - see comment there. macOS Homebrew
// provides ulfius + deps; Linux pulls them via apt. Either way, this
// translation unit only compiles when the headers are present.
#ifdef ARCH_PORTDUINO
#if __has_include(<ulfius.h>)
// First, in its own block so the include sorter keeps it there: configuration.h supplies the
// variant defines mesh-pb-constants.h needs (portduino resolves MAX_NUM_NODES at runtime).
#include "configuration.h"
#include "NodeDB.h"
#include "PhoneAPI.h"
#include "PiWebServer.h"
#include "PowerFSM.h"
#include "RadioLibInterface.h"
#include "airtime.h"
#include "graphics/Screen.h"
#include "main.h"
#include "mesh/wifi/WiFiAPClient.h"
#include "sleep.h"
#include <openssl/bn.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <orcania.h>
#include <string.h>
#include <ulfius.h>
#include <yder.h>
#include <cstring>
#include <string>
#include "PortduinoFS.h"
#include "platform/portduino/PortduinoGlue.h"
#define DEFAULT_REALM "default_realm"
#define PREFIX ""
#define KEY_PATH portduino_config.webserver_ssl_key_path.c_str()
#define CERT_PATH portduino_config.webserver_ssl_cert_path.c_str()
struct _file_config configWeb;
// We need to specify some content-type mapping, so the resources get delivered with the
// right content type and are displayed correctly in the browser
char contentTypes[][2][32] = {{".txt", "text/plain"}, {".html", "text/html"},
{".js", "text/javascript"}, {".png", "image/png"},
{".jpg", "image/jpg"}, {".gz", "application/gzip"},
{".gif", "image/gif"}, {".json", "application/json"},
{".css", "text/css"}, {".ico", "image/vnd.microsoft.icon"},
{".svg", "image/svg+xml"}, {".ts", "text/javascript"},
{".tsx", "text/javascript"}, {"", ""}};
#undef str
volatile bool isWebServerReady;
volatile bool isCertReady;
PiWebServerThread *piwebServerThread;
/**
* Return the filename extension
*/
const char *get_filename_ext(const char *path)
{
const char *dot = strrchr(path, '.');
if (!dot || dot == path)
return "*";
if (strchr(dot, '?') != NULL) {
//*strchr(dot, '?') = '\0';
const char *empty = "\0";
return empty;
}
return dot;
}
/**
* Streaming callback function to ease sending large files
*/
static ssize_t callback_static_file_stream(void *cls, uint64_t pos, char *buf, size_t max)
{
(void)(pos);
if (cls != NULL) {
return fread(buf, 1, max, (FILE *)cls);
} else {
return U_STREAM_END;
}
}
/**
* Cleanup FILE* structure when streaming is complete
*/
static void callback_static_file_stream_free(void *cls)
{
if (cls != NULL) {
fclose((FILE *)cls);
}
}
/**
* static file callback endpoint that delivers the content for WebServer calls
*/
int callback_static_file(const struct _u_request *request, struct _u_response *response, void *user_data)
{
size_t length;
FILE *f;
char *file_requested, *file_path, *url_dup_save, *real_path = NULL;
const char *content_type;
/*
* Comment this if statement if you don't access static files url from root dir, like /app
*/
if (request->callback_position > 0) {
return U_CALLBACK_CONTINUE;
} else if (user_data != NULL && (configWeb.files_path != NULL)) {
file_requested = o_strdup(request->http_url);
url_dup_save = file_requested;
while (file_requested[0] == '/') {
file_requested++;
}
file_requested += o_strlen(configWeb.url_prefix);
while (file_requested[0] == '/') {
file_requested++;
}
if (strchr(file_requested, '#') != NULL) {
*strchr(file_requested, '#') = '\0';
}
if (strchr(file_requested, '?') != NULL) {
*strchr(file_requested, '?') = '\0';
}
if (file_requested == NULL || o_strlen(file_requested) == 0 || 0 == o_strcmp("/", file_requested)) {
o_free(url_dup_save);
url_dup_save = file_requested = o_strdup("index.html");
}
file_path = msprintf("%s/%s", configWeb.files_path, file_requested);
real_path = realpath(file_path, NULL);
if (0 == o_strncmp(configWeb.files_path, real_path, o_strlen(configWeb.files_path))) {
if (access(file_path, F_OK) != -1) {
f = fopen(file_path, "rb");
if (f) {
fseek(f, 0, SEEK_END);
length = ftell(f);
fseek(f, 0, SEEK_SET);
content_type = u_map_get_case(&configWeb.mime_types, get_filename_ext(file_requested));
if (content_type == NULL) {
content_type = u_map_get(&configWeb.mime_types, "*");
LOG_DEBUG("Static File Server - Unknown mime type for ext %s ", get_filename_ext(file_requested));
}
u_map_put(response->map_header, "Content-Type", content_type);
u_map_copy_into(response->map_header, &configWeb.map_header);
if (ulfius_set_stream_response(response, 200, callback_static_file_stream, callback_static_file_stream_free,
length, STATIC_FILE_CHUNK, f) != U_OK) {
LOG_DEBUG("callback_static_file - Error ulfius_set_stream_response");
// The stream-free callback only runs when the stream was accepted, so the
// file must be closed here or the FILE and its fd leak on every failure.
fclose(f);
ulfius_set_string_body_response(response, 500, "Internal server error");
}
}
} else {
if (configWeb.redirect_on_404 == NULL) {
ulfius_set_string_body_response(response, 404, "File not found");
} else {
ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404);
response->status = 302;
}
}
} else {
if (configWeb.redirect_on_404 == NULL) {
ulfius_set_string_body_response(response, 404, "File not found");
} else {
ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404);
response->status = 302;
}
}
o_free(file_path);
o_free(url_dup_save);
free(real_path); // realpath uses malloc
return U_CALLBACK_CONTINUE;
} else {
LOG_DEBUG("Static File Server - user_data NULL or inconsistent");
return U_CALLBACK_ERROR;
}
}
/*
* Adapt the radioapi to the Webservice handleAPIv1ToRadio
* Trigger : WebGui(SAVE)->WebServcice->phoneApi
*/
int handleAPIv1ToRadio(const struct _u_request *req, struct _u_response *res, void *user_data)
{
LOG_DEBUG("handleAPIv1ToRadio web -> radio ");
ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf");
ulfius_add_header_to_response(res, "Access-Control-Allow-Headers", "Content-Type");
ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*");
ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "PUT, OPTIONS");
ulfius_add_header_to_response(res, "X-Protobuf-Schema",
"https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto");
if (strcmp(req->http_verb, "OPTIONS") == 0) {
ulfius_set_response_properties(res, U_OPT_STATUS, 204);
return U_CALLBACK_COMPLETE;
}
byte buffer[MAX_TO_FROM_RADIO_SIZE];
// ulfius allocates binary_body at exactly binary_body_length bytes (NULL for a body-less
// PUT), and the framework accepts bodies larger than our buffer, so clamp both directions.
size_t s = req->binary_body ? req->binary_body_length : 0;
if (s > sizeof(buffer))
s = sizeof(buffer);
if (s > 0)
memcpy(buffer, req->binary_body, s);
// FIXME* Problem with portdunio loosing mountpoint maybe because of running in a real sep. thread
portduinoVFS->mountpoint(configWeb.rootPath);
LOG_DEBUG("Received %d bytes from PUT request", s);
static_cast<HttpAPI *>(user_data)->handleToRadio(buffer, s);
LOG_DEBUG("end web->radio ");
return U_CALLBACK_COMPLETE;
}
/*
* Adapt the radioapi to the Webservice handleAPIv1FromRadio
* Trigger : WebGui(POLL)->handleAPIv1FromRadio->phoneapi->Meshtastic(Radio) events
*/
int handleAPIv1FromRadio(const struct _u_request *req, struct _u_response *res, void *user_data)
{
// LOG_DEBUG("handleAPIv1FromRadio radio -> web");
std::string valueAll;
// Status code is 200 OK by default.
ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf");
ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*");
ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "GET");
ulfius_add_header_to_response(res, "X-Protobuf-Schema",
"https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto");
if (strcmp(req->http_verb, "OPTIONS") == 0) {
ulfius_set_response_properties(res, U_OPT_STATUS, 204);
return U_CALLBACK_COMPLETE;
}
uint8_t txBuf[MAX_STREAM_BUF_SIZE];
uint32_t len = 1;
if (valueAll == "true") {
while (len) {
len = static_cast<HttpAPI *>(user_data)->getFromRadio(txBuf);
ulfius_set_response_properties(res, U_OPT_STATUS, 200, U_OPT_BINARY_BODY, txBuf, len);
const char *tmpa = (const char *)txBuf;
ulfius_set_string_body_response(res, 200, tmpa);
// LOG_DEBUG("\n----webAPI response all:----");
// LOG_DEBUG(tmpa);
// LOG_DEBUG(".");
}
// Otherwise, just return one protobuf
} else {
len = static_cast<HttpAPI *>(user_data)->getFromRadio(txBuf);
const char *tmpa = (const char *)txBuf;
ulfius_set_binary_body_response(res, 200, tmpa, len);
// LOG_DEBUG("\n----webAPI response:");
// LOG_DEBUG(tmpa);
// LOG_DEBUG(".");
}
// LOG_DEBUG("end radio->web", len);
return U_CALLBACK_COMPLETE;
}
/*
OpenSSL RSA Key Gen
*/
int generate_rsa_key(EVP_PKEY **pkey)
{
EVP_PKEY_CTX *pkey_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
if (!pkey_ctx)
return -1;
if (EVP_PKEY_keygen_init(pkey_ctx) <= 0 || EVP_PKEY_CTX_set_rsa_keygen_bits(pkey_ctx, 2048) <= 0 ||
EVP_PKEY_keygen(pkey_ctx, pkey) <= 0) {
EVP_PKEY_CTX_free(pkey_ctx);
return -1;
}
EVP_PKEY_CTX_free(pkey_ctx);
return 0; // SUCCESS
}
int generate_self_signed_x509(EVP_PKEY *pkey, X509 **x509)
{
*x509 = X509_new();
if (!*x509)
return -1;
if (X509_set_version(*x509, 2) != 1)
return -1;
ASN1_INTEGER_set(X509_get_serialNumber(*x509), 1);
X509_gmtime_adj(X509_get_notBefore(*x509), 0);
X509_gmtime_adj(X509_get_notAfter(*x509), 31536000L); // 1 YEAR ACCESS
X509_set_pubkey(*x509, pkey);
// SET Subject Name
X509_NAME *name = X509_get_subject_name(*x509);
X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *)"DE", -1, -1, 0);
X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"Meshtastic", -1, -1, 0);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"meshtastic.local", -1, -1, 0);
// Selfsigned, Issuer = Subject
X509_set_issuer_name(*x509, name);
// Certificate signed with our privte key
if (X509_sign(*x509, pkey, EVP_sha256()) <= 0)
return -1;
return 0;
}
char *read_file_into_string(const char *filename)
{
FILE *file = fopen(filename, "rb");
if (file == NULL) {
LOG_ERROR("Error reading File : %s ", filename);
return NULL;
}
// Size of file
fseek(file, 0, SEEK_END);
long filesize = ftell(file);
rewind(file);
// reserve mem for file + 1 byte
char *buffer = (char *)malloc(filesize + 1);
if (buffer == NULL) {
LOG_ERROR("Malloc failed for file : %s ", filename);
fclose(file);
return NULL;
}
// read content
size_t readSize = fread(buffer, 1, filesize, file);
if (readSize != filesize) {
LOG_ERROR("Error reading file into buffer");
free(buffer);
fclose(file);
return NULL;
}
// add terminator sign at the end
buffer[filesize] = '\0';
fclose(file);
return buffer; // return pointer
}
int PiWebServerThread::CheckSSLandLoad()
{
// read certificate
cert_pem = read_file_into_string(CERT_PATH);
if (cert_pem == NULL) {
LOG_ERROR("SSL Certificate File can't be loaded or missing");
return 1;
}
// read private key
key_pem = read_file_into_string(KEY_PATH);
if (key_pem == NULL) {
LOG_ERROR("File private_key can't be loaded or missing");
// The constructor retries CheckSSLandLoad() after regenerating, which would overwrite
// (and leak) the cert buffer loaded above.
free(cert_pem);
cert_pem = NULL;
return 2;
}
return 0;
}
int PiWebServerThread::CreateSSLCertificate()
{
EVP_PKEY *pkey = NULL;
X509 *x509 = NULL;
if (generate_rsa_key(&pkey) != 0) {
LOG_ERROR("Error generating RSA-Key");
return 1;
}
if (generate_self_signed_x509(pkey, &x509) != 0) {
LOG_ERROR("Error generating X509-Cert");
// generate_self_signed_x509 can fail after allocating *x509; X509_free(NULL) is a no-op
X509_free(x509);
EVP_PKEY_free(pkey);
return 2;
}
// Open file to write private key file
FILE *pkey_file = fopen(KEY_PATH, "wb");
if (!pkey_file) {
LOG_ERROR("Error opening private key file");
X509_free(x509);
EVP_PKEY_free(pkey);
return 3;
}
// write private key file
PEM_write_PrivateKey(pkey_file, pkey, NULL, NULL, 0, NULL, NULL);
fclose(pkey_file);
// open Certificate file
FILE *x509_file = fopen(CERT_PATH, "wb");
if (!x509_file) {
LOG_ERROR("Error opening cert");
X509_free(x509);
EVP_PKEY_free(pkey);
return 4;
}
// write certificate
PEM_write_X509(x509_file, x509);
fclose(x509_file);
EVP_PKEY_free(pkey);
LOG_INFO("Create SSL Key %s successful", KEY_PATH);
X509_free(x509);
LOG_INFO("Create SSL Cert %s successful", CERT_PATH);
return 0;
}
void initWebServer() {}
PiWebServerThread::PiWebServerThread()
{
int ret, retssl, webservport;
if (CheckSSLandLoad() != 0) {
CreateSSLCertificate();
if (CheckSSLandLoad() != 0) {
LOG_ERROR("Major Error Gen & Read SSL Certificate");
}
}
if (portduino_config.webserverport != 0) {
webservport = portduino_config.webserverport;
LOG_INFO("Use webserver port from yaml config %i ", webservport);
} else {
LOG_INFO("Webserver port in yaml config 0, default to 9443");
webservport = 9443;
}
// Web Content Service Instance
if (ulfius_init_instance(&instanceWeb, webservport, NULL, DEFAULT_REALM) != U_OK) {
LOG_ERROR("Webserver start failed, abort");
} else {
LOG_INFO("Webserver started");
u_map_init(&configWeb.mime_types);
u_map_put(&configWeb.mime_types, "*", "application/octet-stream");
u_map_put(&configWeb.mime_types, ".html", "text/html");
u_map_put(&configWeb.mime_types, ".htm", "text/html");
u_map_put(&configWeb.mime_types, ".tsx", "application/javascript");
u_map_put(&configWeb.mime_types, ".ts", "application/javascript");
u_map_put(&configWeb.mime_types, ".css", "text/css");
u_map_put(&configWeb.mime_types, ".js", "application/javascript");
u_map_put(&configWeb.mime_types, ".json", "application/json");
u_map_put(&configWeb.mime_types, ".png", "image/png");
u_map_put(&configWeb.mime_types, ".gif", "image/gif");
u_map_put(&configWeb.mime_types, ".jpeg", "image/jpeg");
u_map_put(&configWeb.mime_types, ".jpg", "image/jpeg");
u_map_put(&configWeb.mime_types, ".ttf", "font/ttf");
u_map_put(&configWeb.mime_types, ".woff", "font/woff");
u_map_put(&configWeb.mime_types, ".ico", "image/x-icon");
u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml");
webrootpath = portduino_config.webserver_root_path;
configWeb.files_path = (char *)webrootpath.c_str();
configWeb.url_prefix = "";
configWeb.rootPath = strdup(portduinoVFS->mountpoint());
u_map_put(instanceWeb.default_headers, "Access-Control-Allow-Origin", "*");
// Maximum body size sent by the client is 1 Kb
instanceWeb.max_post_body_size = 1024;
ulfius_add_endpoint_by_val(&instanceWeb, "GET", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI);
ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI);
ulfius_add_endpoint_by_val(&instanceWeb, "PUT", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI);
ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI);
// Add callback function to all endpoints for the Web Server
ulfius_add_endpoint_by_val(&instanceWeb, "GET", NULL, "/*", 2, &callback_static_file, &configWeb);
// thats for serving without SSL
// retssl = ulfius_start_framework(&instanceWeb);
// thats for serving with SSL
retssl = ulfius_start_secure_framework(&instanceWeb, key_pem, cert_pem);
if (retssl == U_OK) {
LOG_INFO("Web Server framework started on port: %i ", webservport);
LOG_INFO("Web Server root %s", (char *)webrootpath.c_str());
} else {
LOG_ERROR("Web Server framework start failed, err: %d", retssl);
}
}
}
PiWebServerThread::~PiWebServerThread()
{
u_map_clean(&configWeb.mime_types);
ulfius_stop_framework(&instanceWeb);
ulfius_clean_instance(&instanceWeb);
free(configWeb.rootPath);
free(key_pem);
free(cert_pem);
LOG_INFO("End framework");
}
#endif
#endif