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

test

parent bb57df49
Loading
Loading
Loading
Loading
+53 −0
Changes for admin/static/app.js: 53 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -274,6 +274,57 @@ const ProxyPlusAdmin = (() => {
    }
  }

  async function refreshListenerTlsAssignments() {
    const listeners = await api("GET", "/api/v1/listeners");
    const body = document.querySelector("#listener-tls tbody");
    body.innerHTML = "";
    for (const l of listeners) {
      const tr = document.createElement("tr");
      tr.innerHTML = `<td>${l.listener_key}</td><td>${(l.yaml_tls_profiles || []).join(", ") || "(kein TLS)"}</td><td>${(l.effective_tls_profiles || []).join(", ") || "(kein TLS)"}</td><td>${l.overridden ? "ja" : "nein"}</td><td></td>`;
      addEditButton(tr, () => editListenerTlsAssignment(l));
      if (l.overridden) {
        const btn = document.createElement("button");
        btn.textContent = "Zuordnung entfernen";
        btn.onclick = () => clearListenerTlsAssignment(l.listener_key);
        tr.lastElementChild.appendChild(btn);
      }
      body.appendChild(tr);
    }
  }

  function editListenerTlsAssignment(l) {
    document.getElementById("listener-tls-key").value = l.listener_key;
    document.getElementById("listener-tls-profiles").value = (l.effective_tls_profiles || []).join(", ");
    document.getElementById("listener-tls-form").hidden = false;
  }

  async function saveListenerTlsAssignment() {
    document.getElementById("listener-tls-error").textContent = "";
    try {
      const profiles = document.getElementById("listener-tls-profiles").value
        .split(",")
        .map((s) => s.trim())
        .filter(Boolean);
      await api("POST", "/api/v1/listeners/tls-assignment", {
        listener_key: document.getElementById("listener-tls-key").value,
        tls_profiles: profiles,
      });
      await refreshListenerTlsAssignments();
    } catch (e) {
      document.getElementById("listener-tls-error").textContent = e.message;
    }
  }

  async function clearListenerTlsAssignment(listenerKey) {
    if (!confirm("Zuordnung fuer '" + listenerKey + "' wirklich entfernen (zurueck zum YAML-Standard)?")) return;
    try {
      await api("DELETE", "/api/v1/listeners/tls-assignment", { listener_key: listenerKey });
      await refreshListenerTlsAssignments();
    } catch (e) {
      document.getElementById("listener-tls-error").textContent = e.message;
    }
  }

  async function refreshPluginConfigs() {
    const configs = await api("GET", "/api/v1/plugins/config");
    const body = document.querySelector("#plugin-configs tbody");
@@ -333,6 +384,7 @@ const ProxyPlusAdmin = (() => {
      refreshTlsProfiles(),
      refreshDkimKeys(),
      refreshPluginConfigs(),
      refreshListenerTlsAssignments(),
    ]);
  }

