nginx-util: add package 10960/head
authorPeter Stadler <peter.stadler@student.uibk.ac.at>
Thu, 5 Dec 2019 07:48:20 +0000 (08:48 +0100)
committerPeter Stadler <peter.stadler@student.uibk.ac.at>
Fri, 17 Jan 2020 00:11:17 +0000 (01:11 +0100)
This can do the main work of nginx/nginx-ssl init script.
For nginx-ssl it can create selfsigned certificates, too.
It uses libpcre and libopenssl iff nginx(-ssl) uses them.

Signed-off-by: Peter Stadler <peter.stadler@student.uibk.ac.at>
net/nginx-util/Makefile [new file with mode: 0644]
net/nginx-util/src/CMakeLists.txt [new file with mode: 0644]
net/nginx-util/src/nginx-ssl-util.cpp [new file with mode: 0644]
net/nginx-util/src/nginx-util.cpp [new file with mode: 0644]
net/nginx-util/src/nginx-util.hpp [new file with mode: 0644]
net/nginx-util/src/px5g-openssl.hpp [new file with mode: 0644]
net/nginx-util/src/px5g.cpp [new file with mode: 0644]
net/nginx-util/src/regex-pcre.hpp [new file with mode: 0644]
net/nginx-util/src/test-px5g.sh [new file with mode: 0755]
net/nginx-util/src/ubus-cxx.hpp [new file with mode: 0644]

