Loading admin/static/app.js +12 −1 Changes for admin/static/app.js: 12 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -78,7 +78,12 @@ const ProxyPlusAdmin = (() => { body.innerHTML = ""; for (const r of routes) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${r.id}</td><td>${r.path_prefix}</td><td>${r.upstream}</td><td>${r.upstream_pool}</td><td>${r.required_permission}</td><td></td>`; const tlsInfo = r.tls_verify_upstream ? "geprüft" + (r.tls_pinned_fingerprints_sha256 && r.tls_pinned_fingerprints_sha256.length ? " (" + r.tls_pinned_fingerprints_sha256.length + " gepinnt)" : "") : "keine"; tr.innerHTML = `<td>${r.id}</td><td>${r.path_prefix}</td><td>${r.upstream}</td><td>${r.upstream_pool}</td><td>${r.required_permission}</td><td>${tlsInfo}</td><td></td>`; addDeleteButton(tr, () => deleteRoute(r.id)); body.appendChild(tr); } Loading Loading @@ -203,12 +208,18 @@ const ProxyPlusAdmin = (() => { async function createRoute() { document.getElementById("route-error").textContent = ""; try { const pins = document.getElementById("route-tls-pins").value .split(",") .map((s) => s.trim()) .filter(Boolean); await api("POST", "/api/v1/http/routes", { id: document.getElementById("route-id").value, path_prefix: document.getElementById("route-path").value, upstream: document.getElementById("route-upstream").value, upstream_pool: document.getElementById("route-upstream-pool").value, required_permission: document.getElementById("route-permission").value, tls_verify_upstream: document.getElementById("route-tls-verify").checked, tls_pinned_fingerprints_sha256: pins, }); await refreshRoutes(); } catch (e) { Loading admin/static/index.html +3 −1 Changes for admin/static/index.html: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -24,7 +24,7 @@ <table id="status-table"></table> <h2>HTTP-Routen</h2> <table id="http-routes"><thead><tr><th>ID</th><th>Pfad</th><th>Upstream</th><th>Pool</th><th>Berechtigung</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <table id="http-routes"><thead><tr><th>ID</th><th>Pfad</th><th>Upstream</th><th>Pool</th><th>Berechtigung</th><th>TLS-Pruefung</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewRouteForm()">Neue Route</button> <div id="new-route-form" hidden> <input id="route-id" placeholder="ID"> Loading @@ -32,6 +32,8 @@ <input id="route-upstream" placeholder="Upstream (direkt), genau eine URL, z.B. http://10.0.0.5:8080"> <input id="route-upstream-pool" placeholder="oder Pool-ID fuer mehrere Ziele (statt Upstream)"> <input id="route-permission" placeholder="Berechtigung, z.B. http.proxy.use"> <label><input type="checkbox" id="route-tls-verify"> Upstream-TLS-Zertifikat pruefen</label> <input id="route-tls-pins" placeholder="Gepinnte SHA-256-Fingerprints, kommagetrennt (fuer selbstsignierte Upstreams)"> <button onclick="ProxyPlusAdmin.createRoute()">Anlegen</button> <div id="route-error" class="error"></div> </div> Loading src/api/JsonApiServer.cpp +17 −0 Changes for src/api/JsonApiServer.cpp: 17 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -203,6 +203,14 @@ void JsonApiController::registerRoutes() { json_object_object_add(item, "upstream_pool", json_object_new_string(r.upstreamPool.c_str())); json_object_object_add(item, "required_permission", json_object_new_string(r.requiredPermission.c_str())); json_object_object_add(item, "tls_verify_upstream", json_object_new_boolean(r.tlsVerifyUpstream)); JsonGuard fpArr(json_object_new_array()); for (const auto &fp : r.tlsPinnedFingerprintsSha256) { json_object_array_add(fpArr.obj, json_object_new_string(fp.c_str())); } json_object_get(fpArr.obj); json_object_object_add(item, "tls_pinned_fingerprints_sha256", fpArr.obj); json_object_get(item.obj); json_object_array_add(arr.obj, item.obj); } Loading @@ -219,6 +227,15 @@ void JsonApiController::registerRoutes() { rec.upstream = getStringField(body, "upstream"); rec.upstreamPool = getStringField(body, "upstream_pool"); rec.requiredPermission = getStringField(body, "required_permission"); rec.tlsVerifyUpstream = getBoolField(body, "tls_verify_upstream"); json_object *fpArr = nullptr; if (json_object_object_get_ex(body, "tls_pinned_fingerprints_sha256", &fpArr) && fpArr) { int n = json_object_array_length(fpArr); for (int i = 0; i < n; ++i) { const char *fp = json_object_get_string(json_object_array_get_idx(fpArr, i)); if (fp) rec.tlsPinnedFingerprintsSha256.emplace_back(fp); } } if (rec.id.empty() || rec.pathPrefix.empty()) return jsonError("id and path_prefix are required"); if (rec.upstream.empty() && rec.upstreamPool.empty()) { return jsonError("either upstream or upstream_pool is required"); Loading src/http/HttpProxyServer.cpp +39 −1 Changes for src/http/HttpProxyServer.cpp: 39 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ #include <set> #include <netplus/socket.h> #include <netplus/crypto/cert_verify.h> namespace proxyplus::httpproxy { Loading @@ -19,6 +20,43 @@ std::string methodName(int requestType) { } } // Parses a hex-encoded SHA-256 fingerprint (e.g. "AB:CD:..." or "abcd...", as produced by // `openssl x509 -noout -fingerprint -sha256`) into its raw 32 bytes. Returns empty on any // malformed input -- callers skip those rather than fail the whole route. std::vector<uint8_t> parseHexFingerprint(const std::string &hex) { std::string clean; clean.reserve(hex.size()); for (char c : hex) { if (c != ':' && c != ' ') clean.push_back(c); } if (clean.size() != 64) return {}; std::vector<uint8_t> out; out.reserve(32); for (size_t i = 0; i < clean.size(); i += 2) { auto hexVal = [](char c) -> int { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; }; int hi = hexVal(clean[i]); int lo = hexVal(clean[i + 1]); if (hi < 0 || lo < 0) return {}; out.push_back(static_cast<uint8_t>((hi << 4) | lo)); } return out; } netplus::TlsTrustPolicy trustPolicyFor(const store::HttpRouteRecord &route) { netplus::TlsTrustPolicy policy; policy.verifyPeer = route.tlsVerifyUpstream; for (const auto &hex : route.tlsPinnedFingerprintsSha256) { auto raw = parseHexFingerprint(hex); if (!raw.empty()) policy.pinnedSha256Fingerprints.push_back(std::move(raw)); } return policy; } } // namespace HttpProxyController::HttpProxyController(std::vector<netplus::socket *> serverSockets, Loading Loading @@ -278,7 +316,7 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int // vers=3: try HTTP/3 first, falling back to HTTP/2 then HTTP/1.1 over // TLS (or straight to H1 for a cleartext upstream) -- see HttpClient // ctor in libhttppp/src/http.cpp for the fallback ladder. libhttppp::HttpClient client(upstream, /*vers=*/3); libhttppp::HttpClient client(upstream, /*vers=*/3, /*timeoutSec=*/60, trustPolicyFor(matched)); libhttppp::HttpRequest outreq; outreq.setRequestURL(upstream.getPath()); Loading src/store/ConfigStore.cpp +7 −0 Changes for src/store/ConfigStore.cpp: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -96,6 +96,9 @@ void encodeHttpRoute(ByteWriter &w, const HttpRouteRecord &r) { w.str(r.upstream); w.str(r.requiredPermission); w.str(r.upstreamPool); w.u8(r.tlsVerifyUpstream ? 1 : 0); w.u32(static_cast<uint32_t>(r.tlsPinnedFingerprintsSha256.size())); for (const auto &fp : r.tlsPinnedFingerprintsSha256) w.str(fp); } HttpRouteRecord decodeHttpRoute(ByteReader &r) { HttpRouteRecord rec; Loading @@ -104,6 +107,10 @@ HttpRouteRecord decodeHttpRoute(ByteReader &r) { rec.upstream = r.str(); rec.requiredPermission = r.str(); rec.upstreamPool = r.str(); rec.tlsVerifyUpstream = r.u8() != 0; uint32_t fpCount = r.u32(); rec.tlsPinnedFingerprintsSha256.reserve(fpCount); for (uint32_t i = 0; i < fpCount; ++i) rec.tlsPinnedFingerprintsSha256.push_back(r.str()); return rec; } Loading Loading
admin/static/app.js +12 −1 Changes for admin/static/app.js: 12 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -78,7 +78,12 @@ const ProxyPlusAdmin = (() => { body.innerHTML = ""; for (const r of routes) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${r.id}</td><td>${r.path_prefix}</td><td>${r.upstream}</td><td>${r.upstream_pool}</td><td>${r.required_permission}</td><td></td>`; const tlsInfo = r.tls_verify_upstream ? "geprüft" + (r.tls_pinned_fingerprints_sha256 && r.tls_pinned_fingerprints_sha256.length ? " (" + r.tls_pinned_fingerprints_sha256.length + " gepinnt)" : "") : "keine"; tr.innerHTML = `<td>${r.id}</td><td>${r.path_prefix}</td><td>${r.upstream}</td><td>${r.upstream_pool}</td><td>${r.required_permission}</td><td>${tlsInfo}</td><td></td>`; addDeleteButton(tr, () => deleteRoute(r.id)); body.appendChild(tr); } Loading Loading @@ -203,12 +208,18 @@ const ProxyPlusAdmin = (() => { async function createRoute() { document.getElementById("route-error").textContent = ""; try { const pins = document.getElementById("route-tls-pins").value .split(",") .map((s) => s.trim()) .filter(Boolean); await api("POST", "/api/v1/http/routes", { id: document.getElementById("route-id").value, path_prefix: document.getElementById("route-path").value, upstream: document.getElementById("route-upstream").value, upstream_pool: document.getElementById("route-upstream-pool").value, required_permission: document.getElementById("route-permission").value, tls_verify_upstream: document.getElementById("route-tls-verify").checked, tls_pinned_fingerprints_sha256: pins, }); await refreshRoutes(); } catch (e) { Loading
admin/static/index.html +3 −1 Changes for admin/static/index.html: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -24,7 +24,7 @@ <table id="status-table"></table> <h2>HTTP-Routen</h2> <table id="http-routes"><thead><tr><th>ID</th><th>Pfad</th><th>Upstream</th><th>Pool</th><th>Berechtigung</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <table id="http-routes"><thead><tr><th>ID</th><th>Pfad</th><th>Upstream</th><th>Pool</th><th>Berechtigung</th><th>TLS-Pruefung</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <button onclick="ProxyPlusAdmin.showNewRouteForm()">Neue Route</button> <div id="new-route-form" hidden> <input id="route-id" placeholder="ID"> Loading @@ -32,6 +32,8 @@ <input id="route-upstream" placeholder="Upstream (direkt), genau eine URL, z.B. http://10.0.0.5:8080"> <input id="route-upstream-pool" placeholder="oder Pool-ID fuer mehrere Ziele (statt Upstream)"> <input id="route-permission" placeholder="Berechtigung, z.B. http.proxy.use"> <label><input type="checkbox" id="route-tls-verify"> Upstream-TLS-Zertifikat pruefen</label> <input id="route-tls-pins" placeholder="Gepinnte SHA-256-Fingerprints, kommagetrennt (fuer selbstsignierte Upstreams)"> <button onclick="ProxyPlusAdmin.createRoute()">Anlegen</button> <div id="route-error" class="error"></div> </div> Loading
src/api/JsonApiServer.cpp +17 −0 Changes for src/api/JsonApiServer.cpp: 17 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -203,6 +203,14 @@ void JsonApiController::registerRoutes() { json_object_object_add(item, "upstream_pool", json_object_new_string(r.upstreamPool.c_str())); json_object_object_add(item, "required_permission", json_object_new_string(r.requiredPermission.c_str())); json_object_object_add(item, "tls_verify_upstream", json_object_new_boolean(r.tlsVerifyUpstream)); JsonGuard fpArr(json_object_new_array()); for (const auto &fp : r.tlsPinnedFingerprintsSha256) { json_object_array_add(fpArr.obj, json_object_new_string(fp.c_str())); } json_object_get(fpArr.obj); json_object_object_add(item, "tls_pinned_fingerprints_sha256", fpArr.obj); json_object_get(item.obj); json_object_array_add(arr.obj, item.obj); } Loading @@ -219,6 +227,15 @@ void JsonApiController::registerRoutes() { rec.upstream = getStringField(body, "upstream"); rec.upstreamPool = getStringField(body, "upstream_pool"); rec.requiredPermission = getStringField(body, "required_permission"); rec.tlsVerifyUpstream = getBoolField(body, "tls_verify_upstream"); json_object *fpArr = nullptr; if (json_object_object_get_ex(body, "tls_pinned_fingerprints_sha256", &fpArr) && fpArr) { int n = json_object_array_length(fpArr); for (int i = 0; i < n; ++i) { const char *fp = json_object_get_string(json_object_array_get_idx(fpArr, i)); if (fp) rec.tlsPinnedFingerprintsSha256.emplace_back(fp); } } if (rec.id.empty() || rec.pathPrefix.empty()) return jsonError("id and path_prefix are required"); if (rec.upstream.empty() && rec.upstreamPool.empty()) { return jsonError("either upstream or upstream_pool is required"); Loading
src/http/HttpProxyServer.cpp +39 −1 Changes for src/http/HttpProxyServer.cpp: 39 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ #include <set> #include <netplus/socket.h> #include <netplus/crypto/cert_verify.h> namespace proxyplus::httpproxy { Loading @@ -19,6 +20,43 @@ std::string methodName(int requestType) { } } // Parses a hex-encoded SHA-256 fingerprint (e.g. "AB:CD:..." or "abcd...", as produced by // `openssl x509 -noout -fingerprint -sha256`) into its raw 32 bytes. Returns empty on any // malformed input -- callers skip those rather than fail the whole route. std::vector<uint8_t> parseHexFingerprint(const std::string &hex) { std::string clean; clean.reserve(hex.size()); for (char c : hex) { if (c != ':' && c != ' ') clean.push_back(c); } if (clean.size() != 64) return {}; std::vector<uint8_t> out; out.reserve(32); for (size_t i = 0; i < clean.size(); i += 2) { auto hexVal = [](char c) -> int { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; }; int hi = hexVal(clean[i]); int lo = hexVal(clean[i + 1]); if (hi < 0 || lo < 0) return {}; out.push_back(static_cast<uint8_t>((hi << 4) | lo)); } return out; } netplus::TlsTrustPolicy trustPolicyFor(const store::HttpRouteRecord &route) { netplus::TlsTrustPolicy policy; policy.verifyPeer = route.tlsVerifyUpstream; for (const auto &hex : route.tlsPinnedFingerprintsSha256) { auto raw = parseHexFingerprint(hex); if (!raw.empty()) policy.pinnedSha256Fingerprints.push_back(std::move(raw)); } return policy; } } // namespace HttpProxyController::HttpProxyController(std::vector<netplus::socket *> serverSockets, Loading Loading @@ -278,7 +316,7 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int // vers=3: try HTTP/3 first, falling back to HTTP/2 then HTTP/1.1 over // TLS (or straight to H1 for a cleartext upstream) -- see HttpClient // ctor in libhttppp/src/http.cpp for the fallback ladder. libhttppp::HttpClient client(upstream, /*vers=*/3); libhttppp::HttpClient client(upstream, /*vers=*/3, /*timeoutSec=*/60, trustPolicyFor(matched)); libhttppp::HttpRequest outreq; outreq.setRequestURL(upstream.getPath()); Loading
src/store/ConfigStore.cpp +7 −0 Changes for src/store/ConfigStore.cpp: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -96,6 +96,9 @@ void encodeHttpRoute(ByteWriter &w, const HttpRouteRecord &r) { w.str(r.upstream); w.str(r.requiredPermission); w.str(r.upstreamPool); w.u8(r.tlsVerifyUpstream ? 1 : 0); w.u32(static_cast<uint32_t>(r.tlsPinnedFingerprintsSha256.size())); for (const auto &fp : r.tlsPinnedFingerprintsSha256) w.str(fp); } HttpRouteRecord decodeHttpRoute(ByteReader &r) { HttpRouteRecord rec; Loading @@ -104,6 +107,10 @@ HttpRouteRecord decodeHttpRoute(ByteReader &r) { rec.upstream = r.str(); rec.requiredPermission = r.str(); rec.upstreamPool = r.str(); rec.tlsVerifyUpstream = r.u8() != 0; uint32_t fpCount = r.u32(); rec.tlsPinnedFingerprintsSha256.reserve(fpCount); for (uint32_t i = 0; i < fpCount; ++i) rec.tlsPinnedFingerprintsSha256.push_back(r.str()); return rec; } Loading