Loading admin/static/app.js +53 −0 Changes for admin/static/app.js: 53 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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"); Loading Loading @@ -333,6 +384,7 @@ const ProxyPlusAdmin = (() => { refreshTlsProfiles(), refreshDkimKeys(), refreshPluginConfigs(), refreshListenerTlsAssignments(), ]); } Loading Loading @@ -528,6 +580,7 @@ const ProxyPlusAdmin = (() => { createPool, showNewPluginConfigForm, createPluginConfig, saveListenerTlsAssignment, exportConfig, importConfig, }; Loading admin/static/index.html +13 −0 Changes for admin/static/index.html: 13 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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> Loading src/api/JsonApiServer.cpp +71 −0 Changes for src/api/JsonApiServer.cpp: 71 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 Loading src/main.cpp +26 −8 Changes for src/main.cpp: 26 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -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"] Loading Loading @@ -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 Loading @@ -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)); }); Loading src/store/ConfigStore.cpp +65 −0 Changes for src/store/ConfigStore.cpp: 65 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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; } Loading Loading @@ -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; } Loading Loading @@ -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); Loading Loading @@ -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(); } Loading @@ -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) { Loading Loading @@ -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); Loading @@ -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 Loading
admin/static/app.js +53 −0 Changes for admin/static/app.js: 53 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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"); Loading Loading @@ -333,6 +384,7 @@ const ProxyPlusAdmin = (() => { refreshTlsProfiles(), refreshDkimKeys(), refreshPluginConfigs(), refreshListenerTlsAssignments(), ]); } Loading Loading @@ -528,6 +580,7 @@ const ProxyPlusAdmin = (() => { createPool, showNewPluginConfigForm, createPluginConfig, saveListenerTlsAssignment, exportConfig, importConfig, }; Loading
admin/static/index.html +13 −0 Changes for admin/static/index.html: 13 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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> Loading
src/api/JsonApiServer.cpp +71 −0 Changes for src/api/JsonApiServer.cpp: 71 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 Loading
src/main.cpp +26 −8 Changes for src/main.cpp: 26 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -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"] Loading Loading @@ -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 Loading @@ -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)); }); Loading
src/store/ConfigStore.cpp +65 −0 Changes for src/store/ConfigStore.cpp: 65 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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; } Loading Loading @@ -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; } Loading Loading @@ -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); Loading Loading @@ -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(); } Loading @@ -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) { Loading Loading @@ -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); Loading @@ -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