diff --git a/net/nginx-util/Makefile b/net/nginx-util/Makefile
new file mode 100644 (file)
index 0000000..4730b2d
--- /dev/null
@@ -0,0 +1,70 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=nginx-util
+PKG_VERSION:=1.0
+PKG_RELEASE:=1
+
+include $(INCLUDE_DIR)/package.mk
+include $(INCLUDE_DIR)/cmake.mk
+
+define Package/nginx-util
+  SECTION:=net
+  CATEGORY:=Network
+  SUBMENU:=Web Servers/Proxies
+  TITLE:=Builder of LAN listen directives for Nginx
+  DEPENDS:=+libstdcpp +libubus +libubox +libpthread
+  PROVIDES:=nginx-util
+endef
+
+define Package/nginx-ssl-util/default
+  $(Package/nginx-util)
+  TITLE+= and manager of its SSL certificates
+  DEPENDS+=+libopenssl
+endef
+
+define Package/nginx-ssl-util-nopcre
+  $(Package/nginx-ssl-util/default)
+  TITLE+= (using <regex>)
+endef
+
+define Package/nginx-ssl-util
+  $(Package/nginx-ssl-util/default)
+  TITLE+= (using PCRE)
+  DEPENDS+=+libpcre
+endef
+
+define Package/nginx-util/description
+  Utility that builds dynamically LAN listen directives for Nginx.
+endef
+
+Package/nginx-ssl-util/default/description = $(Package/nginx-util/description)\
+  Furthermore, it manages SSL directives for its server parts and can create \
+  corresponding (self-signed) certificates.
+
+Package/nginx-ssl-util/description = \
+  $(Package/nginx-ssl-util/default/description) \
+  It uses the PCRE library for performance.
+
+Package/nginx-ssl-util-nopcre/description = \
+  $(Package/nginx-ssl-util/default/description) \
+  It uses the standard regex library of C++.
+
+define Package/nginx-util/install
+       $(INSTALL_DIR) $(1)/usr/bin
+       $(INSTALL_BIN) $(PKG_BUILD_DIR)/nginx-util $(1)/usr/bin/nginx-util
+endef
+
+define Package/nginx-ssl-util/install
+       $(INSTALL_DIR) $(1)/usr/bin
+       $(INSTALL_BIN) $(PKG_BUILD_DIR)/nginx-ssl-util $(1)/usr/bin/nginx-util
+endef
+
+define Package/nginx-ssl-util-nopcre/install
+       $(INSTALL_DIR) $(1)/usr/bin
+       $(INSTALL_BIN) $(PKG_BUILD_DIR)/nginx-ssl-util-nopcre \
+               $(1)/usr/bin/nginx-util
+endef
+
+$(eval $(call BuildPackage,nginx-util))
+$(eval $(call BuildPackage,nginx-ssl-util))
+$(eval $(call BuildPackage,nginx-ssl-util-nopcre))
diff --git a/net/nginx-util/src/CMakeLists.txt b/net/nginx-util/src/CMakeLists.txt
new file mode 100644 (file)
index 0000000..d78ec5d
--- /dev/null
@@ -0,0 +1,38 @@
+cmake_minimum_required(VERSION 2.6)
+
+PROJECT(nginx-util CXX)
+
+INCLUDE(CheckFunctionExists)
+
+FIND_PATH(ubus_include_dir libubus.h)
+FIND_LIBRARY(ubox NAMES ubox)
+FIND_LIBRARY(ubus NAMES ubus)
+INCLUDE_DIRECTORIES(${ubus_include_dir})
+
+ADD_DEFINITIONS(-Os -Wall -Werror -Wextra --std=c++17 -g3)
+ADD_DEFINITIONS(-Wno-unused-parameter -Wmissing-declarations)
+
+SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "")
+
+ADD_EXECUTABLE(px5g px5g.cpp)
+TARGET_LINK_LIBRARIES(px5g ssl crypto)
+
+ADD_EXECUTABLE(nginx-util nginx-util.cpp)
+TARGET_LINK_LIBRARIES(nginx-util ${ubox} ${ubus} pthread)
+
+ADD_EXECUTABLE(nginx-ssl-util nginx-ssl-util.cpp)
+TARGET_LINK_LIBRARIES(nginx-ssl-util ${ubox} ${ubus} pthread ssl crypto pcre)
+
+ADD_EXECUTABLE(nginx-ssl-util-nopcre nginx-ssl-util.cpp)
+TARGET_COMPILE_DEFINITIONS(nginx-ssl-util-nopcre PUBLIC -DNO_PCRE)
+TARGET_LINK_LIBRARIES(nginx-ssl-util-nopcre ${ubox} ${ubus} pthread ssl crypto)
+
+ADD_EXECUTABLE(nginx-ssl-util-noubus nginx-ssl-util.cpp)
+TARGET_COMPILE_DEFINITIONS(nginx-ssl-util-noubus PUBLIC -DNO_UBUS)
+TARGET_LINK_LIBRARIES(nginx-ssl-util-noubus pthread ssl crypto pcre)
+
+INSTALL(TARGETS px5g RUNTIME DESTINATION bin)
+INSTALL(TARGETS nginx-util RUNTIME DESTINATION bin)
+INSTALL(TARGETS nginx-ssl-util RUNTIME DESTINATION bin)
+INSTALL(TARGETS nginx-ssl-util-nopcre RUNTIME DESTINATION bin)
+# INSTALL(TARGETS nginx-ssl-util-noubus RUNTIME DESTINATION bin)
diff --git a/net/nginx-util/src/nginx-ssl-util.cpp b/net/nginx-util/src/nginx-ssl-util.cpp
new file mode 100644 (file)
index 0000000..f4a8573
--- /dev/null
@@ -0,0 +1,658 @@
+#include <thread>
+
+#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 =
+    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 delete_if(const std::string & conf, const rgx::regex & rgx,
+               const std::string & val="", bool compare=false)
+    -> std::string;
+
+
+void add_ssl_directives_to(const std::string & name, bool isdefault);
+
+
+void create_ssl_certificate(const std::string & crtpath,
+                            const std::string & keypath,
+                            int days=792);
+
+
+void use_cron_to_recreate_certificate(const std::string & name);
+
+
+void add_ssl_if_needed(const std::string & name);
+
+
+void del_ssl_directives_from(const std::string & name, bool isdefault);
+
+
+void del_ssl(const std::string & name);
+
+
+static 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*))"; }
+};
+
+
+static 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+)"; }
+};
+
+
+static 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"}; }
+};
+
+
+static 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*;)"}; }
+};
+
+
+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 + 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+"\""+")";
+    }
+};
+
+
+static constexpr std::string_view _server_name = "server_name";
+
+static constexpr std::string_view _include = "include";
+
+static constexpr std::string_view _ssl_certificate = "ssl_certificate";
+
+static constexpr std::string_view _ssl_certificate_key = "ssl_certificate_key";
+
+static constexpr std::string_view _ssl_session_cache = "ssl_session_cache";
+
+static 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_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>();
+
+
+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_view value = match.str(match.size() - 1);
+
+        if (value==val || value=="'"+val+"'" || value=='"'+val+'"') {
+            return "";
+        }
+    }
+
+    return LINE.STR(val, indent);
+}
+
+
+auto delete_if(const std::string & conf, const rgx::regex & rgx,
+               const std::string & val, const bool compare)
+    -> std::string
+{
+    std::string ret{};
+    auto pos = conf.begin();
+
+    for (rgx::smatch match;
+         rgx::regex_search(pos, conf.end(), match, rgx);
+         pos += match.position(0) + match.length(0))
+    {
+        const std::string_view value = match.str(match.size() - 1);
+        
+        auto skip = 1; // one for delimiter!
+        if (compare && value!=val && value!="'"+val+"'" && value!='"'+val+'"') {
+            skip = match.length(0);
+        }
+        ret.append(pos, pos + match.position(0) + skip);
+    }
+
+    ret.append(pos, conf.end());
+    return ret;
+}
+
+
+void add_ssl_directives_to(const std::string & name, const bool isdefault)
+{
+    const std::string prefix = std::string{CONF_DIR} + name;
+
+    std::string conf = read_file(prefix+".conf");
+
+    const std::string & const_conf = conf; // iteration needs const string.
+    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 (match.str(2).find(name) == std::string::npos) { continue; }
+
+        const std::string indent = match.str(1);
+
+        std::string adds = isdefault ?
+            get_if_missed(conf, NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT,"",indent) :
+            get_if_missed(conf, NGX_INCLUDE_LAN_SSL_LISTEN, "", indent);
+
+        adds += get_if_missed(conf, NGX_SSL_CRT, prefix+".crt", indent);
+
+        adds += get_if_missed(conf, NGX_SSL_KEY, prefix+".key", indent);
+
+        adds += get_if_missed(conf, NGX_SSL_SESSION_CACHE,
+                              SSL_SESSION_CACHE_ARG(name), indent, false);
+
+        adds += get_if_missed(conf, NGX_SSL_SESSION_TIMEOUT,
+                        std::string{SSL_SESSION_TIMEOUT_ARG}, indent, false);
+
+        if (adds.length() > 0) {
+            pos += match.position(0) + match.length(0);
+
+            conf = std::string(const_conf.begin(), pos) + adds +
+                    std::string(pos, const_conf.end());
+
+            conf = isdefault ?
+                delete_if(conf, NGX_INCLUDE_LAN_LISTEN_DEFAULT.RGX()) :
+                delete_if(conf, NGX_INCLUDE_LAN_LISTEN.RGX());
+
+            write_file(prefix+".conf", conf);
+
+            std::cerr<<"Added SSL directives to "<<prefix<<".conf: ";
+            std::cerr<<adds<<std::endl;
+        }
+
+        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.c_str());
+}
+
+
+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;
+}
+
+
+void create_ssl_certificate(const std::string & crtpath,
+                            const std::string & keypath,
+                            const int days)
+{
+    size_t nonce = 0;
+
+    try { nonce = get_nonce(nonce); }
+
+    catch (...) { // the address of a variable should be random enough:
+        auto addr = &crtpath;
+        auto addrptr = static_cast<const size_t *>(
+                        static_cast<const void *>(&addr) );
+        nonce += *addrptr;
+    }
+
+    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());
+    }
+
+}
+
+
+void use_cron_to_recreate_certificate(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_CMD, name);
+
+    if (add.length() > 0) {
+#ifndef NO_UBUS
+        auto service = ubus::call("service","list",UBUS_TIMEOUT).filter("cron");
+
+        if (!service) {
+            std::string errmsg{"use_cron_to_recreate_certificate error: "};
+            errmsg += "Cron unavailable to re-create the ssl certificate for ";
+            errmsg += name + "\n";
+            throw std::runtime_error(errmsg.c_str());
+        } // else active with or without instances:
+#endif
+
+        write_file(filename, std::string{CRON_INTERVAL}+add, std::ios::app);
+
+#ifndef NO_UBUS
+        call("/etc/init.d/cron", "reload");
+#endif
+
+        std::cerr<<"Rebuild the ssl certificate for '";
+        std::cerr<<name<<"' annually with cron."<<std::endl;
+    }
+}
+
+
+void add_ssl_if_needed(const std::string & name)
+{
+    try { add_ssl_directives_to(name, name==LAN_NAME); }
+    catch (...) {
+        std::cerr<<"add_ssl_if_needed error: ";
+        std::cerr<<"cannot add SSL directives to "<<name<<".conf"<<std::endl;
+        throw;
+    }
+
+    const auto crtpath = std::string{CONF_DIR} + name + ".crt";
+    const auto keypath = std::string{CONF_DIR} + name + ".key";
+    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); }
+
+    try { use_cron_to_recreate_certificate(name); }
+    catch (...) {
+        std::cerr<<"add_ssl_if_needed warning: ";
+        std::cerr<<"cannot use cron to rebuild certificate for "<<name<<"\n";
+    }
+}
+
+
+void del_ssl_directives_from(const std::string & name, const bool isdefault)
+{
+    const std::string prefix = std::string{CONF_DIR} + name;
+
+    std::string conf = read_file(prefix+".conf");
+
+    const std::string & const_conf = conf; // iteration needs const string.
+    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 (match.str(2).find(name) == std::string::npos) { continue; }
+
+        const std::string indent = match.str(1);
+
+        std::string adds = isdefault ?
+            get_if_missed(conf, NGX_INCLUDE_LAN_LISTEN_DEFAULT,"",indent) :
+            get_if_missed(conf, NGX_INCLUDE_LAN_LISTEN, "", indent);
+
+        if (adds.length() > 0) {
+            pos += match.position(0) + 1;
+
+            conf = std::string(const_conf.begin(), pos) + adds
+                    + std::string(pos, const_conf.end());
+
+            conf = isdefault ?
+                delete_if(conf, NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT.RGX())
+                : delete_if(conf, NGX_INCLUDE_LAN_SSL_LISTEN.RGX());
+
+            const auto crtpath = prefix+".crt";
+            conf = delete_if(conf, NGX_SSL_CRT.RGX(), crtpath, true);
+
+            const auto keypath = prefix+".key";
+            conf = delete_if(conf, NGX_SSL_KEY.RGX(), keypath, true);
+
+            conf = delete_if(conf, NGX_SSL_SESSION_CACHE.RGX());
+
+            conf = delete_if(conf, NGX_SSL_SESSION_TIMEOUT.RGX());
+
+            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.c_str());
+}
+
+
+void del_ssl(const std::string & name)
+{
+    static const char * filename = "/etc/crontabs/root";
+
+    try {
+        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);
+
+            line = delete_if(line, CRON_CMD.RGX(), std::string{name}, true);
+
+            if (line.substr(0,line.size()-1)==CRON_INTERVAL) { changed = true; }
+            else { conf += line; }
+
+            prev = curr + 1;
+        }
+
+        if (changed) {
+            write_file(filename, conf);
+
+            std::cerr<<"Do not rebuild the ssl certificate for '";
+            std::cerr<<name<<"' 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
+        }
+
+    } catch (...) {
+        std::cerr<<"del_ssl warning: ";
+        std::cerr<<"cannot delete cron job for "<<name<<" in "<<filename<<"\n";
+    }
+
+    try { del_ssl_directives_from(name, name==LAN_NAME); }
+    catch (...) {
+        std::cerr<<"del_ssl error: ";
+        std::cerr<<"cannot delete SSL directives from "<<name<<".conf\n";
+        throw;
+    }
+
+    const auto crtpath = std::string{CONF_DIR} + name + ".crt";
+
+    if (remove(crtpath.c_str())!=0) {
+        auto errmsg = "del_ssl warning: cannot remove "+crtpath;
+        perror(errmsg.c_str());
+    }
+
+    const auto keypath = std::string{CONF_DIR} + name + ".key";
+
+    if (remove(keypath.c_str())!=0) {
+        auto errmsg = "del_ssl warning: cannot remove "+keypath;
+        perror(errmsg.c_str());
+    }
+}
+
+
+// reuse main(...) and common functions:
+#define NGINX_OPENSSL
+#include "nginx-util.cpp"
diff --git a/net/nginx-util/src/nginx-util.cpp b/net/nginx-util/src/nginx-util.cpp
new file mode 100644 (file)
index 0000000..7e67598
--- /dev/null
@@ -0,0 +1,157 @@
+// This file is included in nginx-ssl-util.cpp, which defines NGINX_OPENSSL.
+#ifndef __NGINX_UTIL_C
+#define __NGINX_UTIL_C
+
+#include "nginx-util.hpp"
+
+
+void create_lan_listen()
+{
+    std::string listen = "# This file is re-created if Nginx starts or"
+                    " a LAN address changes.\n";
+    std::string listen_default = listen;
+    std::string ssl_listen = listen;
+    std::string ssl_listen_default = listen;
+
+    auto add_listen = [&listen, &listen_default
+#ifdef NGINX_OPENSSL
+                       ,&ssl_listen, &ssl_listen_default
+#endif
+                      ]
+        (const std::string &pre, const std::string &ip, const std::string &suf)
+        -> void
+    {
+        if (ip.empty()) { return; }
+        const std::string val = pre + ip + suf;
+        listen += "\tlisten " + val + ":80;\n";
+        listen_default += "\tlisten " + val + ":80 default_server;\n";
+#ifdef NGINX_OPENSSL
+        ssl_listen += "\tlisten " + val + ":443 ssl;\n";
+        ssl_listen_default += "\tlisten " + val + ":443 ssl default_server;\n";
+#endif
+    };
+
+    add_listen("", "127.0.0.1", "");
+    add_listen("[", "::1", "]");
+
+#ifndef NO_UBUS
+    auto lan_status = ubus::call("network.interface.lan", "status");
+
+    for (auto ip : lan_status.filter("ipv4-address", "", "address")) {
+        add_listen("",  static_cast<const char *>(blobmsg_data(ip)), "");
+    }
+
+    for (auto ip : lan_status.filter("ipv6-address", "", "address")) {
+        add_listen("[", static_cast<const char *>(blobmsg_data(ip)), "]");
+    }
+#endif
+
+    write_file(LAN_LISTEN, listen);
+    write_file(LAN_LISTEN_DEFAULT, listen_default);
+#ifdef NGINX_OPENSSL
+    write_file(LAN_SSL_LISTEN, ssl_listen);
+    write_file(LAN_SSL_LISTEN_DEFAULT, ssl_listen_default);
+#endif
+}
+
+
+void init_lan()
+{
+    std::exception_ptr ex;
+
+#ifdef NGINX_OPENSSL
+    auto thrd = std::thread([&ex]{
+       try { add_ssl_if_needed(std::string{LAN_NAME}); }
+        catch (...) {
+            std::cerr<<"init_lan error: cannot add SSL for "<<LAN_NAME<<std::endl;
+            ex = std::current_exception();
+        }
+    });
+#endif
+
+    try { create_lan_listen(); }
+    catch (...) {
+        std::cerr<<"init_lan error: cannot create LAN listen directives"<<std::endl;
+        ex = std::current_exception();
+    }
+
+#ifdef NGINX_OPENSSL
+    thrd.join();
+#endif
+
+    if (ex) { std::rethrow_exception(ex); }
+}
+
+
+void get_env()
+{
+    std::cout<<"NGINX_CONF="<<"'"<<NGINX_CONF<<"'"<<std::endl;
+    std::cout<<"CONF_DIR="<<"'"<<CONF_DIR<<"'"<<std::endl;
+    std::cout<<"LAN_NAME="<<"'"<<LAN_NAME<<"'"<<std::endl;
+    std::cout<<"LAN_LISTEN="<<"'"<<LAN_LISTEN<<"'"<<std::endl;
+#ifdef NGINX_OPENSSL
+    std::cout<<"LAN_SSL_LISTEN="<<"'"<<LAN_SSL_LISTEN<<"'"<<std::endl;
+    std::cout<<"SSL_SESSION_CACHE_ARG="<<"'"<<LAN_NAME<<"'"<<std::endl;
+    std::cout<<"SSL_SESSION_TIMEOUT_ARG="<<"'"<<SSL_SESSION_TIMEOUT_ARG<<"'\n";
+    std::cout<<"ADD_SSL_FCT="<<"'"<<ADD_SSL_FCT<<"'"<<std::endl;
+#endif
+}
+
+
+auto main(int argc, char * argv[]) -> int
+{
+    // TODO(pst): use std::span when available:
+    auto args = std::basic_string_view{argv, static_cast<size_t>(argc)};
+
+    auto cmds = std::array{
+        std::array<std::string_view, 2>{"init_lan", ""},
+        std::array<std::string_view, 2>{"get_env", ""},
+#ifdef NGINX_OPENSSL
+        std::array<std::string_view, 2>{ADD_SSL_FCT, " server_name" },
+        std::array<std::string_view, 2>{"del_ssl", " server_name" },
+#endif
+    };
+
+    try {
+
+        if (argc==2 && args[1]==cmds[0][0]) { init_lan(); }
+
+        else if (argc==2 && args[1]==cmds[1][0]) { get_env(); }
+
+#ifdef NGINX_OPENSSL
+        else if (argc==3 && args[1]==cmds[2][0])
+        { add_ssl_if_needed(std::string{args[2]});}
+
+        else if (argc==3 && args[1]==cmds[3][0])
+        { del_ssl(std::string{args[2]}); }
+
+        else if (argc==2 && args[1]==cmds[3][0])
+        { del_ssl(std::string{LAN_NAME}); }
+#endif
+
+        else {
+            auto usage = std::string{"usage: "} + *argv + " [";
+            for (auto cmd : cmds) {
+                usage += std::string{cmd[0]};
+                usage += std::string{cmd[1]} + "|";
+            }
+            usage[usage.size()-1] = ']';
+
+            std::cerr<<usage<<std::endl;
+
+            throw std::runtime_error("main error: argument not recognized");
+        }
+
+        return 0;
+
+    }
+
+    catch (const std::exception & e) { std::cerr<<e.what()<<std::endl; }
+
+    catch (...) { perror("main error"); }
+
+    return 1;
+
+}
+
+#endif
diff --git a/net/nginx-util/src/nginx-util.hpp b/net/nginx-util/src/nginx-util.hpp
new file mode 100644 (file)
index 0000000..f3f14ff
--- /dev/null
@@ -0,0 +1,120 @@
+#ifndef __NGINX_UTIL_H
+#define __NGINX_UTIL_H
+
+#include <array>
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <string_view>
+#include <unistd.h>
+
+#ifndef NO_UBUS
+#include "ubus-cxx.hpp"
+#endif
+
+
+static constexpr auto NGINX_UTIL = std::string_view{"/usr/bin/nginx-util"};
+
+static constexpr auto NGINX_CONF = std::string_view{"/etc/nginx/nginx.conf"};
+
+static constexpr auto CONF_DIR = std::string_view{"/etc/nginx/conf.d/"};
+
+static constexpr auto LAN_NAME = std::string_view{"_lan"};
+
+static constexpr auto LAN_LISTEN =std::string_view{"/var/lib/nginx/lan.listen"};
+
+static constexpr auto LAN_LISTEN_DEFAULT =
+    std::string_view{"/var/lib/nginx/lan.listen.default"};
+
+
+// mode: optional ios::binary and/or ios::app (default ios::trunc)
+void write_file(const std::string_view & name, const std::string & str,
+                std::ios_base::openmode flag=std::ios::trunc);
+
+
+// mode: optional ios::binary (internally ios::ate|ios::in)
+auto read_file(const std::string_view & name,
+                      std::ios_base::openmode mode=std::ios::in) -> std::string;
+
+
+// all S must be convertible to const char[]
+template<typename ...S>
+auto call(const std::string & program, S... args) -> pid_t;
+
+
+void create_lan_listen();
+
+
+void init_lan();
+
+
+void get_env();
+
+
+
+// --------------------- partial implementation: ------------------------------
+
+
+void write_file(const std::string_view & name, const std::string & str,
+                const std::ios_base::openmode flag)
+{
+    std::ofstream file(name.data(), flag);
+    if (!file.good()) {
+        throw std::ofstream::failure(
+            "write_file error: cannot open " + std::string{name});
+    }
+
+    file<<str<<std::flush;
+
+    file.close();
+}
+
+
+auto read_file(const std::string_view & name,
+                      const std::ios_base::openmode mode) -> std::string
+{
+    std::ifstream file(name.data(), mode|std::ios::ate);
+    if (!file.good()) {
+        throw std::ifstream::failure(
+            "read_file error: cannot open " + std::string{name});
+    }
+
+    std::string ret{};
+    const size_t size = file.tellg();
+    ret.reserve(size);
+
+    file.seekg(0);
+    ret.assign((std::istreambuf_iterator<char>(file)),
+                std::istreambuf_iterator<char>());
+
+    file.close();
+    return ret;
+}
+
+
+template<typename ...S>
+auto call(const char * program, S... args) -> pid_t
+{
+    pid_t pid = fork();
+
+    if (pid==0) { //child:
+        std::array<char *, sizeof...(args)+2> argv =
+        { strdup(program), strdup(args)..., nullptr };
+
+        execv(program, argv.data()); // argv cannot be const char * const[]!
+
+        _exit(EXIT_FAILURE);  // exec never returns.
+    } else if (pid>0) { //parent:
+        return pid;
+    }
+
+    std::string errmsg = "call error: cannot fork (";
+    errmsg += std::to_string(errno) + "): " + std::strerror(errno);
+    throw std::runtime_error(errmsg.c_str());
+}
+
+
+#endif
diff --git a/net/nginx-util/src/px5g-openssl.hpp b/net/nginx-util/src/px5g-openssl.hpp
new file mode 100644 (file)
index 0000000..380eba3
--- /dev/null
@@ -0,0 +1,407 @@
+#ifndef _PX5G_OPENSSL_HPP
+#define _PX5G_OPENSSL_HPP
+
+// #define OPENSSL_API_COMPAT 0x10102000L
+#include <fcntl.h>
+#include <memory>
+#include <openssl/bn.h>
+#include <openssl/err.h>
+#include <openssl/pem.h>
+#include <openssl/rsa.h>
+#include <string>
+#include <unistd.h>
+
+static constexpr auto rsa_min_modulus_bits = 512;
+
+using EVP_PKEY_ptr = std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)>;
+
+using X509_NAME_ptr = std::unique_ptr<X509_NAME, decltype(&::X509_NAME_free)>;
+
+
+auto checkend(const std::string & crtpath,
+              time_t seconds=0, bool use_pem=true) -> bool;
+
+
+auto gen_eckey(int curve) -> EVP_PKEY_ptr;
+
+
+auto gen_rsakey(int keysize, BN_ULONG exponent=RSA_F4) -> EVP_PKEY_ptr;
+
+
+void write_key(const EVP_PKEY_ptr & pkey,
+               const std::string & keypath="", bool use_pem=true);
+
+
+auto subject2name(const std::string & subject) -> X509_NAME_ptr;
+
+
+void selfsigned(const EVP_PKEY_ptr & pkey, int days,
+                const std::string & subject="", const std::string & crtpath="",
+                bool use_pem=true);
+
+
+
+// ------------------------- implementation: ----------------------------------
+
+
+inline auto print_error(const char * str, const size_t  /*len*/, void * errmsg)
+    -> int
+{
+    *static_cast<std::string *>(errmsg) += str;
+    return 0;
+}
+
+
+auto checkend(const std::string & crtpath,
+              const time_t seconds, const bool use_pem) -> bool
+{
+    BIO * bio = crtpath.empty() ?
+        BIO_new_fp(stdin, BIO_NOCLOSE | (use_pem ? BIO_FP_TEXT : 0)) :
+        BIO_new_file(crtpath.c_str(), (use_pem ? "r" : "rb"));
+
+    X509 * x509 = nullptr;
+
+    if (bio != nullptr) {
+        x509 = use_pem ?
+            PEM_read_bio_X509_AUX(bio, nullptr, nullptr, nullptr) :
+            d2i_X509_bio(bio, nullptr);
+        BIO_free(bio);
+    }
+
+    if (x509==nullptr) {
+        std::string errmsg{"checkend error: unable to load certificate\n"};
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    time_t checktime = time(nullptr) + seconds;
+    auto cmp = X509_cmp_time(X509_get0_notAfter(x509), &checktime);
+
+    X509_free(x509);
+
+    return (cmp >= 0);
+}
+
+
+auto gen_eckey(const int curve) -> EVP_PKEY_ptr
+{
+    EC_GROUP * group = curve!=0 ? EC_GROUP_new_by_curve_name(curve) : nullptr;
+
+    if (group == nullptr) {
+        std::string errmsg{"gen_eckey error: cannot build group for curve id "};
+        errmsg += std::to_string(curve) + "\n";
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    EC_GROUP_set_asn1_flag(group, OPENSSL_EC_NAMED_CURVE);
+
+    EC_GROUP_set_point_conversion_form(group, POINT_CONVERSION_UNCOMPRESSED);
+
+    auto eckey = EC_KEY_new();
+
+    if (eckey != nullptr) {
+        if ( (EC_KEY_set_group(eckey, group) == 0) ||
+            (EC_KEY_generate_key(eckey) == 0) )
+        {
+            EC_KEY_free(eckey);
+            eckey = nullptr;
+        }
+    }
+
+    EC_GROUP_free(group);
+
+    if (eckey == nullptr) {
+        std::string errmsg{"gen_eckey error: cannot build key with curve id "};
+        errmsg += std::to_string(curve) + "\n";
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    EVP_PKEY_ptr pkey{EVP_PKEY_new(), ::EVP_PKEY_free};
+
+    auto tmp = static_cast<char *>(static_cast<void *>(eckey));
+
+    if (!EVP_PKEY_assign_EC_KEY(pkey.get(), tmp)) {
+        EC_KEY_free(eckey);
+        std::string errmsg{"gen_eckey error: cannot assign EC key to EVP\n"};
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    return pkey;
+}
+
+
+auto gen_rsakey(const int keysize, const BN_ULONG exponent) -> EVP_PKEY_ptr
+{
+    if (keysize<rsa_min_modulus_bits || keysize>OPENSSL_RSA_MAX_MODULUS_BITS) {
+        std::string errmsg{"gen_rsakey error: RSA keysize ("};
+        errmsg += std::to_string(keysize) + ") out of range [512..";
+        errmsg += std::to_string(OPENSSL_RSA_MAX_MODULUS_BITS) + "]";
+        throw std::runtime_error(errmsg.c_str());
+    }
+    auto bignum = BN_new();
+
+    if (bignum == nullptr) {
+        std::string errmsg{"gen_rsakey error: cannot get big number struct\n"};
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    auto rsa = RSA_new();
+
+    if (rsa != nullptr) {
+        if ((BN_set_word(bignum, exponent) == 0) ||
+            (RSA_generate_key_ex(rsa, keysize, bignum, nullptr) == 0))
+        {
+            RSA_free(rsa);
+            rsa = nullptr;
+        }
+    }
+
+    BN_free(bignum);
+
+    if (rsa == nullptr) {
+        std::string errmsg{"gen_rsakey error: cannot create RSA key with size"};
+        errmsg += std::to_string(keysize) + " and exponent ";
+        errmsg += std::to_string(exponent) + "\n";
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    EVP_PKEY_ptr pkey{EVP_PKEY_new(), ::EVP_PKEY_free};
+
+    auto tmp = static_cast<char *>(static_cast<void *>(rsa));
+
+    if (!EVP_PKEY_assign_RSA(pkey.get(), tmp)) {
+        RSA_free(rsa);
+        std::string errmsg{"gen_rsakey error: cannot assign RSA key to EVP\n"};
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    return pkey;
+}
+
+
+void write_key(const EVP_PKEY_ptr & pkey,
+               const std::string & keypath, const bool use_pem)
+{
+    BIO * bio = nullptr;
+
+    if (keypath.empty()) {
+        bio = BIO_new_fp( stdout, BIO_NOCLOSE | (use_pem ? BIO_FP_TEXT : 0) );
+
+    } else { // BIO_new_file(keypath.c_str(), (use_pem ? "w" : "wb") );
+
+        static constexpr auto mask = 0600;
+        // auto fd = open(keypath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mask);
+        auto fd = creat(keypath.c_str(), mask); // the same without va_args.
+
+        if (fd >= 0) {
+            auto fp = fdopen(fd, (use_pem ? "w" : "wb") );
+
+            if (fp != nullptr) {
+                bio = BIO_new_fp(fp, BIO_CLOSE | (use_pem ? BIO_FP_TEXT : 0));
+                if (bio == nullptr) { fclose(fp); } // (fp owns fd)
+            }
+            else { close(fd); }
+        }
+
+    }
+
+    if (bio == nullptr) {
+        std::string errmsg{"write_key error: cannot open "};
+        errmsg += keypath.empty() ? "stdout" : keypath;
+        errmsg += "\n";
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    int len = 0;
+
+    auto key = pkey.get();
+    switch (EVP_PKEY_base_id(key)) { // use same format as px5g:
+        case EVP_PKEY_EC:
+            len = use_pem ?
+                PEM_write_bio_ECPrivateKey(bio, EVP_PKEY_get0_EC_KEY(key),
+                                        nullptr, nullptr, 0, nullptr, nullptr) :
+                i2d_ECPrivateKey_bio(bio, EVP_PKEY_get0_EC_KEY(key));
+            break;
+        case EVP_PKEY_RSA:
+            len = use_pem ?
+                PEM_write_bio_RSAPrivateKey(bio, EVP_PKEY_get0_RSA(key),
+                                        nullptr, nullptr, 0, nullptr, nullptr) :
+                i2d_RSAPrivateKey_bio(bio, EVP_PKEY_get0_RSA(key));
+            break;
+        default:
+            len = use_pem ?
+                PEM_write_bio_PrivateKey(bio, key,
+                                        nullptr, nullptr, 0, nullptr, nullptr) :
+                i2d_PrivateKey_bio(bio, key);
+    }
+
+    BIO_free_all(bio);
+
+    if (len==0) {
+        std::string errmsg{"write_key error: cannot write EVP pkey to "};
+        errmsg += keypath.empty() ? "stdout" : keypath;
+        errmsg += "\n";
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+}
+
+
+auto subject2name(const std::string & subject) -> X509_NAME_ptr
+{
+    if (!subject.empty() && subject[0]!='/') {
+        throw std::runtime_error("subject2name errror: not starting with /");
+    }
+
+    X509_NAME_ptr name = {X509_NAME_new(), ::X509_NAME_free};
+
+    if (!name) {
+        std::string errmsg{"subject2name error: cannot create X509 name \n"};
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    if (subject.empty()) { return name; }
+
+    size_t prev = 1;
+    std::string type{};
+    char chr = '=';
+    for (size_t i=0; subject[i] != 0; ) {
+        ++i;
+        if (subject[i]=='\\' && subject[++i]=='\0') {
+            throw std::runtime_error("subject2name errror: escape at the end");
+        }
+        if (subject[i]!=chr && subject[i]!='\0') { continue; }
+        if (chr == '=') {
+            type = subject.substr(prev, i-prev);
+            chr = '/';
+        } else {
+            auto nid = OBJ_txt2nid(type.c_str());
+            if (nid == NID_undef) {
+                // skip unknown entries (silently?).
+            } else {
+                auto val = static_cast<const unsigned char *>(
+                                static_cast<const void *>(&subject[prev]) );
+
+                auto len = i - prev;
+
+                if ( X509_NAME_add_entry_by_NID(name.get(), nid, MBSTRING_ASC,
+                                                  val, len, -1, 0)
+                    == 0 )
+                {
+                    std::string errmsg{"subject2name error: cannot add "};
+                    errmsg += "/" + type +"="+ subject.substr(prev, len) +"\n";
+                    ERR_print_errors_cb(print_error, &errmsg);
+                    throw std::runtime_error(errmsg.c_str());
+                }
+            }
+            chr = '=';
+        }
+        prev = i+1;
+    }
+
+    return name;
+}
+
+
+void selfsigned(const EVP_PKEY_ptr & pkey, const int days,
+                const std::string & subject, const std::string & crtpath,
+                const bool use_pem)
+{
+    auto x509 = X509_new();
+
+    if (x509 == nullptr) {
+        std::string errmsg{"selfsigned error: cannot create X509 structure\n"};
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+
+    auto freeX509_and_throw = [&x509](const std::string & what)
+    {
+        X509_free(x509);
+        std::string errmsg{"selfsigned error: cannot set "};
+        errmsg += what + " in X509 certificate\n";
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    };
+
+    if (X509_set_version(x509, 2) == 0) { freeX509_and_throw("version"); }
+
+    if (X509_set_pubkey(x509, pkey.get()) == 0) { freeX509_and_throw("pubkey");}
+
+    if ((X509_gmtime_adj(X509_getm_notBefore(x509), 0) == nullptr) ||
+        (X509_time_adj_ex(X509_getm_notAfter(x509), days,0,nullptr) == nullptr))
+    {
+        freeX509_and_throw("times");
+    }
+
+    X509_NAME_ptr name{nullptr, ::X509_NAME_free};
+
+    try { name = subject2name(subject); }
+    catch (...) {
+        X509_free(x509);
+        throw;
+    }
+
+    if (X509_set_subject_name(x509, name.get()) == 0) {
+        freeX509_and_throw("subject");
+    }
+
+    if (X509_set_issuer_name(x509, name.get()) == 0) {
+        freeX509_and_throw("issuer");
+    }
+
+    auto bignum = BN_new();
+
+    if (bignum == nullptr) { freeX509_and_throw("serial (creating big number struct)"); }
+
+    static const auto BITS = 159;
+    if (BN_rand(bignum, BITS, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY) == 0) {
+        BN_free(bignum);
+        freeX509_and_throw("serial (creating random number)");
+    }
+
+    if (BN_to_ASN1_INTEGER(bignum, X509_get_serialNumber(x509)) == nullptr) {
+        BN_free(bignum);
+        freeX509_and_throw("random serial");
+    }
+
+    BN_free(bignum);
+
+    if (X509_sign(x509, pkey.get(), EVP_sha256()) == 0) {
+        freeX509_and_throw("signing digest");
+    }
+
+    BIO * bio = crtpath.empty() ?
+        BIO_new_fp(stdout, BIO_NOCLOSE | (use_pem ? BIO_FP_TEXT : 0)) :
+        BIO_new_file(crtpath.c_str(), (use_pem ? "w" : "wb"));
+
+    int len = 0;
+
+    if (bio != nullptr) {
+        len = use_pem ?
+            PEM_write_bio_X509(bio, x509) :
+            i2d_X509_bio(bio, x509);
+        BIO_free_all(bio);
+    }
+
+    X509_free(x509);
+
+    if (len==0) {
+        std::string errmsg{"selfsigned error: cannot write certificate to "};
+        errmsg += crtpath.empty() ? "stdout" : crtpath;
+        errmsg += "\n";
+        ERR_print_errors_cb(print_error, &errmsg);
+        throw std::runtime_error(errmsg.c_str());
+    }
+}
+
+
+#endif
diff --git a/net/nginx-util/src/px5g.cpp b/net/nginx-util/src/px5g.cpp
new file mode 100644 (file)
index 0000000..56a0632
--- /dev/null
@@ -0,0 +1,451 @@
+#include <array>
+#include <iostream>
+#include <string>
+#include <string_view>
+#include <unistd.h>
+#include "px5g-openssl.hpp"
+
+
+class argv_view { // TODO(pst): use std::span when available.
+
+private:
+
+    std::basic_string_view<const char *> data;
+
+public:
+
+    argv_view(const argv_view &) = delete;
+
+
+    argv_view(argv_view &&) = delete;
+
+
+    auto operator=(const argv_view &) -> argv_view & = delete;
+
+
+    auto operator=(argv_view &&) -> argv_view & = delete;
+
+
+    argv_view(const char ** argv, int argc) :
+    data{argv, static_cast<size_t>(argc)} {}
+
+
+    inline auto operator[] (size_t pos) const -> std::string_view
+    { return std::string_view{data[pos]}; }
+
+
+    [[nodiscard]] inline constexpr auto size() const noexcept -> size_t
+    { return data.size(); }
+
+
+    ~argv_view() = default;
+
+};
+
+
+static const auto default_validity = 30;
+
+
+auto checkend(const argv_view & argv) -> int;
+
+
+void eckey(const argv_view & argv);
+
+
+void rsakey(const argv_view & argv);
+
+
+void selfsigned(const argv_view & argv);
+
+
+inline auto parse_int(const std::string_view & arg) -> int
+{
+    size_t pos;
+    int ret = stoi(std::string{arg}, &pos);
+    if (pos < arg.size()) {
+        throw std::runtime_error("number has trailing char");
+    }
+    return ret;
+}
+
+
+inline auto parse_curve(const std::string_view & name) -> int
+{
+    if (name=="P-384") { return NID_secp384r1; }
+    if (name=="P-521") { return NID_secp521r1; }
+    if (name=="P-256" || name=="secp256r1") { return NID_X9_62_prime256v1; }
+    if (name=="secp192r1") { return NID_X9_62_prime192v1; }
+    return OBJ_sn2nid(name.data());
+    // not: if (curve == 0) { curve = EC_curve_nist2nid(name.c_str()); }
+}
+
+
+auto checkend(const argv_view & argv) -> int
+{
+    bool use_pem = true;
+    std::string crtpath{};
+    time_t seconds = 0;
+
+    for (size_t i=2; i<argv.size(); ++i) {
+        if (argv[i]=="-der") {
+            use_pem = false;
+        } else if (argv[i]=="-in") {
+            ++i;
+
+            if (i >= argv.size()) {
+                throw std::runtime_error("checkend error: -in misses filename");
+            }
+
+            if (!crtpath.empty()) {
+                if (argv[i] == crtpath) {
+                    std::cerr<<"checkend warning: repeated same -in file\n";
+                } else {
+                    throw std::runtime_error
+                                    ("checkend error: more than one -in file");
+                }
+            }
+
+            crtpath = argv[i];
+        }
+
+        else if (argv[i][0]=='-') {
+            std::cerr<<"checkend warning: skipping option "<<argv[i]<<std::endl;
+        } else { // main option:
+            intmax_t num = 0;
+
+            try {
+                num = parse_int(argv[i]);
+            } catch (...) {
+                auto errmsg = std::string{"checkend error: invalid time "};
+                errmsg += argv[i];
+                std::throw_with_nested(std::runtime_error(errmsg.c_str()));
+            }
+
+            seconds = static_cast<time_t>(num);
+
+            if (num!=static_cast<intmax_t>(seconds)) {
+                auto errmsg = std::string{"checkend error: time too big "};
+                errmsg += argv[i];
+                throw std::runtime_error(errmsg.c_str());
+            }
+        }
+    }
+
+    bool valid = checkend(crtpath, seconds, use_pem);
+    std::cout<<"Certificate will"<<(valid ? " not " : " ")<<"expire"<<std::endl;
+
+    return (valid ? 0 : 1);
+}
+
+
+void eckey(const argv_view & argv)
+{
+    bool has_main_option = false;
+    bool use_pem = true;
+    std::string keypath{};
+    int curve = NID_X9_62_prime256v1;
+
+    for (size_t i=2; i < argv.size(); ++i) {
+        if (argv[i]=="-der") {
+            use_pem = false;
+        } else if (argv[i]=="-out") {
+            ++i;
+
+            if (i >= argv.size()) {
+                throw std::runtime_error("eckey error: -out misses filename");
+            }
+
+            if (!keypath.empty()) {
+                if (argv[i]==keypath) {
+                    std::cerr<<"eckey warning: repeated same -out file\n";
+                } else {
+                    throw std::runtime_error
+                    ("eckey error: more than one -out file");
+                }
+            }
+
+            keypath = argv[i];
+        }
+
+        else if (argv[i][0]=='-') {
+            std::cerr<<"eckey warning: skipping option "<<argv[i]<<std::endl;
+        } else { //main option:
+
+            if (has_main_option) {
+                throw std::runtime_error
+                    ("eckey error: more than one main option");
+            } // else:
+            has_main_option = true;
+
+            curve = parse_curve(argv[i]);
+        }
+    }
+
+    write_key(gen_eckey(curve), keypath, use_pem);
+}
+
+
+void rsakey(const argv_view & argv)
+{
+    bool has_main_option = false;
+    bool use_pem = true;
+    std::string keypath{};
+    BN_ULONG exponent = RSA_F4;
+    int keysize = rsa_min_modulus_bits;
+
+    for (size_t i=2; i < argv.size(); ++i) {
+        if (argv[i]=="-der") {
+            use_pem = false;
+        } else if (argv[i]=="-3") {
+            exponent = 3;
+        } else if (argv[i]=="-out") {
+            ++i;
+
+            if (i >= argv.size()) {
+                throw std::runtime_error("rsakey error: -out misses filename");
+            }
+
+            if (!keypath.empty()) {
+                if (argv[i]==keypath) {
+                    std::cerr<<"rsakey warning: repeated -out file"<<std::endl;
+                } else {
+                    throw std::runtime_error
+                        ("rsakey error: more than one -out file");
+                }
+            }
+
+            keypath = argv[i];
+        }
+
+        else if (argv[i][0]=='-') {
+            std::cerr<<"rsakey warning: skipping option "<<argv[i]<<std::endl;
+        } else { //main option:
+
+            if (has_main_option) {
+                throw std::runtime_error("rsakey error: more than one keysize");
+            } // else:
+            has_main_option = true;
+
+            try {
+                keysize = parse_int(argv[i]);
+            } catch (...) {
+                std::string errmsg{"rsakey error: invalid keysize "};
+                errmsg += argv[i];
+                std::throw_with_nested(std::runtime_error(errmsg.c_str()));
+            }
+        }
+    }
+
+    write_key(gen_rsakey(keysize, exponent), keypath, use_pem);
+}
+
+
+void selfsigned(const argv_view & argv)
+{
+    bool use_pem = true;
+    int days = default_validity;
+    std::string keypath{};
+    std::string crtpath{};
+    std::string subject{};
+
+    bool use_rsa = true;
+    int keysize = rsa_min_modulus_bits;
+    BN_ULONG exponent = RSA_F4;
+
+    int curve = NID_X9_62_prime256v1;
+
+    for (size_t i=2; i < argv.size(); ++i) {
+        if (argv[i]=="-der") {
+            use_pem = false;
+        } else if (argv[i]=="-days") {
+            ++i;
+            try {
+                days = parse_int(argv[i]);
+            } catch (...) {
+                std::string errmsg{"selfsigned error: not a number for -days "};
+                errmsg += argv[i].substr(4);
+                std::throw_with_nested(std::runtime_error(errmsg.c_str()));
+            }
+        }
+
+        else if (argv[i]=="-newkey") {
+            ++i;
+
+            if (i >= argv.size()) {
+                throw std::runtime_error
+                    ("selfsigned error: -newkey misses algorithm option");
+            }
+
+            static constexpr auto rsa_prefix = std::string_view{"rsa:"};
+
+            if (argv[i]=="ec") {
+                use_rsa = false;
+            } else if (argv[i].rfind(rsa_prefix, 0) == 0) {
+                use_rsa = true;
+                try {
+                    keysize = parse_int(argv[i].substr(rsa_prefix.size()));
+                } catch (...) {
+                    std::string errmsg{"selfsigned error: invalid keysize "};
+                    errmsg += argv[i].substr(4);
+                    std::throw_with_nested(std::runtime_error(errmsg.c_str()));
+                }
+            } else {
+                throw std::runtime_error("selfsigned error: invalid algorithm");
+            }
+        }
+
+        else if (argv[i]=="-pkeyopt") {
+            ++i;
+
+            if (i >= argv.size()) {
+                throw std::runtime_error
+                    ("selfsigned error: -pkeyopt misses value");
+            }
+
+            static constexpr auto curve_prefix =
+                std::string_view{"ec_paramgen_curve:"};
+
+            if (argv[i].rfind(curve_prefix, 0) != 0) {
+                throw std::runtime_error("selfsigned error: -pkeyopt invalid");
+            }
+
+            curve = parse_curve(argv[i].substr(curve_prefix.size()));
+        }
+
+        else if (argv[i]=="-keyout") {
+            ++i;
+
+            if (i >= argv.size()) {
+                throw std::runtime_error
+                    ("selfsigned error: -keyout misses path");
+            }
+
+            if (!keypath.empty()) {
+                if (argv[i]==keypath) {
+                    std::cerr<<"selfsigned warning: repeated -keyout file\n";
+                } else {
+                    throw std::runtime_error
+                        ("selfsigned error: more than one -keyout file");
+                }
+            }
+
+            keypath = argv[i];
+        }
+
+        else if (argv[i]=="-out") {
+            ++i;
+
+            if (i >= argv.size()) {
+                throw std::runtime_error
+                    ("selfsigned error: -out misses filename");
+            }
+
+            if (!crtpath.empty()) {
+                if (argv[i]==crtpath) {
+                    std::cerr<<"selfsigned warning: repeated same -out file\n";
+                } else {
+                    throw std::runtime_error
+                        ("selfsigned error: more than one -out file");
+                }
+            }
+
+            crtpath = argv[i];
+        }
+
+        else if (argv[i]=="-subj") {
+            ++i;
+
+            if (i >= argv.size()) {
+                throw std::runtime_error
+                    ("selfsigned error: -subj misses value");
+            }
+
+            if (!subject.empty()) {
+                if (argv[i]==subject) {
+                    std::cerr<<"selfsigned warning: repeated same -subj\n";
+                } else {
+                    throw std::runtime_error
+                        ("selfsigned error: more than one -subj value");
+                }
+            }
+
+            subject = argv[i];
+        }
+
+        else {
+            std::cerr<<"selfsigned warning: skipping option "<<argv[i]<<std::endl;
+        }
+    }
+
+    auto pkey = use_rsa ? gen_rsakey(keysize, exponent) : gen_eckey(curve);
+
+    selfsigned(pkey, days, subject, crtpath, use_pem);
+
+    if (!keypath.empty()) { write_key(pkey, keypath, use_pem); }
+}
+
+
+auto main(int argc, const char ** argv) -> int
+{
+    auto args = argv_view{argv, argc};
+
+    auto cmds = std::array{
+        std::array<std::string, 2>{"checkend",
+            " [-der] [-in certificate_path] [seconds_remaining]"
+        },
+        std::array<std::string, 2>{"eckey",
+            " [-der] [-out key_path] [curve_name]"
+        },
+        std::array<std::string, 2>{"rsakey",
+            " [-der] [-out key_path] [-3] [key_size]"
+        },
+        std::array<std::string, 2>{"selfsigned",
+            " [-der] [-keyout key_path] [-out certificate_path]"
+            " [-newkey ec|rsa:key_size] [-pkeyopt ec_paramgen_curve:name]"
+            " [-days validity] [-subj /C=.../ST=.../L=.../O=.../CN=.../... ]"
+        },
+    };
+
+    try {
+        if (argc < 2) { throw std::runtime_error("error: no argument"); }
+
+        if (args[1]==cmds[0][0]) {return checkend(args);}
+
+        if (args[1]==cmds[1][0]) { eckey(args); }
+
+        else if (args[1]==cmds[2][0]) { rsakey(args); }
+
+        else if (args[1]==cmds[3][0]) { selfsigned(args); }
+
+        else { throw std::runtime_error("error: argument not recognized"); }
+    }
+
+    catch (const std::exception & e)  {
+
+        auto usage = std::string{"usage: \n"}  ;
+        for (auto cmd : cmds) {
+            usage += std::string{4, ' '} + *argv +" "+ cmd[0] + cmd[1] +"\n";
+        }
+
+        std::cerr<<usage<<std::flush;
+
+        auto print_nested =
+            [](auto && self, const std::exception & outer, int depth=0) -> void
+        {
+            std::cerr<<std::string(depth, '\t')<<outer.what()<<std::endl;
+            try { std::rethrow_if_nested(outer); }
+            catch (const std::exception & inner) { self(self, inner, depth+1); }
+        };
+
+        print_nested(print_nested, e);
+
+        return 1;
+    }
+
+    catch (...) {
+        std::cerr<<*argv<<" unknown error."<<std::endl;
+        return 2;
+    }
+
+    return 0;
+}
diff --git a/net/nginx-util/src/regex-pcre.hpp b/net/nginx-util/src/regex-pcre.hpp
new file mode 100644 (file)
index 0000000..8408b60
--- /dev/null
@@ -0,0 +1,505 @@
+// implementing *some* <regex> functions using pcre for performance:
+
+#ifndef __REGEXP_PCRE_HPP
+#define __REGEXP_PCRE_HPP
+
+#include <array>
+#include <pcre.h>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+
+namespace rgx {
+
+
+namespace regex_constants {
+  enum error_type
+    {
+      _enum_error_collate,
+      _enum_error_ctype,
+      _enum_error_escape,
+      _enum_error_backref,
+      _enum_error_brack,
+      _enum_error_paren,
+      _enum_error_brace,
+      _enum_error_badbrace,
+      _enum_error_range,
+      _enum_error_space,
+      _enum_error_badrepeat,
+      _enum_error_complexity,
+      _enum_error_stack,
+      _enum_error_last
+    };
+    static const error_type error_collate(_enum_error_collate);
+    static const error_type error_ctype(_enum_error_ctype);
+    static const error_type error_escape(_enum_error_escape);
+    static const error_type error_backref(_enum_error_backref);
+    static const error_type error_brack(_enum_error_brack);
+    static const error_type error_paren(_enum_error_paren);
+    static const error_type error_brace(_enum_error_brace);
+    static const error_type error_badbrace(_enum_error_badbrace);
+    static const error_type error_range(_enum_error_range);
+    static const error_type error_space(_enum_error_space);
+    static const error_type error_badrepeat(_enum_error_badrepeat);
+    static const error_type error_complexity(_enum_error_complexity);
+    static const error_type error_stack(_enum_error_stack);
+} // namespace regex_constants
+
+
+
+class regex_error : public std::runtime_error {
+
+private:
+
+    regex_constants::error_type errcode;
+
+
+public:
+
+    explicit regex_error(regex_constants::error_type code,
+                         const char * what="regex error")
+    : runtime_error(what), errcode(code)
+    { }
+
+
+    [[nodiscard]] auto code() const -> regex_constants::error_type
+    { return errcode; }
+
+};
+
+
+
+class regex {
+
+private:
+
+    int errcode = 0;
+
+    const char * errptr = nullptr;
+
+    int erroffset = 0;
+
+    pcre * const re = nullptr;
+
+    static const std::array<regex_constants::error_type,86> errcode_pcre2regex;
+
+    static const auto BASE = 10;
+
+
+public:
+
+    inline regex() = default;
+
+
+    inline regex(const regex &) = delete;
+
+
+    inline regex(regex &&) = default;
+
+
+    inline auto operator=(const regex &) -> regex & = delete;
+
+
+    inline auto operator=(regex &&) -> regex & = delete;
+
+
+    explicit regex(const std::string & str)
+    : re{ pcre_compile2(str.c_str(), 0, &errcode, &errptr, &erroffset,nullptr) }
+    {
+        if (re==nullptr) {
+            std::string what = std::string("regex error: ") + errptr + '\n';
+            what += "    '" + str + "'\n";
+            what += "     " + std::string(erroffset, ' ') + '^';
+
+            throw regex_error(errcode_pcre2regex.at(errcode), what.c_str());
+        }
+    }
+
+
+    ~regex() { if (re != nullptr) { pcre_free(re); } }
+
+
+    inline auto operator()() const -> const pcre * { return re; }
+
+};
+
+
+
+class smatch {
+
+    friend auto regex_search(std::string::const_iterator begin,
+                             std::string::const_iterator end,
+                             smatch & match,
+                             const regex & rgx);
+
+
+private:
+
+    std::string::const_iterator begin;
+
+    std::string::const_iterator end;
+
+    std::vector <int> vec{};
+
+    int n = 0;
+
+
+public:
+
+    [[nodiscard]] inline auto position(int i=0) const {
+        return (i<0 || i>=n) ? std::string::npos : vec[2*i];
+    }
+
+
+    [[nodiscard]] inline auto length(int i=0) const {
+        return (i<0 || i>=n) ? 0 : vec[2*i+1] - vec[2*i];
+    }
+
+
+    [[nodiscard]] auto str(int i=0) const -> std::string { // should we throw?
+        if (i<0 || i>=n) { return ""; }
+        int x = vec[2*i];
+        if (x<0) { return ""; }
+        int y = vec[2*i+1];
+        return std::string{begin + x, begin + y};
+    }
+
+
+    [[nodiscard]] auto format(const std::string & fmt) const;
+
+
+    [[nodiscard]] auto size() const -> int { return n; }
+
+
+    [[nodiscard]] inline auto empty() const { return n<0; }
+
+
+    [[nodiscard]] inline auto ready() const { return !vec.empty(); }
+
+};
+
+
+inline auto regex_search(const std::string & subj, const regex & rgx);
+
+
+auto regex_replace(const std::string & subj,
+                          const regex & rgx,
+                          const std::string & insert);
+
+
+inline auto regex_search(const std::string & subj, smatch & match,
+                         const regex & rgx);
+
+
+auto regex_search(std::string::const_iterator begin,
+                  std::string::const_iterator end,
+                  smatch & match,
+                  const regex & rgx);
+
+
+
+// ------------------------- implementation: ----------------------------------
+
+
+inline auto regex_search(const std::string & subj, const regex & rgx)
+{
+    if (rgx()==nullptr) {
+        throw std::runtime_error("regex_search error: no regex given");
+    }
+    int n = pcre_exec(rgx(), nullptr, subj.c_str(), subj.length(),
+                      0, 0, nullptr, 0);
+    return n>=0;
+}
+
+
+auto regex_search(const std::string::const_iterator begin,
+                  const std::string::const_iterator end,
+                  smatch & match,
+                  const regex & rgx)
+{
+    if (rgx()==nullptr) {
+        throw std::runtime_error("regex_search error: no regex given");
+    }
+
+    int sz = 0;
+    pcre_fullinfo(rgx(), nullptr, PCRE_INFO_CAPTURECOUNT, &sz);
+    sz = 3*(sz + 1);
+
+    match.vec.reserve(sz);
+
+    const char * subj = &*begin;
+    size_t len = &*end - subj;
+
+    match.begin = begin;
+    match.end = end;
+
+    match.n = pcre_exec(rgx(), nullptr, subj, len, 0, 0, &match.vec[0], sz);
+
+    if (match.n<0) { return false; }
+    if (match.n==0) { match.n = sz/3; }
+
+    return true;
+}
+
+
+inline auto regex_search(const std::string & subj, smatch & match,
+                         const regex & rgx)
+{
+    return regex_search(subj.begin(), subj.end(), match, rgx);
+}
+
+
+auto smatch::format(const std::string & fmt) const {
+    std::string ret{};
+    size_t index = 0;
+
+    size_t pos;
+    while ((pos=fmt.find('$', index)) != std::string::npos) {
+        ret.append(fmt, index, pos-index);
+        index = pos + 1;
+
+        char chr = fmt[index++];
+        int n = 0;
+        static const auto BASE = 10;
+        switch(chr) {
+
+            case '&': // match
+                ret += this->str(0);
+                break;
+
+            case '`': // prefix
+                ret.append(begin, begin+vec[0]);
+                break;
+
+            case '\'': // suffix
+                ret.append(begin+vec[1], end);
+                break;
+
+            default: // number => submatch
+                while (isdigit(chr) != 0) {
+                    n = BASE*n + chr - '0';
+                    chr = fmt[index++];
+                }
+
+                ret += n>0 ? str(n) : std::string{"$"};
+
+                [[fallthrough]];
+
+            case '$': // escaped
+                ret += chr;
+        }
+    }
+    ret.append(fmt, index);
+    return ret;
+}
+
+
+auto regex_replace(const std::string & subj,
+                          const regex & rgx,
+                          const std::string & insert)
+{
+    if (rgx()==nullptr) {
+        throw std::runtime_error("regex_replace error: no regex given");
+    }
+
+    std::string ret{};
+    auto pos = subj.begin();
+
+    for (smatch match;
+         regex_search(pos, subj.end(), match, rgx);
+         pos += match.position(0) + match.length(0))
+    {
+        ret.append(pos, pos + match.position(0));
+        ret.append(match.format(insert));
+    }
+
+    ret.append(pos, subj.end());
+    return ret;
+}
+
+
+
+// ------------ There is only the translation table below : -------------------
+
+
+const std::array<regex_constants::error_type, 86> regex::errcode_pcre2regex = {
+    //   0  no error
+    regex_constants::error_type::_enum_error_last,
+    //   1  \ at end of pattern
+    regex_constants::error_escape,
+    //   2  \c at end of pattern
+    regex_constants::error_escape,
+    //   3  unrecognized character follows \ .
+    regex_constants::error_escape,
+    //   4  numbers out of order in {} quantifier
+    regex_constants::error_badbrace,
+    //   5  number too big in {} quantifier
+    regex_constants::error_badbrace,
+    //   6  missing terminating  for character class
+    regex_constants::error_brack,
+    //   7  invalid escape sequence in character class
+    regex_constants::error_escape,
+    //   8  range out of order in character class
+    regex_constants::error_range,
+    //   9  nothing to repeat
+    regex_constants::error_badrepeat,
+    //  10  [this code is not in use
+    regex_constants::error_type::_enum_error_last,
+    //  11  internal error: unexpected repeat
+    regex_constants::error_badrepeat,
+    //  12  unrecognized character after (? or (?-
+    regex_constants::error_backref,
+    //  13  POSIX named classes are supported only within a class
+    regex_constants::error_range,
+    //  14  missing )
+    regex_constants::error_paren,
+    //  15  reference to non-existent subpattern
+    regex_constants::error_backref,
+    //  16  erroffset passed as NULL
+    regex_constants::error_type::_enum_error_last,
+    //  17  unknown option bit(s) set
+    regex_constants::error_type::_enum_error_last,
+    //  18  missing ) after comment
+    regex_constants::error_paren,
+    //  19  [this code is not in use
+    regex_constants::error_type::_enum_error_last,
+    //  20  regular expression is too large
+    regex_constants::error_space,
+    //  21  failed to get memory
+    regex_constants::error_stack,
+    //  22  unmatched parentheses
+    regex_constants::error_paren,
+    //  23  internal error: code overflow
+    regex_constants::error_stack,
+    //  24  unrecognized character after (?<
+    regex_constants::error_backref,
+    //  25  lookbehind assertion is not fixed length
+    regex_constants::error_backref,
+    //  26  malformed number or name after (?(
+    regex_constants::error_backref,
+    //  27  conditional group contains more than two branches
+    regex_constants::error_backref,
+    //  28  assertion expected after (?(
+    regex_constants::error_backref,
+    //  29  (?R or (?[+-digits must be followed by )
+    regex_constants::error_backref,
+    //  30  unknown POSIX class name
+    regex_constants::error_ctype,
+    //  31  POSIX collating elements are not supported
+    regex_constants::error_collate,
+    //  32  this version of PCRE is compiled without UTF support
+    regex_constants::error_collate,
+    //  33  [this code is not in use
+    regex_constants::error_type::_enum_error_last,
+    //  34  character value in \x{} or \o{} is too large
+    regex_constants::error_escape,
+    //  35  invalid condition (?(0)
+    regex_constants::error_backref,
+    //  36  \C not allowed in lookbehind assertion
+    regex_constants::error_escape,
+    //  37  PCRE does not support \L, \l, \N{name}, \U, or \u
+    regex_constants::error_escape,
+    //  38  number after (?C is > 255
+    regex_constants::error_backref,
+    //  39  closing ) for (?C expected
+    regex_constants::error_paren,
+    //  40  recursive call could loop indefinitely
+    regex_constants::error_complexity,
+    //  41  unrecognized character after (?P
+    regex_constants::error_backref,
+    //  42  syntax error in subpattern name (missing terminator)
+    regex_constants::error_paren,
+    //  43  two named subpatterns have the same name
+    regex_constants::error_backref,
+    //  44  invalid UTF-8 string (specifically UTF-8)
+    regex_constants::error_collate,
+    //  45  support for \P, \p, and \X has not been compiled
+    regex_constants::error_escape,
+    //  46  malformed \P or \p sequence
+    regex_constants::error_escape,
+    //  47  unknown property name after \P or \p
+    regex_constants::error_escape,
+    //  48  subpattern name is too long (maximum 32 characters)
+    regex_constants::error_backref,
+    //  49  too many named subpatterns (maximum 10000)
+    regex_constants::error_complexity,
+    //  50  [this code is not in use
+    regex_constants::error_type::_enum_error_last,
+    //  51  octal value is greater than \377 in 8-bit non-UTF-8 mode
+    regex_constants::error_escape,
+    //  52  internal error: overran compiling workspace
+    regex_constants::error_type::_enum_error_last,
+    //  53  internal error: previously-checked referenced subpattern not found
+    regex_constants::error_type::_enum_error_last,
+    //  54  DEFINE group contains more than one branch
+    regex_constants::error_backref,
+    //  55  repeating a DEFINE group is not allowed
+    regex_constants::error_backref,
+    //  56  inconsistent NEWLINE options
+    regex_constants::error_escape,
+    //  57  \g is not followed by a braced, angle-bracketed, or quoted name/number or by a plain number
+    regex_constants::error_backref,
+    //  58  a numbered reference must not be zero
+    regex_constants::error_backref,
+    //  59  an argument is not allowed for (*ACCEPT), (*FAIL), or (*COMMIT)
+    regex_constants::error_complexity,
+    //  60  (*VERB) not recognized or malformed
+    regex_constants::error_complexity,
+    //  61  number is too big
+    regex_constants::error_complexity,
+    //  62  subpattern name expected
+    regex_constants::error_backref,
+    //  63  digit expected after (?+
+    regex_constants::error_backref,
+    //  64   is an invalid data character in JavaScript compatibility mode
+    regex_constants::error_escape,
+    //  65  different names for subpatterns of the same number are not allowed
+    regex_constants::error_backref,
+    //  66  (*MARK) must have an argument
+    regex_constants::error_complexity,
+    //  67  this version of PCRE is not compiled with Unicode property support
+    regex_constants::error_collate,
+    //  68  \c must be followed by an ASCII character
+    regex_constants::error_escape,
+    //  69  \k is not followed by a braced, angle-bracketed, or quoted name
+    regex_constants::error_backref,
+    //  70  internal error: unknown opcode in find_fixedlength()
+    regex_constants::error_type::_enum_error_last,
+    //  71  \N is not supported in a class
+    regex_constants::error_ctype,
+    //  72  too many forward references
+    regex_constants::error_backref,
+    //  73  disallowed Unicode code point (>= 0xd800 && <= 0xdfff)
+    regex_constants::error_escape,
+    //  74  invalid UTF-16 string (specifically UTF-16)
+    regex_constants::error_collate,
+    //  75  name is too long in (*MARK), (*PRUNE), (*SKIP), or (*THEN)
+    regex_constants::error_complexity,
+    //  76  character value in \u.... sequence is too large
+    regex_constants::error_escape,
+    //  77  invalid UTF-32 string (specifically UTF-32)
+    regex_constants::error_collate,
+    //  78  setting UTF is disabled by the application
+    regex_constants::error_collate,
+    //  79  non-hex character in \x{} (closing brace missing?)
+    regex_constants::error_escape,
+    //  80  non-octal character in \o{} (closing brace missing?)
+    regex_constants::error_escape,
+    //  81  missing opening brace after \o
+    regex_constants::error_brace,
+    //  82  parentheses are too deeply nested
+    regex_constants::error_complexity,
+    //  83  invalid range in character class
+    regex_constants::error_range,
+    //  84  group name must start with a non-digit
+    regex_constants::error_backref,
+    //  85  parentheses are too deeply nested (stack check)
+    regex_constants::error_stack
+};
+
+
+} // namespace rgx
+
+
+#endif
diff --git a/net/nginx-util/src/test-px5g.sh b/net/nginx-util/src/test-px5g.sh
new file mode 100755 (executable)
index 0000000..9bae051
--- /dev/null
@@ -0,0 +1,138 @@
+#!/bin/bash
+
+PRINT_PASSED=2
+
+
+OPENSSL_PEM="$(mktemp)"
+OPENSSL_DER="$(mktemp)"
+
+NONCE=$(dd if=/dev/urandom bs=1 count=4 2>/dev/null | hexdump -e '1/1 "%02x"')
+SUBJECT=/C="ZZ"/ST="Somewhere"/L="None"/O="OpenWrt'$NONCE'"/CN="OpenWrt"
+
+openssl req -x509 -nodes -days 1 -keyout /dev/null 2>/dev/null \
+    -out "$OPENSSL_PEM" -subj "$SUBJECT" \
+|| ( printf "error: generating PEM certificate with openssl"; return 1)
+openssl req -x509 -nodes -days 1 -keyout /dev/null 2>/dev/null \
+    -out "$OPENSSL_DER" -outform der -subj "$SUBJECT" \
+|| ( printf "error: generating DER certificate with openssl"; return 1)
+
+
+function test() {
+    MSG="$1 >/dev/null \t (-> $2?) \t"
+    eval "$1 >/dev/null "
+    if [ $? -eq $2 ]
+    then
+        [ "$PRINT_PASSED" -gt 0 ] && printf "$MSG passed.\n"
+    else
+        printf "$MSG failed!!!\n"
+        [ "$PRINT_PASSED" -gt 1 ] && exit 1
+    fi
+}
+
+
+[ "$PRINT_PASSED" -gt 0 ] && printf "\nTesting openssl itself ...\n"
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * right PEM:\n"
+test 'cat "$OPENSSL_PEM" | openssl x509 -checkend 0                         ' 0
+test 'cat "$OPENSSL_PEM" | openssl x509 -checkend 86300                     ' 0
+test 'cat "$OPENSSL_PEM" | openssl x509 -checkend 86400                     ' 1
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * right DER:\n"
+test 'cat "$OPENSSL_DER" | openssl x509 -checkend 0    -inform der          ' 0
+test 'cat "$OPENSSL_DER" | openssl x509 -checkend 86300 -inform der         ' 0
+test 'cat "$OPENSSL_DER" | openssl x509 -checkend 86400 -inform der         ' 1
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * wrong:\n"
+test 'cat "$OPENSSL_PEM" | openssl x509 -checkend 0 -inform der  2>/dev/null' 1
+test 'cat "$OPENSSL_DER" | openssl x509 -checkend 0              2>/dev/null' 1
+
+
+[ "$PRINT_PASSED" -gt 0 ] && printf "\nTesting px5g checkend ...\n"
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * right PEM:\n"
+test 'cat "$OPENSSL_PEM" | ./px5g checkend 0                                ' 0
+test 'cat "$OPENSSL_PEM" | ./px5g checkend 86300                            ' 0
+test 'cat "$OPENSSL_PEM" | ./px5g checkend 86400                            ' 1
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * right DER:\n"
+test 'cat "$OPENSSL_DER" | ./px5g checkend -der 0                           ' 0
+test 'cat "$OPENSSL_DER" | ./px5g checkend -der 86300                       ' 0
+test 'cat "$OPENSSL_DER" | ./px5g checkend -der 86400                       ' 1
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * in option:\n"
+test 'cat "$OPENSSL_DER" | ./px5g checkend -in /proc/self/fd/0 -der 0       ' 0
+test 'cat "$OPENSSL_DER" | ./px5g checkend -der -in /proc/self/fd/0 99      ' 0
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * wrong:\n"
+test 'cat "$OPENSSL_PEM" | ./px5g checkend -der 0                2>/dev/null' 1
+test 'cat "$OPENSSL_DER" | ./px5g checkend 0                     2>/dev/null' 1
+
+
+[ "$PRINT_PASSED" -gt 0 ] && printf "\nTesting px5g eckey ...\n"
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * standard curves:\n"
+test './px5g eckey P-256        | openssl ec -check              2>/dev/null' 0
+test './px5g eckey P-384        | openssl ec -check              2>/dev/null' 0
+test './px5g eckey secp384r1    | openssl ec -check              2>/dev/null' 0
+test './px5g eckey secp256r1    | openssl ec -check              2>/dev/null' 0
+test './px5g eckey secp256k1    | openssl ec -check              2>/dev/null' 0
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * more curves:\n"
+test './px5g eckey P-521        | openssl ec -check              2>/dev/null' 0
+test './px5g eckey secp521r1    | openssl ec -check              2>/dev/null' 0
+test './px5g eckey secp224r1    | openssl ec -check              2>/dev/null' 0
+test './px5g eckey secp224k1    | openssl ec -check              2>/dev/null' 0
+test './px5g eckey secp192r1    | openssl ec -check              2>/dev/null' 0
+test './px5g eckey secp192k1    | openssl ec -check              2>/dev/null' 0
+test './px5g eckey brainpoolP512r1        | openssl ec -check    2>/dev/null' 0
+test './px5g eckey brainpoolP384r1        | openssl ec -check    2>/dev/null' 0
+test './px5g eckey brainpoolP256r1        | openssl ec -check    2>/dev/null' 0
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * other options:\n"
+test './px5g eckey -out /proc/self/fd/1   | openssl ec -check    2>/dev/null' 0
+test './px5g eckey -der         | openssl ec -check -inform der  2>/dev/null' 0
+
+
+[ "$PRINT_PASSED" -gt 0 ] && printf "\nTesting px5g rsakey ...\n"
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * standard exponent:\n"
+test './px5g rsakey             | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey 512         | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey 1024        | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey 2048        | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey 4096        | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey 1111        | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey 0                                            2>/dev/null' 1
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * small exponent:\n"
+test './px5g rsakey -3          | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey -3 512      | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey -3 1024     | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey -3 2048     | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey -3 4096     | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey -3 1111     | openssl rsa -check             2>/dev/null' 0
+test './px5g rsakey -3 0                                         2>/dev/null' 1
+
+[ "$PRINT_PASSED" -gt 1 ] && printf "  * other options:\n"
+test './px5g rsakey -out /proc/self/fd/1  | openssl rsa -check   2>/dev/null' 0
+test './px5g rsakey -der        | openssl rsa -check -inform der 2>/dev/null' 0
+
+
+[ "$PRINT_PASSED" -gt 0 ] && printf "\nTesting px5g selfsigned ...\n"
+
+test './px5g selfsigned -der | openssl x509 -checkend 0 -inform der         ' 0
+test './px5g selfsigned -days 1               | openssl x509 -checkend 0    ' 0
+test './px5g selfsigned -days 1               | openssl x509 -checkend 86300' 0
+test './px5g selfsigned -days 1               | openssl x509 -checkend 86400' 1
+test './px5g selfsigned -out /proc/self/fd/1  | openssl x509 -checkend 0    ' 0
+test './px5g selfsigned -newkey rsa:666       | openssl x509 -checkend 0    ' 0
+test './px5g selfsigned -newkey ec            | openssl x509 -checkend 0    ' 0
+test './px5g selfsigned -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 \
+      | openssl x509 -checkend 0                                        ' 0
+test './px5g selfsigned -subj $SUBJECT | openssl x509 -noout \
+      -subject -nameopt compat | grep -q subject=$SUBJECT    2>/dev/null' 0
+test './px5g selfsigned -out /dev/null -keyout /proc/self/fd/1 \
+      | openssl rsa -check 2>/dev/null                                  ' 0
+
+
+rm "$OPENSSL_PEM" "$OPENSSL_DER"
diff --git a/net/nginx-util/src/ubus-cxx.hpp b/net/nginx-util/src/ubus-cxx.hpp
new file mode 100644 (file)
index 0000000..21dc26f
--- /dev/null
@@ -0,0 +1,446 @@
+#ifndef _UBUS_CXX_HPP
+#define _UBUS_CXX_HPP
+
+extern "C" { //TODO(pst): remove when in upstream
+#include <libubus.h>
+}
+#include <cassert>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+#ifndef NDEBUG
+#include <iostream>
+#endif
+
+
+// // example for checking if there is a key:
+// if (ubus::call("service", "list", 1000).filter("cron")) {
+//     std::cout<<"Cron is active (with or without instances) "<<std::endl;
+// }
+
+// // example for getting values:
+// auto lan_status = ubus::call("network.interface.lan", "status");
+// for (auto x : lan_status.filter("ipv6-address", "", "address")) {
+//     std::cout<<"["<<blobmsg_get_string(x)<<"] ";
+// }
+// for (auto x : lan_status.filter("ipv4-address", "").filter("address")) {
+//     std::cout<<blobmsg_get_string(x)<<" ";
+// }
+// std::cout<<std::endl;
+
+// // example for exploring:
+// ubus::strings keys{"ipv4-address", "", "*"};
+// for (auto x : ubus::call("network.interface.lan", "status").filter(keys)) {
+//     std::cout<<blobmsg_name(x)<<": ";
+//     switch (blob_id(x)) {
+//         case BLOBMSG_TYPE_UNSPEC: std::cout<<"[unspecified]"; break;
+//         case BLOBMSG_TYPE_ARRAY: std::cout<<"[array]"; break;
+//         case BLOBMSG_TYPE_TABLE: std::cout<<"[table]"; break;
+//         case BLOBMSG_TYPE_STRING: std::cout<<blobmsg_get_string(x); break;
+//         case BLOBMSG_TYPE_INT64: std::cout<<blobmsg_get_u64(x); break;
+//         case BLOBMSG_TYPE_INT32: std::cout<<blobmsg_get_u32(x); break;
+//         case BLOBMSG_TYPE_INT16: std::cout<<blobmsg_get_u16(x); break;
+//         case BLOBMSG_TYPE_BOOL: std::cout<<blobmsg_get_bool(x); break;
+//         case BLOBMSG_TYPE_DOUBLE: std::cout<<blobmsg_get_double(x); break;
+//         default: std::cout<<"[unknown]";
+//     }
+//     std::cout<<std::endl;
+// }
+
+// // example for recursive exploring (output like from the original ubus call)
+// const auto explore = [](auto message) -> void
+// {
+//     auto end = message.end();
+//     auto explore_internal =
+//         [&end](auto & explore_ref, auto it, size_t depth=1) -> void
+//     {
+//         std::cout<<std::endl;
+//         bool first = true;
+//         for (; it!=end; ++it) {
+//             auto * attr = *it;
+//             if (first) { first = false; }
+//             else { std::cout<<",\n"; }
+//             std::cout<<std::string(depth, '\t');
+//             std::string name = blobmsg_name(attr);
+//             if (name != "") {  std::cout<<"\""<<name<<"\": "; }
+//             switch (blob_id(attr)) {
+//                 case BLOBMSG_TYPE_UNSPEC: std::cout<<"(unspecified)"; break;
+//                 case BLOBMSG_TYPE_ARRAY:
+//                     std::cout<<"[";
+//                     explore_ref(explore_ref, ubus::iterator{attr}, depth+1);
+//                     std::cout<<"\n"<<std::string(depth, '\t')<<"]";
+//                     break;
+//                 case BLOBMSG_TYPE_TABLE:
+//                     std::cout<<"{";
+//                     explore_ref(explore_ref, ubus::iterator{attr}, depth+1);
+//                     std::cout<<"\n"<<std::string(depth, '\t')<<"}";
+//                     break;
+//                 case BLOBMSG_TYPE_STRING:
+//                     std::cout<<"\""<<blobmsg_get_string(attr)<<"\"";
+//                     break;
+//                 case BLOBMSG_TYPE_INT64:
+//                     std::cout<<blobmsg_get_u64(attr);
+//                     break;
+//                 case BLOBMSG_TYPE_INT32:
+//                     std::cout<<blobmsg_get_u32(attr);
+//                     break;
+//                 case BLOBMSG_TYPE_INT16:
+//                     std::cout<<blobmsg_get_u16(attr);
+//                     break;
+//                 case BLOBMSG_TYPE_BOOL:
+//                     std::cout<<(blobmsg_get_bool(attr) ? "true" : "false");
+//                     break;
+//                 case BLOBMSG_TYPE_DOUBLE:
+//                     std::cout<<blobmsg_get_double(attr);
+//                     break;
+//                 default: std::cout<<"(unknown)"; break;
+//             }
+//         }
+//     };
+//     std::cout<<"{";
+//     explore_internal(explore_internal, message.begin());
+//     std::cout<<"\n}"<<std::endl;
+// };
+// explore(ubus::call("network.interface.lan", "status"));
+
+
+
+namespace ubus {
+
+
+using strings = std::vector<std::string>;
+
+
+inline void append(strings & /*dest*/) {}
+
+
+template<class ...Strings>
+inline void append(strings & dest, strings src, Strings ...more)
+{
+    dest.reserve(dest.size() + src.size());
+    dest.insert(std::end(dest), std::make_move_iterator(std::begin(src)),
+                std::make_move_iterator(std::end(src)));
+    append(dest, std::move(more)...);
+}
+
+
+template<class S, class ...Strings>
+inline void append(strings & dest, S src, Strings ...more)
+{
+    dest.push_back(std::move(src));
+    append(dest, std::move(more)...);
+}
+
+
+
+class iterator {
+
+private:
+
+    const strings & keys;
+
+    const size_t n = 0;
+
+    size_t i = 0;
+
+    const blob_attr * pos = nullptr;
+
+    std::unique_ptr<iterator> cur{};
+
+    iterator * parent = nullptr;
+
+    size_t rem = 0;
+
+
+    [[nodiscard]] inline auto matches() const -> bool
+    {
+        return (keys[i].empty() || blobmsg_name(cur->pos)==keys[i]);
+    }
+
+
+    explicit iterator(iterator * par)
+    : keys{par->keys}, n{par->n}, pos{par->pos}, cur{this}, parent{par}
+    {
+        if (pos!=nullptr) {
+            rem = blobmsg_data_len(pos);
+            pos = static_cast<blob_attr *>(blobmsg_data(pos));
+        }
+    }
+
+
+public:
+
+
+    explicit iterator(const blob_attr * msg, const strings & filter)
+    : keys{filter}, n{keys.size()-1}, pos{msg}, cur{this}
+    {
+        if (pos!=nullptr) {
+            rem = blobmsg_data_len(pos);
+            pos = static_cast<blob_attr *>(blobmsg_data(pos));
+
+            if (rem==0) { pos = nullptr; }
+            else if (i!=n || !matches()) { ++*this; }
+        }
+    }
+
+
+    inline iterator(iterator &&) = default;
+
+
+    inline iterator(const iterator &) = delete;
+
+
+    inline auto operator=(const iterator &) -> iterator & = delete;
+
+
+    inline auto operator=(iterator &&) -> iterator & = delete;
+
+
+    inline auto operator*() { return cur->pos; }
+
+
+    inline auto operator!=(const iterator & rhs)
+    { return (cur->rem!=rhs.cur->rem || cur->pos!=rhs.cur->pos); }
+
+
+    auto operator++() -> iterator &;
+
+
+    inline ~iterator() = default;
+
+};
+
+
+
+class message {
+
+private:
+
+    const std::shared_ptr<const blob_attr> msg{}; // initialized by callback.
+
+    const strings keys{};
+
+
+public:
+
+    inline explicit message(std::shared_ptr<const blob_attr>  message,
+                        strings filter={""})
+    : msg{std::move(message)}, keys{std::move(filter)} {}
+
+
+    inline message(message &&) = default;
+
+
+    inline message(const message &) = delete;
+
+
+    inline auto operator=(message &&) -> message & = delete;
+
+
+    inline auto operator=(const message &) -> message & = delete;
+
+
+    [[nodiscard]] inline auto begin() const -> iterator
+    { return iterator{msg.get(), keys}; }
+
+
+    [[nodiscard]] inline auto end() const -> iterator
+    { return iterator{nullptr, keys}; }
+
+
+    inline explicit operator bool() const { return begin()!=end(); }
+
+
+    template<class ...Strings>
+    auto filter(Strings ...filter)
+    {
+        strings both{};
+        if (keys.size()!=1 || !keys[0].empty()) { both = keys; }
+        append(both, std::move(filter)...);
+        return std::move(message{msg, std::move(both)});
+    }
+
+
+    inline ~message() = default;
+
+};
+
+
+
+class ubus {
+
+private:
+
+    static std::mutex buffering;
+
+
+public:
+
+    inline ubus() = delete;
+
+
+    inline ubus(ubus &&) = delete;
+
+
+    inline ubus(const ubus &) = delete;
+
+
+    inline auto operator=(ubus &&) -> auto && = delete;
+
+
+    inline auto operator=(const ubus &) -> auto & = delete;
+
+
+    static auto get_context() -> ubus_context *
+    {
+        static auto ubus_freeing = [] (ubus_context * ctx) { ubus_free(ctx); };
+        static std::unique_ptr<ubus_context, decltype(ubus_freeing)>
+            lazy_ctx{ubus_connect(nullptr), ubus_freeing};
+
+        if (!lazy_ctx) { // it could be available on a later call:
+            static std::mutex connecting;
+
+            connecting.lock();
+            if (!lazy_ctx) { lazy_ctx.reset(ubus_connect(nullptr)); }
+            connecting.unlock();
+
+            if (!lazy_ctx) {
+                throw std::runtime_error("ubus error: cannot connect context");
+            }
+        }
+
+        return lazy_ctx.get();
+    }
+
+
+    static auto lock_and_get_shared_blob_buf() -> blob_buf *
+    {
+        static blob_buf buf;
+
+        static auto blob_buf_freeing = [] (blob_buf * b) { blob_buf_free(b); };
+        static std::unique_ptr<blob_buf, decltype(blob_buf_freeing)>
+                created_to_free_on_the_end_of_life{&buf, blob_buf_freeing};
+
+        buffering.lock();
+        blob_buf_init(&buf, 0);
+        return &buf;
+    }
+
+
+    static void unlock_shared_blob_buf() { buffering.unlock(); }
+
+
+    inline ~ubus() = delete;
+
+};
+
+
+auto call(const char * path, const char * method="", int timeout=500);
+
+
+
+// ------------------------- implementation: ----------------------------------
+
+
+std::mutex ubus::buffering;
+
+
+inline auto iterator::operator++() -> iterator &
+{
+    for(;;) {
+        #ifndef NDEBUG
+            std::cout<<std::string(i,'>')<<" look for "<<keys[i]<<" at ";
+            std::cout<<blobmsg_name(cur->pos)<<std::endl;
+        #endif
+
+        auto id = blob_id(cur->pos);
+        if ( (id==BLOBMSG_TYPE_TABLE || id==BLOBMSG_TYPE_ARRAY)
+                && i<n
+                && matches()
+                && blobmsg_data_len(cur->pos)>0 )
+        { //immmerge:
+            ++i;
+
+            auto tmp = cur.release();
+            cur = std::unique_ptr<iterator>{new iterator(tmp)};
+
+        } else {
+            while (true) {
+                cur->rem -= blob_pad_len(cur->pos);
+                cur->pos = blob_next(cur->pos);
+                auto len = blob_pad_len(cur->pos);
+
+                if (cur->rem>0 && len<=cur->rem && len>=sizeof(blob_attr))
+                { break; }
+
+                //emerge:
+                auto tmp = cur->parent;
+
+                if (tmp == nullptr) {
+                    cur->pos = nullptr;
+                    return *cur;
+                }
+
+                cur.reset(tmp);
+
+                --i;
+            }
+        }
+        if (i==n && matches()) { return *cur; }
+    }
+}
+
+
+inline auto call(const char * path, const char * method, const int timeout)
+{
+    auto ctx = ubus::get_context();
+
+    uint32_t id;
+    int err = ubus_lookup_id(ctx, path, &id);
+
+    if (err==0) { // call
+        ubus_request req{};
+
+        auto buf = ubus::lock_and_get_shared_blob_buf();
+        err = ubus_invoke_async(ctx, id, method, buf->head, &req);
+        ubus::unlock_shared_blob_buf();
+
+        if (err==0) {
+            using msg_t = std::shared_ptr<const blob_attr>;
+
+            msg_t msg;
+            req.priv = &msg;
+
+            /* Cannot capture anything (msg), the lambda would be another type.
+            * Pass a location where to save the message as priv pointer when
+            * invoking and get it back here:
+            */
+            req.data_cb = [](ubus_request * req, int /*type*/, blob_attr * msg)
+            {
+                if ((req == nullptr) || (msg == nullptr)) { return; }
+
+                auto saved = static_cast<msg_t *>(req->priv);
+                if (saved==nullptr || *saved) { return; }
+
+                saved->reset(blob_memdup(msg), free);
+                if (!*saved) { throw std::bad_alloc(); }
+            };
+
+            err = ubus_complete_request(ctx, &req, timeout);
+
+            if (err==0) { return message{msg}; }
+        }
+    }
+
+    std::string errmsg = "ubus::call error: cannot invoke";
+    errmsg +=  " (" + std::to_string(err) + ") " + path + " " + method;
+    throw std::runtime_error(errmsg.c_str());
+}
+
+
+} // namespace ubus
+
+
+#endif