Commit 73dfc23f authored by jan.koester's avatar jan.koester
Browse files

test

parent 06a67019
Loading
Loading
Loading
Loading
Loading
+5 −2
Changes for CMakeLists.txt: 5 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -13,9 +13,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_ENABLE_EXPORTS 1)

set(DATAPATH "${CMAKE_INSTALL_DATADIR}/fenrirds" CACHE FILEPATH "path where stored webinterface files are located")
set(PLUGINPATH "${CMAKE_INSTALL_LIBDIR}/fenrirds" CACHE FILEPATH "default plugin search path")
set(PLUGINPATH "${CMAKE_INSTALL_FULL_LIBDIR}/fenrirds" CACHE FILEPATH "default plugin search path")
set(STATEPATH "${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/lib/fenrirds" CACHE FILEPATH "default path of the databases in a config written by the setup")

add_definitions(-D${CMAKE_HOST_SYSTEM_NAME} -DDATAPATH="${DATAPATH}" -DPLUGINPATH="${PLUGINPATH}")
add_definitions(-D${CMAKE_HOST_SYSTEM_NAME} -DDATAPATH="${DATAPATH}" -DPLUGINPATH="${PLUGINPATH}" -DSTATEPATH="${STATEPATH}")

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

@@ -86,6 +87,8 @@ install(FILES
    data/fenrirds.js
    data/style.css
    data/wizzard.html
    data/setup.html
    data/serverconfig.js
    data/login.html
    data/index.html
    data/fenrirds.png
+117 −0
Changes for data/admin.html: 117 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@
<head>
    <title>FenrirDirectoryServer Admin</title>
    <link rel="stylesheet" href="/style.css">
    <script src="/serverconfig.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        .admin-table { border-collapse: separate; border-spacing: 0; width: 100%; margin: 1em 0; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
@@ -68,6 +69,7 @@
                <li id="nav-services" style="display:none"><a href="#" onclick="loadServices(); return false;">Services</a></li>
                <li id="nav-gpos"><a href="#" onclick="loadGpos(); return false;">Policies</a></li>
                <li id="nav-sessions"><a href="#" onclick="loadSessions(); return false;">Sessions</a></li>
                <li id="nav-server" style="display:none"><a href="#" onclick="loadServerConfig(); return false;">Server</a></li>
                <li id="nav-cluster" style="display:none"><a href="#" onclick="loadClusterStatus(); return false;">Cluster</a></li>
                <li id="nav-export" style="display:none"><a href="#" onclick="loadExport(); return false;">Export/Import</a></li>
                <li class="nav-logout"><a href="#" onclick="doLogout(); return false;">Logout</a></li>
@@ -1244,6 +1246,119 @@ function submitEditGpo(gpid) {
    });
}

/* ---- Server settings (config file, written by the root config helper) ---- */
function loadServerConfig() {
    currentView = loadServerConfig;
    setActiveNav('nav-server');
    document.getElementById("domain-select").innerHTML = '';
    ajaxGet("/settings/admin/api/getconfig", function(err, data) {
        if (err) { showError(err); return; }
        var html = '<div class="view-header"><h3>Server</h3></div>';
        html += '<p>Settings in <span class="mono">' + escapeHtml(data.path) + '</span>. '
              + 'Port, address and TLS changes take effect after a restart.</p>';
        html += ServerConfig.render(data);
        html += '<p id="srvcfgmsg"></p>';
        html += '<ul class="toolbar">';
        html += '<li><a class="button" href="#" onclick="saveServerConfig(false); return false;">Save</a></li>';
        html += '<li><a class="button" href="#" onclick="saveServerConfig(true); return false;">Save and restart</a></li>';
        html += '</ul>';
        html += '<h4>Unconfigured nodes</h4>';
        html += '<p>Nodes in setup mode on the local network (mDNS). Adding one sends it this node\'s '
              + 'replication or cluster settings, including their keys, over plain HTTP.</p>';
        html += '<ul class="toolbar"><li><a class="button" href="#" onclick="discoverNodes(); return false;">Search</a></li></ul>';
        html += '<div id="nodelist"></div><p id="nodemsg"></p>';
        setContent(html);
        ServerConfig.updateRole();
    });
}

