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 @@ -83,7 +83,7 @@ const ProxyPlusAdmin = (() => { ? " (" + 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>`; tr.innerHTML = `<td>${r.id}</td><td>${r.domain}</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 @@ -214,6 +214,7 @@ const ProxyPlusAdmin = (() => { .filter(Boolean); await api("POST", "/api/v1/http/routes", { id: document.getElementById("route-id").value, domain: document.getElementById("route-domain").value || "*", path_prefix: document.getElementById("route-path").value, upstream: document.getElementById("route-upstream").value, upstream_pool: document.getElementById("route-upstream-pool").value, 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 @@ -24,10 +24,11 @@ <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>TLS-Pruefung</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <table id="http-routes"><thead><tr><th>ID</th><th>Domain</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"> <input id="route-domain" placeholder="Domain, * fuer alle"> <input id="route-path" placeholder="Pfad-Prefix, z.B. /app"> <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)"> Loading src/api/JsonApiServer.cpp +2 −0 Changes for src/api/JsonApiServer.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -198,6 +198,7 @@ void JsonApiController::registerRoutes() { for (const auto &r : _configStore.listHttpRoutes()) { JsonGuard item(json_object_new_object()); json_object_object_add(item, "id", json_object_new_string(r.id.c_str())); json_object_object_add(item, "domain", json_object_new_string(r.domain.c_str())); json_object_object_add(item, "path_prefix", json_object_new_string(r.pathPrefix.c_str())); json_object_object_add(item, "upstream", json_object_new_string(r.upstream.c_str())); json_object_object_add(item, "upstream_pool", json_object_new_string(r.upstreamPool.c_str())); Loading @@ -223,6 +224,7 @@ void JsonApiController::registerRoutes() { if (!body.obj) return jsonError("invalid JSON body"); store::HttpRouteRecord rec; rec.id = getStringField(body, "id"); rec.domain = getStringField(body, "domain", "*"); rec.pathPrefix = getStringField(body, "path_prefix"); rec.upstream = getStringField(body, "upstream"); rec.upstreamPool = getStringField(body, "upstream_pool"); Loading src/http/HttpProxyServer.cpp +31 −3 Changes for src/http/HttpProxyServer.cpp: 31 added lines, 3 removed lines. Original line number Diff line number Diff line #include "HttpProxyServer.h" #include <algorithm> #include <cctype> #include <iostream> #include <set> Loading Loading @@ -47,6 +49,16 @@ std::vector<uint8_t> parseHexFingerprint(const std::string &hex) { return out; } // Normalizes a Host header for route matching: strips the ":port" suffix (routes match on // hostname only) and lowercases (DNS names are case-insensitive, but stored/typed route // domains and incoming headers won't reliably agree on case otherwise). std::string normalizeHost(const std::string &hostHeader) { std::string host = hostHeader.substr(0, hostHeader.find(':')); std::transform(host.begin(), host.end(), host.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); return host; } netplus::TlsTrustPolicy trustPolicyFor(const store::HttpRouteRecord &route) { netplus::TlsTrustPolicy policy; policy.verifyPeer = route.tlsVerifyUpstream; Loading Loading @@ -231,13 +243,29 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int if (method == "GET" && tryServeAcmeChallenge(curreq, url)) return; std::string requestHost; if (auto *hostHdr = curreq.getHeaderData("host")) { if (auto *v = hostHdr->getfirstValue()) requestHost = normalizeHost(v->getvalue()); } // Prefer an exact domain match over a wildcard ("*"/empty = any Host) one regardless // of path length, so e.g. "example.com" + "/" outranks "*" + "/app" for a request to // example.com/app -- only among routes of the same domain-specificity does the longer // pathPrefix win. const store::HttpRouteRecord *best = nullptr; bool bestIsDomainSpecific = false; auto routes = _configStore.listHttpRoutes(); for (const auto &route : routes) { if (url.rfind(route.pathPrefix, 0) == 0) { if (!best || route.pathPrefix.size() > best->pathPrefix.size()) { if (url.rfind(route.pathPrefix, 0) != 0) continue; bool wildcard = route.domain.empty() || route.domain == "*"; if (!wildcard && normalizeHost(route.domain) != requestHost) continue; bool domainSpecific = !wildcard; bool better = !best || (domainSpecific && !bestIsDomainSpecific) || (domainSpecific == bestIsDomainSpecific && route.pathPrefix.size() > best->pathPrefix.size()); if (better) { best = &route; } bestIsDomainSpecific = domainSpecific; } } // routes is a local vector of copies; keep 'best' valid by re-finding after the loop Loading src/store/ConfigStore.cpp +2 −0 Changes for src/store/ConfigStore.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -92,6 +92,7 @@ private: void encodeHttpRoute(ByteWriter &w, const HttpRouteRecord &r) { w.str(r.id); w.str(r.domain); w.str(r.pathPrefix); w.str(r.upstream); w.str(r.requiredPermission); Loading @@ -103,6 +104,7 @@ void encodeHttpRoute(ByteWriter &w, const HttpRouteRecord &r) { HttpRouteRecord decodeHttpRoute(ByteReader &r) { HttpRouteRecord rec; rec.id = r.str(); rec.domain = r.str(); rec.pathPrefix = r.str(); rec.upstream = r.str(); rec.requiredPermission = r.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 @@ -83,7 +83,7 @@ const ProxyPlusAdmin = (() => { ? " (" + 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>`; tr.innerHTML = `<td>${r.id}</td><td>${r.domain}</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 @@ -214,6 +214,7 @@ const ProxyPlusAdmin = (() => { .filter(Boolean); await api("POST", "/api/v1/http/routes", { id: document.getElementById("route-id").value, domain: document.getElementById("route-domain").value || "*", path_prefix: document.getElementById("route-path").value, upstream: document.getElementById("route-upstream").value, upstream_pool: document.getElementById("route-upstream-pool").value, 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 @@ -24,10 +24,11 @@ <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>TLS-Pruefung</th><th>Aktionen</th></tr></thead><tbody></tbody></table> <table id="http-routes"><thead><tr><th>ID</th><th>Domain</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"> <input id="route-domain" placeholder="Domain, * fuer alle"> <input id="route-path" placeholder="Pfad-Prefix, z.B. /app"> <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)"> Loading
src/api/JsonApiServer.cpp +2 −0 Changes for src/api/JsonApiServer.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -198,6 +198,7 @@ void JsonApiController::registerRoutes() { for (const auto &r : _configStore.listHttpRoutes()) { JsonGuard item(json_object_new_object()); json_object_object_add(item, "id", json_object_new_string(r.id.c_str())); json_object_object_add(item, "domain", json_object_new_string(r.domain.c_str())); json_object_object_add(item, "path_prefix", json_object_new_string(r.pathPrefix.c_str())); json_object_object_add(item, "upstream", json_object_new_string(r.upstream.c_str())); json_object_object_add(item, "upstream_pool", json_object_new_string(r.upstreamPool.c_str())); Loading @@ -223,6 +224,7 @@ void JsonApiController::registerRoutes() { if (!body.obj) return jsonError("invalid JSON body"); store::HttpRouteRecord rec; rec.id = getStringField(body, "id"); rec.domain = getStringField(body, "domain", "*"); rec.pathPrefix = getStringField(body, "path_prefix"); rec.upstream = getStringField(body, "upstream"); rec.upstreamPool = getStringField(body, "upstream_pool"); Loading
src/http/HttpProxyServer.cpp +31 −3 Changes for src/http/HttpProxyServer.cpp: 31 added lines, 3 removed lines. Original line number Diff line number Diff line #include "HttpProxyServer.h" #include <algorithm> #include <cctype> #include <iostream> #include <set> Loading Loading @@ -47,6 +49,16 @@ std::vector<uint8_t> parseHexFingerprint(const std::string &hex) { return out; } // Normalizes a Host header for route matching: strips the ":port" suffix (routes match on // hostname only) and lowercases (DNS names are case-insensitive, but stored/typed route // domains and incoming headers won't reliably agree on case otherwise). std::string normalizeHost(const std::string &hostHeader) { std::string host = hostHeader.substr(0, hostHeader.find(':')); std::transform(host.begin(), host.end(), host.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); return host; } netplus::TlsTrustPolicy trustPolicyFor(const store::HttpRouteRecord &route) { netplus::TlsTrustPolicy policy; policy.verifyPeer = route.tlsVerifyUpstream; Loading Loading @@ -231,13 +243,29 @@ void HttpProxyController::RequestEvent(libhttppp::HttpRequest &curreq, const int if (method == "GET" && tryServeAcmeChallenge(curreq, url)) return; std::string requestHost; if (auto *hostHdr = curreq.getHeaderData("host")) { if (auto *v = hostHdr->getfirstValue()) requestHost = normalizeHost(v->getvalue()); } // Prefer an exact domain match over a wildcard ("*"/empty = any Host) one regardless // of path length, so e.g. "example.com" + "/" outranks "*" + "/app" for a request to // example.com/app -- only among routes of the same domain-specificity does the longer // pathPrefix win. const store::HttpRouteRecord *best = nullptr; bool bestIsDomainSpecific = false; auto routes = _configStore.listHttpRoutes(); for (const auto &route : routes) { if (url.rfind(route.pathPrefix, 0) == 0) { if (!best || route.pathPrefix.size() > best->pathPrefix.size()) { if (url.rfind(route.pathPrefix, 0) != 0) continue; bool wildcard = route.domain.empty() || route.domain == "*"; if (!wildcard && normalizeHost(route.domain) != requestHost) continue; bool domainSpecific = !wildcard; bool better = !best || (domainSpecific && !bestIsDomainSpecific) || (domainSpecific == bestIsDomainSpecific && route.pathPrefix.size() > best->pathPrefix.size()); if (better) { best = &route; } bestIsDomainSpecific = domainSpecific; } } // routes is a local vector of copies; keep 'best' valid by re-finding after the loop Loading
src/store/ConfigStore.cpp +2 −0 Changes for src/store/ConfigStore.cpp: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -92,6 +92,7 @@ private: void encodeHttpRoute(ByteWriter &w, const HttpRouteRecord &r) { w.str(r.id); w.str(r.domain); w.str(r.pathPrefix); w.str(r.upstream); w.str(r.requiredPermission); Loading @@ -103,6 +104,7 @@ void encodeHttpRoute(ByteWriter &w, const HttpRouteRecord &r) { HttpRouteRecord decodeHttpRoute(ByteReader &r) { HttpRouteRecord rec; rec.id = r.str(); rec.domain = r.str(); rec.pathPrefix = r.str(); rec.upstream = r.str(); rec.requiredPermission = r.str(); Loading