Add a new web server backend based on libwebsockets (lws) as an
alternative to mongoose. The new backend provides the same HTTP API:
POST /upload - stream SWU images to the installer, supporting both
multipart/form-data and application/octet-stream
POST /restart - trigger the post-update action
GET * - static file serving from a configurable web root
GET (ws) - WebSocket status stream with the same JSON protocol
as the mongoose backend
Kconfig is restructured to introduce a WEBSERVER choice menu so that
mongoose and lws are mutually exclusive options. The config options
MONGOOSESSL and MONGOOSEIPV6 cover both backends while the name is kept.
Likewise, the mongoose_interface.h is implemented by lws but not renamed.
Three of the CLI/config file parameters that mongoose carries are not
implemented. Two of them configure HTTP digest authentication that is
implemented in libwebsockets for the client side only. The other one is
file listing, which would involve a HTML template.
Kconfig | 36 +
Makefile | 2 +-
Makefile.flags | 5 +
core/swupdate.c | 2 +-
crypto/Makefile | 3 +
doc/source/improvement_proposals.rst | 4 -
doc/source/swupdate.rst | 5 +-
examples/configuration/swupdate.cfg | 20 +-
lws/Kconfig | 18 +
lws/Makefile | 5 +
lws/lws_interface.c | 881 +++++++++++++++++++++++++
mongoose/Kconfig | 33 -
scripts/acceptance-tests/CheckImage.mk | 2 +-
13 files changed, 965 insertions(+), 51 deletions(-)
create mode 100644 lws/Kconfig
create mode 100644 lws/Makefile
create mode 100644 lws/lws_interface.c
diff --git a/Kconfig b/Kconfig
index 8b6139de..615a622a 100644
--- a/Kconfig
+++ b/Kconfig
@@ -399,8 +399,44 @@ config CHANNEL_CURL
source "suricatta/Kconfig"
+menuconfig WEBSERVER
+ bool "Web Server"
+ help
+ Enable update from remote using a web server on the target.
+
+if WEBSERVER
+choice
+ prompt "Web Server Type"
+ default MONGOOSE
+ help
+ Choose the web server
+
source "mongoose/Kconfig"
+source "lws/Kconfig"
+
+endchoice
+
+config MONGOOSEIPV6
+ bool "IPv6 support"
+ default y
+ help
+ Enable IPv6 support in the web server.
+
+config MONGOOSESSL
+ bool "SSL support"
+ depends on SSL_IMPL_OPENSSL || SSL_IMPL_WOLFSSL || SSL_IMPL_MBEDTLS || LWS
+ help
+ Enable TLS/SSL support in the web server.
+
+ For the lws backend, TLS is handled by the TLS backend that libwebsockets
+ was compiled against.
+
+comment "SSL support needs an SSL implementation"
+ depends on !SSL_IMPL_OPENSSL && !SSL_IMPL_WOLFSSL && !SSL_IMPL_MBEDTLS && !LWS
+
+endif
+
comment "Security"
source "crypto/Kconfig"
diff --git a/Makefile b/Makefile
index 58004474..7898985e 100644
--- a/Makefile
+++ b/Makefile
@@ -374,7 +374,7 @@ include $(srctree)/Makefile.flags
objs-y := core handlers bootloader suricatta
objs-$(CONFIG_SWUPDATE_CRYPTO) += crypto
-libs-y := corelib mongoose parser fs containers
+libs-y := corelib mongoose lws parser fs containers
bindings-y := bindings
tools-y := tools
diff --git a/Makefile.flags b/Makefile.flags
index 267d74ab..435bf3c8 100644
--- a/Makefile.flags
+++ b/Makefile.flags
@@ -301,6 +301,11 @@ ifneq ($(CONFIG_SWUFORWARDER_HANDLER),)
LDLIBS += websockets uriparser
endif
+# libwebsockets webserver
+ifneq ($(CONFIG_LWS),)
+LDLIBS += websockets
+endif
+
# Delta Update
ifneq ($(CONFIG_DELTA),)
LDLIBS += zck
diff --git a/core/swupdate.c b/core/swupdate.c
index a7ad57ae..c9c2b75c 100644
--- a/core/swupdate.c
+++ b/core/swupdate.c
@@ -1132,7 +1132,7 @@ int main(int argc, char **argv)
wait_threads_ready();
/* Start embedded web server */
-#if defined(CONFIG_MONGOOSE)
+#if defined(CONFIG_WEBSERVER)
if (opt_w) {
uid_t uid;
gid_t gid;
diff --git a/crypto/Makefile b/crypto/Makefile
index 5c775e7b..86d4590e 100644
--- a/crypto/Makefile
+++ b/crypto/Makefile
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: GPL-2.0-only
ifeq ($(CONFIG_SSL_IMPL_OPENSSL),y)
+obj-$(CONFIG_LWS) += swupdate_HASH_openssl.o
obj-$(CONFIG_HASH_VERIFY) += swupdate_HASH_openssl.o
obj-$(CONFIG_SIGALG_RAWRSA) += swupdate_rsa_verify_openssl.o
obj-$(CONFIG_SIGALG_RSAPSS) += swupdate_rsa_verify_openssl.o
@@ -12,6 +13,7 @@ obj-$(CONFIG_SIGALG_ASYM_DEC_CMS) += swupdate_decrypt_openssl_cms.o
endif
ifeq ($(CONFIG_SSL_IMPL_WOLFSSL),y)
+obj-$(CONFIG_LWS) += swupdate_HASH_wolfssl.o
obj-$(CONFIG_HASH_VERIFY) += swupdate_HASH_wolfssl.o
obj-$(CONFIG_SIGALG_RAWRSA) += swupdate_rsa_verify_wolfssl.o
obj-$(CONFIG_SIGALG_RSAPSS) += swupdate_rsa_verify_wolfssl.o
@@ -20,6 +22,7 @@ obj-$(CONFIG_ENCRYPTED_IMAGES) += swupdate_decrypt_wolfssl.o
endif
ifeq ($(CONFIG_SSL_IMPL_MBEDTLS),y)
+obj-$(CONFIG_LWS) += swupdate_HASH_mbedtls.o
obj-$(CONFIG_HASH_VERIFY) += swupdate_HASH_mbedtls.o
obj-$(CONFIG_ENCRYPTED_IMAGES) += swupdate_decrypt_mbedtls.o
obj-$(CONFIG_SIGALG_RAWRSA) += swupdate_rsa_verify_mbedtls.o
diff --git a/doc/source/improvement_proposals.rst b/doc/source/improvement_proposals.rst
index 020bebe3..b790f3b9 100644
--- a/doc/source/improvement_proposals.rst
+++ b/doc/source/improvement_proposals.rst
@@ -246,10 +246,6 @@ Webserver that allows streaming.
Reported from ML there is this place for enhancement:
- use Mongoose as library
-- use alternative Webserver. Civetweb is a fork from Mongoose and could be integrated.
-- Webserver itself is not doing a lot - it just provides API to push a SWU and Websocket to report progress.
- If SWUpdate is behind a reverse-proxy, an easy own developed internal Webserver
- can be a solution.
* Status: Wait
* Request for Support : Sponsor
diff --git a/doc/source/swupdate.rst b/doc/source/swupdate.rst
index 62681c2a..df3e3202 100644
--- a/doc/source/swupdate.rst
+++ b/doc/source/swupdate.rst
@@ -674,6 +674,7 @@ Webserver command line parameters
Example: ``swupdate -w "-r /www -p 8080"``
+Arguments only available with Mongoose are marked with '\#'.
Mandatory arguments are marked with '\*':
+-------------------------+----------+--------------------------------------------+
@@ -697,10 +698,10 @@ Mandatory arguments are marked with '\*':
| | | clients stops to send data. If hit, an |
| | | update is aborted. Default=0 (unlimited) |
+-------------------------+----------+--------------------------------------------+
-| --auth-domain <string> | string | Set authentication domain |
+| --auth-domain <string> | string | \# Set authentication domain |
| | | Default: none |
+-------------------------+----------+--------------------------------------------+
-| --global-auth-file | string | Set authentication file if any |
+| --global-auth-file | string | \# Set authentication file if any |
| <string> | | Default: none |
+-------------------------+----------+--------------------------------------------+
diff --git a/examples/configuration/swupdate.cfg b/examples/configuration/swupdate.cfg
index 0bcf6ffc..121e86da 100644
--- a/examples/configuration/swupdate.cfg
+++ b/examples/configuration/swupdate.cfg
@@ -374,9 +374,7 @@ gservice :
# groupid : integer
# groupId for Webserver process
# listening_ports : integer
-# Webserver listening ports
-# enable_directory_listing : boolean
-# true to list directories
+# Webserver listening port
# ssl_certificate : string
# path to SSL certificat
# ssl_certificate_key : string
@@ -384,12 +382,6 @@ gservice :
# api : integer
# 1 = simple REST API for M2M communication
# 2 = WebApp with Websockets support
-# global-auth-file : string
-# path to the global authorization file, if any
-# default = none
-# auth-domain : string
-# path to auth-domain, if any
-# default = none
# run-postupdate : boolean (default true)
# run the postupdate command automatically after
# a successful update
@@ -397,6 +389,16 @@ gservice :
# when an update is started. If no data is received
# during this time, connection is closed by the Webserver
# and update is aborted.
+#
+# MONGOOSE-ONLY
+# enable_directory_listing : boolean
+# true to list directories
+# global-auth-file : string
+# path to the global authorization file, if any
+# default = none
+# auth-domain : string
+# path to auth-domain, if any
+# default = none
webserver :
{
diff --git a/lws/Kconfig b/lws/Kconfig
new file mode 100644
index 00000000..6405f8c4
--- /dev/null
+++ b/lws/Kconfig
@@ -0,0 +1,18 @@
+# SPDX-FileCopyrightText: 2026 Bastian Germann
+#
+# SPDX-License-Identifier: GPL-2.0-only
+
+config LWS
+ bool "libwebsockets"
+ depends on HAVE_LIBWEBSOCKETS
+ help
+ Enable a webserver built on libwebsockets.
+
+ Endpoints:
+ POST /upload - upload a SWU image
+ (multipart/form-data or application/octet-stream)
+ POST /restart - trigger the post-update action
+ GET * - static file serving from a configurable web root
+ (set via -r or the web-root config option)
+ GET (ws) - WebSocket status stream
+ (same JSON protocol as the mongoose webserver)
diff --git a/lws/Makefile b/lws/Makefile
new file mode 100644
index 00000000..1789ffdb
--- /dev/null
+++ b/lws/Makefile
@@ -0,0 +1,5 @@
+# SPDX-FileCopyrightText: 2026 Bastian Germann
+#
+# SPDX-License-Identifier: GPL-2.0-only
+
+lib-$(CONFIG_LWS) += lws_interface.o
diff --git a/lws/lws_interface.c b/lws/lws_interface.c
new file mode 100644
index 00000000..7dd41e9e
--- /dev/null
+++ b/lws/lws_interface.c
@@ -0,0 +1,881 @@
+/* SPDX-FileCopyrightText: 2026 Bastian Germann
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+/*
+ * SWUpdate webserver based on libwebsockets.
+ *
+ * Provides in a single server:
+ * POST /upload - stream SWU images to the installer
+ * POST /restart - trigger the post-update action
+ * GET * - static file serving from a configurable web root
+ * GET (ws) - WebSocket status endpoint
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+#include <errno.h>
+#include <getopt.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <pthread.h>
+#include <sys/types.h>
+#include <time.h>
+
+#include <libwebsockets.h>
+
+#include "mongoose_interface.h"
+#include "network_ipc.h"
+#include "progress.h"
+#include "util.h"
+#include "parselib.h"
+#include "swupdate_settings.h"
+#include "pctl.h"
+
+/* -----------------------------------------------------------------------
+ * Constants
+ * --------------------------------------------------------------------- */
+
+#define LWS_DEFAULT_PORT 8080
+#define LWS_MAX_WS_SESSIONS 16
+
+/* -----------------------------------------------------------------------
+ * Message node (singly-linked list for the WebSocket broadcast queue)
+ * --------------------------------------------------------------------- */
+
+struct msg_node {
+ struct msg_node *next;
+ size_t len;
+ char json[]; /* flexible array – allocated inline */
+};
+
+static struct msg_node *msg_node_new(const char *json)
+{
+ size_t len = strlen(json);
+ struct msg_node *n = malloc(sizeof(*n) + len + 1);
+ if (!n)
+ return NULL;
+ n->next = NULL;
+ n->len = len;
+ memcpy(n->json, json, len + 1);
+ return n;
+}
+
+/* -----------------------------------------------------------------------
+ * Global state
+ * --------------------------------------------------------------------- */
+
+static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
+static struct msg_node *g_head;
+static struct msg_node *g_tail;
+static struct lws_context *g_ctx;
+static bool s_run_postupdate;
+static const char *s_document_root; /* set once before lws_create_context */
+static unsigned int s_watchdog_secs; /* 0 = disabled */
+
+static volatile sig_atomic_t s_signo;
+
+/* -----------------------------------------------------------------------
+ * Per-session state
+ * --------------------------------------------------------------------- */
+
+/* HTTP request routes */
+#define ROUTE_NONE 0
+#define ROUTE_UPLOAD 1
+
+/* Upload content types */
+#define CTYPE_NONE 0
+#define CTYPE_MULTIPART 1
+#define CTYPE_OCTET 2
+
+struct pss {
+ /* HTTP / upload */
+ int route;
+ int ctype;
+ int ipc_fd; /* -1 when not open */
+ struct lws_spa *spa; /* multipart parser; NULL if unused */
+ bool upload_error;
+ time_t last_io_time; /* for upload watchdog */
+
+ /* WebSocket */
+ struct msg_node *send_head;
+ struct msg_node *send_tail;
+ int slot; /* index in g_sessions[]; -1 if not WS */
+};
+
+/* -----------------------------------------------------------------------
+ * WebSocket session registry (lws service thread only – no lock needed)
+ * --------------------------------------------------------------------- */
+
+static struct pss *g_sessions[LWS_MAX_WS_SESSIONS];
+
+static void session_add(struct pss *pss)
+{
+ for (int i = 0; i < LWS_MAX_WS_SESSIONS; i++) {
+ if (!g_sessions[i]) {
+ g_sessions[i] = pss;
+ pss->slot = i;
+ return;
+ }
+ }
+ pss->slot = -1;
+ WARN("LWS: too many concurrent WebSocket sessions (max %d)",
+ LWS_MAX_WS_SESSIONS);
+}
+
+static void session_remove(struct pss *pss)
+{
+ if (pss->slot >= 0 && pss->slot < LWS_MAX_WS_SESSIONS)
+ g_sessions[pss->slot] = NULL;
+
+ struct msg_node *m = pss->send_head;
+ while (m) {
+ struct msg_node *nxt = m->next;
+ free(m);
+ m = nxt;
+ }
+ pss->send_head = pss->send_tail = NULL;
+}
+
+/* -----------------------------------------------------------------------
+ * WebSocket broadcast (called from IPC listener threads)
+ * --------------------------------------------------------------------- */
+
+static void ws_broadcast(const char *json)
+{
+ struct msg_node *n = msg_node_new(json);
+ if (!n)
+ return;
+
+ pthread_mutex_lock(&g_lock);
+ if (g_tail)
+ g_tail->next = n;
+ else
+ g_head = n;
+ g_tail = n;
+ pthread_mutex_unlock(&g_lock);
+
+ if (g_ctx)
+ lws_cancel_service(g_ctx);
+}
+
+/* -----------------------------------------------------------------------
+ * SPA (multipart/form-data) file upload callback
+ * --------------------------------------------------------------------- */
+
+static int spa_fileupload_cb(void *data, const char *name,
+ const char *filename, char *buf, int len,
+ enum lws_spa_fileupload_states state)
+{
+ struct pss *pss = data;
+
+ (void)name;
+
+ switch (state) {
+ case LWS_UFS_OPEN: {
+ struct swupdate_request req;
+ swupdate_prepare_req(&req);
+ req.source = SOURCE_WEBSERVER;
+ if (filename)
+ strncpy(
req.info, filename, sizeof(
req.info) - 1);
+ pss->ipc_fd = ipc_inst_start_ext(&req, sizeof(req));
+ if (pss->ipc_fd < 0) {
+ ERROR("LWS: upload: ipc_inst_start_ext failed");
+ pss->upload_error = true;
+ } else {
+ swupdate_download_update(0, 0);
+ }
+ break;
+ }
+
+ case LWS_UFS_CONTENT:
+ case LWS_UFS_FINAL_CONTENT:
+ if (!pss->upload_error && pss->ipc_fd >= 0 && buf && len > 0) {
+ if (write(pss->ipc_fd, buf, (size_t)len) < 0) {
+ ERROR("LWS: upload: IPC write: %s",
+ strerror(errno));
+ pss->upload_error = true;
+ }
+ }
+ if (state == LWS_UFS_FINAL_CONTENT && pss->ipc_fd >= 0) {
+ ipc_end(pss->ipc_fd);
+ pss->ipc_fd = -1;
+ }
+ break;
+
+ case LWS_UFS_CLOSE:
+ break;
+ }
+ return 0;
+}
+
+
+
+/* -----------------------------------------------------------------------
+ * Unified HTTP + WebSocket callback
+ * --------------------------------------------------------------------- */
+
+static const struct lws_protocols protocols[]; /* forward declaration */
+
+static int callback_swupdate(struct lws *wsi, enum lws_callback_reasons reason,
+ void *user, void *in, size_t len)
+{
+ struct pss *pss = (struct pss *)user;
+
+ switch (reason) {
+
+ /* ----------------------------------------------------------------
+ * Incoming HTTP request (headers complete)
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_HTTP: {
+ char uri[256] = "";
+ bool is_post;
+
+ is_post = lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI) > 0;
+ if (is_post)
+ lws_hdr_copy(wsi, uri, sizeof(uri), WSI_TOKEN_POST_URI);
+ else
+ lws_hdr_copy(wsi, uri, sizeof(uri), WSI_TOKEN_GET_URI);
+
+ /* Initialise per-session HTTP state (pss is zero-filled by lws) */
+ pss->ipc_fd = -1;
+ pss->slot = -1;
+
+ if (is_post && strcmp(uri, "/upload") == 0) {
+ char ct[256] = "";
+ pss->route = ROUTE_UPLOAD;
+ lws_hdr_copy(wsi, ct, sizeof(ct),
+ WSI_TOKEN_HTTP_CONTENT_TYPE);
+
+ if (strncasecmp(ct, "multipart/form-data", 19) == 0) {
+ static const char * const fields[] =
+ { "file", NULL };
+ pss->ctype = CTYPE_MULTIPART;
+ pss->spa = lws_spa_create(wsi, fields, 1,
+ 4096,
+ spa_fileupload_cb,
+ pss);
+ if (!pss->spa) {
+ ERROR("LWS: lws_spa_create failed");
+ lws_return_http_status(wsi,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ NULL);
+ goto done_http;
+ }
+ } else {
+ /* Treat anything else as raw octet-stream */
+ struct swupdate_request req;
+ pss->ctype = CTYPE_OCTET;
+ swupdate_prepare_req(&req);
+ req.source = SOURCE_WEBSERVER;
+ pss->ipc_fd = ipc_inst_start_ext(&req,
+ sizeof(req));
+ if (pss->ipc_fd < 0) {
+ ERROR("LWS: upload: ipc_inst_start_ext"
+ " failed");
+ lws_return_http_status(wsi,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ NULL);
+ goto done_http;
+ }
+ swupdate_download_update(0, 0);
+ }
+ pss->last_io_time = time(NULL);
+ if (s_watchdog_secs)
+ lws_set_timer_usecs(wsi,
+ (lws_usec_t)s_watchdog_secs *
+ LWS_USEC_PER_SEC);
+ return 0;
+
+ } else if (is_post && strcmp(uri, "/restart") == 0) {
+ ipc_message msg = {};
+ int ret = ipc_postupdate(&msg);
+ lws_return_http_status(wsi,
+ (ret || msg.type != ACK)
+ ? HTTP_STATUS_INTERNAL_SERVER_ERROR
+ : HTTP_STATUS_OK,
+ NULL);
+ goto done_http;
+
+ } else if (!is_post) {
+ /* Static file serving */
+ if (s_document_root && *s_document_root) {
+ const char *p = (*uri == '/') ? uri : "/";
+ if (strcmp(p, "/") == 0)
+ p = "/index.html";
+
+ char filepath[512];
+ snprintf(filepath, sizeof(filepath), "%s%s",
+ s_document_root, p);
+
+ const char *mime = lws_get_mimetype(filepath, NULL);
+ if (!mime)
+ mime = "application/octet-stream";
+ int n = lws_serve_http_file(wsi, filepath, mime,
+ NULL, 0);
+ if (n < 0 || (n > 0 &&
+ lws_http_transaction_completed(wsi)))
+ return -1;
+ return 0;
+ }
+ }
+
+ lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
+done_http:
+ return lws_http_transaction_completed(wsi) ? -1 : 0;
+ }
+
+ /* ----------------------------------------------------------------
+ * Receiving POST body chunks
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_HTTP_BODY:
+ if (pss->route == ROUTE_UPLOAD) {
+ pss->last_io_time = time(NULL);
+ if (pss->ctype == CTYPE_MULTIPART) {
+ if (pss->spa)
+ lws_spa_process(pss->spa, in, (int)len);
+ } else if (pss->ctype == CTYPE_OCTET) {
+ if (!pss->upload_error && pss->ipc_fd >= 0) {
+ if (write(pss->ipc_fd, in, len) < 0) {
+ ERROR("LWS: upload: IPC write:"
+ " %s", strerror(errno));
+ pss->upload_error = true;
+ }
+ }
+ }
+ }
+ break;
+
+ /* ----------------------------------------------------------------
+ * POST body fully received
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_HTTP_BODY_COMPLETION:
+ if (pss->route == ROUTE_UPLOAD) {
+ if (pss->ctype == CTYPE_MULTIPART && pss->spa) {
+ lws_spa_finalize(pss->spa);
+ lws_spa_destroy(pss->spa);
+ pss->spa = NULL;
+ } else if (pss->ctype == CTYPE_OCTET &&
+ pss->ipc_fd >= 0) {
+ ipc_end(pss->ipc_fd);
+ pss->ipc_fd = -1;
+ }
+ lws_return_http_status(wsi,
+ pss->upload_error
+ ? HTTP_STATUS_INTERNAL_SERVER_ERROR
+ : HTTP_STATUS_OK,
+ NULL);
+ }
+ return lws_http_transaction_completed(wsi) ? -1 : 0;
+
+ /* ----------------------------------------------------------------
+ * Static file serving complete
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_HTTP_FILE_COMPLETION:
+ return lws_http_transaction_completed(wsi) ? -1 : 0;
+
+ /* ----------------------------------------------------------------
+ * Connection closed (HTTP or mid-upload)
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_CLOSED_HTTP:
+ if (pss->ipc_fd >= 0) {
+ close(pss->ipc_fd);
+ pss->ipc_fd = -1;
+ }
+ if (pss->spa) {
+ lws_spa_destroy(pss->spa);
+ pss->spa = NULL;
+ }
+ break;
+
+ /* ----------------------------------------------------------------
+ * WebSocket handshake complete
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_ESTABLISHED:
+ pss->ipc_fd = -1;
+ pss->spa = NULL;
+ pss->send_head = NULL;
+ pss->send_tail = NULL;
+ pss->slot = -1;
+ session_add(pss);
+ break;
+
+ /* ----------------------------------------------------------------
+ * WebSocket connection closed
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_CLOSED:
+ session_remove(pss);
+ break;
+
+ /* ----------------------------------------------------------------
+ * Send next pending WebSocket message
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_SERVER_WRITEABLE: {
+ if (!pss->send_head)
+ break;
+
+ struct msg_node *msg = pss->send_head;
+ pss->send_head = msg->next;
+ if (!pss->send_head)
+ pss->send_tail = NULL;
+
+ uint8_t *buf = malloc(LWS_PRE + msg->len);
+ if (buf) {
+ memcpy(buf + LWS_PRE, msg->json, msg->len);
+ lws_write(wsi, buf + LWS_PRE, msg->len,
+ LWS_WRITE_TEXT);
+ free(buf);
+ }
+ free(msg);
+
+ if (pss->send_head)
+ lws_callback_on_writable(wsi);
+ break;
+ }
+
+ /* ----------------------------------------------------------------
+ * Broadcast queue woken by ws_broadcast() → lws_cancel_service()
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_EVENT_WAIT_CANCELLED: {
+ pthread_mutex_lock(&g_lock);
+ struct msg_node *bcast = g_head;
+ g_head = g_tail = NULL;
+ pthread_mutex_unlock(&g_lock);
+
+ while (bcast) {
+ struct msg_node *nxt = bcast->next;
+ for (int i = 0; i < LWS_MAX_WS_SESSIONS; i++) {
+ struct pss *s = g_sessions[i];
+ if (!s)
+ continue;
+ struct msg_node *copy =
+ msg_node_new(bcast->json);
+ if (!copy)
+ continue;
+ if (s->send_tail)
+ s->send_tail->next = copy;
+ else
+ s->send_head = copy;
+ s->send_tail = copy;
+ }
+ free(bcast);
+ bcast = nxt;
+ }
+ lws_callback_on_writable_all_protocol(g_ctx, &protocols[0]);
+ break;
+ }
+
+ /* ----------------------------------------------------------------
+ * Upload watchdog timer
+ * -------------------------------------------------------------- */
+ case LWS_CALLBACK_TIMER:
+ if (pss->route == ROUTE_UPLOAD && s_watchdog_secs) {
+ time_t elapsed = time(NULL) - pss->last_io_time;
+ if ((unsigned int)elapsed >= s_watchdog_secs) {
+ ERROR("LWS: upload timeout after %ld seconds,"
+ " closing connection", (long)elapsed);
+ lws_return_http_status(wsi,
+ HTTP_STATUS_REQUEST_TIMEOUT, NULL);
+ return -1;
+ }
+ /* Not yet expired – reschedule */
+ lws_set_timer_usecs(wsi,
+ (lws_usec_t)s_watchdog_secs *
+ LWS_USEC_PER_SEC);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return lws_callback_http_dummy(wsi, reason, user, in, len);
+}
+
+static const struct lws_protocols protocols[] = {
+ {
+ .name = "swupdate",
+ .callback = callback_swupdate,
+ .per_session_data_size = sizeof(struct pss),
+ .rx_buffer_size = 4096,
+ },
+ { NULL, NULL, 0, 0, 0, NULL, 0 }
+};
+
+/* -----------------------------------------------------------------------
+ * IPC listener threads (same JSON protocol as mongoose_interface.c)
+ * --------------------------------------------------------------------- */
+
+static void *broadcast_message_thread(void __attribute__((__unused__)) *data)
+{
+ int fd = -1;
+
+ for (;;) {
+ ipc_message msg;
+ int ret;
+
+ if (fd < 0)
+ fd = ipc_notify_connect();
+ if (fd < 0) {
+ sleep(1);
+ continue;
+ }
+
+ ret = ipc_notify_receive(&fd, &msg);
+ if (ret != sizeof(msg))
+ break;
+
+ if (strlen(msg.data.notify.msg) != 0 &&
+ msg.data.status.current != PROGRESS) {
+ char text[4096];
+ char str[4160];
+
+ snescape(text, sizeof(text), msg.data.notify.msg);
+
+ snprintf(str, sizeof(str),
+ "{\r\n"
+ "\t\"type\": \"message\",\r\n"
+ "\t\"level\": \"%d\",\r\n"
+ "\t\"text\": \"%s\"\r\n"
+ "}\r\n",
+ level_to_rfc_5424(msg.data.notify.level),
+ text);
+ ws_broadcast(str);
+ }
+ }
+
+ return NULL;
+}
+
+static void *broadcast_progress_thread(void __attribute__((__unused__)) *data)
+{
+ RECOVERY_STATUS status = -1;
+ sourcetype source = -1;
+ unsigned int step = 0;
+ uint8_t percent = 0;
+ int fd = -1;
+
+ for (;;) {
+ struct progress_msg msg;
+ char str[1024];
+ char escaped[512];
+ int ret;
+
+ if (fd < 0)
+ fd = progress_ipc_connect(true);
+ if (fd < 0) {
+ sleep(1);
+ continue;
+ }
+
+ ret = progress_ipc_receive(&fd, &msg);
+ if (ret != sizeof(msg))
+ break;
+
+ if (msg.status != PROGRESS &&
+ (msg.status != status || msg.status == FAILURE)) {
+ status = msg.status;
+ snescape(escaped, sizeof(escaped),
+ get_status_string(msg.status));
+ snprintf(str, sizeof(str),
+ "{\r\n"
+ "\t\"type\": \"status\",\r\n"
+ "\t\"status\": \"%s\"\r\n"
+ "}\r\n",
+ escaped);
+ ws_broadcast(str);
+ }
+
+ if (msg.source != source) {
+ source = msg.source;
+ snprintf(str, sizeof(str),
+ "{\r\n"
+ "\t\"type\": \"source\",\r\n"
+ "\t\"source\": \"%s\"\r\n"
+ "}\r\n",
+ get_source_string(msg.source));
+ ws_broadcast(str);
+ }
+
+ if (msg.status == SUCCESS && msg.source == SOURCE_WEBSERVER &&
+ s_run_postupdate) {
+ ipc_message ipc = {};
+ ipc_postupdate(&ipc);
+ }
+
+ if (msg.infolen) {
+ snescape(escaped, sizeof(escaped),
msg.info);
+ snprintf(str, sizeof(str),
+ "{\r\n"
+ "\t\"type\": \"info\",\r\n"
+ "\t\"source\": \"%s\"\r\n"
+ "}\r\n",
+ escaped);
+ ws_broadcast(str);
+ }
+
+ if ((msg.cur_step != step || msg.cur_percent != percent) &&
+ msg.cur_step) {
+ step = msg.cur_step;
+ percent = msg.cur_percent;
+ snescape(escaped, sizeof(escaped),
+ msg.cur_step ? msg.cur_image : "");
+ snprintf(str, sizeof(str),
+ "{\r\n"
+ "\t\"type\": \"step\",\r\n"
+ "\t\"number\": \"%d\",\r\n"
+ "\t\"step\": \"%d\",\r\n"
+ "\t\"name\": \"%s\",\r\n"
+ "\t\"percent\": \"%d\"\r\n"
+ "}\r\n",
+ msg.nsteps, msg.cur_step,
+ escaped, msg.cur_percent);
+ ws_broadcast(str);
+ }
+ }
+
+ return NULL;
+}
+
+/* -----------------------------------------------------------------------
+ * Configuration + entry point
+ * --------------------------------------------------------------------- */
+
+struct lws_opts {
+ int port;
+ char *document_root;
+ bool run_postupdate;
+ unsigned int watchdog_secs;
+#ifdef CONFIG_MONGOOSESSL
+ bool ssl;
+ char *ssl_cert;
+ char *ssl_key;
+#endif
+};
+
+static int lws_settings(void *elem, void __attribute__((__unused__)) *data)
+{
+ struct lws_opts *opts = data;
+ char tmp[256] = "";
+
+ GET_FIELD_INT(LIBCFG_PARSER, elem, "listening_ports", &opts->port);
+ GET_FIELD_STRING_RESET(LIBCFG_PARSER, elem, "document_root", tmp);
+ if (strlen(tmp)) {
+ free(opts->document_root);
+ opts->document_root = strdup(tmp);
+ }
+ GET_FIELD_BOOL(LIBCFG_PARSER, elem, "run-postupdate",
+ &opts->run_postupdate);
+ GET_FIELD_INT(LIBCFG_PARSER, elem, "timeout",
+ (int *)&opts->watchdog_secs);
+#ifdef CONFIG_MONGOOSESSL
+ GET_FIELD_STRING_RESET(LIBCFG_PARSER, elem, "ssl_certificate", tmp);
+ if (strlen(tmp)) {
+ free(opts->ssl_cert);
+ opts->ssl_cert = strdup(tmp);
+ }
+ GET_FIELD_STRING_RESET(LIBCFG_PARSER, elem, "ssl_certificate_key", tmp);
+ if (strlen(tmp)) {
+ free(opts->ssl_key);
+ opts->ssl_key = strdup(tmp);
+ }
+#endif
+ return 0;
+}
+
+static struct option long_options[] = {
+ { "port", required_argument, NULL, 'p' },
+ { "document-root", required_argument, NULL, 'r' },
+ { "timeout", required_argument, NULL, 't' },
+#ifdef CONFIG_MONGOOSESSL
+ { "ssl", no_argument, NULL, 's' },
+ { "ssl-cert", required_argument, NULL, 'C' },
+ { "ssl-key", required_argument, NULL, 'K' },
+#endif
+ { NULL, 0, NULL, 0 }
+};
+
+void mongoose_print_help(void)
+{
+ fprintf(
+ stdout,
+ "\tlws arguments:\n"
+ "\t -p, --port <port> : server port number (default: %d)\n"
+#ifdef CONFIG_MONGOOSESSL
+ "\t -s, --ssl : enable ssl support\n"
+ "\t -C, --ssl-cert <cert> : ssl certificate to present to clients\n"
+ "\t -K, --ssl-key <key> : key corresponding to the ssl certificate\n"
+#endif
+ "\t -r, --document-root <path> : path to document root directory\n"
+ "\t -t, --timeout : timeout to check if connection is lost (default: check disabled)\n",
+ LWS_DEFAULT_PORT);
+}
+
+static void signal_handler(int signo) {
+ s_signo = signo;
+}
+
+int start_mongoose(const char *cfgfname, int argc, char *argv[])
+{
+ struct lws_opts opts = {
+ .port = LWS_DEFAULT_PORT,
+ .document_root = NULL,
+ .run_postupdate = true,
+ .watchdog_secs = 0,
+#ifdef CONFIG_MONGOOSESSL
+ .ssl = false,
+ .ssl_cert = NULL,
+ .ssl_key = NULL,
+#endif
+ };
+
+ if (cfgfname) {
+ swupdate_cfg_handle handle;
+ swupdate_cfg_init(&handle);
+ if (swupdate_cfg_read_file(&handle, cfgfname) == 0)
+ read_module_settings(&handle, "webserver",
+ lws_settings, &opts);
+ swupdate_cfg_destroy(&handle);
+ }
+
+ optind = 1;
+ int choice;
+ while ((choice = getopt_long(argc, argv,
+#ifdef CONFIG_MONGOOSESSL
+ "p:r:t:sC:K:",
+#else
+ "p:r:t:",
+#endif
+ long_options, NULL)) != -1) {
+ switch (choice) {
+ case 'p':
+ opts.port = (int)strtol(optarg, NULL, 10);
+ break;
+ case 'r':
+ free(opts.document_root);
+ opts.document_root = strdup(optarg);
+ break;
+ case 't':
+ opts.watchdog_secs = (unsigned int)strtoul(optarg, NULL, 10);
+ break;
+#ifdef CONFIG_MONGOOSESSL
+ case 's':
+ opts.ssl = true;
+ break;
+ case 'C':
+ free(opts.ssl_cert);
+ opts.ssl_cert = strdup(optarg);
+ break;
+ case 'K':
+ free(opts.ssl_key);
+ opts.ssl_key = strdup(optarg);
+ break;
+#endif
+ default:
+ free(opts.document_root);
+ return -EINVAL;
+ }
+ }
+
+ if (optind < argc) {
+ ERROR("Non-option arguments given to lws, see --help.");
+ mongoose_print_help();
+ free(opts.document_root);
+#ifdef CONFIG_MONGOOSESSL
+ free(opts.ssl_cert);
+ free(opts.ssl_key);
+#endif
+ return -EINVAL;
+ }
+
+ s_document_root = opts.document_root ? opts.document_root : "";
+ s_run_postupdate = opts.run_postupdate;
+ s_watchdog_secs = opts.watchdog_secs;
+
+ signal(SIGINT, signal_handler);
+ signal(SIGTERM, signal_handler);
+
+ struct lws_context_creation_info info;
+ memset(&info, 0, sizeof(info));
+ info.port = opts.port;
+ info.protocols = protocols;
+ info.gid = (gid_t)-1;
+ info.uid = (uid_t)-1;
+#ifndef CONFIG_MONGOOSEIPV6
+ info.options |= LWS_SERVER_OPTION_DISABLE_IPV6;
+#endif
+#ifdef CONFIG_MONGOOSESSL
+ if (opts.ssl) {
+ if (opts.ssl_cert && opts.ssl_key) {
+ info.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
+ info.ssl_cert_filepath = opts.ssl_cert;
+ info.ssl_private_key_filepath = opts.ssl_key;
+ } else {
+ ERROR("LWS: TLS requires both --ssl-cert and --ssl-key");
+ free(opts.document_root);
+ free(opts.ssl_cert);
+ free(opts.ssl_key);
+ return -EINVAL;
+ }
+ }
+#endif
+
+ lws_set_log_level(LLL_ERR | LLL_WARN, NULL);
+
+ g_ctx = lws_create_context(&info);
+ if (!g_ctx) {
+ ERROR("LWS: lws_create_context failed");
+ free(opts.document_root);
+#ifdef CONFIG_MONGOOSESSL
+ free(opts.ssl_cert);
+ free(opts.ssl_key);
+#endif
+ return -1;
+ }
+
+ INFO("LWS webserver listening on port %d", opts.port);
+
+ pthread_t tid;
+
+ if (pthread_create(&tid, NULL, broadcast_message_thread, NULL) != 0) {
+ ERROR("LWS: cannot create message thread: %m");
+ lws_context_destroy(g_ctx);
+ free(opts.document_root);
+#ifdef CONFIG_MONGOOSESSL
+ free(opts.ssl_cert);
+ free(opts.ssl_key);
+#endif
+ return -1;
+ }
+ pthread_detach(tid);
+
+ if (pthread_create(&tid, NULL, broadcast_progress_thread, NULL) != 0) {
+ ERROR("LWS: cannot create progress thread: %m");
+ lws_context_destroy(g_ctx);
+ free(opts.document_root);
+#ifdef CONFIG_MONGOOSESSL
+ free(opts.ssl_cert);
+ free(opts.ssl_key);
+#endif
+ return -1;
+ }
+ pthread_detach(tid);
+
+ /* Run the lws event loop in this thread until a signal arrives */
+ while (s_signo == 0)
+ lws_service(g_ctx, 50);
+
+ lws_context_destroy(g_ctx);
+ g_ctx = NULL;
+ free(opts.document_root);
+#ifdef CONFIG_MONGOOSESSL
+ free(opts.ssl_cert);
+ free(opts.ssl_key);
+#endif
+ return 0;
+}
diff --git a/mongoose/Kconfig b/mongoose/Kconfig
index a1e1afe3..a65643eb 100644
--- a/mongoose/Kconfig
+++ b/mongoose/Kconfig
@@ -2,40 +2,7 @@
#
# SPDX-License-Identifier: GPL-2.0-only
-menuconfig WEBSERVER
- bool "Web Server"
- help
- Enable update from remote using a web server on the target.
-
-if WEBSERVER
-choice
- prompt "Web Server Type"
- default MONGOOSE
- help
- Choose the web server
-
config MONGOOSE
bool "mongoose"
help
Mongoose embeddded web server
-
-endchoice
-
-config MONGOOSEIPV6
- bool "IPv6 support"
- default y
- depends on MONGOOSE
- help
- It enables ipv6 support into mongoose
-
-config MONGOOSESSL
- bool "SSL support"
- depends on MONGOOSE
- depends on SSL_IMPL_OPENSSL || SSL_IMPL_WOLFSSL || SSL_IMPL_MBEDTLS
- help
- It enables SSL support into mongoose
-
-comment "SSL support needs an SSL implementation"
- depends on !SSL_IMPL_OPENSSL && !SSL_IMPL_WOLFSSL && !SSL_IMPL_MBEDTLS
-
-endif
diff --git a/scripts/acceptance-tests/CheckImage.mk b/scripts/acceptance-tests/CheckImage.mk
index 1f04f2c8..23f6aebd 100644
--- a/scripts/acceptance-tests/CheckImage.mk
+++ b/scripts/acceptance-tests/CheckImage.mk
@@ -41,7 +41,7 @@ ifeq ($(CONFIG_SIGNED_IMAGES),$(CONFIG_SIGALG_CMS))
tests-$(CONFIG_LIBCONFIG) += $(if $(CONFIG_RAW), ValidImageTest)
endif
tests-y += InvOptsNoImg
-tests-$(CONFIG_MONGOOSE) += InvOptsCheckWithWeb
+tests-$(CONFIG_WEBSERVER) += InvOptsCheckWithWeb
tests-$(CONFIG_SURICATTA) += InvOptsCheckWithSur
tests-$(CONFIG_SIGALG_CMS) += InvSigNameCheck
tests-$(CONFIG_SIGALG_CMS) += ValidSigNameCheck