var g_foundNodes = [];

function nodeMsg(text, isError) {
    var el = document.getElementById("nodemsg");
    if (!el) return;
    el.className = isError ? "msg-error" : "msg-ok";
    el.textContent = text;
}

function discoverNodes() {
    document.getElementById("nodelist").innerHTML = '<p>Searching&hellip;</p>';
    nodeMsg("", false);
    ajaxGet("/settings/admin/api/discovernodes", function(err, data) {
        if (err) { document.getElementById("nodelist").innerHTML = ''; nodeMsg(err, true); return; }
        g_foundNodes = data.nodes || [];
        if (!g_foundNodes.length) {
            document.getElementById("nodelist").innerHTML = '<p>No unconfigured node found.</p>';
            return;
        }
        var html = '<div class="table-wrap"><table class="admin-table"><tr><th>Host</th><th>Address</th><th>ID</th><th></th></tr>';
        for (var i = 0; i < g_foundNodes.length; i++) {
            var n = g_foundNodes[i];
            html += '<tr><td>' + escapeHtml(n.host || n.instance) + '</td>'
                  + '<td class="mono">' + escapeHtml(n.address + ':' + n.port) + '</td>'
                  + '<td class="mono">' + escapeHtml(n.id) + '</td><td class="actions">'
                  + '<a class="button" href="#" onclick="joinNode(' + i + ', \'slave\'); return false;">Add as replication slave</a> ';
            if (g_permissions && g_permissions.cluster)
                html += '<a class="button" href="#" onclick="joinNode(' + i + ', \'cluster\'); return false;">Add to cluster</a>';
            html += '</td></tr>';
        }
        document.getElementById("nodelist").innerHTML = html + '</table></div>';
    });
}

function joinNode(idx, mode) {
    var n = g_foundNodes[idx];
    var what = mode === 'cluster'
        ? 'Add ' + (n.host || n.address) + ' (' + n.address + ') to the cluster? All cluster nodes restart.'
        : 'Make ' + (n.host || n.address) + ' (' + n.address + ') a replication slave of this node?';
    if (!confirm(what + '\n\nOnly continue if you recognise this node (ID ' + n.id + ').'))
        return;
    nodeMsg("Configuring " + n.address + " ...", false);
    ajaxPost("/settings/admin/api/joinnode",
             { address: n.address, port: n.port, host: n.host, mode: mode }, function(err, data) {
        if (err) {
            var m = err;
            try { m = JSON.parse(err.substring(err.indexOf(':') + 2)).error || err; } catch (e) {}
            nodeMsg(m, true);
            return;
        }
        var text = n.address + ' is configured and restarts' + (data.name ? ' as slave "' + data.name + '"' : '') + '.';
        if (data.warnings && data.warnings.length)
            text += ' Note: ' + data.warnings.join('; ');
        if (data.restart) {
            nodeMsg(text + ' This node restarts too ...', false);
            ServerConfig.waitFor(window.location.protocol + '//' + window.location.host + '/settings/index', nodeMsg);
        } else {
            nodeMsg(text, false);
        }
    });
}

function saveServerConfig(restart) {
    var values = ServerConfig.collect();
    var msg = function(text, isError) {
        var el = document.getElementById("srvcfgmsg");
        if (!el) return;
        el.className = isError ? "msg-error" : "msg-ok";
        el.textContent = text;
    };
    ajaxPost("/settings/admin/api/setconfig", { values: values, restart: restart }, function(err, data) {
        if (err) {
            var m = err;
            try { m = JSON.parse(err.substring(err.indexOf(':') + 2)).error || err; } catch (e) {}
            msg(m, true);
            return;
        }
        if (!restart) {
            msg("Saved. The changes take effect after the next restart.", false);
            return;
        }
        var url = ServerConfig.newUrl(values) + "settings/index";
        msg("Saved, the server restarts ...", false);
        ServerConfig.waitFor(url, msg);
    });
}

