packages/net/nginx-util/src/nginx-ssl-util.hpp
Peter Stadler f62599d27e nginx-util: use UCI for server configuration
**tl;dr:** The functions `{add,del}_ssl` modify a server
section of the UCI config if there is no `.conf` file with
the same name in `/etc/nginx/conf.d/`.

Then `init_lan` creates `/var/lib/nginx/uci.conf` files by
copying the `/etc/nginx/uci.conf.template` and standard
options from the UCI config; additionally the special path
`logd` can be used in `{access,error}_log`.

The init does not change the configuration beside
re-creating self-signed certificates when needed. This is
also the only purpose of the new `check_ssl`, which is
installed as yearly cron job.

**Initialization:**

Invoking `nginx-util init_lan` parses the UCI configuration
for package `nginx`. It creates a server part in
`/var/lib/nginx/uci.conf` for each `section server '$name'`
by copying all UCI options but the following:

* `option uci_manage_ssl` is skipped. It is set to
'self-signed' by `nginx-util add_ssl $name`, removed by
`nginx-util del_ssl $name` and used by
`nginx-util check_ssl` (see below).

* `logd` as path in `error_log` or `access_log` writes them
to STDERR respective STDOUT, which are fowarded by Nginx's
init to the log daemon. Specifically:
`option error_log 'logd'` becomes `error_log stderr;` and
`option access_log 'logd openwrt'` becomes
`access_log /proc/self/fd/1 openwrt;`

Other `[option|list] key 'value'` entries just become
`key value;` directives.

The init.d calls internally also `check_ssl` for rebuilding
self-signed SSL certificates if needed (see below). And it
still sets up `/var/lib/nginx/lan{,_ssl}.listen` files as
it is doing in the current version (so they stay available).

**Defaults:**

The package installs the file `/etc/nginx/restrict_locally`
containing allow/deny directives for restricting the access
to LAN addresses by including it into a server part. The
default server '_lan' includes this file and listens on all
IPs (instead of only the local IPs as it did before; other
servers do not need to listen explicitly on the local IPs
anymore). The default server is contained together with a
server that redirects HTTP requests for inexistent URLs to
HTTPS in the UCI configuration file `/etc/config/nginx`.
Furthermore, the packages installs a
`/etc/nginx/uci.conf.template` containing the current setup
and a marker, which will be replaced by the created UCI
servers when calling `init_lan`.

**Other:**

If there is a file named `/etc/nginx/conf.d/$name.conf` the
functions `init_lan`, `add_ssl $name` and `del_ssl $name`
will use that file instead of a UCI server section (this is
similar to the current version).

Else it selects the UCI `section server $name`, or, when
there is no such section, it searches for the first one
having `option server_name '… $name …'`. For this section:

* `nginx-util add_ssl $name` will add to it:
`option uci_manage_ssl 'self-signed'`
`option ssl_certificate '/etc/nginx/conf.d/$name.crt'`
`option ssl_certificate_key '/etc/nginx/conf.d/$name.key'`
`option ssl_session_cache 'shared:SSL:32k'`
`option ssl_session_timeout '64m'`
If these options are already present, they will stay the
same; just the first option `uci_manage_ssl` will always be
changed to 'self-signed'. The command also changes all
`listen` list items to use port 443 and ssl instead of port
80 (without ssl). If they stated another port than 80
before, they are kept the same. Furthermore, it creates a
self-signed SSL certificate if necessary, i.e., if there is
no *valid* certificate and key at the locations given by
the options `ssl_certificate` and `ssl_certificate_key`.

* `nginx-util del_ssl $name` checks if `uci_manage_ssl` is
set 'self-signed' in the corresponding UCI section. Only
then it removes all of the above options regardless of the
value looking just at the key name. Then, it also changes
all `listen` list items to use port 80 (without ssl)
instead of port 443 with ssl. If stating another port than
443, they are kept the same. Furthermore, it removes the
SSL certificate and key that were indicated by
`ssl_certificate{,_key}`.

* `nginx-util check_ssl` looks through all server sections
of the UCI config for `uci_manage_ssl 'self-signed'`. On
every hit it checks if the SSL certificate-key-pair
indicated by the options `ssl_certificate{,_key}` is
expired. Then it re-creates a self-signed certificate.
If there exists at least one `section server` with
`uci_manage_ssl 'self-signed'`, it will try to install
itself as cron job. If there are no such sections, it
removes that cron job if possible.

For installing a ssl certificate and key managed by
another app, you can call:
`nginx-util add_ssl $name $manager $crtpath $keypath`
Hereby `$name` is as above, `$manager` is an arbitrary
string, and the the ssl certificate and its key are
indicated by their absolute path. If you want to remove
the directives again, then you can use:
`nginx-util del_ssl $name $manager`

Signed-off-by: Peter Stadler <peter.stadler@student.uibk.ac.at>
2020-11-28 18:34:39 +01:00

1119 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 += '\\'; [[fallthrough]];
case '_': [[fallthrough]];
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