@@ -528,6 +580,7 @@ const ProxyPlusAdmin = (() => {
    createPool,
    showNewPluginConfigForm,
    createPluginConfig,
    saveListenerTlsAssignment,
    exportConfig,
    importConfig,
  };
+13 −0
Changes for admin/static/index.html: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -73,6 +73,19 @@
    <div id="tls-profile-error" class="error"></div>
  </div>

  <h2>HTTP-Listener: TLS-Zuordnung</h2>
  <p>Zeigt die beim Start aus der YAML gebundenen Listener. Eine Zuordnung hier legt fest,
     welches TLS-Profil ein Listener verwendet, ohne die YAML von Hand zu bearbeiten --
     wirkt aber wie jede http_listeners-Aenderung erst nach einem Neustart (Listener werden
     nur beim Start gebunden).</p>
  <table id="listener-tls"><thead><tr><th>Listener</th><th>YAML-Profile</th><th>Effektive Profile</th><th>Ueberschrieben</th><th>Aktionen</th></tr></thead><tbody></tbody></table>
  <div id="listener-tls-form" hidden>
    <input id="listener-tls-key" readonly>
    <input id="listener-tls-profiles" placeholder="TLS-Profile, kommagetrennt (leer = kein TLS), 2+ = SNI">
    <button onclick="ProxyPlusAdmin.saveListenerTlsAssignment()">Speichern</button>
    <div id="listener-tls-error" class="error"></div>
  </div>

  <h2>DKIM-Schluessel</h2>
  <table id="dkim-keys"><thead><tr><th>Domain</th><th>Selector</th><th>Key-Pfad</th><th>Aktiv</th><th>Aktionen</th></tr></thead><tbody></tbody></table>
  <button onclick="ProxyPlusAdmin.showNewDkimKeyForm()">Neuer Schluessel</button>
+71 −0
Changes for src/api/JsonApiServer.cpp: 71 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -601,6 +601,77 @@ void JsonApiController::registerRoutes() {
             return json_object_to_json_string(resp);
         }});

    // Read-only view of the HTTP listeners bound at startup (config.httpListeners, static
    // YAML) plus which TLS profile(s) each one is *actually* using right now -- either the
    // YAML's own tls_profile/tls_profiles, or a store::ListenerTlsAssignmentRecord override
    // if one exists (see main.cpp's effectiveTlsProfiles(), which this mirrors). Lets the
    // GUI show "this listener would use profile X after a restart" without duplicating
    // main.cpp's bootstrap logic in JS.
    _routes.push_back(
        {"GET", "/api/v1/listeners", "tls.profiles.read", [this](RouteContext &) -> std::string {
             JsonGuard arr(json_object_new_array());
             for (const auto &listener : _cfg.httpListeners) {
                 std::string key = listener.address + ":" + std::to_string(listener.port);
                 std::vector<std::string> yamlProfiles = !listener.tlsProfiles.empty()
                     ? listener.tlsProfiles
                     : (!listener.tlsProfile.empty() ? std::vector<std::string>{listener.tlsProfile}
                                                      : std::vector<std::string>{});
                 auto override_ = _configStore.getListenerTlsAssignment(key);

                 json_object *item = json_object_new_object();
                 json_object_object_add(item, "listener_key", json_object_new_string(key.c_str()));
                 json_object_object_add(item, "address", json_object_new_string(listener.address.c_str()));
                 json_object_object_add(item, "port", json_object_new_int(listener.port));
                 json_object *yamlArr = json_object_new_array();
                 for (const auto &p : yamlProfiles) json_object_array_add(yamlArr, json_object_new_string(p.c_str()));
                 json_object_object_add(item, "yaml_tls_profiles", yamlArr);
                 json_object *effArr = json_object_new_array();
                 for (const auto &p : override_ ? override_->tlsProfiles : yamlProfiles) {
                     json_object_array_add(effArr, json_object_new_string(p.c_str()));
                 }
                 json_object_object_add(item, "effective_tls_profiles", effArr);
                 json_object_object_add(item, "overridden", json_object_new_boolean(override_.has_value()));
                 json_object_array_add(arr.obj, item);
             }
             return json_object_to_json_string(arr);
         }});

    // Points a listener at a different (or newly created) TLS profile without hand-editing
    // the bootstrap YAML -- still needs a process restart to take effect (listeners are
    // bound once at startup, see main.cpp), same as any other http_listeners[] change.
    _routes.push_back(
        {"POST", "/api/v1/listeners/tls-assignment", "tls.profiles.write", [this](RouteContext &ctx) -> std::string {
             JsonGuard body(json_tokener_parse(ctx.body.c_str()));
             if (!body.obj) return jsonError("invalid JSON body");
             store::ListenerTlsAssignmentRecord rec;
             rec.listenerKey = getStringField(body, "listener_key");
             if (rec.listenerKey.empty()) return jsonError("listener_key is required");
             json_object *profilesArr = nullptr;
             if (json_object_object_get_ex(body, "tls_profiles", &profilesArr) && profilesArr) {
                 int n = json_object_array_length(profilesArr);
                 for (int i = 0; i < n; ++i) {
                     const char *p = json_object_get_string(json_object_array_get_idx(profilesArr, i));
                     if (p) rec.tlsProfiles.emplace_back(p);
                 }
             }
             _configStore.putListenerTlsAssignment(rec);
             JsonGuard resp(json_object_new_object());
             json_object_object_add(resp, "status", json_object_new_string("created"));
             return json_object_to_json_string(resp);
         }});

    _routes.push_back(
        {"DELETE", "/api/v1/listeners/tls-assignment", "tls.profiles.write", [this](RouteContext &ctx) -> std::string {
             JsonGuard body(json_tokener_parse(ctx.body.c_str()));
             if (!body.obj) return jsonError("invalid JSON body");
             std::string listenerKey = getStringField(body, "listener_key");
             if (listenerKey.empty()) return jsonError("listener_key is required");
             _configStore.deleteListenerTlsAssignment(listenerKey);
             JsonGuard resp(json_object_new_object());
             json_object_object_add(resp, "status", json_object_new_string("deleted"));
             return json_object_to_json_string(resp);
         }});

    // Load-balancer pools, shared by HttpRouteRecord::upstreamPool and
    // SmtpRelayRuleRecord::smarthostPool (see src/lb/LoadBalancer). "kind" is informational
    // only -- LoadBalancer itself doesn't care whether a target string is an HTTP URL or an