/* ---- Cluster Status ---- */
function loadClusterStatus() {
    currentView = loadClusterStatus;
@@ -1353,6 +1468,8 @@ window.onload = function() {
        if (data.superadmin) {
            document.getElementById('nav-domains').style.display = '';
        }
        if (data.serverconfig)
            document.getElementById('nav-server').style.display = '';
        if (data.superadmin || gpo[GPO_MANAGE_USERS])
            document.getElementById('nav-users').style.display = '';
        if (data.superadmin || gpo[GPO_MANAGE_GROUPS])

data/serverconfig.js

0 → 100644
+167 −0
Changes for data/serverconfig.js: 167 added lines, 0 removed lines.
Original line number Diff line number Diff line
/* Server settings form, shared by the setup wizard (setup.html, no config
 * file yet) and the "Server" view of the admin console. The keys are the
 * config paths below FENRIRDS/ that the root config helper accepts. */
var ServerConfig = (function() {
    var SECTIONS = [
        { title: "Web interface", fields: [
            { key: "BIND", label: "Bind address", hint: "0.0.0.0 = all interfaces" },
            { key: "PORT", label: "Port", type: "number" },
            { key: "MAXCONN", label: "Max. connections", type: "number" },
            { key: "SSL/CERT", label: "TLS certificate", hint: "optional, a file below the config directory" },
            { key: "SSL/KEY", label: "TLS key", hint: "optional, a file below the config directory" }
        ]},
        { title: "Domain controller", fields: [
            { key: "DNS/HOSTNAME", label: "DC host name", hint: "e.g. dc1.example.com" },
            { key: "DNS/ADDRESS", label: "DC IP address", hint: "the address clients reach this server on" },
            { key: "AD/ADMIN_PASSWORD", label: "Administrator password", type: "secret",
              hint: "set on the Administrator account when a domain is provisioned" }
        ]},
        { title: "Service ports", fields: [
            { key: "KDC/BIND", label: "Kerberos bind" },
            { key: "KDC/PORT", label: "Kerberos port", type: "number" },
            { key: "AD/BIND", label: "LDAP bind" },
            { key: "AD/PORT", label: "LDAP port", type: "number" },
            { key: "AD/MAXCONN", label: "LDAP max. connections", type: "number" },
            { key: "DNS/BIND", label: "DNS bind" },
            { key: "DNS/PORT", label: "DNS port", type: "number" },
            { key: "SMB/BIND", label: "SMB bind" },
            { key: "SMB/PORT", label: "SMB port", type: "number" }
        ]},
        { title: "Replication", fields: [
            { key: "REPLICATION/ROLE", label: "Role", type: "select", options: ["none", "master", "slave"] },
            { key: "REPLICATION/SLAVES", label: "Slaves", type: "list", role: "master",
              hint: "one name:key per line; a name alone keeps its key" },
            { key: "REPLICATION/NAME", label: "Slave name", role: "slave" },
            { key: "REPLICATION/KEY", label: "Slave key", type: "secret", role: "slave" },
            { key: "REPLICATION/MASTERS", label: "Masters", type: "list", role: "slave",
              hint: "one URL per line, e.g. https://master:9090" },
            { key: "REPLICATION/INTERVAL", label: "Interval (s)", type: "number", role: "slave" },
            { key: "REPLICATION/SSLVERIFY", label: "Verify TLS", type: "bool", role: "slave" }
        ]},
        { title: "Cluster", fields: [
            { key: "CLUSTER/ENABLE", label: "Enabled", type: "bool",
              hint: "needs TLS (certificate and key above)" },
            { key: "CLUSTER/BIND", label: "Bind address" },
            { key: "CLUSTER/PORT", label: "Port", type: "number" },
            { key: "CLUSTER/AUTH_NAME", label: "Auth name" },
            { key: "CLUSTER/AUTH_KEY", label: "Auth key", type: "secret" },
            { key: "CLUSTER/DATA_BLOCKS", label: "Data blocks", type: "number" },
            { key: "CLUSTER/PARITY_BLOCKS", label: "Parity blocks", type: "number" },
            { key: "CLUSTER/SSLVERIFY", label: "Verify TLS", type: "bool" },
            { key: "CLUSTER/PEERS", label: "Peers", type: "list",
              hint: "one ip:port per line, this node included" }
        ]},
        { title: "Maintenance", fields: [
            { key: "VACUUM_INTERVAL", label: "Vacuum interval (h)", type: "number" }
        ]}
    ];

    function esc(s) {
        return String(s == null ? '' : s)
            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    }

    function fid(key) {
        return "srvcfg_" + key.replace(/\//g, "_");
    }

    function fieldHtml(f, values, secrets) {
        var id = fid(f.key), v = values[f.key], input;
        if (f.type === "select") {
            input = '<select id="' + id + '" onchange="ServerConfig.updateRole()">';
            for (var i = 0; i < f.options.length; i++) {
                var o = f.options[i];
                input += '<option value="' + o + '"' + ((v || "none") === o ? ' selected' : '') + '>' + o + '</option>';
            }
            input += '</select>';
        } else if (f.type === "bool") {
            input = '<input type="checkbox" id="' + id + '"' + (v === "true" ? ' checked' : '') + '>';
        } else if (f.type === "list") {
            input = '<textarea id="' + id + '" rows="3">' + esc((v || []).join("\n")) + '</textarea>';
        } else if (f.type === "secret") {
            var isSet = secrets && secrets[f.key];
            input = '<input type="password" id="' + id + '" autocomplete="new-password"'
                  + (isSet ? ' placeholder="unchanged"' : '') + '>';
        } else {
            input = '<input type="' + (f.type === "number" ? 'number' : 'text') + '" id="' + id
                  + '" value="' + esc(v) + '">';
        }
        return '<tr' + (f.role ? ' class="srvcfg-role" data-role="' + f.role + '"' : '') + '>'
             + '<td><label for="' + id + '">' + esc(f.label) + '</label></td>'
             + '<td>' + input + (f.hint ? '<br><small>' + esc(f.hint) + '</small>' : '') + '</td></tr>';
    }

    /* data: the reply of getconfig / setup/api/get */
    function render(data) {
        var values = (data && data.values) || {}, secrets = (data && data.secrets) || {};
        var html = '<div class="srvcfg">';
        for (var s = 0; s < SECTIONS.length; s++) {
            html += '<fieldset><legend>' + esc(SECTIONS[s].title) + '</legend><table>';
            for (var i = 0; i < SECTIONS[s].fields.length; i++)
                html += fieldHtml(SECTIONS[s].fields[i], values, secrets);
            html += '</table></fieldset>';
        }
        return html + '</div>';
    }

    function role() {
        var el = document.getElementById(fid("REPLICATION/ROLE"));
        return el ? el.value : "none";
    }

    function updateRole() {
        var r = role(), rows = document.querySelectorAll('.srvcfg-role');
        for (var i = 0; i < rows.length; i++)
            rows[i].style.display = rows[i].getAttribute('data-role') === r ? '' : 'none';
    }

    /* the values to send; fields of the other replication role are left out
     * so switching the role doesn't wipe them */
    function collect() {
        var out = {}, r = role();
        for (var s = 0; s < SECTIONS.length; s++) {
            for (var i = 0; i < SECTIONS[s].fields.length; i++) {
                var f = SECTIONS[s].fields[i];
                if (f.role && f.role !== r) continue;
                var el = document.getElementById(fid(f.key));
                if (!el) continue;
                if (f.type === "bool")
                    out[f.key] = el.checked ? "true" : "false";
                else if (f.type === "list")
                    out[f.key] = el.value.split("\n").map(function(l) { return l.trim(); })
                                          .filter(function(l) { return l !== ""; });
                else
                    out[f.key] = el.value;
            }
        }
        return out;
    }

    /* where the web interface will answer once the server restarted */
    function newUrl(values) {
        var ssl = values["SSL/CERT"] && values["SSL/KEY"];
        var port = values["PORT"] || window.location.port;
        return (ssl ? "https://" : "http://") + window.location.hostname + (port ? ":" + port : "") + "/";
    }

    /* polls url until the restarted server answers, then opens it */
    function waitFor(url, onStatus) {
        var tries = 0;
        function attempt() {
            if (++tries > 60) {
                onStatus("The server did not come back. Check the log and open " + url + " by hand.", true);
                return;
            }
            fetch(url, { mode: "no-cors", cache: "no-store" }).then(function() {
                window.location.href = url;
            }, function() {
                setTimeout(attempt, 2000);
            });
        }
        /* the old server answers for about a second after saving */
        setTimeout(attempt, 3000);
    }

    return { render: render, updateRole: updateRole, collect: collect, newUrl: newUrl, waitFor: waitFor };
})();

data/setup.html

0 → 100644
+68 −0
Changes for data/setup.html: 68 added lines, 0 removed lines.
Original line number Diff line number Diff line
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>FenrirDirectoryServer Setup</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="/style.css">
        <script src="/serverconfig.js"></script>
    </head>
    <body>
        <div id="setup">
            <p><span>FenrirDirectoryServer Setup</span></p>
            <p>There is no configuration file yet. Enter the server settings; they are
               written to <span class="mono" id="cfgpath"></span> and the server restarts.
               After that you create the first administrator account.</p>
            <div id="setupform">Loading&hellip;</div>
            <p id="setupmsg"></p>
            <input type="submit" id="setupsave" value="Save and start" onclick="saveSetup()" disabled>
        </div>
        <script>
        function setupMsg(text, isError) {
            var el = document.getElementById("setupmsg");
            el.className = isError ? "msg-error" : "msg-ok";
            el.textContent = text;
        }

        function saveSetup() {
            var values = ServerConfig.collect();
            var btn = document.getElementById("setupsave");
            btn.disabled = true;
            fetch("/setup/api/save", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ values: values })
            }).then(function(r) {
                return r.json().then(function(j) { return { ok: r.ok, data: j }; });
            }).then(function(res) {
                if (!res.ok) {
                    setupMsg(res.data.error || "saving failed", true);
                    btn.disabled = false;
                    return;
                }
                var url = ServerConfig.newUrl(values);
                setupMsg("Configuration saved, the server restarts and opens " + url + " ...", false);
                ServerConfig.waitFor(url, setupMsg);
            }).catch(function(e) {
                setupMsg("saving failed: " + e, true);
                btn.disabled = false;
            });
        }

        fetch("/setup/api/get", { cache: "no-store" }).then(function(r) {
            return r.json().then(function(j) { return { ok: r.ok, data: j }; });
        }).then(function(res) {
            if (!res.ok) {
                document.getElementById("setupform").textContent = "";
                setupMsg(res.data.error || "loading the defaults failed", true);
                return;
            }
            document.getElementById("cfgpath").textContent = res.data.path || "";
            document.getElementById("setupform").innerHTML = ServerConfig.render(res.data);
            ServerConfig.updateRole();
            document.getElementById("setupsave").disabled = false;
        }).catch(function(e) {
            setupMsg("loading the defaults failed: " + e, true);
        });
        </script>
    </body>
