Loading src/config/BootstrapConfig.cpp +5 −0 Changes for src/config/BootstrapConfig.cpp: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ const std::set<std::string> &knownLeafPatterns() { "/proxyplus/http/listen/#/address", "/proxyplus/http/listen/#/port", "/proxyplus/http/listen/#/tls_profile", "/proxyplus/http/listen/#/tls_profiles/#", "/proxyplus/smtp/listen/#/address", "/proxyplus/smtp/listen/#/port", "/proxyplus/smtp/listen/#/tls_profile", Loading Loading @@ -133,6 +134,10 @@ std::vector<ListenerConfig> readListeners(confplus::Config &conf, const std::str lc.address = readValue(conf, pfx + "/address", ""); lc.port = readInt(conf, pfx + "/port", 0); lc.tlsProfile = readValue(conf, pfx + "/tls_profile", ""); lc.tlsProfiles = readStringList(conf, pfx + "/tls_profiles"); if (!lc.tlsProfile.empty() && !lc.tlsProfiles.empty()) { throw ConfigValidationError(pfx + ": set either tls_profile or tls_profiles, not both"); } out.push_back(lc); } return out; Loading src/config/BootstrapConfig.h +6 −0 Changes for src/config/BootstrapConfig.h: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,12 @@ struct ListenerConfig { std::string address; int port = 0; std::string tlsProfile; // "" = plaintext; resolves to <dataPath>/tls/<profile>.{crt,key} // SNI virtual hosting: two or more TLS profiles sharing this one listener/port, each // presented only for its own TlsProfileRecord::domains rather than one cert for everyone. // Mutually exclusive with tlsProfile above -- set at most one of the two (see // BootstrapConfig.cpp's readListeners()); empty = single-profile listener as before. std::vector<std::string> tlsProfiles; }; enum class NodeMode { Single, Cluster }; Loading src/http/HttpProxyServer.cpp +20 −0 Changes for src/http/HttpProxyServer.cpp: 20 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -454,6 +454,26 @@ HttpProxyServer::HttpProxyServer(config::ListenerConfig listener, std::string ce , _pluginManager(pluginManager) , _loadBalancer(loadBalancer) , _acmeChallenges(acmeChallenges) { bindAndListen(); } HttpProxyServer::HttpProxyServer(config::ListenerConfig listener, const std::map<std::string, netplus::ssl::CertificateBundle> &certsByHostname, store::ConfigStore &configStore, auth::IAuthService &authService, plugin::PluginManager &pluginManager, lb::LoadBalancer &loadBalancer, tls::AcmeChallengeStore *acmeChallenges) : _listener(listener) , _tlsActive(true) , _httpd(std::make_unique<libhttppp::HttpD>(listener.address, listener.port, 512, certsByHostname)) , _configStore(configStore) , _authService(authService) , _pluginManager(pluginManager) , _loadBalancer(loadBalancer) , _acmeChallenges(acmeChallenges) { bindAndListen(); } void HttpProxyServer::bindAndListen() { // netplus::tcp/ssl/quic only create the socket at construction; bind()/listen() are // separate calls the event loop would otherwise defer until run() -- too late, since // run() executes on a worker thread after main.cpp has already dropped root (see this Loading src/http/HttpProxyServer.h +19 −0 Changes for src/http/HttpProxyServer.h: 19 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -83,6 +83,17 @@ public: lb::LoadBalancer &loadBalancer, tls::AcmeChallengeStore *acmeChallenges = nullptr); // SNI virtual hosting variant: certsByHostname carries one CertificateBundle per domain // sharing this listener (see ListenerConfig::tlsProfiles) instead of the single cert/key // path pair above. HttpProxyServer(config::ListenerConfig listener, const std::map<std::string, netplus::ssl::CertificateBundle> &certsByHostname, store::ConfigStore &configStore, auth::IAuthService &authService, plugin::PluginManager &pluginManager, lb::LoadBalancer &loadBalancer, tls::AcmeChallengeStore *acmeChallenges = nullptr); void run(); // Hot cert reload for CertificateManager (see src/tls/CertificateManager) -- safe to Loading @@ -91,7 +102,15 @@ public: return _httpd->reloadCertificates(certPath, keyPath); } // Per-hostname equivalent, for a listener constructed via the SNI ctor above. bool reloadCertificate(const std::string &hostname, const std::string &certPath, const std::string &keyPath) { return _httpd->reloadCertificate(hostname, certPath, keyPath); } private: void bindAndListen(); config::ListenerConfig _listener; bool _tlsActive; std::unique_ptr<libhttppp::HttpD> _httpd; Loading src/main.cpp +53 −0 Changes for src/main.cpp: 53 added lines, 0 removed lines. Original line number Diff line number Diff line #include <algorithm> #include <fstream> #include <iostream> #include <map> #include <memory> #include <sstream> #include <thread> Loading Loading @@ -29,6 +31,36 @@ std::string certPathFor(const proxyplus::config::AppConfig &cfg, const std::stri std::string keyPathFor(const proxyplus::config::AppConfig &cfg, const std::string &profile) { return profile.empty() ? "" : cfg.dataPath + "/tls/" + profile + ".key"; } // 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"] // both end up presented correctly off the same :443 listener. std::map<std::string, netplus::ssl::CertificateBundle> loadSniCertMap( const proxyplus::config::AppConfig &cfg, const proxyplus::store::ConfigStore &configStore, const std::vector<std::string> &profiles) { std::map<std::string, netplus::ssl::CertificateBundle> out; auto allProfiles = configStore.listTlsProfiles(); for (const auto &profileName : profiles) { auto it = std::find_if(allProfiles.begin(), allProfiles.end(), [&](const auto &p) { return p.profile == profileName; }); if (it == allProfiles.end()) { throw std::runtime_error("tls_profiles entry '" + profileName + "' has no matching TLS profile in the config store -- " "create it via POST /api/v1/tls/profiles first"); } if (it->domains.empty()) { throw std::runtime_error("TLS profile '" + profileName + "' has no domains -- SNI dispatch needs at least one"); } netplus::ssl::CertificateBundle bundle; if (!bundle.loadFromFile(certPathFor(cfg, profileName), keyPathFor(cfg, profileName))) { throw std::runtime_error("failed to load cert/key for TLS profile '" + profileName + "'"); } for (const auto &domain : it->domains) out[domain] = bundle; } return out; } } // namespace #ifndef PROXYPLUS_DEFAULT_STATIC_DIR Loading Loading @@ -148,6 +180,27 @@ 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); 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) { 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 : std::vector<std::string>{}; certManager.registerReloadCallback(profileName, [server, &cfg, profileName, domains] { for (const auto &domain : domains) { server->reloadCertificate(domain, certPathFor(cfg, profileName), keyPathFor(cfg, profileName)); } }); } continue; } httpServers.push_back(std::make_unique<proxyplus::httpproxy::HttpProxyServer>( listener, certPathFor(cfg, listener.tlsProfile), keyPathFor(cfg, listener.tlsProfile), configStore, authService, pluginManager, Loading Loading
src/config/BootstrapConfig.cpp +5 −0 Changes for src/config/BootstrapConfig.cpp: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ const std::set<std::string> &knownLeafPatterns() { "/proxyplus/http/listen/#/address", "/proxyplus/http/listen/#/port", "/proxyplus/http/listen/#/tls_profile", "/proxyplus/http/listen/#/tls_profiles/#", "/proxyplus/smtp/listen/#/address", "/proxyplus/smtp/listen/#/port", "/proxyplus/smtp/listen/#/tls_profile", Loading Loading @@ -133,6 +134,10 @@ std::vector<ListenerConfig> readListeners(confplus::Config &conf, const std::str lc.address = readValue(conf, pfx + "/address", ""); lc.port = readInt(conf, pfx + "/port", 0); lc.tlsProfile = readValue(conf, pfx + "/tls_profile", ""); lc.tlsProfiles = readStringList(conf, pfx + "/tls_profiles"); if (!lc.tlsProfile.empty() && !lc.tlsProfiles.empty()) { throw ConfigValidationError(pfx + ": set either tls_profile or tls_profiles, not both"); } out.push_back(lc); } return out; Loading
src/config/BootstrapConfig.h +6 −0 Changes for src/config/BootstrapConfig.h: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,12 @@ struct ListenerConfig { std::string address; int port = 0; std::string tlsProfile; // "" = plaintext; resolves to <dataPath>/tls/<profile>.{crt,key} // SNI virtual hosting: two or more TLS profiles sharing this one listener/port, each // presented only for its own TlsProfileRecord::domains rather than one cert for everyone. // Mutually exclusive with tlsProfile above -- set at most one of the two (see // BootstrapConfig.cpp's readListeners()); empty = single-profile listener as before. std::vector<std::string> tlsProfiles; }; enum class NodeMode { Single, Cluster }; Loading
src/http/HttpProxyServer.cpp +20 −0 Changes for src/http/HttpProxyServer.cpp: 20 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -454,6 +454,26 @@ HttpProxyServer::HttpProxyServer(config::ListenerConfig listener, std::string ce , _pluginManager(pluginManager) , _loadBalancer(loadBalancer) , _acmeChallenges(acmeChallenges) { bindAndListen(); } HttpProxyServer::HttpProxyServer(config::ListenerConfig listener, const std::map<std::string, netplus::ssl::CertificateBundle> &certsByHostname, store::ConfigStore &configStore, auth::IAuthService &authService, plugin::PluginManager &pluginManager, lb::LoadBalancer &loadBalancer, tls::AcmeChallengeStore *acmeChallenges) : _listener(listener) , _tlsActive(true) , _httpd(std::make_unique<libhttppp::HttpD>(listener.address, listener.port, 512, certsByHostname)) , _configStore(configStore) , _authService(authService) , _pluginManager(pluginManager) , _loadBalancer(loadBalancer) , _acmeChallenges(acmeChallenges) { bindAndListen(); } void HttpProxyServer::bindAndListen() { // netplus::tcp/ssl/quic only create the socket at construction; bind()/listen() are // separate calls the event loop would otherwise defer until run() -- too late, since // run() executes on a worker thread after main.cpp has already dropped root (see this Loading
src/http/HttpProxyServer.h +19 −0 Changes for src/http/HttpProxyServer.h: 19 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -83,6 +83,17 @@ public: lb::LoadBalancer &loadBalancer, tls::AcmeChallengeStore *acmeChallenges = nullptr); // SNI virtual hosting variant: certsByHostname carries one CertificateBundle per domain // sharing this listener (see ListenerConfig::tlsProfiles) instead of the single cert/key // path pair above. HttpProxyServer(config::ListenerConfig listener, const std::map<std::string, netplus::ssl::CertificateBundle> &certsByHostname, store::ConfigStore &configStore, auth::IAuthService &authService, plugin::PluginManager &pluginManager, lb::LoadBalancer &loadBalancer, tls::AcmeChallengeStore *acmeChallenges = nullptr); void run(); // Hot cert reload for CertificateManager (see src/tls/CertificateManager) -- safe to Loading @@ -91,7 +102,15 @@ public: return _httpd->reloadCertificates(certPath, keyPath); } // Per-hostname equivalent, for a listener constructed via the SNI ctor above. bool reloadCertificate(const std::string &hostname, const std::string &certPath, const std::string &keyPath) { return _httpd->reloadCertificate(hostname, certPath, keyPath); } private: void bindAndListen(); config::ListenerConfig _listener; bool _tlsActive; std::unique_ptr<libhttppp::HttpD> _httpd; Loading
src/main.cpp +53 −0 Changes for src/main.cpp: 53 added lines, 0 removed lines. Original line number Diff line number Diff line #include <algorithm> #include <fstream> #include <iostream> #include <map> #include <memory> #include <sstream> #include <thread> Loading Loading @@ -29,6 +31,36 @@ std::string certPathFor(const proxyplus::config::AppConfig &cfg, const std::stri std::string keyPathFor(const proxyplus::config::AppConfig &cfg, const std::string &profile) { return profile.empty() ? "" : cfg.dataPath + "/tls/" + profile + ".key"; } // 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"] // both end up presented correctly off the same :443 listener. std::map<std::string, netplus::ssl::CertificateBundle> loadSniCertMap( const proxyplus::config::AppConfig &cfg, const proxyplus::store::ConfigStore &configStore, const std::vector<std::string> &profiles) { std::map<std::string, netplus::ssl::CertificateBundle> out; auto allProfiles = configStore.listTlsProfiles(); for (const auto &profileName : profiles) { auto it = std::find_if(allProfiles.begin(), allProfiles.end(), [&](const auto &p) { return p.profile == profileName; }); if (it == allProfiles.end()) { throw std::runtime_error("tls_profiles entry '" + profileName + "' has no matching TLS profile in the config store -- " "create it via POST /api/v1/tls/profiles first"); } if (it->domains.empty()) { throw std::runtime_error("TLS profile '" + profileName + "' has no domains -- SNI dispatch needs at least one"); } netplus::ssl::CertificateBundle bundle; if (!bundle.loadFromFile(certPathFor(cfg, profileName), keyPathFor(cfg, profileName))) { throw std::runtime_error("failed to load cert/key for TLS profile '" + profileName + "'"); } for (const auto &domain : it->domains) out[domain] = bundle; } return out; } } // namespace #ifndef PROXYPLUS_DEFAULT_STATIC_DIR Loading Loading @@ -148,6 +180,27 @@ 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); 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) { 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 : std::vector<std::string>{}; certManager.registerReloadCallback(profileName, [server, &cfg, profileName, domains] { for (const auto &domain : domains) { server->reloadCertificate(domain, certPathFor(cfg, profileName), keyPathFor(cfg, profileName)); } }); } continue; } httpServers.push_back(std::make_unique<proxyplus::httpproxy::HttpProxyServer>( listener, certPathFor(cfg, listener.tlsProfile), keyPathFor(cfg, listener.tlsProfile), configStore, authService, pluginManager, Loading