Commit 5d6a3ed3 authored by Jan Koester's avatar Jan Koester
Browse files

test

parent 79937c2b
Loading
Loading
Loading
Loading
+5 −1
Changes for CMakeLists.txt: 5 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -11,7 +11,7 @@ project(webctl VERSION 0.2.3 LANGUAGES CXX)
#                    after binding
#   modules/         one shared object per area (users, network, shares,
#                    raid, lvm, filesystems, services, updates, snmp,
#                    system),
#                    time, system),
#                    dlopen()ed by the backend from its module directory
#   www/             the AJAX UI; www/modules/<name>.js per module

@@ -108,6 +108,10 @@ target_link_libraries(webctl_mod_filesystems PRIVATE sysbus::sysbus)
# snmpdd/api.h (snmpdd-dev)
webctl_module(snmp modules/snmp/snmp.cpp)
target_link_libraries(webctl_mod_snmp PRIVATE sysbus::sysbus)
# time: NTP client/server of ntpplus over sysbus; needs ntpplus/api.h
# (ntpplus-dev)
webctl_module(time modules/time/time.cpp)
target_link_libraries(webctl_mod_time PRIVATE sysbus::sysbus)

install(TARGETS webctl-backend webctl-frontend RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
install(DIRECTORY www/ DESTINATION ${WEBCTL_WWWDIR})

modules/time/time.cpp

0 → 100644
+235 −0
Changes for modules/time/time.cpp: 235 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 module "time": NTP (client and server) of ntpplus, entirely
 * over its sysbus API (include/ntpplus/api.h).
 *
 *   info                -> {settings, state, sources}
 *       (not "status": the frontend takes a reply's "status" as its HTTP
 *       status and drops it)
 *       settings: client, sources[{host, type, iburst}], minPoll, maxPoll,
 *           step, stepThreshold, server, listen[], allow[], localStratum
 *       state: time, synchronized, kernelSynchronized, stratum, offset,
 *           jitter, frequency, refid, sysPeer, lastUpdate, poll, steps,
 *           leap, client, server, answered, refused, serverError
 *       sources[{host, type, resolving, error, peers[{address, state,
 *           note, stratum, refid, reach, poll, when, offset, delay,
 *           jitter}]}]
 *   save {settings}     replaces them all
 *   sync                every source at once; may step the clock
 *   settime {time}      the clock by hand (Unix seconds)
 */

#include <cstdlib>
#include <string>
#include <vector>

#include <ntpplus/api.h>
#include <sysbus/client.h>
#include <webctl/module.h>

using webctl::error;
using webctl::jstr;
using webctl::tr;

namespace {

using Fields = std::vector<std::pair<std::string, std::string>>;

const char *yesNo(bool b) { return b ? "yes" : "no"; }

json_object *num(const std::string &v) { return json_object_new_double(std::strtod(v.c_str(), nullptr)); }
json_object *integer(const std::string &v) { return json_object_new_int64(std::strtoll(v.c_str(), nullptr, 10)); }
json_object *flag(const std::string &v) { return json_object_new_boolean(v == "yes"); }

json_object *callNtpplus(sysbus::Client &bus, const char *method, const Fields &args, sysbus::Message &reply) {
    Fields all = args;
    all.emplace_back("lang", webctl::language());
    // ntpplus answers at once; long means it hangs (webctl's calls are serialized)
    reply = bus.callService(ntpplus::Service, method, all, 15000);
    if (reply.type != sysbus::MessageType::Error) return nullptr;
    const std::string &name = reply.member;
    if (name == sysbus::error::ServiceUnknown)
        return error(tr("ntpplus isn't running (Services: ntpplus)", "ntpplus läuft nicht (Dienste: ntpplus)"), 503);
    int status = name == sysbus::error::AccessDenied ? 403 : name == sysbus::error::InvalidArgs ? 400 : 500;
    return error(reply.get("message", name), status);
}

json_object *settingsJson(const sysbus::Message &r) {
    json_object *o = json_object_new_object();
    json_object *sources = json_object_new_array(), *listen = json_object_new_array(), *allow = json_object_new_array();
    json_object *src = nullptr;
    for (auto &f : r.fields) {
        const std::string &k = f.first, &v = f.second;
        if (k == "source") {
            src = json_object_new_object();
            json_object_array_add(sources, src);
            json_object_object_add(src, "host", jstr(v));
        } else if (src && k == "type") json_object_object_add(src, "type", jstr(v));
        else if (src && k == "iburst") json_object_object_add(src, "iburst", flag(v));
        else if (k == "client" || k == "server") json_object_object_add(o, k.c_str(), flag(v));
        else if (k == "minpoll") json_object_object_add(o, "minPoll", integer(v));
        else if (k == "maxpoll") json_object_object_add(o, "maxPoll", integer(v));
        else if (k == "step") json_object_object_add(o, "step", jstr(v));
        else if (k == "step_threshold") json_object_object_add(o, "stepThreshold", num(v));
        else if (k == "local_stratum") json_object_object_add(o, "localStratum", integer(v));
        else if (k == "listen") json_object_array_add(listen, jstr(v));
        else if (k == "allow") json_object_array_add(allow, jstr(v));
    }
    json_object_object_add(o, "sources", sources);
    json_object_object_add(o, "listen", listen);
    json_object_object_add(o, "allow", allow);
    return o;
}

// GetStatus: the system fields, then source records with their peers
void statusJson(const sysbus::Message &r, json_object *&state, json_object *&sources) {
    state = json_object_new_object();
    sources = json_object_new_array();
    json_object *src = nullptr, *peers = nullptr, *peer = nullptr;
    for (auto &f : r.fields) {
        const std::string &k = f.first, &v = f.second;
        if (k == "source") {
            src = json_object_new_object();
            peers = json_object_new_array();
            peer = nullptr;
            json_object_array_add(sources, src);
            json_object_object_add(src, "host", jstr(v));
            json_object_object_add(src, "peers", peers);
        } else if (k == "peer" && src) {
            peer = json_object_new_object();
            json_object_array_add(peers, peer);
            json_object_object_add(peer, "address", jstr(v));
        } else if (peer) {
            if (k == "state" || k == "note" || k == "refid" || k == "reach") json_object_object_add(peer, k.c_str(), jstr(v));
            else if (k == "stratum" || k == "poll" || k == "when") json_object_object_add(peer, k.c_str(), integer(v));
            else if (k == "offset" || k == "delay" || k == "jitter") json_object_object_add(peer, k.c_str(), num(v));
        } else if (src) {
            if (k == "type" || k == "error") json_object_object_add(src, k.c_str(), jstr(v));
            else if (k == "resolving") json_object_object_add(src, k.c_str(), flag(v));
        } else if (k == "synchronized" || k == "client" || k == "server") {
            json_object_object_add(state, k.c_str(), flag(v));
        } else if (k == "kernel_synchronized") {
            json_object_object_add(state, "kernelSynchronized", flag(v));
        } else if (k == "time" || k == "offset" || k == "jitter" || k == "frequency") {
            json_object_object_add(state, k.c_str(), num(v));
        } else if (k == "stratum" || k == "poll" || k == "steps" || k == "answered" || k == "refused") {
            json_object_object_add(state, k.c_str(), integer(v));
        } else if (k == "last_update") {
            json_object_object_add(state, "lastUpdate", integer(v));
        } else if (k == "sys_peer") {
            json_object_object_add(state, "sysPeer", jstr(v));
        } else if (k == "server_error") {
            json_object_object_add(state, "serverError", jstr(v));
        } else if (k == "refid" || k == "leap") {
            json_object_object_add(state, k.c_str(), jstr(v));
        }
    }
}

std::vector<std::string> strings(json_object *o, const char *key) {
    std::vector<std::string> out;
    json_object *a = nullptr;
    if (!json_object_object_get_ex(o, key, &a) || !a || !json_object_is_type(a, json_type_array)) return out;
    for (size_t i = 0; i < json_object_array_length(a); ++i) {
        const char *s = json_object_get_string(json_object_array_get_idx(a, i));
        if (s && *s) out.push_back(s);
    }
    return out;
}

std::string settingsFields(json_object *s, Fields &out) {
    if (!s || !json_object_is_type(s, json_type_object)) return tr("no settings given", "keine Einstellungen angegeben");
    out.emplace_back("client", yesNo(webctl::boolean(s, "client")));
    out.emplace_back("minpoll", std::to_string(webctl::integer(s, "minPoll", 6)));
    out.emplace_back("maxpoll", std::to_string(webctl::integer(s, "maxPoll", 10)));
    out.emplace_back("step", webctl::str(s, "step", "startup"));
    out.emplace_back("step_threshold", webctl::str(s, "stepThreshold", "1"));
    out.emplace_back("server", yesNo(webctl::boolean(s, "server")));
    out.emplace_back("local_stratum", std::to_string(webctl::integer(s, "localStratum", 0)));
    for (auto &a : strings(s, "listen")) out.emplace_back("listen", a);
    for (auto &a : strings(s, "allow")) out.emplace_back("allow", a);
    json_object *list = nullptr;
    if (json_object_object_get_ex(s, "sources", &list) && list && json_object_is_type(list, json_type_array)) {
        for (size_t i = 0; i < json_object_array_length(list); ++i) {
            json_object *src = json_object_array_get_idx(list, i);
            if (!src || !json_object_is_type(src, json_type_object)) return tr("invalid source", "ungültige Quelle");
            out.emplace_back("source", webctl::str(src, "host"));
            out.emplace_back("type", webctl::str(src, "type", "server"));
            out.emplace_back("iburst", yesNo(webctl::boolean(src, "iburst", true)));
        }
    }
    return "";
}

json_object *call(const webctl_request *req) {
    webctl::LanguageScope lang(req->lang);
    std::string action = req->action;
    sysbus::Client bus;
    if (!bus.connect())
        return error(tr("sysbus isn't reachable: %1", "sysbus ist nicht erreichbar: %1", {bus.lastError()}), 503);

    sysbus::Message reply;
    if (action == "info") {
        if (json_object *err = callNtpplus(bus, ntpplus::method::GetSettings, {}, reply)) return err;
        json_object *out = json_object_new_object();
        json_object_object_add(out, "settings", settingsJson(reply));
        if (json_object *err = callNtpplus(bus, ntpplus::method::GetStatus, {}, reply)) {
            json_object_put(out);
            return err;
        }
        json_object *state, *sources;
        statusJson(reply, state, sources);
        json_object_object_add(out, "state", state);
        json_object_object_add(out, "sources", sources);
        return out;
    }
    if (action == "save") {
        json_object *s = nullptr;
        json_object_object_get_ex(req->args, "settings", &s);
        Fields f;
        std::string bad = settingsFields(s, f);
        if (!bad.empty()) return error(bad);
        if (json_object *err = callNtpplus(bus, ntpplus::method::SetSettings, f, reply)) return err;
        return webctl::ok();
    }
    if (action == "sync") {
        if (json_object *err = callNtpplus(bus, ntpplus::method::SyncNow, {}, reply)) return err;
        return webctl::ok();
    }
    if (action == "settime") {
        std::string t = webctl::str(req->args, "time");
        if (t.empty()) return error(tr("no time given", "keine Zeit angegeben"));
        if (json_object *err = callNtpplus(bus, ntpplus::method::SetTime, {{"time", t}}, reply)) return err;
        return webctl::ok();
    }
    return error(tr("unknown action %1", "unbekannte Aktion %1", {action}), 404);
}

} // namespace

WEBCTL_MODULE({WEBCTL_MODULE_ABI, "time", "Time", 37, nullptr, nullptr, call});

www/modules/time.js

0 → 100644
+230 −0

File added.

Preview size limit exceeded, changes collapsed.