</html>
+36 −0
Changes for data/style.css: 36 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -261,6 +261,42 @@ select[multiple] option { padding: 0.3em 0.5em; border-radius: 4px; }
#login input[type="submit"],
#wizzard input[type="submit"] { width: 100%; padding: 0.6em; margin-top: 0.5em; }

/* ---- Setup wizard / server settings ---- */
#setup {
    width: min(720px, calc(100vw - 32px));
    margin: 6vh auto 4vh;
    padding: 2em;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 14px;
    box-shadow: var(--shadow);
}

#setup p span {
    font-size: 1.75em !important;
    font-weight: 700;
}

#setup input[type="submit"] { width: 100%; padding: 0.6em; margin-top: 0.5em; }

.srvcfg fieldset {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin: 0 0 1em;
    padding: 0.5em 1em 0.75em;
}

.srvcfg legend { font-weight: 600; padding: 0 0.4em; }
.srvcfg table { width: 100%; border-collapse: collapse; }
.srvcfg td { padding: 0.3em 0.5em 0.3em 0; vertical-align: top; }
.srvcfg td:first-child { width: 35%; color: var(--text-muted); padding-top: 0.6em; }
.srvcfg input[type="text"],
.srvcfg input[type="number"],
.srvcfg input[type="password"],
.srvcfg select,
.srvcfg textarea { width: 100%; }
.srvcfg small { color: var(--text-muted); }

#login > span {
    display: block;
    margin-top: 1em;
Loading