Commit a47b7afb authored by jan.koester's avatar jan.koester
Browse files

Initial import of webctl

parents
Loading
Loading
Loading
Loading

CMakeLists.txt

0 → 100644
+107 −0
Changes for CMakeLists.txt: 107 added lines, 0 removed lines.
Original line number Diff line number Diff line
cmake_minimum_required(VERSION 3.16)
project(webctl VERSION 0.1.0 LANGUAGES CXX)

# webctl: a web interface for FenrirOS.
#
#   webctl-backend   root daemon: login (root only, via getspnam()/crypt()),
#                    sessions, and the modules that change the system;
#                    answers on a unix socket only
#   webctl-frontend  HTTPS on 0.0.0.0:8443: static UI + JSON API, every call
#                    forwarded to the backend; drops to the webctl user
#                    after binding
#   modules/         one shared object per area (users, network, shares,
#                    raid, lvm, filesystems, services, system),
#                    dlopen()ed by the backend from its module directory
#   www/             the AJAX UI; www/modules/<name>.js per module

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()

include(GNUInstallDirs)

# libhttppp's package config doesn't chain its dependencies
find_package(libnetplus REQUIRED)
find_package(libcmdplus REQUIRED)
find_package(libhttppp REQUIRED)
find_package(libconfplus REQUIRED)
find_package(json-c REQUIRED)
find_package(sysbus REQUIRED)

set(WEBCTL_SYSCONFDIR "/etc/webctl" CACHE PATH "configuration directory")
set(WEBCTL_MODULEDIR "${CMAKE_INSTALL_FULL_LIBDIR}/webctl/modules" CACHE PATH "backend module directory")
set(WEBCTL_WWWDIR "${CMAKE_INSTALL_FULL_DATADIR}/webctl/www" CACHE PATH "UI files")

add_library(webctl_common STATIC
    src/common/protocol.cpp
    src/common/config.cpp
)
target_include_directories(webctl_common PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include
    ${CMAKE_CURRENT_SOURCE_DIR}/src/common
)
target_compile_definitions(webctl_common PUBLIC
    WEBCTL_CONFIG="${WEBCTL_SYSCONFDIR}/webctl.yaml"
    WEBCTL_MODULEDIR="${WEBCTL_MODULEDIR}"
    WEBCTL_WWWDIR="${WEBCTL_WWWDIR}"
    WEBCTL_SYSCONFDIR="${WEBCTL_SYSCONFDIR}"
)
target_link_libraries(webctl_common PUBLIC json-c::json-c confplus::confplus)

add_executable(webctl-backend
    src/backend/main.cpp
    src/backend/auth.cpp
    src/backend/sessions.cpp
    src/backend/modules.cpp
)
target_link_libraries(webctl-backend PRIVATE webctl_common ${CMAKE_DL_LIBS})
# modules resolve json-c against the backend's copy
set_target_properties(webctl-backend PROPERTIES ENABLE_EXPORTS ON)

add_executable(webctl-frontend
    src/frontend/main.cpp
)
target_link_libraries(webctl-frontend PRIVATE
    webctl_common httppp::httppp netplus::netplus cmdplus::cmdplus)

