Commit 6b26abf5 authored by jan.koester's avatar jan.koester
Browse files

cleanup

parent 7534c9f5
Loading
Loading
Loading
Loading
+21 −35
Original line number Diff line number Diff line
@@ -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"
+22 −18
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
                }
@@ -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);
@@ -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;
        }
    }
}

@@ -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;
@@ -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++) {
@@ -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;
            }

@@ -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>();
@@ -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;
            }

@@ -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
+2 −1
Original line number Diff line number Diff line
@@ -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;
    };
};
+77 −42
Original line number Diff line number Diff line
@@ -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()) {
@@ -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
    }
}

@@ -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);
@@ -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;
}

@@ -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;
}
+1 −1
Original line number Diff line number Diff line
@@ -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;