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

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

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

**Initialization:**

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

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

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

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

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

**Defaults:**

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

**Other:**

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

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

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

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

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

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

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

458 lines
13 KiB
C++

#include <unistd.h>
#include <array>
#include <iostream>
#include <numeric>
#include <string>
#include <string_view>
#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 = 0;
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));
}
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);
}
}
}
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));
}
}
}
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));
}
}
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));
}
}
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::accumulate(
cmds.begin(), cmds.end(), std::string{"usage: \n"},
[=](const auto& use, const auto& cmd) {
return use + 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;
}