Loading admin/static/app.js +2 −1 Changes for admin/static/app.js: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -152,7 +152,7 @@ const ProxyPlusAdmin = (() => { body.innerHTML = ""; for (const p of profiles) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${p.profile}</td><td>${(p.domains || []).join(", ")}</td><td>${p.acme_enabled ? "ja" : "nein"}</td><td>${p.acme_email}</td><td>${p.ca_plugin}</td><td></td>`; tr.innerHTML = `<td>${p.profile}</td><td>${(p.domains || []).join(", ")}</td><td>${p.acme_enabled ? "ja" : "nein"}</td><td>${p.self_signed ? "ja" : "nein"}</td><td>${p.acme_email}</td><td>${p.ca_plugin}</td><td></td>`; addDeleteButton(tr, () => deleteTlsProfile(p.profile)); body.appendChild(tr); } Loading Loading @@ -265,6 +265,7 @@ const ProxyPlusAdmin = (() => { acme_email: document.getElementById("tls-profile-email").value, acme_directory_url: document.getElementById("tls-profile-directory").value, ca_plugin: document.getElementById("tls-profile-ca-plugin").value, self_signed: document.getElementById("tls-profile-selfsigned").checked, }); await refreshTlsProfiles(); } catch (e) { Loading admin/static/index.html +2 −1 Changes for admin/static/index.html: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -52,7 +52,7 @@ </div> <h2>TLS-Profile / Zertifikate</h2> <table id="tls-profiles"><thead><tr><th>Profil</th><th>Domains</th><th>ACME</th><th>E-Mail</th><th>CA-Plugin</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <table id="tls-profiles"><thead><tr><th>Profil</th><th>Domains</th><th>ACME</th><th>Self-Signed</th><th>E-Mail</th><th>CA-Plugin</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewTlsProfileForm()">Neues Profil</button> <div id="new-tls-profile-form" hidden> <input id="tls-profile-id" placeholder="Profil-ID, z.B. www"> Loading @@ -65,6 +65,7 @@ <option value="letsencrypt">letsencrypt</option> <option value="zerossl">zerossl</option> </select> <label><input type="checkbox" id="tls-profile-selfsigned"> Self-Signed (lokal generiert, kein ACME -- fuer interne Frontends ohne oeffentlichen Hostnamen)</label> <button onclick="ProxyPlusAdmin.createTlsProfile()">Anlegen</button> <div id="tls-profile-error" class="error"></div> </div> Loading docs/ARCHITECTURE.md +14 −0 Changes for docs/ARCHITECTURE.md: 14 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -214,6 +214,20 @@ auf diesem System nicht installiert. Die Krypto-Grundlage ist ausschließlich wird **nie** geraten —, ruft es auf, schreibt `<dataPath>/tls/<profil>.{crt,key}` atomar und ruft registrierte Reload-Callbacks auf. **Self-Signed als ACME-Alternative.** Ein `TlsProfileRecord` kann statt `acmeEnabled` das Feld `selfSigned` setzen (mutually exclusive, von `JsonApiServer` erzwungen) — für Frontends ohne öffentlichen, per HTTP-01 beweisbaren Hostnamen (internes/LAN-Listener, Erst-Boot der Admin-UI vor jeder DNS-Konfiguration). `CertificateManager::provisionSelfSigned()` generiert dabei lokal (kein Netzwerk, keine ACME-Rate-Limits) über `netplus::generateSelfSignedCert()` — dieselbe selbstsignierte-Zertifikat-Funktion aus `libnetplus/src/utils/certgen.cpp`, jetzt mit `subjectAltName` aus `TlsProfileRecord::domains` (moderne TLS-Clients ignorieren die Subject-CN vollständig ohne SAN) — und schreibt nach demselben `<dataPath>/tls/<profil>.{crt,key}`-Pfad wie ACME. Anders als ACME läuft das nicht nur im Hintergrund-Thread: `main.cpp` ruft `provisionSelfSigned()` synchron auf, **bevor** die HTTP/SMTP/Admin-Listener konstruiert werden, da `HttpD`/`SmtpD`s Konstruktor bei fehlender Zertifikatsdatei sofort wirft (kein Vorlauf wie bei ACME's Hintergrund-Renewal möglich). Erneuerung nahe Ablauf läuft weiterhin über denselben `checkLoop()` wie ACME. **Bekannte Lücken dieser Implementierung:** - Nur **HTTP-01**, kein DNS-01/TLS-ALPN-01 — keine Wildcard-Zertifikate möglich. - Nur **RSA-2048** (Account- und Zertifikatsschlüssel), kein ECDSA. Loading test/test_configstore.cpp +19 −0 Changes for test/test_configstore.cpp: 19 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -34,11 +34,23 @@ int main() { perm.gpoId = "11111111-1111-1111-1111-111111111111"; store.putPermission(perm); TlsProfileRecord tlsProfile; tlsProfile.profile = "internal"; tlsProfile.domains = {"proxy.internal.lan"}; tlsProfile.selfSigned = true; store.putTlsProfile(tlsProfile); assert(store.listHttpRoutes().size() == 1); assert(store.listSmtpRelayRules().size() == 1); auto gpoId = store.gpoIdForPermission("smtp.domains.write", ResourceContext{"", ""}); assert(gpoId && *gpoId == perm.gpoId); assert(store.version() > 0); auto profiles = store.listTlsProfiles(); assert(profiles.size() == 1); assert(profiles[0].selfSigned); assert(!profiles[0].acmeEnabled); assert(profiles[0].domains == tlsProfile.domains); } // Reopen: WAL replay must reconstruct identical state. Loading @@ -53,6 +65,11 @@ int main() { assert(relays.size() == 1); assert(relays[0].smarthost == "smtp.example.com:587"); auto profiles = store.listTlsProfiles(); assert(profiles.size() == 1); assert(profiles[0].profile == "internal"); assert(profiles[0].selfSigned); // compact() must fold the WAL into a snapshot without losing data. store.compact(); assert(store.listHttpRoutes().size() == 1); Loading @@ -63,6 +80,8 @@ int main() { ConfigStore store(dir); assert(store.listHttpRoutes().size() == 1); assert(store.listSmtpRelayRules().size() == 1); assert(store.listTlsProfiles().size() == 1); assert(store.listTlsProfiles()[0].selfSigned); } std::system(("rm -rf " + dir).c_str()); Loading Loading
admin/static/app.js +2 −1 Changes for admin/static/app.js: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -152,7 +152,7 @@ const ProxyPlusAdmin = (() => { body.innerHTML = ""; for (const p of profiles) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${p.profile}</td><td>${(p.domains || []).join(", ")}</td><td>${p.acme_enabled ? "ja" : "nein"}</td><td>${p.acme_email}</td><td>${p.ca_plugin}</td><td></td>`; tr.innerHTML = `<td>${p.profile}</td><td>${(p.domains || []).join(", ")}</td><td>${p.acme_enabled ? "ja" : "nein"}</td><td>${p.self_signed ? "ja" : "nein"}</td><td>${p.acme_email}</td><td>${p.ca_plugin}</td><td></td>`; addDeleteButton(tr, () => deleteTlsProfile(p.profile)); body.appendChild(tr); } Loading Loading @@ -265,6 +265,7 @@ const ProxyPlusAdmin = (() => { acme_email: document.getElementById("tls-profile-email").value, acme_directory_url: document.getElementById("tls-profile-directory").value, ca_plugin: document.getElementById("tls-profile-ca-plugin").value, self_signed: document.getElementById("tls-profile-selfsigned").checked, }); await refreshTlsProfiles(); } catch (e) { Loading
admin/static/index.html +2 −1 Changes for admin/static/index.html: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -52,7 +52,7 @@ </div> <h2>TLS-Profile / Zertifikate</h2> <table id="tls-profiles"><thead><tr><th>Profil</th><th>Domains</th><th>ACME</th><th>E-Mail</th><th>CA-Plugin</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <table id="tls-profiles"><thead><tr><th>Profil</th><th>Domains</th><th>ACME</th><th>Self-Signed</th><th>E-Mail</th><th>CA-Plugin</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewTlsProfileForm()">Neues Profil</button> <div id="new-tls-profile-form" hidden> <input id="tls-profile-id" placeholder="Profil-ID, z.B. www"> Loading @@ -65,6 +65,7 @@ <option value="letsencrypt">letsencrypt</option> <option value="zerossl">zerossl</option> </select> <label><input type="checkbox" id="tls-profile-selfsigned"> Self-Signed (lokal generiert, kein ACME -- fuer interne Frontends ohne oeffentlichen Hostnamen)</label> <button onclick="ProxyPlusAdmin.createTlsProfile()">Anlegen</button> <div id="tls-profile-error" class="error"></div> </div> Loading
docs/ARCHITECTURE.md +14 −0 Changes for docs/ARCHITECTURE.md: 14 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -214,6 +214,20 @@ auf diesem System nicht installiert. Die Krypto-Grundlage ist ausschließlich wird **nie** geraten —, ruft es auf, schreibt `<dataPath>/tls/<profil>.{crt,key}` atomar und ruft registrierte Reload-Callbacks auf. **Self-Signed als ACME-Alternative.** Ein `TlsProfileRecord` kann statt `acmeEnabled` das Feld `selfSigned` setzen (mutually exclusive, von `JsonApiServer` erzwungen) — für Frontends ohne öffentlichen, per HTTP-01 beweisbaren Hostnamen (internes/LAN-Listener, Erst-Boot der Admin-UI vor jeder DNS-Konfiguration). `CertificateManager::provisionSelfSigned()` generiert dabei lokal (kein Netzwerk, keine ACME-Rate-Limits) über `netplus::generateSelfSignedCert()` — dieselbe selbstsignierte-Zertifikat-Funktion aus `libnetplus/src/utils/certgen.cpp`, jetzt mit `subjectAltName` aus `TlsProfileRecord::domains` (moderne TLS-Clients ignorieren die Subject-CN vollständig ohne SAN) — und schreibt nach demselben `<dataPath>/tls/<profil>.{crt,key}`-Pfad wie ACME. Anders als ACME läuft das nicht nur im Hintergrund-Thread: `main.cpp` ruft `provisionSelfSigned()` synchron auf, **bevor** die HTTP/SMTP/Admin-Listener konstruiert werden, da `HttpD`/`SmtpD`s Konstruktor bei fehlender Zertifikatsdatei sofort wirft (kein Vorlauf wie bei ACME's Hintergrund-Renewal möglich). Erneuerung nahe Ablauf läuft weiterhin über denselben `checkLoop()` wie ACME. **Bekannte Lücken dieser Implementierung:** - Nur **HTTP-01**, kein DNS-01/TLS-ALPN-01 — keine Wildcard-Zertifikate möglich. - Nur **RSA-2048** (Account- und Zertifikatsschlüssel), kein ECDSA. Loading
test/test_configstore.cpp +19 −0 Changes for test/test_configstore.cpp: 19 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -34,11 +34,23 @@ int main() { perm.gpoId = "11111111-1111-1111-1111-111111111111"; store.putPermission(perm); TlsProfileRecord tlsProfile; tlsProfile.profile = "internal"; tlsProfile.domains = {"proxy.internal.lan"}; tlsProfile.selfSigned = true; store.putTlsProfile(tlsProfile); assert(store.listHttpRoutes().size() == 1); assert(store.listSmtpRelayRules().size() == 1); auto gpoId = store.gpoIdForPermission("smtp.domains.write", ResourceContext{"", ""}); assert(gpoId && *gpoId == perm.gpoId); assert(store.version() > 0); auto profiles = store.listTlsProfiles(); assert(profiles.size() == 1); assert(profiles[0].selfSigned); assert(!profiles[0].acmeEnabled); assert(profiles[0].domains == tlsProfile.domains); } // Reopen: WAL replay must reconstruct identical state. Loading @@ -53,6 +65,11 @@ int main() { assert(relays.size() == 1); assert(relays[0].smarthost == "smtp.example.com:587"); auto profiles = store.listTlsProfiles(); assert(profiles.size() == 1); assert(profiles[0].profile == "internal"); assert(profiles[0].selfSigned); // compact() must fold the WAL into a snapshot without losing data. store.compact(); assert(store.listHttpRoutes().size() == 1); Loading @@ -63,6 +80,8 @@ int main() { ConfigStore store(dir); assert(store.listHttpRoutes().size() == 1); assert(store.listSmtpRelayRules().size() == 1); assert(store.listTlsProfiles().size() == 1); assert(store.listTlsProfiles()[0].selfSigned); } std::system(("rm -rf " + dir).c_str()); Loading