Loading config.yaml +21 −35 Original line number Diff line number Diff line Loading @@ -56,45 +56,31 @@ BLOGI: # CERTDIR: "/etc/blogi/certs" # ACMEURL: "https://acme-v02.api.letsencrypt.org/directory" # Multi-domain support: each domain gets its own DB, template, settings, etc. # If DOMAINS is not present, single-domain mode is used (backward compatible). # DOMAINS is required — unconfigured hosts get 404. # NAME can appear multiple times per entry to serve multiple hostnames with one config. DOMAINS: - NAME: "blog.example.com" DATABASE: DRIVER: "sqlite" CONNECTION: "/var/lib/blogi/blog-example.db" # REPLICAS: # - DRIVER: "pqxx" # CONNECTION: "host=replica.example.com dbname=blogi user=blogi" NAMES: "www.example.com" DB_DRIVER: "sqlite" DB_CONNECTION: "/var/lib/blogi/blog-example.db" TEMPLATE: "/usr/local/share/blogi/themes/default" STARTPAGE: "/blog/content/index" HTTP: URL: - "https://blog.example.com" - "https://www.example.com" URL: "https://blog.example.com" PREFIX: "/blog" PLUGINDIR: - "/usr/local/lib/blogi/plugins" AUTHDB: URL: "https://auth.example.com" CLIENTNAME: "blogiclient" CLIENTSECRET: "clientsecret" MEDIADB: URL: "https://127.0.0.1:8442" PLUGINDIR: "/usr/local/lib/blogi/plugins" AUTH_URL: "https://auth.example.com" AUTH_CLIENTNAME: "blogiclient" AUTH_CLIENTSECRET: "clientsecret" MEDIA_URL: "https://127.0.0.1:8442" - NAME: "blog.another.org" DATABASE: DRIVER: "sqlite" CONNECTION: "/var/lib/blogi/blog-another.db" DB_DRIVER: "sqlite" DB_CONNECTION: "/var/lib/blogi/blog-another.db" TEMPLATE: "/usr/local/share/blogi/themes/dark" STARTPAGE: "/content/index" HTTP: URL: - "https://blog.another.org" URL: "https://blog.another.org" PREFIX: "" PLUGINDIR: - "/usr/local/lib/blogi/plugins" AUTHDB: URL: "https://auth.another.org" CLIENTNAME: "anotherclient" CLIENTSECRET: "anothersecret" MEDIADB: URL: "https://media.another.org:8442" PLUGINDIR: "/usr/local/lib/blogi/plugins" AUTH_URL: "https://auth.another.org" AUTH_CLIENTNAME: "anotherclient" AUTH_CLIENTSECRET: "anothersecret" MEDIA_URL: "https://media.another.org:8442" src/blogi.cpp +22 −18 Original line number Diff line number Diff line Loading @@ -100,7 +100,7 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt // Helper to initialize a DomainContext from a DomainConfig auto initCtx = [&](DomainContext &ctx, const DomainConfig &dcfg) { ctx.domainName = dcfg.name; ctx.domainName = dcfg.names[0]; ctx.prefix = dcfg.prefix; ctx.startPage = dcfg.startPage; ctx.siteUrl = dcfg.siteUrl; Loading @@ -108,7 +108,7 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt ctx.plgArgs->debug = debug; ctx.plgArgs->config = Blogi::Cfg.get(); std::cerr << "Initializing domain '" << dcfg.name << "' with DB Driver: " << dcfg.dbDriver << " on " << dcfg.dbConnection << std::endl; std::cerr << "Initializing domain '" << dcfg.names[0] << "' with DB Driver: " << dcfg.dbDriver << " on " << dcfg.dbConnection << std::endl; const int maxRetries = 10; const int retrySec = 3; Loading @@ -130,7 +130,7 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt } break; } catch(std::runtime_error &e) { std::cerr << "can't connect db for domain '" << dcfg.name << "' (attempt " << attempt << "/" << maxRetries << "): " << e.what() << std::endl; std::cerr << "can't connect db for domain '" << dcfg.names[0] << "' (attempt " << attempt << "/" << maxRetries << "): " << e.what() << std::endl; if (attempt == maxRetries) { throw e; } Loading Loading @@ -179,7 +179,7 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt getConfig(*initdb, "SMTPSENDER", ctx.plgArgs->smtp->_Sender); getConfig(*initdb, "SMTPPASSWORD", ctx.plgArgs->smtp->_SPass); } catch(std::runtime_error &e) { std::cerr << "SMTP config not found for domain '" << dcfg.name << "', skipping: " << e.what() << std::endl; std::cerr << "SMTP config not found for domain '" << dcfg.names[0] << "', skipping: " << e.what() << std::endl; } ctx.plgArgs->auth = std::make_unique<Auth>(ctx.plgArgs->database, *ctx.plgArgs->config); Loading Loading @@ -219,11 +219,13 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt for (size_t d = 0; d < Blogi::Cfg->getDomainCount(); ++d) { const DomainConfig &dcfg = Blogi::Cfg->getDomainConfig(d); auto ctx = std::make_unique<DomainContext>(); auto ctx = std::make_shared<DomainContext>(); initCtx(*ctx, dcfg); std::string key = dcfg.name; for (const auto &n : dcfg.names) { std::string key = n; for (auto &c : key) c = std::tolower(c); _domains[key] = std::move(ctx); _domains[key] = ctx; } } } Loading @@ -236,12 +238,12 @@ bool blogi::Blogi::reloadConfig() { try { auto newCfg = std::make_unique<blogi::Config>(configPath); std::map<std::string, std::unique_ptr<DomainContext>> newDomains; std::map<std::string, std::shared_ptr<DomainContext>> newDomains; for (size_t d = 0; d < newCfg->getDomainCount(); ++d) { const DomainConfig &dcfg = newCfg->getDomainConfig(d); auto ctx = std::make_unique<DomainContext>(); ctx->domainName = dcfg.name; auto ctx = std::make_shared<DomainContext>(); ctx->domainName = dcfg.names[0]; ctx->prefix = dcfg.prefix; ctx->startPage = dcfg.startPage; ctx->siteUrl = dcfg.siteUrl; Loading @@ -249,7 +251,7 @@ bool blogi::Blogi::reloadConfig() { ctx->plgArgs->debug = _debug; ctx->plgArgs->config = newCfg.get(); std::cerr << "[blogi] Reload: initializing domain '" << dcfg.name << "'" << std::endl; std::cerr << "[blogi] Reload: initializing domain '" << dcfg.names[0] << "'" << std::endl; try { for (int i = 0; i < threads; i++) { Loading @@ -260,7 +262,7 @@ bool blogi::Blogi::reloadConfig() { ctx->plgArgs->database.emplace_back(std::move(db)); } } catch (std::runtime_error &e) { std::cerr << "[blogi] Reload: DB connect failed for '" << dcfg.name << "': " << e.what() << std::endl; std::cerr << "[blogi] Reload: DB connect failed for '" << dcfg.names[0] << "': " << e.what() << std::endl; continue; } Loading @@ -286,7 +288,7 @@ bool blogi::Blogi::reloadConfig() { if (dcfg.dbDriver != "sqlite") optsql << "ALTER TABLE options ALTER COLUMN value TYPE text;"; try { initdb->exec(optsql, optres); } catch (std::runtime_error &e) { std::cerr << "[blogi] Reload: DB init failed for '" << dcfg.name << "': " << e.what() << std::endl; std::cerr << "[blogi] Reload: DB init failed for '" << dcfg.names[0] << "': " << e.what() << std::endl; } ctx->plgArgs->smtp = std::make_unique<SmtpSettings>(); Loading @@ -307,7 +309,7 @@ bool blogi::Blogi::reloadConfig() { ctx->plgArgs->theme->renderPage(0, "index.html", ctx->page, ctx->index); ctx->plgArgs->theme->renderPage(0, "mobile.html", ctx->mPage, ctx->mIndex); } catch (libhtmlpp::HTMLException &e) { std::cerr << "[blogi] Reload: theme render failed for '" << dcfg.name << "': " << e.what() << std::endl; std::cerr << "[blogi] Reload: theme render failed for '" << dcfg.names[0] << "': " << e.what() << std::endl; continue; } Loading @@ -326,9 +328,11 @@ bool blogi::Blogi::reloadConfig() { ctx->plugins->loadPlugins(dcfg.plgDirs[i], ctx->plgArgs.get()); } std::string key = dcfg.name; for (const auto &n : dcfg.names) { std::string key = n; for (auto &c : key) c = std::tolower(c); newDomains[key] = std::move(ctx); newDomains[key] = ctx; } } // Swap in new config and domains atomically Loading src/blogi.h +2 −1 Original line number Diff line number Diff line Loading @@ -85,7 +85,8 @@ namespace blogi { void initDomainContext(DomainContext &ctx, const DomainConfig &dcfg, bool debug); bool _debug = false; // Multi-domain contexts keyed by domain name (lowercase) std::map<std::string, std::unique_ptr<DomainContext>> _domains; // shared_ptr because one domain config with multiple names shares the same context std::map<std::string, std::shared_ptr<DomainContext>> _domains; std::mutex _domainsMutex; }; }; src/conf.cpp +77 −42 Original line number Diff line number Diff line Loading @@ -135,43 +135,78 @@ blogi::Config::Config(const std::string &path) : confplus::Config(path), _Config } // Parse multi-domain configurations // libconfplus YAML stores sequence-of-mappings as flat keys with positional values: // /BLOGI/DOMAINS/NAME pos 0 = "domain1", pos 1 = "domain2" // /BLOGI/DOMAINS/DB_DRIVER pos 0 = "pgsql", pos 1 = "sqlite" // Use NAME element count to determine number of domains. // libconfplus YAML stores sequence-of-mappings as flat keys with positional values. // Each domain entry MUST define its own DB_DRIVER, DB_CONNECTION, etc. // NAME is an array: one config entry can serve multiple domain names. // No global fallbacks — unconfigured domains get 404. try { auto *nameKey = getKey("/BLOGI/DOMAINS/NAME"); if (!nameKey) throw std::runtime_error("no domains"); size_t domCount = getElements(nameKey); auto *dbDriverKey = getKey("/BLOGI/DOMAINS/DB_DRIVER"); if (!dbDriverKey) throw std::runtime_error("no domains"); size_t domCount = getElements(dbDriverKey); for (size_t i = 0; i < domCount; ++i) { DomainConfig dc; try { dc.name = getValue(nameKey, i); } catch(...) { continue; } try { dc.dbDriver = getValue(getKey("/BLOGI/DOMAINS/DB_DRIVER"), i); } catch(...) { dc.dbDriver = _DBDriver; } try { dc.dbConnection = getValue(getKey("/BLOGI/DOMAINS/DB_CONNECTION"), i); } catch(...) { dc.dbConnection = _DBConnection; } // NAME is stored as a sub-array per domain entry via DOMAINS/NAME try { auto *nameKey = getKey("/BLOGI/DOMAINS/NAME"); if (nameKey) { // In flat config, multiple names for the same domain entry // are stored as comma-separated or as multiple positional values. // With the current YAML parser, each domain entry gets one NAME value, // but we also support NAMES as a separate multi-value key. std::string n = getValue(nameKey, i); if (!n.empty()) dc.names.push_back(n); } } catch(...) {} // Additional names via DOMAINS/NAMES (array per domain) try { auto *namesKey = getKey("/BLOGI/DOMAINS/NAMES"); if (namesKey) { size_t namesCount = getElements(namesKey); for (size_t j = 0; j < namesCount; ++j) { std::string n = getValue(namesKey, j); // Only add names that belong to this domain index // In flat config with seqMapping, all names at index i belong to domain i if (!n.empty()) { bool dup = false; for (const auto &existing : dc.names) if (existing == n) { dup = true; break; } if (!dup) dc.names.push_back(n); } } } } catch(...) {} dc.dbReplicas = _DBReplicas; if (dc.names.empty()) continue; try { dc.authUrl = getValue(getKey("/BLOGI/DOMAINS/AUTH_URL"), i); } catch(...) { dc.authUrl = _AuthUrl; } try { dc.clientName = getValue(getKey("/BLOGI/DOMAINS/AUTH_CLIENTNAME"), i); } catch(...) { dc.clientName = _ClientName; } try { dc.clientSecret = getValue(getKey("/BLOGI/DOMAINS/AUTH_CLIENTSECRET"), i); } catch(...) { dc.clientSecret = _ClientSecret; } try { dc.siteUrl = getValue(getKey("/BLOGI/DOMAINS/URL"), i); } catch(...) { dc.siteUrl = _HttpUrl; } try { dc.dbDriver = getValue(dbDriverKey, i); } catch(...) { continue; } try { dc.dbConnection = getValue(getKey("/BLOGI/DOMAINS/DB_CONNECTION"), i); } catch(...) { continue; } try { auto *repKey = getKey("/BLOGI/DOMAINS/DB_REPLICAS"); if (repKey) { // TODO: per-domain replicas if needed } } catch(...) {} try { dc.authUrl = getValue(getKey("/BLOGI/DOMAINS/AUTH_URL"), i); } catch(...) {} try { dc.clientName = getValue(getKey("/BLOGI/DOMAINS/AUTH_CLIENTNAME"), i); } catch(...) {} try { dc.clientSecret = getValue(getKey("/BLOGI/DOMAINS/AUTH_CLIENTSECRET"), i); } catch(...) {} try { dc.siteUrl = getValue(getKey("/BLOGI/DOMAINS/URL"), i); } catch(...) {} dc.siteUrls.clear(); if (!dc.siteUrl.empty()) dc.siteUrls.push_back(dc.siteUrl); try { dc.prefix = getValue(getKey("/BLOGI/DOMAINS/PREFIX"), i); } catch(...) { dc.prefix = _HttpPrefix; } try { dc.prefix = getValue(getKey("/BLOGI/DOMAINS/PREFIX"), i); } catch(...) {} if (dc.prefix == "/") dc.prefix = ""; try { dc.templatePath = getValue(getKey("/BLOGI/DOMAINS/TEMPLATE"), i); } catch(...) { dc.templatePath = _Template; } try { dc.startPage = getValue(getKey("/BLOGI/DOMAINS/STARTPAGE"), i); } catch(...) { dc.startPage = _StartPage; } try { dc.mediaDBUrl = getValue(getKey("/BLOGI/DOMAINS/MEDIA_URL"), i); } catch(...) { dc.mediaDBUrl = _MediaDBUrl; } try { dc.tmpDir = getValue(getKey("/BLOGI/DOMAINS/TMPDIR"), i); } catch(...) { dc.tmpDir = _TmpDir; } try { dc.templatePath = getValue(getKey("/BLOGI/DOMAINS/TEMPLATE"), i); } catch(...) {} try { dc.startPage = getValue(getKey("/BLOGI/DOMAINS/STARTPAGE"), i); } catch(...) {} try { dc.mediaDBUrl = getValue(getKey("/BLOGI/DOMAINS/MEDIA_URL"), i); } catch(...) {} try { dc.tmpDir = getValue(getKey("/BLOGI/DOMAINS/TMPDIR"), i); } catch(...) { dc.tmpDir = "/tmp/blogi"; } // Plugin directory (single value per domain in flat config) try { std::string plgdir = getValue(getKey("/BLOGI/DOMAINS/PLUGINDIR"), i); if (!plgdir.empty()) dc.plgDirs.push_back(plgdir); } catch(...) { dc.plgDirs = _PlgDir; } } catch(...) {} // Auth source from domain values if (!dc.authUrl.empty()) { Loading @@ -179,14 +214,14 @@ blogi::Config::Config(const std::string &path) : confplus::Config(path), _Config src.url = dc.authUrl; src.clientName = dc.clientName; src.clientSecret = dc.clientSecret; src.domain = dc.name; src.domain = dc.names[0]; dc.authSources.push_back(std::move(src)); } _Domains.push_back(std::move(dc)); } } catch(...) { // No DOMAINS section — single-domain mode (backward compatible) // No DOMAINS section } } Loading Loading @@ -315,7 +350,8 @@ const blogi::AuthSource *blogi::Config::findAuthSource(const std::string &domain } // Search domain-specific auth sources for (const auto &dc : _Domains) { std::string dcName = dc.name; for (const auto &n : dc.names) { std::string dcName = n; std::string searchDomain = domain; for (auto &c : dcName) c = std::tolower(c); for (auto &c : searchDomain) c = std::tolower(c); Loading @@ -323,10 +359,8 @@ const blogi::AuthSource *blogi::Config::findAuthSource(const std::string &domain return &dc.authSources[0]; } } // Fallback: if no domain match and only one global source, use it if (_AuthSources.size() == 1) { return &_AuthSources[0]; } // No match return nullptr; } Loading Loading @@ -360,11 +394,12 @@ const blogi::DomainConfig *blogi::Config::findDomainConfig(const std::string &ho if (colon != std::string::npos) search = search.substr(0, colon); for (const auto &dc : _Domains) { std::string dcName = dc.name; for (const auto &n : dc.names) { std::string dcName = n; for (auto &c : dcName) c = std::tolower(c); if (dcName == search) return &dc; } // Fallback: first domain config if only one defined if (_Domains.size() == 1) return &_Domains[0]; } // No match — return nullptr (caller should return 404) return nullptr; } src/conf.h +1 −1 Original line number Diff line number Diff line Loading @@ -52,7 +52,7 @@ namespace blogi { }; struct DomainConfig { std::string name; std::vector<std::string> names; std::string dbDriver; std::string dbConnection; std::vector<ReplicaConfig> dbReplicas; Loading Loading
config.yaml +21 −35 Original line number Diff line number Diff line Loading @@ -56,45 +56,31 @@ BLOGI: # CERTDIR: "/etc/blogi/certs" # ACMEURL: "https://acme-v02.api.letsencrypt.org/directory" # Multi-domain support: each domain gets its own DB, template, settings, etc. # If DOMAINS is not present, single-domain mode is used (backward compatible). # DOMAINS is required — unconfigured hosts get 404. # NAME can appear multiple times per entry to serve multiple hostnames with one config. DOMAINS: - NAME: "blog.example.com" DATABASE: DRIVER: "sqlite" CONNECTION: "/var/lib/blogi/blog-example.db" # REPLICAS: # - DRIVER: "pqxx" # CONNECTION: "host=replica.example.com dbname=blogi user=blogi" NAMES: "www.example.com" DB_DRIVER: "sqlite" DB_CONNECTION: "/var/lib/blogi/blog-example.db" TEMPLATE: "/usr/local/share/blogi/themes/default" STARTPAGE: "/blog/content/index" HTTP: URL: - "https://blog.example.com" - "https://www.example.com" URL: "https://blog.example.com" PREFIX: "/blog" PLUGINDIR: - "/usr/local/lib/blogi/plugins" AUTHDB: URL: "https://auth.example.com" CLIENTNAME: "blogiclient" CLIENTSECRET: "clientsecret" MEDIADB: URL: "https://127.0.0.1:8442" PLUGINDIR: "/usr/local/lib/blogi/plugins" AUTH_URL: "https://auth.example.com" AUTH_CLIENTNAME: "blogiclient" AUTH_CLIENTSECRET: "clientsecret" MEDIA_URL: "https://127.0.0.1:8442" - NAME: "blog.another.org" DATABASE: DRIVER: "sqlite" CONNECTION: "/var/lib/blogi/blog-another.db" DB_DRIVER: "sqlite" DB_CONNECTION: "/var/lib/blogi/blog-another.db" TEMPLATE: "/usr/local/share/blogi/themes/dark" STARTPAGE: "/content/index" HTTP: URL: - "https://blog.another.org" URL: "https://blog.another.org" PREFIX: "" PLUGINDIR: - "/usr/local/lib/blogi/plugins" AUTHDB: URL: "https://auth.another.org" CLIENTNAME: "anotherclient" CLIENTSECRET: "anothersecret" MEDIADB: URL: "https://media.another.org:8442" PLUGINDIR: "/usr/local/lib/blogi/plugins" AUTH_URL: "https://auth.another.org" AUTH_CLIENTNAME: "anotherclient" AUTH_CLIENTSECRET: "anothersecret" MEDIA_URL: "https://media.another.org:8442"
src/blogi.cpp +22 −18 Original line number Diff line number Diff line Loading @@ -100,7 +100,7 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt // Helper to initialize a DomainContext from a DomainConfig auto initCtx = [&](DomainContext &ctx, const DomainConfig &dcfg) { ctx.domainName = dcfg.name; ctx.domainName = dcfg.names[0]; ctx.prefix = dcfg.prefix; ctx.startPage = dcfg.startPage; ctx.siteUrl = dcfg.siteUrl; Loading @@ -108,7 +108,7 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt ctx.plgArgs->debug = debug; ctx.plgArgs->config = Blogi::Cfg.get(); std::cerr << "Initializing domain '" << dcfg.name << "' with DB Driver: " << dcfg.dbDriver << " on " << dcfg.dbConnection << std::endl; std::cerr << "Initializing domain '" << dcfg.names[0] << "' with DB Driver: " << dcfg.dbDriver << " on " << dcfg.dbConnection << std::endl; const int maxRetries = 10; const int retrySec = 3; Loading @@ -130,7 +130,7 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt } break; } catch(std::runtime_error &e) { std::cerr << "can't connect db for domain '" << dcfg.name << "' (attempt " << attempt << "/" << maxRetries << "): " << e.what() << std::endl; std::cerr << "can't connect db for domain '" << dcfg.names[0] << "' (attempt " << attempt << "/" << maxRetries << "): " << e.what() << std::endl; if (attempt == maxRetries) { throw e; } Loading Loading @@ -179,7 +179,7 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt getConfig(*initdb, "SMTPSENDER", ctx.plgArgs->smtp->_Sender); getConfig(*initdb, "SMTPPASSWORD", ctx.plgArgs->smtp->_SPass); } catch(std::runtime_error &e) { std::cerr << "SMTP config not found for domain '" << dcfg.name << "', skipping: " << e.what() << std::endl; std::cerr << "SMTP config not found for domain '" << dcfg.names[0] << "', skipping: " << e.what() << std::endl; } ctx.plgArgs->auth = std::make_unique<Auth>(ctx.plgArgs->database, *ctx.plgArgs->config); Loading Loading @@ -219,11 +219,13 @@ blogi::Blogi::Blogi(std::vector<netplus::socket*> serversocket,bool debug) : Htt for (size_t d = 0; d < Blogi::Cfg->getDomainCount(); ++d) { const DomainConfig &dcfg = Blogi::Cfg->getDomainConfig(d); auto ctx = std::make_unique<DomainContext>(); auto ctx = std::make_shared<DomainContext>(); initCtx(*ctx, dcfg); std::string key = dcfg.name; for (const auto &n : dcfg.names) { std::string key = n; for (auto &c : key) c = std::tolower(c); _domains[key] = std::move(ctx); _domains[key] = ctx; } } } Loading @@ -236,12 +238,12 @@ bool blogi::Blogi::reloadConfig() { try { auto newCfg = std::make_unique<blogi::Config>(configPath); std::map<std::string, std::unique_ptr<DomainContext>> newDomains; std::map<std::string, std::shared_ptr<DomainContext>> newDomains; for (size_t d = 0; d < newCfg->getDomainCount(); ++d) { const DomainConfig &dcfg = newCfg->getDomainConfig(d); auto ctx = std::make_unique<DomainContext>(); ctx->domainName = dcfg.name; auto ctx = std::make_shared<DomainContext>(); ctx->domainName = dcfg.names[0]; ctx->prefix = dcfg.prefix; ctx->startPage = dcfg.startPage; ctx->siteUrl = dcfg.siteUrl; Loading @@ -249,7 +251,7 @@ bool blogi::Blogi::reloadConfig() { ctx->plgArgs->debug = _debug; ctx->plgArgs->config = newCfg.get(); std::cerr << "[blogi] Reload: initializing domain '" << dcfg.name << "'" << std::endl; std::cerr << "[blogi] Reload: initializing domain '" << dcfg.names[0] << "'" << std::endl; try { for (int i = 0; i < threads; i++) { Loading @@ -260,7 +262,7 @@ bool blogi::Blogi::reloadConfig() { ctx->plgArgs->database.emplace_back(std::move(db)); } } catch (std::runtime_error &e) { std::cerr << "[blogi] Reload: DB connect failed for '" << dcfg.name << "': " << e.what() << std::endl; std::cerr << "[blogi] Reload: DB connect failed for '" << dcfg.names[0] << "': " << e.what() << std::endl; continue; } Loading @@ -286,7 +288,7 @@ bool blogi::Blogi::reloadConfig() { if (dcfg.dbDriver != "sqlite") optsql << "ALTER TABLE options ALTER COLUMN value TYPE text;"; try { initdb->exec(optsql, optres); } catch (std::runtime_error &e) { std::cerr << "[blogi] Reload: DB init failed for '" << dcfg.name << "': " << e.what() << std::endl; std::cerr << "[blogi] Reload: DB init failed for '" << dcfg.names[0] << "': " << e.what() << std::endl; } ctx->plgArgs->smtp = std::make_unique<SmtpSettings>(); Loading @@ -307,7 +309,7 @@ bool blogi::Blogi::reloadConfig() { ctx->plgArgs->theme->renderPage(0, "index.html", ctx->page, ctx->index); ctx->plgArgs->theme->renderPage(0, "mobile.html", ctx->mPage, ctx->mIndex); } catch (libhtmlpp::HTMLException &e) { std::cerr << "[blogi] Reload: theme render failed for '" << dcfg.name << "': " << e.what() << std::endl; std::cerr << "[blogi] Reload: theme render failed for '" << dcfg.names[0] << "': " << e.what() << std::endl; continue; } Loading @@ -326,9 +328,11 @@ bool blogi::Blogi::reloadConfig() { ctx->plugins->loadPlugins(dcfg.plgDirs[i], ctx->plgArgs.get()); } std::string key = dcfg.name; for (const auto &n : dcfg.names) { std::string key = n; for (auto &c : key) c = std::tolower(c); newDomains[key] = std::move(ctx); newDomains[key] = ctx; } } // Swap in new config and domains atomically Loading
src/blogi.h +2 −1 Original line number Diff line number Diff line Loading @@ -85,7 +85,8 @@ namespace blogi { void initDomainContext(DomainContext &ctx, const DomainConfig &dcfg, bool debug); bool _debug = false; // Multi-domain contexts keyed by domain name (lowercase) std::map<std::string, std::unique_ptr<DomainContext>> _domains; // shared_ptr because one domain config with multiple names shares the same context std::map<std::string, std::shared_ptr<DomainContext>> _domains; std::mutex _domainsMutex; }; };
src/conf.cpp +77 −42 Original line number Diff line number Diff line Loading @@ -135,43 +135,78 @@ blogi::Config::Config(const std::string &path) : confplus::Config(path), _Config } // Parse multi-domain configurations // libconfplus YAML stores sequence-of-mappings as flat keys with positional values: // /BLOGI/DOMAINS/NAME pos 0 = "domain1", pos 1 = "domain2" // /BLOGI/DOMAINS/DB_DRIVER pos 0 = "pgsql", pos 1 = "sqlite" // Use NAME element count to determine number of domains. // libconfplus YAML stores sequence-of-mappings as flat keys with positional values. // Each domain entry MUST define its own DB_DRIVER, DB_CONNECTION, etc. // NAME is an array: one config entry can serve multiple domain names. // No global fallbacks — unconfigured domains get 404. try { auto *nameKey = getKey("/BLOGI/DOMAINS/NAME"); if (!nameKey) throw std::runtime_error("no domains"); size_t domCount = getElements(nameKey); auto *dbDriverKey = getKey("/BLOGI/DOMAINS/DB_DRIVER"); if (!dbDriverKey) throw std::runtime_error("no domains"); size_t domCount = getElements(dbDriverKey); for (size_t i = 0; i < domCount; ++i) { DomainConfig dc; try { dc.name = getValue(nameKey, i); } catch(...) { continue; } try { dc.dbDriver = getValue(getKey("/BLOGI/DOMAINS/DB_DRIVER"), i); } catch(...) { dc.dbDriver = _DBDriver; } try { dc.dbConnection = getValue(getKey("/BLOGI/DOMAINS/DB_CONNECTION"), i); } catch(...) { dc.dbConnection = _DBConnection; } // NAME is stored as a sub-array per domain entry via DOMAINS/NAME try { auto *nameKey = getKey("/BLOGI/DOMAINS/NAME"); if (nameKey) { // In flat config, multiple names for the same domain entry // are stored as comma-separated or as multiple positional values. // With the current YAML parser, each domain entry gets one NAME value, // but we also support NAMES as a separate multi-value key. std::string n = getValue(nameKey, i); if (!n.empty()) dc.names.push_back(n); } } catch(...) {} // Additional names via DOMAINS/NAMES (array per domain) try { auto *namesKey = getKey("/BLOGI/DOMAINS/NAMES"); if (namesKey) { size_t namesCount = getElements(namesKey); for (size_t j = 0; j < namesCount; ++j) { std::string n = getValue(namesKey, j); // Only add names that belong to this domain index // In flat config with seqMapping, all names at index i belong to domain i if (!n.empty()) { bool dup = false; for (const auto &existing : dc.names) if (existing == n) { dup = true; break; } if (!dup) dc.names.push_back(n); } } } } catch(...) {} dc.dbReplicas = _DBReplicas; if (dc.names.empty()) continue; try { dc.authUrl = getValue(getKey("/BLOGI/DOMAINS/AUTH_URL"), i); } catch(...) { dc.authUrl = _AuthUrl; } try { dc.clientName = getValue(getKey("/BLOGI/DOMAINS/AUTH_CLIENTNAME"), i); } catch(...) { dc.clientName = _ClientName; } try { dc.clientSecret = getValue(getKey("/BLOGI/DOMAINS/AUTH_CLIENTSECRET"), i); } catch(...) { dc.clientSecret = _ClientSecret; } try { dc.siteUrl = getValue(getKey("/BLOGI/DOMAINS/URL"), i); } catch(...) { dc.siteUrl = _HttpUrl; } try { dc.dbDriver = getValue(dbDriverKey, i); } catch(...) { continue; } try { dc.dbConnection = getValue(getKey("/BLOGI/DOMAINS/DB_CONNECTION"), i); } catch(...) { continue; } try { auto *repKey = getKey("/BLOGI/DOMAINS/DB_REPLICAS"); if (repKey) { // TODO: per-domain replicas if needed } } catch(...) {} try { dc.authUrl = getValue(getKey("/BLOGI/DOMAINS/AUTH_URL"), i); } catch(...) {} try { dc.clientName = getValue(getKey("/BLOGI/DOMAINS/AUTH_CLIENTNAME"), i); } catch(...) {} try { dc.clientSecret = getValue(getKey("/BLOGI/DOMAINS/AUTH_CLIENTSECRET"), i); } catch(...) {} try { dc.siteUrl = getValue(getKey("/BLOGI/DOMAINS/URL"), i); } catch(...) {} dc.siteUrls.clear(); if (!dc.siteUrl.empty()) dc.siteUrls.push_back(dc.siteUrl); try { dc.prefix = getValue(getKey("/BLOGI/DOMAINS/PREFIX"), i); } catch(...) { dc.prefix = _HttpPrefix; } try { dc.prefix = getValue(getKey("/BLOGI/DOMAINS/PREFIX"), i); } catch(...) {} if (dc.prefix == "/") dc.prefix = ""; try { dc.templatePath = getValue(getKey("/BLOGI/DOMAINS/TEMPLATE"), i); } catch(...) { dc.templatePath = _Template; } try { dc.startPage = getValue(getKey("/BLOGI/DOMAINS/STARTPAGE"), i); } catch(...) { dc.startPage = _StartPage; } try { dc.mediaDBUrl = getValue(getKey("/BLOGI/DOMAINS/MEDIA_URL"), i); } catch(...) { dc.mediaDBUrl = _MediaDBUrl; } try { dc.tmpDir = getValue(getKey("/BLOGI/DOMAINS/TMPDIR"), i); } catch(...) { dc.tmpDir = _TmpDir; } try { dc.templatePath = getValue(getKey("/BLOGI/DOMAINS/TEMPLATE"), i); } catch(...) {} try { dc.startPage = getValue(getKey("/BLOGI/DOMAINS/STARTPAGE"), i); } catch(...) {} try { dc.mediaDBUrl = getValue(getKey("/BLOGI/DOMAINS/MEDIA_URL"), i); } catch(...) {} try { dc.tmpDir = getValue(getKey("/BLOGI/DOMAINS/TMPDIR"), i); } catch(...) { dc.tmpDir = "/tmp/blogi"; } // Plugin directory (single value per domain in flat config) try { std::string plgdir = getValue(getKey("/BLOGI/DOMAINS/PLUGINDIR"), i); if (!plgdir.empty()) dc.plgDirs.push_back(plgdir); } catch(...) { dc.plgDirs = _PlgDir; } } catch(...) {} // Auth source from domain values if (!dc.authUrl.empty()) { Loading @@ -179,14 +214,14 @@ blogi::Config::Config(const std::string &path) : confplus::Config(path), _Config src.url = dc.authUrl; src.clientName = dc.clientName; src.clientSecret = dc.clientSecret; src.domain = dc.name; src.domain = dc.names[0]; dc.authSources.push_back(std::move(src)); } _Domains.push_back(std::move(dc)); } } catch(...) { // No DOMAINS section — single-domain mode (backward compatible) // No DOMAINS section } } Loading Loading @@ -315,7 +350,8 @@ const blogi::AuthSource *blogi::Config::findAuthSource(const std::string &domain } // Search domain-specific auth sources for (const auto &dc : _Domains) { std::string dcName = dc.name; for (const auto &n : dc.names) { std::string dcName = n; std::string searchDomain = domain; for (auto &c : dcName) c = std::tolower(c); for (auto &c : searchDomain) c = std::tolower(c); Loading @@ -323,10 +359,8 @@ const blogi::AuthSource *blogi::Config::findAuthSource(const std::string &domain return &dc.authSources[0]; } } // Fallback: if no domain match and only one global source, use it if (_AuthSources.size() == 1) { return &_AuthSources[0]; } // No match return nullptr; } Loading Loading @@ -360,11 +394,12 @@ const blogi::DomainConfig *blogi::Config::findDomainConfig(const std::string &ho if (colon != std::string::npos) search = search.substr(0, colon); for (const auto &dc : _Domains) { std::string dcName = dc.name; for (const auto &n : dc.names) { std::string dcName = n; for (auto &c : dcName) c = std::tolower(c); if (dcName == search) return &dc; } // Fallback: first domain config if only one defined if (_Domains.size() == 1) return &_Domains[0]; } // No match — return nullptr (caller should return 404) return nullptr; }
src/conf.h +1 −1 Original line number Diff line number Diff line Loading @@ -52,7 +52,7 @@ namespace blogi { }; struct DomainConfig { std::string name; std::vector<std::string> names; std::string dbDriver; std::string dbConnection; std::vector<ReplicaConfig> dbReplicas; Loading