# modules: <name>.so, no "lib" prefix
function(webctl_module name)
    add_library(webctl_mod_${name} MODULE ${ARGN})
    # only webctl_module_info is exported (WEBCTL_MODULE)
    set_target_properties(webctl_mod_${name} PROPERTIES PREFIX "" OUTPUT_NAME ${name}
                          CXX_VISIBILITY_PRESET hidden)
    target_include_directories(webctl_mod_${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include
                               ${CMAKE_CURRENT_SOURCE_DIR}/modules/common)
    target_link_libraries(webctl_mod_${name} PRIVATE json-c::json-c)
    install(TARGETS webctl_mod_${name} LIBRARY DESTINATION ${WEBCTL_MODULEDIR})
endfunction()

webctl_module(users modules/users/users.cpp)
webctl_module(network modules/network/network.cpp)
target_link_libraries(webctl_mod_network PRIVATE sysbus::sysbus)
webctl_module(services modules/services/services.cpp)
target_link_libraries(webctl_mod_services PRIVATE sysbus::sysbus)
webctl_module(system modules/system/system.cpp)
target_link_libraries(webctl_mod_system PRIVATE sysbus::sysbus)
# needs sharedd/api.h (sharedd-dev)
webctl_module(shares modules/shares/shares.cpp)
target_link_libraries(webctl_mod_shares PRIVATE sysbus::sysbus)
# storage modules: modules/common/blockdev.cpp (free devices, guarded
# probing), modules/common/jobs.cpp (actions in child processes)
# needs the kernel's <linux/raid/md_p.h>, <linux/raid/md_u.h>
webctl_module(raid modules/raid/raid.cpp modules/common/blockdev.cpp modules/common/jobs.cpp)
# needs the kernel's <linux/dm-ioctl.h>
webctl_module(lvm modules/lvm/lvm.cpp modules/common/blockdev.cpp modules/common/jobs.cpp)
# mounts are syslite .mount units (sysbus CreateMount); mke2fs/mkfs.fat format
webctl_module(filesystems modules/filesystems/filesystems.cpp modules/common/blockdev.cpp)
target_link_libraries(webctl_mod_filesystems PRIVATE sysbus::sysbus)

install(TARGETS webctl-backend webctl-frontend RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
install(DIRECTORY www/ DESTINATION ${WEBCTL_WWWDIR})
install(DIRECTORY include/webctl DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES dist/webctl.yaml DESTINATION ${WEBCTL_SYSCONFDIR})
set(WEBCTL_UNITDIR "/etc/syslite/services" CACHE PATH "syslite unit directory")
install(FILES dist/webctl-backend.service dist/webctl-frontend.service DESTINATION ${WEBCTL_UNITDIR})
+11 −0
Changes for dist/webctl-backend.service: 11 added lines, 0 removed lines.
Original line number Diff line number Diff line
[Unit]
Description=webctl backend (root daemon of the web interface)
After=network.target

[Service]
Type=simple
ExecStart=/usr/sbin/webctl-backend
Restart=always

[Install]
WantedBy=multi-user.target
+13 −0
Changes for dist/webctl-frontend.service: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
[Unit]
Description=webctl web interface (HTTPS, port 8443)
After=network.target webctl-backend.service

[Service]
Type=simple
# starts as root to bind the port and read the key, then drops to the
# webctl user (see webctl.yaml) before serving anything
ExecStart=/usr/sbin/webctl-frontend
Restart=always

[Install]
WantedBy=multi-user.target

dist/webctl.yaml

0 → 100644
+20 −0
Changes for dist/webctl.yaml: 20 added lines, 0 removed lines.
Original line number Diff line number Diff line
# webctl configuration (read with libconfplus). Every key is optional;
# the values below are the defaults.
webctl:
  backend:
    # the only way into the root daemon; root and the frontend user only
    socket: /run/webctl/backend.sock
    modules: /usr/lib/webctl/modules
    session_timeout: 1800      # seconds without a request
    session_lifetime: 43200    # seconds since login
    login_attempts: 5          # failed logins per address ...
    login_lockout: 300         # ... lock it out this many seconds

  frontend:
    address: 0.0.0.0
    port: 8443
    user: webctl               # serves as this user after binding
    www: /usr/share/webctl/www
    # created self-signed at the first start if missing
    cert: /etc/webctl/tls/cert.pem
    key: /etc/webctl/tls/key.pem
+177 −0
Changes for include/webctl/module.h: 177 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

/* webctl backend module interface.
 *
 * A module is a shared object in the backend's module directory
 * (/usr/lib/webctl/modules) that defines
 *
 *     WEBCTL_MODULE({ WEBCTL_MODULE_ABI, "users", "Benutzer", 10,
 *                     init, fini, call });
 *
 * The backend loads every *.so there (owned by root, not writable by
 * anyone else) at start, in name order. It runs as root: a module's call()
 * is the only code that changes the system, and only for a logged-in
 * session. The UI half of a module is www/modules/<name>.js, which the
 * frontend loads when the backend lists the module.
 *
 * call() gets the action name and its arguments (always a JSON object)
 * and returns a new JSON object, which the caller owns. Failures are
 * returned too, built with webctl::error(): {"error": "...", "status": N}
 * where status is the HTTP status the frontend answers with (400 if
 * absent). Calls are serialized: a module never runs concurrently with
 * itself or another module.
 *
 * Messages are shown in the UI's language (req->lang, "en" or "de"):
 * start call() with webctl::LanguageScope and write them as
 * webctl::tr("English %1", "Deutsch %1", {arg}). */

#pragma once

#include <json-c/json.h>

#ifdef __cplusplus
extern "C" {
#endif

#define WEBCTL_MODULE_ABI 1
#define WEBCTL_MODULE_SYMBOL "webctl_module_info"

typedef struct webctl_request {
    const char *user;          /* the logged-in user */
    const char *action;
    struct json_object *args;  /* borrowed; always an object */
    const char *lang;          /* the UI's language: "en" or "de" */
} webctl_request;

typedef struct webctl_module {
    unsigned abi;              /* WEBCTL_MODULE_ABI */
    const char *name;          /* API and script name: [a-z0-9_-]+ */
    const char *title;         /* menu entry */
    int order;                 /* menu position, ascending */
    int (*init)(void);         /* optional; nonzero: not loaded */
    void (*fini)(void);        /* optional */
    struct json_object *(*call)(const webctl_request *req);
} webctl_module;

#ifdef __cplusplus
}

#define WEBCTL_MODULE(...) \
    extern "C" __attribute__((visibility("default"))) \
    const webctl_module webctl_module_info = __VA_ARGS__

#include <cstdlib>
#include <initializer_list>
#include <string>

namespace webctl {

// The language messages are written in: "en" (default) or "de".
inline std::string &language() {
    static std::string lang = "en";
    return lang;
}

inline std::string normalizeLanguage(const char *lang) {
    return lang && lang[0] == 'd' && lang[1] == 'e' ? "de" : "en";
}

// Sets language() for the current call.
struct LanguageScope {
    std::string saved;
    explicit LanguageScope(const char *lang) : saved(language()) { language() = normalizeLanguage(lang); }
    ~LanguageScope() { language() = saved; }
};

// The message in lang, with %1..%9 replaced by args.
inline std::string trIn(const std::string &lang, const char *en, const char *de,
                        std::initializer_list<std::string> args = {}) {
    const char *text = lang == "de" ? de : en;
    std::string out;
    for (const char *p = text; *p; ++p) {
        if (p[0] == '%' && p[1] >= '1' && p[1] <= '9' && (size_t)(p[1] - '1') < args.size()) {
            out += *(args.begin() + (p[1] - '1'));
            ++p;
        } else {
            out += *p;
        }
    }
    return out;
}

inline std::string tr(const char *en, const char *de, std::initializer_list<std::string> args = {}) {
    return trIn(language(), en, de, args);
}

inline json_object *error(const std::string &msg, int status = 400) {
    json_object *o = json_object_new_object();
    json_object_object_add(o, "error", json_object_new_string(msg.c_str()));
    json_object_object_add(o, "status", json_object_new_int(status));
    return o;
}

inline json_object *ok() {
    json_object *o = json_object_new_object();
    json_object_object_add(o, "ok", json_object_new_boolean(1));
    return o;
}

inline bool has(json_object *args, const char *key) {
    json_object *v = nullptr;
    return json_object_object_get_ex(args, key, &v) && v;
}

inline std::string str(json_object *args, const char *key, const std::string &def = "") {
    json_object *v = nullptr;
    if (!json_object_object_get_ex(args, key, &v) || !v) return def;
    const char *s = json_object_get_string(v);
    return s ? s : def;
}

inline bool boolean(json_object *args, const char *key, bool def = false) {
    json_object *v = nullptr;
    if (!json_object_object_get_ex(args, key, &v) || !v) return def;
    return json_object_get_boolean(v) != 0;
}

inline long integer(json_object *args, const char *key, long def = -1) {
    json_object *v = nullptr;
    if (!json_object_object_get_ex(args, key, &v) || !v) return def;
    if (json_object_is_type(v, json_type_string)) {
        const char *s = json_object_get_string(v);
        char *end = nullptr;
        long n = s && *s ? strtol(s, &end, 10) : def;
        return end && *end == '\0' ? n : def;
    }
    return (long)json_object_get_int64(v);
}

inline json_object *jstr(const std::string &s) { return json_object_new_string(s.c_str()); }

} // namespace webctl
#endif
Loading