1124 lines
36 KiB
C++
1124 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
|