1
0
mirror of https://git.openwrt.org/feed/packages.git synced 2024-06-17 12:53:54 +02:00
openwrt-packages/net/nginx-util/src/nginx-ssl-util.hpp
Peter Stadler 49a84e9b22 nginx-util: do not use fallthrough attribute
fixes issue #15653

Signed-off-by: Peter Stadler <peter.stadler@student.uibk.ac.at>
2021-05-24 21:27:25 +02:00

1125 lines
36 KiB
C++

#ifndef __NGINX_SSL_UTIL_HPP
#define __NGINX_SSL_UTIL_HPP
#ifdef NO_PCRE
#include <regex>
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<std::string (*)(const std::string&, const std::string&), 2>;
class Line {
private:
_Line _line;
public:
explicit Line(const _Line& line) noexcept : _line{line} {}
template <const _Line&... xn>
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<const char*, 2>& 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 <char clim = '\0'>
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 <const std::string_view& strptr, char clim = '\0'>
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<NGINX_UTIL>, _space, _escape<_check_ssl, '\''>, _newline>();
static const auto CRON_CMD = Line::build<_space,
_escape<NGINX_UTIL>,
_space,
_escape<ADD_SSL_FCT, '\''>,
_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<LAN_LISTEN, '\''>, _end>();
static const auto NGX_INCLUDE_LAN_LISTEN_DEFAULT =
Line::build<_begin, _escape<_include>, _space, _escape<LAN_LISTEN_DEFAULT, '\''>, _end>();
static const auto NGX_INCLUDE_LAN_SSL_LISTEN =
Line::build<_begin, _escape<_include>, _space, _escape<LAN_SSL_LISTEN, '\''>, _end>();
static const auto NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT =
Line::build<_begin, _escape<_include>, _space, _escape<LAN_SSL_LISTEN_DEFAULT, '\''>, _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<const char*, 2>{
R"(^\s*([^:]*:|\[[^\]]*\]:)?80(\s|$|;))",
"$01443 ssl$2",
};
static const auto NGX_PORT_443 = std::array<const char*, 2>{
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<const char*, 2>& 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 <typename T>
inline auto num2hex(T bytes) -> std::array<char, 2 * sizeof(bytes) + 1>
{
constexpr auto n = 2 * sizeof(bytes);
std::array<char, n + 1> str{};
for (size_t i = 0; i < n; ++i) {
static const std::array<char, 17> 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 <typename T>
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<unsigned>(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<size_t>(&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