+26 −8
Changes for src/main.cpp: 26 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -35,6 +35,24 @@ std::string keyPathFor(const proxyplus::config::AppConfig &cfg, const std::strin
    return profile.empty() ? "" : cfg.dataPath + "/tls/" + profile + ".key";
}

std::string listenerKeyFor(const proxyplus::config::ListenerConfig &listener) {
    return listener.address + ":" + std::to_string(listener.port);
}

// Which TLS profile(s) a listener actually uses: a store::ListenerTlsAssignmentRecord set
// via the admin GUI/API overrides the bootstrap YAML's tls_profile/tls_profiles for that
// listener (see Records.h's ListenerTlsAssignmentRecord for why), falling back to the YAML
// as always when no override exists. 0 entries = plaintext, 1 = single-profile, 2+ = SNI --
// same three-way split the YAML fields already encode across tlsProfile/tlsProfiles.
std::vector<std::string> effectiveTlsProfiles(const proxyplus::store::ConfigStore &configStore,
                                               const proxyplus::config::ListenerConfig &listener) {
    auto override_ = configStore.getListenerTlsAssignment(listenerKeyFor(listener));
    if (override_) return override_->tlsProfiles;
    if (!listener.tlsProfiles.empty()) return listener.tlsProfiles;
    if (!listener.tlsProfile.empty()) return {listener.tlsProfile};
    return {};
}

