#ifndef __NGINX_SSL_UTIL_HPP #define __NGINX_SSL_UTIL_HPP #ifdef NO_PCRE #include namespace rgx = std; #else #include "regex-pcre.hpp" #endif #include "nginx-util.hpp" #include "px5g-openssl.hpp" #ifndef NO_UBUS static constexpr auto UBUS_TIMEOUT = 1000; #endif // once a year: static constexpr auto CRON_INTERVAL = std::string_view{"3 3 12 12 *"}; static constexpr auto LAN_SSL_LISTEN = std::string_view{"/var/lib/nginx/lan_ssl.listen"}; static constexpr auto LAN_SSL_LISTEN_DEFAULT = // TODO(pst) deprecate std::string_view{"/var/lib/nginx/lan_ssl.listen.default"}; static constexpr auto ADD_SSL_FCT = std::string_view{"add_ssl"}; static constexpr auto SSL_SESSION_CACHE_ARG = [](const std::string_view & /*name*/) -> std::string { return "shared:SSL:32k"; }; static constexpr auto SSL_SESSION_TIMEOUT_ARG = std::string_view{"64m"}; using _Line = std::array; class Line { private: _Line _line; public: explicit Line(const _Line& line) noexcept : _line{line} {} template static auto build() noexcept -> Line { return Line{_Line{[](const std::string& p, const std::string& b) -> std::string { return (... + xn[0](p, b)); }, [](const std::string& p, const std::string& b) -> std::string { return (... + xn[1](p, b)); }}}; } [[nodiscard]] auto STR(const std::string& param, const std::string& begin) const -> std::string { return _line[0](param, begin); } [[nodiscard]] auto RGX() const -> rgx::regex { return rgx::regex{_line[1]("", "")}; } }; auto get_if_missed(const std::string& conf, const Line& LINE, const std::string& val, const std::string& indent = "\n ", bool compare = true) -> std::string; auto replace_if(const std::string& conf, const rgx::regex& rgx, const std::string& val, const std::string& insert) -> std::string; auto replace_listen(const std::string& conf, const std::array& ngx_port) -> std::string; auto check_ssl_certificate(const std::string& crtpath, const std::string& keypath) -> bool; auto contains(const std::string& sentence, const std::string& word) -> bool; auto get_uci_section_for_name(const std::string& name) -> uci::section; void add_ssl_if_needed(const std::string& name); void add_ssl_if_needed(const std::string& name, std::string_view manage, std::string_view crt, std::string_view key); void install_cron_job(const Line& CRON_LINE, const std::string& name = ""); void remove_cron_job(const Line& CRON_LINE, const std::string& name = ""); auto del_ssl_legacy(const std::string& name) -> bool; void del_ssl(const std::string& name); void del_ssl(const std::string& name, std::string_view manage); auto check_ssl(const uci::package& pkg, bool is_enabled) -> bool; inline void check_ssl(const uci::package& pkg) { if (!check_ssl(pkg, is_enabled(pkg))) { #ifndef NO_UBUS if (ubus::call("service", "list", UBUS_TIMEOUT).filter("nginx")) { call("/etc/init.d/nginx", "reload"); std::cerr << "Reload Nginx.\n"; } #endif } } constexpr auto _begin = _Line{ [](const std::string& /*param*/, const std::string& begin) -> std::string { return begin; }, [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { return R"([{;](?:\s*#[^\n]*(?=\n))*(\s*))"; }}; constexpr auto _space = _Line{[](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { return std::string{" "}; }, [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { return R"(\s+)"; }}; constexpr auto _newline = _Line{ [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { return std::string{"\n"}; }, [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { return std::string{"(\n)"}; } // capture it as _end captures it, too. }; constexpr auto _end = _Line{[](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { return std::string{";"}; }, [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { return std::string{R"(\s*(;(?:[\t ]*#[^\n]*)?))"}; }}; template static constexpr auto _capture = _Line{ [](const std::string& param, const std::string & /*begin*/) -> std::string { return '\'' + param + '\''; }, [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { const auto lim = clim == '\0' ? std::string{"\\s"} : std::string{clim}; return std::string{R"(((?:(?:"[^"]*")|(?:[^'")"} + lim + "][^" + lim + "]*)|(?:'[^']*'))+)"; }}; template static constexpr auto _escape = _Line{ [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { return clim == '\0' ? std::string{strptr.data()} : clim + std::string{strptr.data()} + clim; }, [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string { std::string ret{}; for (char c : strptr) { switch (c) { case '^': ret += '\\'; ret += c; break; case '_': case '-': ret += c; break; default: if ((isalpha(c) != 0) || (isdigit(c) != 0)) { ret += c; } else { ret += std::string{"["} + c + "]"; } } } return "(?:" + ret + "|'" + ret + "'" + "|\"" + ret + "\"" + ")"; }}; constexpr std::string_view _check_ssl = "check_ssl"; constexpr std::string_view _server_name = "server_name"; constexpr std::string_view _listen = "listen"; constexpr std::string_view _include = "include"; constexpr std::string_view _ssl_certificate = "ssl_certificate"; constexpr std::string_view _ssl_certificate_key = "ssl_certificate_key"; constexpr std::string_view _ssl_session_cache = "ssl_session_cache"; constexpr std::string_view _ssl_session_timeout = "ssl_session_timeout"; // For a compile time regex lib, this must be fixed, use one of these options: // * Hand craft or macro concat them (loosing more or less flexibility). // * Use Macro concatenation of __VA_ARGS__ with the help of: // https://p99.gforge.inria.fr/p99-html/group__preprocessor__for.html // * Use constexpr---not available for strings or char * for now---look at lib. static const auto CRON_CHECK = Line::build<_space, _escape, _space, _escape<_check_ssl, '\''>, _newline>(); static const auto CRON_CMD = Line::build<_space, _escape, _space, _escape, _space, _capture<>, _newline>(); static const auto NGX_SERVER_NAME = Line::build<_begin, _escape<_server_name>, _space, _capture<';'>, _end>(); static const auto NGX_INCLUDE_LAN_LISTEN = Line::build<_begin, _escape<_include>, _space, _escape, _end>(); static const auto NGX_INCLUDE_LAN_LISTEN_DEFAULT = Line::build<_begin, _escape<_include>, _space, _escape, _end>(); static const auto NGX_INCLUDE_LAN_SSL_LISTEN = Line::build<_begin, _escape<_include>, _space, _escape, _end>(); static const auto NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT = Line::build<_begin, _escape<_include>, _space, _escape, _end>(); static const auto NGX_SSL_CRT = Line::build<_begin, _escape<_ssl_certificate>, _space, _capture<';'>, _end>(); static const auto NGX_SSL_KEY = Line::build<_begin, _escape<_ssl_certificate_key>, _space, _capture<';'>, _end>(); static const auto NGX_SSL_SESSION_CACHE = Line::build<_begin, _escape<_ssl_session_cache>, _space, _capture<';'>, _end>(); static const auto NGX_SSL_SESSION_TIMEOUT = Line::build<_begin, _escape<_ssl_session_timeout>, _space, _capture<';'>, _end>(); static const auto NGX_LISTEN = Line::build<_begin, _escape<_listen>, _space, _capture<';'>, _end>(); static const auto NGX_PORT_80 = std::array{ R"(^\s*([^:]*:|\[[^\]]*\]:)?80(\s|$|;))", "$01443 ssl$2", }; static const auto NGX_PORT_443 = std::array{ R"(^\s*([^:]*:|\[[^\]]*\]:)?443(\s.*)?\sssl(\s|$|;))", "$0180$2$3", }; // ------------------------- implementation: ---------------------------------- auto get_if_missed(const std::string& conf, const Line& LINE, const std::string& val, const std::string& indent, bool compare) -> std::string { if (!compare || val.empty()) { return rgx::regex_search(conf, LINE.RGX()) ? "" : LINE.STR(val, indent); } rgx::smatch match; // assuming last capture has the value! for (auto pos = conf.begin(); rgx::regex_search(pos, conf.end(), match, LINE.RGX()); pos += match.position(0) + match.length(0)) { const std::string value = match.str(match.size() - 2); if (value == val || value == "'" + val + "'" || value == '"' + val + '"') { return ""; } } return LINE.STR(val, indent); } auto replace_if(const std::string& conf, const rgx::regex& rgx, const std::string& val, const std::string& insert) -> std::string { std::string ret{}; auto pos = conf.begin(); auto skip = 0; for (rgx::smatch match; rgx::regex_search(pos, conf.end(), match, rgx); pos += match.position(match.size() - 1)) { auto i = match.size() - 2; const std::string value = match.str(i); bool compare = !val.empty(); if (compare && value != val && value != "'" + val + "'" && value != '"' + val + '"') { ret.append(pos + skip, pos + match.position(i) + match.length(i)); skip = 0; } else { ret.append(pos + skip, pos + match.position(match.size() > 2 ? 1 : 0)); ret += insert; skip = 1; } } ret.append(pos + skip, conf.end()); return ret; } auto replace_listen(const std::string& conf, const std::array& ngx_port) -> std::string { std::string ret{}; auto pos = conf.begin(); for (rgx::smatch match; rgx::regex_search(pos, conf.end(), match, NGX_LISTEN.RGX()); pos += match.position(match.size() - 1)) { auto i = match.size() - 2; ret.append(pos, pos + match.position(i)); ret += rgx::regex_replace(match.str(i), rgx::regex{ngx_port[0]}, ngx_port[1]); } ret.append(pos, conf.end()); return ret; } inline void add_ssl_directives_to(const std::string& name) { const std::string prefix = std::string{CONF_DIR} + name; const std::string const_conf = read_file(prefix + ".conf"); rgx::smatch match; // captures str(1)=indentation spaces, str(2)=server name for (auto pos = const_conf.begin(); rgx::regex_search(pos, const_conf.end(), match, NGX_SERVER_NAME.RGX()); pos += match.position(0) + match.length(0)) { if (!contains(match.str(2), name)) { continue; } // else: const std::string indent = match.str(1); auto adds = std::string{}; adds += get_if_missed(const_conf, NGX_SSL_CRT, prefix + ".crt", indent); adds += get_if_missed(const_conf, NGX_SSL_KEY, prefix + ".key", indent); adds += get_if_missed(const_conf, NGX_SSL_SESSION_CACHE, SSL_SESSION_CACHE_ARG(name), indent, false); adds += get_if_missed(const_conf, NGX_SSL_SESSION_TIMEOUT, std::string{SSL_SESSION_TIMEOUT_ARG}, indent, false); pos += match.position(0) + match.length(0); std::string conf = std::string(const_conf.begin(), pos) + adds + std::string(pos, const_conf.end()); conf = replace_if(conf, NGX_INCLUDE_LAN_LISTEN_DEFAULT.RGX(), "", NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT.STR("", indent)); conf = replace_if(conf, NGX_INCLUDE_LAN_LISTEN.RGX(), "", NGX_INCLUDE_LAN_SSL_LISTEN.STR("", indent)); conf = replace_listen(conf, NGX_PORT_80); if (conf != const_conf) { write_file(prefix + ".conf", conf); std::cerr << "Added SSL directives to " << prefix << ".conf\n"; } return; } auto errmsg = std::string{"add_ssl_directives_to error: "}; errmsg += "cannot add SSL directives to " + name + ".conf, missing: "; errmsg += NGX_SERVER_NAME.STR(name, "\n ") + "\n"; throw std::runtime_error(errmsg); } template inline auto num2hex(T bytes) -> std::array { constexpr auto n = 2 * sizeof(bytes); std::array str{}; for (size_t i = 0; i < n; ++i) { static const std::array hex{"0123456789ABCDEF"}; static constexpr auto get = 0x0fU; str.at(i) = hex.at(bytes & get); static constexpr auto move = 4U; bytes >>= move; } str[n] = '\0'; return str; } template inline auto get_nonce(const T salt = 0) -> T { T nonce = 0; std::ifstream urandom{"/dev/urandom"}; static constexpr auto move = 6U; constexpr size_t steps = (sizeof(nonce) * 8 - 1) / move + 1; for (size_t i = 0; i < steps; ++i) { if (!urandom.good()) { throw std::runtime_error("get_nonce error"); } nonce = (nonce << move) + static_cast(urandom.get()); } nonce ^= salt; return nonce; } inline void create_ssl_certificate(const std::string& crtpath, const std::string& keypath, const int days = 792) { size_t nonce = 0; try { nonce = get_nonce(nonce); } catch (...) { // the address of a variable should be random enough: // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) sic: nonce += reinterpret_cast(&crtpath); } auto noncestr = num2hex(nonce); const auto tmpcrtpath = crtpath + ".new-" + noncestr.data(); const auto tmpkeypath = keypath + ".new-" + noncestr.data(); try { auto pkey = gen_eckey(NID_secp384r1); write_key(pkey, tmpkeypath); std::string subject{"/C=ZZ/ST=Somewhere/L=None/CN=OpenWrt/O=OpenWrt"}; subject += noncestr.data(); selfsigned(pkey, days, subject, tmpcrtpath); static constexpr auto to_seconds = 24 * 60 * 60; static constexpr auto leeway = 42; if (!checkend(tmpcrtpath, days * to_seconds - leeway)) { throw std::runtime_error("bug: created certificate is not valid!!"); } } catch (...) { std::cerr << "create_ssl_certificate error: "; std::cerr << "cannot create selfsigned certificate, "; std::cerr << "removing temporary files ..." << std::endl; if (remove(tmpcrtpath.c_str()) != 0) { auto errmsg = "\t cannot remove " + tmpcrtpath; perror(errmsg.c_str()); } if (remove(tmpkeypath.c_str()) != 0) { auto errmsg = "\t cannot remove " + tmpkeypath; perror(errmsg.c_str()); } throw; } if (rename(tmpcrtpath.c_str(), crtpath.c_str()) != 0 || rename(tmpkeypath.c_str(), keypath.c_str()) != 0) { auto errmsg = std::string{"create_ssl_certificate warning: "}; errmsg += "cannot move " + tmpcrtpath + " to " + crtpath; errmsg += " or " + tmpkeypath + " to " + keypath + ", continuing ... "; perror(errmsg.c_str()); } std::cerr << "Created self-signed SSL certificate '" << crtpath; std::cerr << "' with key '" << keypath << "'.\n"; } auto check_ssl_certificate(const std::string& crtpath, const std::string& keypath) -> bool { { // paths are relative to dir: auto dir = std::string_view{"/etc/nginx"}; auto crt_rel = crtpath[0] != '/'; auto key_rel = keypath[0] != '/'; if ((crt_rel || key_rel) && (chdir(dir.data()) != 0)) { auto errmsg = std::string{"check_ssl_certificate error: entering "}; errmsg += dir; perror(errmsg.c_str()); errmsg += " (need to change directory since the given "; errmsg += crt_rel ? "ssl_certificate '" + crtpath : std::string{}; errmsg += crt_rel && key_rel ? "' and " : ""; errmsg += key_rel ? "ssl_certificate_key '" + keypath : std::string{}; errmsg += crt_rel && key_rel ? "' are" : "' is a"; errmsg += " relative path"; errmsg += crt_rel && key_rel ? "s)" : ")"; throw std::runtime_error(errmsg); } } constexpr auto remaining_seconds = (365 + 32) * 24 * 60 * 60; constexpr auto validity_days = 3 * (365 + 31); bool is_valid = true; if (access(keypath.c_str(), R_OK) != 0 || access(crtpath.c_str(), R_OK) != 0) { is_valid = false; } else { try { if (!checkend(crtpath, remaining_seconds)) { is_valid = false; } } catch (...) { // something went wrong, maybe it is in DER format: try { if (!checkend(crtpath, remaining_seconds, false)) { is_valid = false; } } catch (...) { // it has neither DER nor PEM format, rebuild. is_valid = false; } } } if (!is_valid) { create_ssl_certificate(crtpath, keypath, validity_days); } return is_valid; } auto contains(const std::string& sentence, const std::string& word) -> bool { auto pos = sentence.find(word); if (pos == std::string::npos) { return false; } if (pos != 0 && (isgraph(sentence[pos - 1]) != 0)) { return false; } if (isgraph(sentence[pos + word.size()]) != 0) { return false; } // else: return true; } auto get_uci_section_for_name(const std::string& name) -> uci::section { auto pkg = uci::package{"nginx"}; // let it throw. auto uci_enabled = is_enabled(pkg); if (uci_enabled) { for (auto sec : pkg) { if (sec.name() == name) { return sec; } } // try interpreting 'name' as FQDN: for (auto sec : pkg) { for (auto opt : sec) { if (opt.name() == "server_name") { for (auto itm : opt) { if (contains(itm.name(), name)) { return sec; } } } } } } auto errmsg = std::string{"lookup error: neither there is a file named '"}; errmsg += std::string{CONF_DIR} + name + ".conf' nor the UCI config has "; if (uci_enabled) { errmsg += "a nginx server with section name or 'server_name': " + name; } else { errmsg += "been enabled by:\n\tuci set nginx.global.uci_enable=true"; } throw std::runtime_error(errmsg); } inline auto add_ssl_to_config(const std::string& name, const std::string_view manage = "self-signed", const std::string_view crt = "", const std::string_view key = "") { auto sec = get_uci_section_for_name(name); // let it throw. auto secname = sec.name(); struct { std::string crt; std::string key; } ret; std::cerr << "Adding SSL directives to UCI server: nginx." << secname << "\n"; std::cerr << "\t" << MANAGE_SSL << "='" << manage << "'\n"; sec.set(MANAGE_SSL.data(), manage.data()); if (!crt.empty() && !key.empty()) { sec.set("ssl_certificate", crt.data()); std::cerr << "\tssl_certificate='" << crt << "'\n"; sec.set("ssl_certificate_key", key.data()); std::cerr << "\tssl_certificate_key='" << key << "'\n"; } auto cache = false; auto timeout = false; for (auto opt : sec) { if (opt.name() == "ssl_session_cache") { cache = true; continue; } // else: if (opt.name() == "ssl_session_timeout") { timeout = true; continue; } // else: for (auto itm : opt) { if (opt.name() == "ssl_certificate_key") { ret.key = itm.name(); } else if (opt.name() == "ssl_certificate") { ret.crt = itm.name(); } else if (opt.name() == "listen") { auto val = regex_replace(itm.name(), rgx::regex{NGX_PORT_80[0]}, NGX_PORT_80[1]); if (val != itm.name()) { std::cerr << "\t" << opt.name() << "='" << val << "' (replacing)\n"; itm.rename(val.c_str()); } } } } if (ret.crt.empty()) { ret.crt = std::string{CONF_DIR} + name + ".crt"; std::cerr << "\tssl_certificate='" << ret.crt << "'\n"; sec.set("ssl_certificate", ret.crt.c_str()); } if (ret.key.empty()) { ret.key = std::string{CONF_DIR} + name + ".key"; std::cerr << "\tssl_certificate_key='" << ret.key << "'\n"; sec.set("ssl_certificate_key", ret.key.c_str()); } if (!cache) { std::cerr << "\tssl_session_cache='" << SSL_SESSION_CACHE_ARG(name) << "'\n"; sec.set("ssl_session_cache", SSL_SESSION_CACHE_ARG(name).data()); } if (!timeout) { std::cerr << "\tssl_session_timeout='" << SSL_SESSION_TIMEOUT_ARG << "'\n"; sec.set("ssl_session_timeout", SSL_SESSION_TIMEOUT_ARG.data()); } sec.commit(); return ret; } void install_cron_job(const Line& CRON_LINE, const std::string& name) { static const char* filename = "/etc/crontabs/root"; std::string conf{}; try { conf = read_file(filename); } catch (const std::ifstream::failure&) { /* is ok if not found, create. */ } const std::string add = get_if_missed(conf, CRON_LINE, name); if (add.length() > 0) { #ifndef NO_UBUS if (!ubus::call("service", "list", UBUS_TIMEOUT).filter("cron")) { std::string errmsg{"install_cron_job error: "}; errmsg += "Cron unavailable to re-create the ssl certificate"; errmsg += (name.empty() ? std::string{"s\n"} : " for '" + name + "'\n"); throw std::runtime_error(errmsg); } // else active with or without instances: #endif const auto* pre = (conf.length() == 0 || conf.back() == '\n' ? "" : "\n"); write_file(filename, pre + std::string{CRON_INTERVAL} + add, std::ios::app); #ifndef NO_UBUS call("/etc/init.d/cron", "reload"); #endif std::cerr << "Rebuild the self-signed SSL certificate"; std::cerr << (name.empty() ? std::string{"s"} : " for '" + name + "'"); std::cerr << " annually with cron." << std::endl; } } void add_ssl_if_needed(const std::string& name) { const auto legacypath = std::string{CONF_DIR} + name + ".conf"; if (access(legacypath.c_str(), R_OK) == 0) { add_ssl_directives_to(name); // let it throw. const auto crtpath = std::string{CONF_DIR} + name + ".crt"; const auto keypath = std::string{CONF_DIR} + name + ".key"; check_ssl_certificate(crtpath, keypath); // let it throw. try { install_cron_job(CRON_CMD, name); } catch (...) { std::cerr << "add_ssl_if_needed warning: cannot use cron to rebuild "; std::cerr << "the self-signed SSL certificate for " << name << "\n"; } return; } // else: auto paths = add_ssl_to_config(name); // let it throw. check_ssl_certificate(paths.crt, paths.key); // let it throw. try { install_cron_job(CRON_CHECK); } catch (...) { std::cerr << "add_ssl_if_needed warning: cannot use cron to rebuild "; std::cerr << "the self-signed SSL certificates.\n"; } } void add_ssl_if_needed(const std::string& name, const std::string_view manage, const std::string_view crt, const std::string_view key) { if (crt[0] != '/') { auto errmsg = std::string{"add_ssl_if_needed error: ssl_certificate "}; errmsg += "path cannot be relative '" + std::string{crt} + "'"; throw std::runtime_error(errmsg); } if (key[0] != '/') { auto errmsg = std::string{"add_ssl_if_needed error: path to ssl_key "}; errmsg += "cannot be relative '" + std::string{key} + "'"; throw std::runtime_error(errmsg); } const auto legacypath = std::string{CONF_DIR} + name + ".conf"; if (access(legacypath.c_str(), R_OK) != 0) { add_ssl_to_config(name, manage, crt, key); // let it throw. return; } // else: // symlink crt+key to the paths that add_ssl_directives_to uses (if needed): auto crtpath = std::string{CONF_DIR} + name + ".crt"; if (crtpath != crt && /* then */ symlink(crt.data(), crtpath.c_str()) != 0) { auto errmsg = std::string{"add_ssl_if_needed error: cannot link "}; errmsg += "ssl_certificate " + crtpath + " -> " + crt.data() + " ("; errmsg += std::to_string(errno) + "): " + std::strerror(errno); throw std::runtime_error(errmsg); } auto keypath = std::string{CONF_DIR} + name + ".key"; if (keypath != key && /* then */ symlink(key.data(), keypath.c_str()) != 0) { auto errmsg = std::string{"add_ssl_if_needed error: cannot link "}; errmsg += "ssl_certificate_key " + keypath + " -> " + key.data() + " ("; errmsg += std::to_string(errno) + "): " + std::strerror(errno); throw std::runtime_error(errmsg); } add_ssl_directives_to(name); // let it throw. } void remove_cron_job(const Line& CRON_LINE, const std::string& name) { static const char* filename = "/etc/crontabs/root"; const auto const_conf = read_file(filename); bool changed = false; auto conf = std::string{}; size_t prev = 0; size_t curr = 0; while ((curr = const_conf.find('\n', prev)) != std::string::npos) { auto line = const_conf.substr(prev, curr - prev + 1); if (line == replace_if(line, CRON_LINE.RGX(), name, "")) { conf += line; } else { changed = true; } prev = curr + 1; } if (changed) { write_file(filename, conf); std::cerr << "Do not rebuild the self-signed SSL certificate"; std::cerr << (name.empty() ? std::string{"s"} : " for '" + name + "'"); std::cerr << " annually with cron anymore." << std::endl; #ifndef NO_UBUS if (ubus::call("service", "list", UBUS_TIMEOUT).filter("cron")) { call("/etc/init.d/cron", "reload"); } #endif } } inline void del_ssl_directives_from(const std::string& name) { const std::string prefix = std::string{CONF_DIR} + name; const std::string const_conf = read_file(prefix + ".conf"); rgx::smatch match; // captures str(1)=indentation spaces, str(2)=server name for (auto pos = const_conf.begin(); rgx::regex_search(pos, const_conf.end(), match, NGX_SERVER_NAME.RGX()); pos += match.position(0) + match.length(0)) { if (!contains(match.str(2), name)) { continue; } // else: const std::string indent = match.str(1); std::string conf = const_conf; conf = replace_listen(conf, NGX_PORT_443); conf = replace_if(conf, NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT.RGX(), "", NGX_INCLUDE_LAN_LISTEN_DEFAULT.STR("", indent)); conf = replace_if(conf, NGX_INCLUDE_LAN_SSL_LISTEN.RGX(), "", NGX_INCLUDE_LAN_LISTEN.STR("", indent)); // NOLINTNEXTLINE(performance-inefficient-string-concatenation) prefix: conf = replace_if(conf, NGX_SSL_CRT.RGX(), prefix + ".crt", ""); // NOLINTNEXTLINE(performance-inefficient-string-concatenation) prefix: conf = replace_if(conf, NGX_SSL_KEY.RGX(), prefix + ".key", ""); conf = replace_if(conf, NGX_SSL_SESSION_CACHE.RGX(), "", ""); conf = replace_if(conf, NGX_SSL_SESSION_TIMEOUT.RGX(), "", ""); if (conf != const_conf) { write_file(prefix + ".conf", conf); std::cerr << "Deleted SSL directives from " << prefix << ".conf\n"; } return; } auto errmsg = std::string{"del_ssl_directives_from error: "}; errmsg += "cannot delete SSL directives from " + name + ".conf, missing: "; errmsg += NGX_SERVER_NAME.STR(name, "\n ") + "\n"; throw std::runtime_error(errmsg); } inline auto del_ssl_from_config(const std::string& name, const std::string_view manage = "self-signed") { auto sec = get_uci_section_for_name(name); // let it throw. auto secname = sec.name(); struct { std::string crt; std::string key; } ret; std::cerr << "Deleting SSL directives from UCI server: nginx." << secname << "\n"; auto manage_match = false; for (auto opt : sec) { for (auto itm : opt) { if (opt.name() == "ssl_certificate_key") { ret.key = itm.name(); } else if (opt.name() == "ssl_certificate") { ret.crt = itm.name(); } else if (opt.name() == "ssl_session_cache" || opt.name() == "ssl_session_timeout") { } else if (opt.name() == MANAGE_SSL && itm.name() == manage) { manage_match = true; } else if (opt.name() == "listen") { auto val = regex_replace(itm.name(), rgx::regex{NGX_PORT_443[0]}, NGX_PORT_443[1]); if (val != itm.name()) { std::cerr << "\t" << opt.name() << " (set back to '" << val << "')\n"; itm.rename(val.c_str()); } continue; /* not deleting opt, look at other itm : opt */ } else { continue; /* not deleting opt, look at other itm : opt */ } // Delete matching opt (not skipped by continue): std::cerr << "\t" << opt.name() << " (was '" << itm.name() << "')\n"; opt.del(); break; } } if (manage_match) { sec.commit(); return ret; } // else: auto errmsg = std::string{"del_ssl error: not changing config wihtout: "}; errmsg += "uci set nginx." + secname + "." + MANAGE_SSL.data() + "='" + manage.data(); errmsg += "'"; throw std::runtime_error(errmsg); } auto del_ssl_legacy(const std::string& name) -> bool { const auto legacypath = std::string{CONF_DIR} + name + ".conf"; if (access(legacypath.c_str(), R_OK) != 0) { return false; } try { remove_cron_job(CRON_CMD, name); } catch (...) { std::cerr << "del_ssl warning: cannot remove cron job rebuilding "; std::cerr << "the self-signed SSL certificate for " << name << "\n"; } try { del_ssl_directives_from(name); } catch (...) { std::cerr << "del_ssl error: "; std::cerr << "cannot delete SSL directives from " << name << ".conf\n"; throw; } return true; } void del_ssl(const std::string& name) { auto crtpath = std::string{}; auto keypath = std::string{}; if (del_ssl_legacy(name)) { // let it throw. crtpath = std::string{CONF_DIR} + name + ".crt"; keypath = std::string{CONF_DIR} + name + ".key"; } else { auto paths = del_ssl_from_config(name); // let it throw. crtpath = paths.crt; keypath = paths.key; } if (remove(crtpath.c_str()) != 0) { auto errmsg = "del_ssl warning: cannot remove " + crtpath; perror(errmsg.c_str()); } if (remove(keypath.c_str()) != 0) { auto errmsg = "del_ssl warning: cannot remove " + keypath; perror(errmsg.c_str()); } } void del_ssl(const std::string& name, const std::string_view manage) { const auto legacypath = std::string{CONF_DIR} + name + ".conf"; if (access(legacypath.c_str(), R_OK) != 0) { del_ssl_from_config(name, manage); // let it throw. return; } // else: del_ssl_directives_from(name); // let it throw. for (const auto* ext : {".crt", ".key"}) { struct stat sb {}; auto path = std::string{CONF_DIR} + name + ext; // managed version of add_ssl_if_needed created symlinks (if needed): // NOLINTNEXTLINE(hicpp-signed-bitwise) S_ISLNK macro: if (lstat(path.c_str(), &sb) == 0 && S_ISLNK(sb.st_mode)) { if (remove(path.c_str()) != 0) { auto errmsg = "del_ssl warning: cannot remove " + path; perror(errmsg.c_str()); } } } } auto check_ssl(const uci::package& pkg, bool is_enabled) -> bool { auto are_valid = true; auto is_enabled_and_at_least_one_has_manage_ssl = false; if (is_enabled) { for (auto sec : pkg) { if (sec.anonymous() || sec.type() != "server") { continue; } // else: const auto legacypath = std::string{CONF_DIR} + sec.name() + ".conf"; if (access(legacypath.c_str(), R_OK) == 0) { continue; } // else: auto keypath = std::string{}; auto crtpath = std::string{}; auto self_signed = false; for (auto opt : sec) { for (auto itm : opt) { if (opt.name() == "ssl_certificate_key") { keypath = itm.name(); } else if (opt.name() == "ssl_certificate") { crtpath = itm.name(); } else if (opt.name() == MANAGE_SSL) { if (itm.name() == "self-signed") { self_signed = true; } // else if (itm.name()=="???") { /* manage other */ } else { continue; } // no supported manage_ssl string. is_enabled_and_at_least_one_has_manage_ssl = true; } } } if (self_signed && !crtpath.empty() && !keypath.empty()) { try { if (!check_ssl_certificate(crtpath, keypath)) { are_valid = false; } } catch (...) { std::cerr << "check_ssl warning: cannot build certificate '"; std::cerr << crtpath << "' or key '" << keypath << "'.\n"; } } } } auto suffix = std::string_view{" the cron job checking the managed SSL certificates.\n"}; if (is_enabled_and_at_least_one_has_manage_ssl) { try { install_cron_job(CRON_CHECK); } catch (...) { std::cerr << "check_ssl warning: cannot install" << suffix; } } else if (access("/etc/crontabs/root", R_OK) == 0) { try { remove_cron_job(CRON_CHECK); } catch (...) { std::cerr << "check_ssl warning: cannot remove" << suffix; } } // else: do nothing return are_valid; } #endif