// Builds the hostname->CertificateBundle map for an SNI listener (ListenerConfig::tlsProfiles):
// every domain of every named profile gets that profile's bundle, so e.g. profile "shop" with
// domains ["shop.example.com"] and profile "www" with domains ["example.com", "www.example.com"]
@@ -279,14 +297,15 @@ int main(int argc, char **argv) {

        if (cfg.maintenanceMode != proxyplus::config::MaintenanceMode::On) {
            for (const auto &listener : cfg.httpListeners) {
                if (!listener.tlsProfiles.empty()) {
                    auto sniCerts = loadSniCertMap(cfg, configStore, listener.tlsProfiles);
                auto tlsProfiles = effectiveTlsProfiles(configStore, listener);
                if (tlsProfiles.size() >= 2) {
                    auto sniCerts = loadSniCertMap(cfg, configStore, tlsProfiles);
                    httpServers.push_back(std::make_unique<proxyplus::httpproxy::HttpProxyServer>(
                        listener, sniCerts, configStore, authService, pluginManager, loadBalancer,
                        &acmeChallenges));
                    auto *server = httpServers.back().get();
                    auto allProfiles = configStore.listTlsProfiles();
                    for (const auto &profileName : listener.tlsProfiles) {
                    for (const auto &profileName : tlsProfiles) {
                        auto it = std::find_if(allProfiles.begin(), allProfiles.end(),
                                                [&](const auto &p) { return p.profile == profileName; });
                        std::vector<std::string> domains = it != allProfiles.end() ? it->domains
@@ -300,13 +319,12 @@ int main(int argc, char **argv) {
                    }
                    continue;
                }
                std::string profile = tlsProfiles.empty() ? "" : tlsProfiles.front();
                httpServers.push_back(std::make_unique<proxyplus::httpproxy::HttpProxyServer>(
                    listener, certPathFor(cfg, listener.tlsProfile),
                    keyPathFor(cfg, listener.tlsProfile), configStore, authService, pluginManager,
                    loadBalancer, &acmeChallenges));
                if (!listener.tlsProfile.empty()) {
                    listener, certPathFor(cfg, profile), keyPathFor(cfg, profile), configStore,
                    authService, pluginManager, loadBalancer, &acmeChallenges));
                if (!profile.empty()) {
                    auto *server = httpServers.back().get();
                    std::string profile = listener.tlsProfile;
                    certManager.registerReloadCallback(profile, [server, &cfg, profile] {
                        server->reloadCertificates(certPathFor(cfg, profile), keyPathFor(cfg, profile));
                    });
+65 −0
Changes for src/store/ConfigStore.cpp: 65 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -246,6 +246,19 @@ DkimKeyRecord decodeDkimKey(ByteReader &r) {
    return rec;
}

void encodeListenerTlsAssignment(ByteWriter &w, const ListenerTlsAssignmentRecord &r) {
    w.str(r.listenerKey);
    w.u32(static_cast<uint32_t>(r.tlsProfiles.size()));
    for (const auto &p : r.tlsProfiles) w.str(p);
}
ListenerTlsAssignmentRecord decodeListenerTlsAssignment(ByteReader &r) {
    ListenerTlsAssignmentRecord rec;
    rec.listenerKey = r.str();
    uint32_t n = r.u32();
    for (uint32_t i = 0; i < n; ++i) rec.tlsProfiles.push_back(r.str());
    return rec;
}

std::string permissionKey(const std::string &permission, const std::string &resourcePattern) {
    return permission + "\x1f" + resourcePattern;
}
@@ -396,6 +409,14 @@ void ConfigStore::applyRecord(RecordTable table, RecordOp op, const std::vector<
                _dkimKeys.erase(r.str());
            }
            break;
        case RecordTable::ListenerTlsAssignment:
            if (op == RecordOp::Put) {
                auto rec = decodeListenerTlsAssignment(r);
                _listenerTlsAssignments[rec.listenerKey] = rec;
            } else {
                _listenerTlsAssignments.erase(r.str());
            }
            break;
    }
    ++_version;
}
@@ -567,6 +588,37 @@ std::optional<DkimKeyRecord> ConfigStore::getDkimKey(const std::string &domain)
    return it->second;
}

void ConfigStore::putListenerTlsAssignment(const ListenerTlsAssignmentRecord &rec) {
    std::unique_lock lk(_mutex);
    ByteWriter w;
    encodeListenerTlsAssignment(w, rec);
    appendWal(RecordTable::ListenerTlsAssignment, RecordOp::Put, w.buffer());
    _listenerTlsAssignments[rec.listenerKey] = rec;
}

void ConfigStore::deleteListenerTlsAssignment(const std::string &listenerKey) {
    std::unique_lock lk(_mutex);
    ByteWriter w;
    w.str(listenerKey);
    appendWal(RecordTable::ListenerTlsAssignment, RecordOp::Delete, w.buffer());
    _listenerTlsAssignments.erase(listenerKey);
}

std::vector<ListenerTlsAssignmentRecord> ConfigStore::listListenerTlsAssignments() const {
    std::shared_lock lk(_mutex);
    std::vector<ListenerTlsAssignmentRecord> out;
    for (const auto &[key, rec] : _listenerTlsAssignments) out.push_back(rec);
    return out;
}

std::optional<ListenerTlsAssignmentRecord> ConfigStore::getListenerTlsAssignment(
    const std::string &listenerKey) const {
    std::shared_lock lk(_mutex);
    auto it = _listenerTlsAssignments.find(listenerKey);
    if (it == _listenerTlsAssignments.end()) return std::nullopt;
    return it->second;
}

std::optional<std::string> ConfigStore::gpoIdForPermission(
    const std::string &permission, const auth::ResourceContext &resource) const {
    std::shared_lock lk(_mutex);
@@ -604,6 +656,9 @@ std::vector<uint8_t> ConfigStore::snapshot() const {
    w.u32(static_cast<uint32_t>(_dkimKeys.size()));
    for (const auto &[domain, rec] : _dkimKeys) encodeDkimKey(w, rec);

    w.u32(static_cast<uint32_t>(_listenerTlsAssignments.size()));
    for (const auto &[key, rec] : _listenerTlsAssignments) encodeListenerTlsAssignment(w, rec);

    return w.buffer();
}

@@ -624,6 +679,7 @@ bool ConfigStore::applySnapshotIfNewer(const std::vector<uint8_t> &blob) {
    std::unordered_map<std::string, TlsProfileRecord> tlsProfiles;
    std::unordered_map<std::string, UpstreamPoolRecord> upstreamPools;
    std::unordered_map<std::string, DkimKeyRecord> dkimKeys;
    std::unordered_map<std::string, ListenerTlsAssignmentRecord> listenerTlsAssignments;

    uint32_t n = r.u32();
    for (uint32_t i = 0; i < n; ++i) {
@@ -670,6 +726,14 @@ bool ConfigStore::applySnapshotIfNewer(const std::vector<uint8_t> &blob) {
            dkimKeys[rec.domain] = rec;
        }
    }
    // Listener TLS assignments were added after DKIM keys; same guard.
    if (!r.atEnd()) {
        n = r.u32();
        for (uint32_t i = 0; i < n; ++i) {
            auto rec = decodeListenerTlsAssignment(r);
            listenerTlsAssignments[rec.listenerKey] = rec;
        }
    }

    _httpRoutes = std::move(httpRoutes);
    _smtpRelayRules = std::move(smtpRules);
@@ -678,6 +742,7 @@ bool ConfigStore::applySnapshotIfNewer(const std::vector<uint8_t> &blob) {
    _tlsProfiles = std::move(tlsProfiles);
    _upstreamPools = std::move(upstreamPools);
    _dkimKeys = std::move(dkimKeys);
    _listenerTlsAssignments = std::move(listenerTlsAssignments);
    _version = incomingVersion;
    return true;
}
Loading