blob: 5a64b000a847c1d217bd04bf22630b8bc463bb1f [file] [log] [blame]
b.liue9582032025-04-17 19:18:16 +08001#ifndef __NGINX_SSL_UTIL_HPP
2#define __NGINX_SSL_UTIL_HPP
3
4#ifdef NO_PCRE
5#include <regex>
6namespace rgx = std;
7#else
8#include "regex-pcre.hpp"
9#endif
10
11#include "nginx-util.hpp"
12#include "px5g-openssl.hpp"
13
14#ifndef NO_UBUS
15static constexpr auto UBUS_TIMEOUT = 1000;
16#endif
17
18// once a year:
19static constexpr auto CRON_INTERVAL = std::string_view{"3 3 12 12 *"};
20
21static constexpr auto LAN_SSL_LISTEN = std::string_view{"/var/lib/nginx/lan_ssl.listen"};
22
23static constexpr auto LAN_SSL_LISTEN_DEFAULT = // TODO(pst) deprecate
24 std::string_view{"/var/lib/nginx/lan_ssl.listen.default"};
25
26static constexpr auto ADD_SSL_FCT = std::string_view{"add_ssl"};
27
28static constexpr auto SSL_SESSION_CACHE_ARG = [](const std::string_view & /*name*/) -> std::string {
29 return "shared:SSL:32k";
30};
31
32static constexpr auto SSL_SESSION_TIMEOUT_ARG = std::string_view{"64m"};
33
34using _Line = std::array<std::string (*)(const std::string&, const std::string&), 2>;
35
36class Line {
37 private:
38 _Line _line;
39
40 public:
41 explicit Line(const _Line& line) noexcept : _line{line} {}
42
43 template <const _Line&... xn>
44 static auto build() noexcept -> Line
45 {
46 return Line{_Line{[](const std::string& p, const std::string& b) -> std::string {
47 return (... + xn[0](p, b));
48 },
49 [](const std::string& p, const std::string& b) -> std::string {
50 return (... + xn[1](p, b));
51 }}};
52 }
53
54 [[nodiscard]] auto STR(const std::string& param, const std::string& begin) const -> std::string
55 {
56 return _line[0](param, begin);
57 }
58
59 [[nodiscard]] auto RGX() const -> rgx::regex
60 {
61 return rgx::regex{_line[1]("", "")};
62 }
63};
64
65auto get_if_missed(const std::string& conf,
66 const Line& LINE,
67 const std::string& val,
68 const std::string& indent = "\n ",
69 bool compare = true) -> std::string;
70
71auto replace_if(const std::string& conf,
72 const rgx::regex& rgx,
73 const std::string& val,
74 const std::string& insert) -> std::string;
75
76auto replace_listen(const std::string& conf, const std::array<const char*, 2>& ngx_port)
77 -> std::string;
78
79auto check_ssl_certificate(const std::string& crtpath, const std::string& keypath) -> bool;
80
81auto contains(const std::string& sentence, const std::string& word) -> bool;
82
83auto get_uci_section_for_name(const std::string& name) -> uci::section;
84
85void add_ssl_if_needed(const std::string& name);
86
87void add_ssl_if_needed(const std::string& name,
88 std::string_view manage,
89 std::string_view crt,
90 std::string_view key);
91
92void install_cron_job(const Line& CRON_LINE, const std::string& name = "");
93
94void remove_cron_job(const Line& CRON_LINE, const std::string& name = "");
95
96auto del_ssl_legacy(const std::string& name) -> bool;
97
98void del_ssl(const std::string& name);
99
100void del_ssl(const std::string& name, std::string_view manage);
101
102auto check_ssl(const uci::package& pkg, bool is_enabled) -> bool;
103
104inline void check_ssl(const uci::package& pkg)
105{
106 if (!check_ssl(pkg, is_enabled(pkg))) {
107#ifndef NO_UBUS
108 if (ubus::call("service", "list", UBUS_TIMEOUT).filter("nginx")) {
109 call("/etc/init.d/nginx", "reload");
110 std::cerr << "Reload Nginx.\n";
111 }
112#endif
113 }
114}
115
116constexpr auto _begin = _Line{
117 [](const std::string& /*param*/, const std::string& begin) -> std::string { return begin; },
118
119 [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string {
120 return R"([{;](?:\s*#[^\n]*(?=\n))*(\s*))";
121 }};
122
123constexpr auto _space = _Line{[](const std::string& /*param*/, const std::string &
124 /*begin*/) -> std::string { return std::string{" "}; },
125
126 [](const std::string& /*param*/, const std::string &
127 /*begin*/) -> std::string { return R"(\s+)"; }};
128
129constexpr auto _newline = _Line{
130 [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string {
131 return std::string{"\n"};
132 },
133
134 [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string {
135 return std::string{"(\n)"};
136 } // capture it as _end captures it, too.
137};
138
139constexpr auto _end =
140 _Line{[](const std::string& /*param*/, const std::string & /*begin*/) -> std::string {
141 return std::string{";"};
142 },
143
144 [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string {
145 return std::string{R"(\s*(;(?:[\t ]*#[^\n]*)?))"};
146 }};
147
148template <char clim = '\0'>
149static constexpr auto _capture = _Line{
150 [](const std::string& param, const std::string & /*begin*/) -> std::string {
151 return '\'' + param + '\'';
152 },
153
154 [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string {
155 const auto lim = clim == '\0' ? std::string{"\\s"} : std::string{clim};
156 return std::string{R"(((?:(?:"[^"]*")|(?:[^'")"} + lim + "][^" + lim + "]*)|(?:'[^']*'))+)";
157 }};
158
159template <const std::string_view& strptr, char clim = '\0'>
160static constexpr auto _escape = _Line{
161 [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string {
162 return clim == '\0' ? std::string{strptr.data()} : clim + std::string{strptr.data()} + clim;
163 },
164
165 [](const std::string& /*param*/, const std::string & /*begin*/) -> std::string {
166 std::string ret{};
167 for (char c : strptr) {
168 switch (c) {
169 case '^':
170 ret += '\\';
171 ret += c;
172 break;
173 case '_':
174 case '-':
175 ret += c;
176 break;
177 default:
178 if ((isalpha(c) != 0) || (isdigit(c) != 0)) {
179 ret += c;
180 }
181 else {
182 ret += std::string{"["} + c + "]";
183 }
184 }
185 }
186 return "(?:" + ret + "|'" + ret + "'" + "|\"" + ret + "\"" + ")";
187 }};
188
189constexpr std::string_view _check_ssl = "check_ssl";
190
191constexpr std::string_view _server_name = "server_name";
192
193constexpr std::string_view _listen = "listen";
194
195constexpr std::string_view _include = "include";
196
197constexpr std::string_view _ssl_certificate = "ssl_certificate";
198
199constexpr std::string_view _ssl_certificate_key = "ssl_certificate_key";
200
201constexpr std::string_view _ssl_session_cache = "ssl_session_cache";
202
203constexpr std::string_view _ssl_session_timeout = "ssl_session_timeout";
204
205// For a compile time regex lib, this must be fixed, use one of these options:
206// * Hand craft or macro concat them (loosing more or less flexibility).
207// * Use Macro concatenation of __VA_ARGS__ with the help of:
208// https://p99.gforge.inria.fr/p99-html/group__preprocessor__for.html
209// * Use constexpr---not available for strings or char * for now---look at lib.
210
211static const auto CRON_CHECK =
212 Line::build<_space, _escape<NGINX_UTIL>, _space, _escape<_check_ssl, '\''>, _newline>();
213
214static const auto CRON_CMD = Line::build<_space,
215 _escape<NGINX_UTIL>,
216 _space,
217 _escape<ADD_SSL_FCT, '\''>,
218 _space,
219 _capture<>,
220 _newline>();
221
222static const auto NGX_SERVER_NAME =
223 Line::build<_begin, _escape<_server_name>, _space, _capture<';'>, _end>();
224
225static const auto NGX_INCLUDE_LAN_LISTEN =
226 Line::build<_begin, _escape<_include>, _space, _escape<LAN_LISTEN, '\''>, _end>();
227
228static const auto NGX_INCLUDE_LAN_LISTEN_DEFAULT =
229 Line::build<_begin, _escape<_include>, _space, _escape<LAN_LISTEN_DEFAULT, '\''>, _end>();
230
231static const auto NGX_INCLUDE_LAN_SSL_LISTEN =
232 Line::build<_begin, _escape<_include>, _space, _escape<LAN_SSL_LISTEN, '\''>, _end>();
233
234static const auto NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT =
235 Line::build<_begin, _escape<_include>, _space, _escape<LAN_SSL_LISTEN_DEFAULT, '\''>, _end>();
236
237static const auto NGX_SSL_CRT =
238 Line::build<_begin, _escape<_ssl_certificate>, _space, _capture<';'>, _end>();
239
240static const auto NGX_SSL_KEY =
241 Line::build<_begin, _escape<_ssl_certificate_key>, _space, _capture<';'>, _end>();
242
243static const auto NGX_SSL_SESSION_CACHE =
244 Line::build<_begin, _escape<_ssl_session_cache>, _space, _capture<';'>, _end>();
245
246static const auto NGX_SSL_SESSION_TIMEOUT =
247 Line::build<_begin, _escape<_ssl_session_timeout>, _space, _capture<';'>, _end>();
248
249static const auto NGX_LISTEN = Line::build<_begin, _escape<_listen>, _space, _capture<';'>, _end>();
250
251static const auto NGX_PORT_80 = std::array<const char*, 2>{
252 R"(^\s*([^:]*:|\[[^\]]*\]:)?80(\s|$|;))",
253 "$01443 ssl$2",
254};
255
256static const auto NGX_PORT_443 = std::array<const char*, 2>{
257 R"(^\s*([^:]*:|\[[^\]]*\]:)?443(\s.*)?\sssl(\s|$|;))",
258 "$0180$2$3",
259};
260
261// ------------------------- implementation: ----------------------------------
262
263auto get_if_missed(const std::string& conf,
264 const Line& LINE,
265 const std::string& val,
266 const std::string& indent,
267 bool compare) -> std::string
268{
269 if (!compare || val.empty()) {
270 return rgx::regex_search(conf, LINE.RGX()) ? "" : LINE.STR(val, indent);
271 }
272
273 rgx::smatch match; // assuming last capture has the value!
274
275 for (auto pos = conf.begin(); rgx::regex_search(pos, conf.end(), match, LINE.RGX());
276 pos += match.position(0) + match.length(0))
277 {
278 const std::string value = match.str(match.size() - 2);
279
280 if (value == val || value == "'" + val + "'" || value == '"' + val + '"') {
281 return "";
282 }
283 }
284
285 return LINE.STR(val, indent);
286}
287
288auto replace_if(const std::string& conf,
289 const rgx::regex& rgx,
290 const std::string& val,
291 const std::string& insert) -> std::string
292{
293 std::string ret{};
294 auto pos = conf.begin();
295
296 auto skip = 0;
297 for (rgx::smatch match; rgx::regex_search(pos, conf.end(), match, rgx);
298 pos += match.position(match.size() - 1))
299 {
300 auto i = match.size() - 2;
301 const std::string value = match.str(i);
302
303 bool compare = !val.empty();
304 if (compare && value != val && value != "'" + val + "'" && value != '"' + val + '"') {
305 ret.append(pos + skip, pos + match.position(i) + match.length(i));
306 skip = 0;
307 }
308 else {
309 ret.append(pos + skip, pos + match.position(match.size() > 2 ? 1 : 0));
310 ret += insert;
311 skip = 1;
312 }
313 }
314
315 ret.append(pos + skip, conf.end());
316 return ret;
317}
318
319auto replace_listen(const std::string& conf, const std::array<const char*, 2>& ngx_port)
320 -> std::string
321{
322 std::string ret{};
323 auto pos = conf.begin();
324
325 for (rgx::smatch match; rgx::regex_search(pos, conf.end(), match, NGX_LISTEN.RGX());
326 pos += match.position(match.size() - 1))
327 {
328 auto i = match.size() - 2;
329 ret.append(pos, pos + match.position(i));
330 ret += rgx::regex_replace(match.str(i), rgx::regex{ngx_port[0]}, ngx_port[1]);
331 }
332
333 ret.append(pos, conf.end());
334 return ret;
335}
336
337inline void add_ssl_directives_to(const std::string& name)
338{
339 const std::string prefix = std::string{CONF_DIR} + name;
340
341 const std::string const_conf = read_file(prefix + ".conf");
342
343 rgx::smatch match; // captures str(1)=indentation spaces, str(2)=server name
344 for (auto pos = const_conf.begin();
345 rgx::regex_search(pos, const_conf.end(), match, NGX_SERVER_NAME.RGX());
346 pos += match.position(0) + match.length(0))
347 {
348 if (!contains(match.str(2), name)) {
349 continue;
350 } // else:
351
352 const std::string indent = match.str(1);
353
354 auto adds = std::string{};
355
356 adds += get_if_missed(const_conf, NGX_SSL_CRT, prefix + ".crt", indent);
357
358 adds += get_if_missed(const_conf, NGX_SSL_KEY, prefix + ".key", indent);
359
360 adds += get_if_missed(const_conf, NGX_SSL_SESSION_CACHE, SSL_SESSION_CACHE_ARG(name),
361 indent, false);
362
363 adds += get_if_missed(const_conf, NGX_SSL_SESSION_TIMEOUT,
364 std::string{SSL_SESSION_TIMEOUT_ARG}, indent, false);
365
366 pos += match.position(0) + match.length(0);
367 std::string conf =
368 std::string(const_conf.begin(), pos) + adds + std::string(pos, const_conf.end());
369
370 conf = replace_if(conf, NGX_INCLUDE_LAN_LISTEN_DEFAULT.RGX(), "",
371 NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT.STR("", indent));
372
373 conf = replace_if(conf, NGX_INCLUDE_LAN_LISTEN.RGX(), "",
374 NGX_INCLUDE_LAN_SSL_LISTEN.STR("", indent));
375
376 conf = replace_listen(conf, NGX_PORT_80);
377
378 if (conf != const_conf) {
379 write_file(prefix + ".conf", conf);
380 std::cerr << "Added SSL directives to " << prefix << ".conf\n";
381 }
382
383 return;
384 }
385
386 auto errmsg = std::string{"add_ssl_directives_to error: "};
387 errmsg += "cannot add SSL directives to " + name + ".conf, missing: ";
388 errmsg += NGX_SERVER_NAME.STR(name, "\n ") + "\n";
389 throw std::runtime_error(errmsg);
390}
391
392template <typename T>
393inline auto num2hex(T bytes) -> std::array<char, 2 * sizeof(bytes) + 1>
394{
395 constexpr auto n = 2 * sizeof(bytes);
396 std::array<char, n + 1> str{};
397
398 for (size_t i = 0; i < n; ++i) {
399 static const std::array<char, 17> hex{"0123456789ABCDEF"};
400 static constexpr auto get = 0x0fU;
401 str.at(i) = hex.at(bytes & get);
402
403 static constexpr auto move = 4U;
404 bytes >>= move;
405 }
406
407 str[n] = '\0';
408 return str;
409}
410
411template <typename T>
412inline auto get_nonce(const T salt = 0) -> T
413{
414 T nonce = 0;
415
416 std::ifstream urandom{"/dev/urandom"};
417
418 static constexpr auto move = 6U;
419
420 constexpr size_t steps = (sizeof(nonce) * 8 - 1) / move + 1;
421
422 for (size_t i = 0; i < steps; ++i) {
423 if (!urandom.good()) {
424 throw std::runtime_error("get_nonce error");
425 }
426 nonce = (nonce << move) + static_cast<unsigned>(urandom.get());
427 }
428
429 nonce ^= salt;
430
431 return nonce;
432}
433
434inline void create_ssl_certificate(const std::string& crtpath,
435 const std::string& keypath,
436 const int days = 792)
437{
438 size_t nonce = 0;
439
440 try {
441 nonce = get_nonce(nonce);
442 }
443
444 catch (...) { // the address of a variable should be random enough:
445 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) sic:
446 nonce += reinterpret_cast<size_t>(&crtpath);
447 }
448
449 auto noncestr = num2hex(nonce);
450
451 const auto tmpcrtpath = crtpath + ".new-" + noncestr.data();
452 const auto tmpkeypath = keypath + ".new-" + noncestr.data();
453
454 try {
455 auto pkey = gen_eckey(NID_secp384r1);
456
457 write_key(pkey, tmpkeypath);
458
459 std::string subject{"/C=ZZ/ST=Somewhere/L=None/CN=OpenWrt/O=OpenWrt"};
460 subject += noncestr.data();
461
462 selfsigned(pkey, days, subject, tmpcrtpath);
463
464 static constexpr auto to_seconds = 24 * 60 * 60;
465 static constexpr auto leeway = 42;
466 if (!checkend(tmpcrtpath, days * to_seconds - leeway)) {
467 throw std::runtime_error("bug: created certificate is not valid!!");
468 }
469 }
470 catch (...) {
471 std::cerr << "create_ssl_certificate error: ";
472 std::cerr << "cannot create selfsigned certificate, ";
473 std::cerr << "removing temporary files ..." << std::endl;
474
475 if (remove(tmpcrtpath.c_str()) != 0) {
476 auto errmsg = "\t cannot remove " + tmpcrtpath;
477 perror(errmsg.c_str());
478 }
479
480 if (remove(tmpkeypath.c_str()) != 0) {
481 auto errmsg = "\t cannot remove " + tmpkeypath;
482 perror(errmsg.c_str());
483 }
484
485 throw;
486 }
487
488 if (rename(tmpcrtpath.c_str(), crtpath.c_str()) != 0 ||
489 rename(tmpkeypath.c_str(), keypath.c_str()) != 0)
490 {
491 auto errmsg = std::string{"create_ssl_certificate warning: "};
492 errmsg += "cannot move " + tmpcrtpath + " to " + crtpath;
493 errmsg += " or " + tmpkeypath + " to " + keypath + ", continuing ... ";
494 perror(errmsg.c_str());
495 }
496
497 std::cerr << "Created self-signed SSL certificate '" << crtpath;
498 std::cerr << "' with key '" << keypath << "'.\n";
499}
500
501auto check_ssl_certificate(const std::string& crtpath, const std::string& keypath) -> bool
502{
503 { // paths are relative to dir:
504 auto dir = std::string_view{"/etc/nginx"};
505 auto crt_rel = crtpath[0] != '/';
506 auto key_rel = keypath[0] != '/';
507 if ((crt_rel || key_rel) && (chdir(dir.data()) != 0)) {
508 auto errmsg = std::string{"check_ssl_certificate error: entering "};
509 errmsg += dir;
510 perror(errmsg.c_str());
511 errmsg += " (need to change directory since the given ";
512 errmsg += crt_rel ? "ssl_certificate '" + crtpath : std::string{};
513 errmsg += crt_rel && key_rel ? "' and " : "";
514 errmsg += key_rel ? "ssl_certificate_key '" + keypath : std::string{};
515 errmsg += crt_rel && key_rel ? "' are" : "' is a";
516 errmsg += " relative path";
517 errmsg += crt_rel && key_rel ? "s)" : ")";
518 throw std::runtime_error(errmsg);
519 }
520 }
521
522 constexpr auto remaining_seconds = (365 + 32) * 24 * 60 * 60;
523 constexpr auto validity_days = 3 * (365 + 31);
524
525 bool is_valid = true;
526
527 if (access(keypath.c_str(), R_OK) != 0 || access(crtpath.c_str(), R_OK) != 0) {
528 is_valid = false;
529 }
530
531 else {
532 try {
533 if (!checkend(crtpath, remaining_seconds)) {
534 is_valid = false;
535 }
536 }
537 catch (...) { // something went wrong, maybe it is in DER format:
538 try {
539 if (!checkend(crtpath, remaining_seconds, false)) {
540 is_valid = false;
541 }
542 }
543 catch (...) { // it has neither DER nor PEM format, rebuild.
544 is_valid = false;
545 }
546 }
547 }
548
549 if (!is_valid) {
550 create_ssl_certificate(crtpath, keypath, validity_days);
551 }
552
553 return is_valid;
554}
555
556auto contains(const std::string& sentence, const std::string& word) -> bool
557{
558 auto pos = sentence.find(word);
559 if (pos == std::string::npos) {
560 return false;
561 }
562 if (pos != 0 && (isgraph(sentence[pos - 1]) != 0)) {
563 return false;
564 }
565 if (isgraph(sentence[pos + word.size()]) != 0) {
566 return false;
567 }
568 // else:
569 return true;
570}
571
572auto get_uci_section_for_name(const std::string& name) -> uci::section
573{
574 auto pkg = uci::package{"nginx"}; // let it throw.
575
576 auto uci_enabled = is_enabled(pkg);
577
578 if (uci_enabled) {
579 for (auto sec : pkg) {
580 if (sec.name() == name) {
581 return sec;
582 }
583 }
584 // try interpreting 'name' as FQDN:
585 for (auto sec : pkg) {
586 for (auto opt : sec) {
587 if (opt.name() == "server_name") {
588 for (auto itm : opt) {
589 if (contains(itm.name(), name)) {
590 return sec;
591 }
592 }
593 }
594 }
595 }
596 }
597
598 auto errmsg = std::string{"lookup error: neither there is a file named '"};
599 errmsg += std::string{CONF_DIR} + name + ".conf' nor the UCI config has ";
600 if (uci_enabled) {
601 errmsg += "a nginx server with section name or 'server_name': " + name;
602 }
603 else {
604 errmsg += "been enabled by:\n\tuci set nginx.global.uci_enable=true";
605 }
606 throw std::runtime_error(errmsg);
607}
608
609inline auto add_ssl_to_config(const std::string& name,
610 const std::string_view manage = "self-signed",
611 const std::string_view crt = "",
612 const std::string_view key = "")
613{
614 auto sec = get_uci_section_for_name(name); // let it throw.
615 auto secname = sec.name();
616
617 struct {
618 std::string crt;
619 std::string key;
620 } ret;
621
622 std::cerr << "Adding SSL directives to UCI server: nginx." << secname << "\n";
623
624 std::cerr << "\t" << MANAGE_SSL << "='" << manage << "'\n";
625 sec.set(MANAGE_SSL.data(), manage.data());
626
627 if (!crt.empty() && !key.empty()) {
628 sec.set("ssl_certificate", crt.data());
629 std::cerr << "\tssl_certificate='" << crt << "'\n";
630 sec.set("ssl_certificate_key", key.data());
631 std::cerr << "\tssl_certificate_key='" << key << "'\n";
632 }
633
634 auto cache = false;
635 auto timeout = false;
636 for (auto opt : sec) {
637 if (opt.name() == "ssl_session_cache") {
638 cache = true;
639 continue;
640 } // else:
641
642 if (opt.name() == "ssl_session_timeout") {
643 timeout = true;
644 continue;
645 }
646
647 // else:
648 for (auto itm : opt) {
649 if (opt.name() == "ssl_certificate_key") {
650 ret.key = itm.name();
651 }
652
653 else if (opt.name() == "ssl_certificate") {
654 ret.crt = itm.name();
655 }
656
657 else if (opt.name() == "listen") {
658 auto val = regex_replace(itm.name(), rgx::regex{NGX_PORT_80[0]}, NGX_PORT_80[1]);
659 if (val != itm.name()) {
660 std::cerr << "\t" << opt.name() << "='" << val << "' (replacing)\n";
661 itm.rename(val.c_str());
662 }
663 }
664 }
665 }
666
667 if (ret.crt.empty()) {
668 ret.crt = std::string{CONF_DIR} + name + ".crt";
669 std::cerr << "\tssl_certificate='" << ret.crt << "'\n";
670 sec.set("ssl_certificate", ret.crt.c_str());
671 }
672
673 if (ret.key.empty()) {
674 ret.key = std::string{CONF_DIR} + name + ".key";
675 std::cerr << "\tssl_certificate_key='" << ret.key << "'\n";
676 sec.set("ssl_certificate_key", ret.key.c_str());
677 }
678
679 if (!cache) {
680 std::cerr << "\tssl_session_cache='" << SSL_SESSION_CACHE_ARG(name) << "'\n";
681 sec.set("ssl_session_cache", SSL_SESSION_CACHE_ARG(name).data());
682 }
683
684 if (!timeout) {
685 std::cerr << "\tssl_session_timeout='" << SSL_SESSION_TIMEOUT_ARG << "'\n";
686 sec.set("ssl_session_timeout", SSL_SESSION_TIMEOUT_ARG.data());
687 }
688
689 sec.commit();
690
691 return ret;
692}
693
694void install_cron_job(const Line& CRON_LINE, const std::string& name)
695{
696 static const char* filename = "/etc/crontabs/root";
697
698 std::string conf{};
699 try {
700 conf = read_file(filename);
701 }
702 catch (const std::ifstream::failure&) { /* is ok if not found, create. */
703 }
704
705 const std::string add = get_if_missed(conf, CRON_LINE, name);
706
707 if (add.length() > 0) {
708#ifndef NO_UBUS
709 if (!ubus::call("service", "list", UBUS_TIMEOUT).filter("cron")) {
710 std::string errmsg{"install_cron_job error: "};
711 errmsg += "Cron unavailable to re-create the ssl certificate";
712 errmsg += (name.empty() ? std::string{"s\n"} : " for '" + name + "'\n");
713 throw std::runtime_error(errmsg);
714 } // else active with or without instances:
715#endif
716
717 const auto* pre = (conf.length() == 0 || conf.back() == '\n' ? "" : "\n");
718 write_file(filename, pre + std::string{CRON_INTERVAL} + add, std::ios::app);
719
720#ifndef NO_UBUS
721 call("/etc/init.d/cron", "reload");
722#endif
723
724 std::cerr << "Rebuild the self-signed SSL certificate";
725 std::cerr << (name.empty() ? std::string{"s"} : " for '" + name + "'");
726 std::cerr << " annually with cron." << std::endl;
727 }
728}
729
730void add_ssl_if_needed(const std::string& name)
731{
732 const auto legacypath = std::string{CONF_DIR} + name + ".conf";
733 if (access(legacypath.c_str(), R_OK) == 0) {
734 add_ssl_directives_to(name); // let it throw.
735
736 const auto crtpath = std::string{CONF_DIR} + name + ".crt";
737 const auto keypath = std::string{CONF_DIR} + name + ".key";
738 check_ssl_certificate(crtpath, keypath); // let it throw.
739
740 try {
741 install_cron_job(CRON_CMD, name);
742 }
743 catch (...) {
744 std::cerr << "add_ssl_if_needed warning: cannot use cron to rebuild ";
745 std::cerr << "the self-signed SSL certificate for " << name << "\n";
746 }
747 return;
748 } // else:
749
750 auto paths = add_ssl_to_config(name); // let it throw.
751
752 check_ssl_certificate(paths.crt, paths.key); // let it throw.
753
754 try {
755 install_cron_job(CRON_CHECK);
756 }
757 catch (...) {
758 std::cerr << "add_ssl_if_needed warning: cannot use cron to rebuild ";
759 std::cerr << "the self-signed SSL certificates.\n";
760 }
761}
762
763void add_ssl_if_needed(const std::string& name,
764 const std::string_view manage,
765 const std::string_view crt,
766 const std::string_view key)
767{
768 if (crt[0] != '/') {
769 auto errmsg = std::string{"add_ssl_if_needed error: ssl_certificate "};
770 errmsg += "path cannot be relative '" + std::string{crt} + "'";
771 throw std::runtime_error(errmsg);
772 }
773
774 if (key[0] != '/') {
775 auto errmsg = std::string{"add_ssl_if_needed error: path to ssl_key "};
776 errmsg += "cannot be relative '" + std::string{key} + "'";
777 throw std::runtime_error(errmsg);
778 }
779
780 const auto legacypath = std::string{CONF_DIR} + name + ".conf";
781
782 if (access(legacypath.c_str(), R_OK) != 0) {
783 add_ssl_to_config(name, manage, crt, key); // let it throw.
784 return;
785 } // else:
786
787 // symlink crt+key to the paths that add_ssl_directives_to uses (if needed):
788
789 auto crtpath = std::string{CONF_DIR} + name + ".crt";
790 if (crtpath != crt && /* then */ symlink(crt.data(), crtpath.c_str()) != 0) {
791 auto errmsg = std::string{"add_ssl_if_needed error: cannot link "};
792 errmsg += "ssl_certificate " + crtpath + " -> " + crt.data() + " (";
793 errmsg += std::to_string(errno) + "): " + std::strerror(errno);
794 throw std::runtime_error(errmsg);
795 }
796
797 auto keypath = std::string{CONF_DIR} + name + ".key";
798 if (keypath != key && /* then */ symlink(key.data(), keypath.c_str()) != 0) {
799 auto errmsg = std::string{"add_ssl_if_needed error: cannot link "};
800 errmsg += "ssl_certificate_key " + keypath + " -> " + key.data() + " (";
801 errmsg += std::to_string(errno) + "): " + std::strerror(errno);
802 throw std::runtime_error(errmsg);
803 }
804
805 add_ssl_directives_to(name); // let it throw.
806}
807
808void remove_cron_job(const Line& CRON_LINE, const std::string& name)
809{
810 static const char* filename = "/etc/crontabs/root";
811
812 const auto const_conf = read_file(filename);
813
814 bool changed = false;
815 auto conf = std::string{};
816
817 size_t prev = 0;
818 size_t curr = 0;
819 while ((curr = const_conf.find('\n', prev)) != std::string::npos) {
820 auto line = const_conf.substr(prev, curr - prev + 1);
821
822 if (line == replace_if(line, CRON_LINE.RGX(), name, "")) {
823 conf += line;
824 }
825 else {
826 changed = true;
827 }
828
829 prev = curr + 1;
830 }
831
832 if (changed) {
833 write_file(filename, conf);
834
835 std::cerr << "Do not rebuild the self-signed SSL certificate";
836 std::cerr << (name.empty() ? std::string{"s"} : " for '" + name + "'");
837 std::cerr << " annually with cron anymore." << std::endl;
838
839#ifndef NO_UBUS
840 if (ubus::call("service", "list", UBUS_TIMEOUT).filter("cron")) {
841 call("/etc/init.d/cron", "reload");
842 }
843#endif
844 }
845}
846
847inline void del_ssl_directives_from(const std::string& name)
848{
849 const std::string prefix = std::string{CONF_DIR} + name;
850
851 const std::string const_conf = read_file(prefix + ".conf");
852
853 rgx::smatch match; // captures str(1)=indentation spaces, str(2)=server name
854 for (auto pos = const_conf.begin();
855 rgx::regex_search(pos, const_conf.end(), match, NGX_SERVER_NAME.RGX());
856 pos += match.position(0) + match.length(0))
857 {
858 if (!contains(match.str(2), name)) {
859 continue;
860 } // else:
861
862 const std::string indent = match.str(1);
863
864 std::string conf = const_conf;
865
866 conf = replace_listen(conf, NGX_PORT_443);
867
868 conf = replace_if(conf, NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT.RGX(), "",
869 NGX_INCLUDE_LAN_LISTEN_DEFAULT.STR("", indent));
870
871 conf = replace_if(conf, NGX_INCLUDE_LAN_SSL_LISTEN.RGX(), "",
872 NGX_INCLUDE_LAN_LISTEN.STR("", indent));
873
874 // NOLINTNEXTLINE(performance-inefficient-string-concatenation) prefix:
875 conf = replace_if(conf, NGX_SSL_CRT.RGX(), prefix + ".crt", "");
876
877 // NOLINTNEXTLINE(performance-inefficient-string-concatenation) prefix:
878 conf = replace_if(conf, NGX_SSL_KEY.RGX(), prefix + ".key", "");
879
880 conf = replace_if(conf, NGX_SSL_SESSION_CACHE.RGX(), "", "");
881
882 conf = replace_if(conf, NGX_SSL_SESSION_TIMEOUT.RGX(), "", "");
883
884 if (conf != const_conf) {
885 write_file(prefix + ".conf", conf);
886 std::cerr << "Deleted SSL directives from " << prefix << ".conf\n";
887 }
888
889 return;
890 }
891
892 auto errmsg = std::string{"del_ssl_directives_from error: "};
893 errmsg += "cannot delete SSL directives from " + name + ".conf, missing: ";
894 errmsg += NGX_SERVER_NAME.STR(name, "\n ") + "\n";
895 throw std::runtime_error(errmsg);
896}
897
898inline auto del_ssl_from_config(const std::string& name,
899 const std::string_view manage = "self-signed")
900{
901 auto sec = get_uci_section_for_name(name); // let it throw.
902 auto secname = sec.name();
903
904 struct {
905 std::string crt;
906 std::string key;
907 } ret;
908
909 std::cerr << "Deleting SSL directives from UCI server: nginx." << secname << "\n";
910
911 auto manage_match = false;
912 for (auto opt : sec) {
913 for (auto itm : opt) {
914 if (opt.name() == "ssl_certificate_key") {
915 ret.key = itm.name();
916 }
917
918 else if (opt.name() == "ssl_certificate") {
919 ret.crt = itm.name();
920 }
921
922 else if (opt.name() == "ssl_session_cache" || opt.name() == "ssl_session_timeout") {
923 }
924
925 else if (opt.name() == MANAGE_SSL && itm.name() == manage) {
926 manage_match = true;
927 }
928
929 else if (opt.name() == "listen") {
930 auto val = regex_replace(itm.name(), rgx::regex{NGX_PORT_443[0]}, NGX_PORT_443[1]);
931 if (val != itm.name()) {
932 std::cerr << "\t" << opt.name() << " (set back to '" << val << "')\n";
933 itm.rename(val.c_str());
934 }
935 continue; /* not deleting opt, look at other itm : opt */
936 }
937
938 else {
939 continue; /* not deleting opt, look at other itm : opt */
940 }
941
942 // Delete matching opt (not skipped by continue):
943 std::cerr << "\t" << opt.name() << " (was '" << itm.name() << "')\n";
944 opt.del();
945 break;
946 }
947 }
948 if (manage_match) {
949 sec.commit();
950 return ret;
951 } // else:
952
953 auto errmsg = std::string{"del_ssl error: not changing config wihtout: "};
954 errmsg += "uci set nginx." + secname + "." + MANAGE_SSL.data() + "='" + manage.data();
955 errmsg += "'";
956 throw std::runtime_error(errmsg);
957}
958
959auto del_ssl_legacy(const std::string& name) -> bool
960{
961 const auto legacypath = std::string{CONF_DIR} + name + ".conf";
962
963 if (access(legacypath.c_str(), R_OK) != 0) {
964 return false;
965 }
966
967 try {
968 remove_cron_job(CRON_CMD, name);
969 }
970 catch (...) {
971 std::cerr << "del_ssl warning: cannot remove cron job rebuilding ";
972 std::cerr << "the self-signed SSL certificate for " << name << "\n";
973 }
974
975 try {
976 del_ssl_directives_from(name);
977 }
978 catch (...) {
979 std::cerr << "del_ssl error: ";
980 std::cerr << "cannot delete SSL directives from " << name << ".conf\n";
981 throw;
982 }
983
984 return true;
985}
986
987void del_ssl(const std::string& name)
988{
989 auto crtpath = std::string{};
990 auto keypath = std::string{};
991
992 if (del_ssl_legacy(name)) { // let it throw.
993 crtpath = std::string{CONF_DIR} + name + ".crt";
994 keypath = std::string{CONF_DIR} + name + ".key";
995 }
996
997 else {
998 auto paths = del_ssl_from_config(name); // let it throw.
999 crtpath = paths.crt;
1000 keypath = paths.key;
1001 }
1002
1003 if (remove(crtpath.c_str()) != 0) {
1004 auto errmsg = "del_ssl warning: cannot remove " + crtpath;
1005 perror(errmsg.c_str());
1006 }
1007
1008 if (remove(keypath.c_str()) != 0) {
1009 auto errmsg = "del_ssl warning: cannot remove " + keypath;
1010 perror(errmsg.c_str());
1011 }
1012}
1013
1014void del_ssl(const std::string& name, const std::string_view manage)
1015{
1016 const auto legacypath = std::string{CONF_DIR} + name + ".conf";
1017
1018 if (access(legacypath.c_str(), R_OK) != 0) {
1019 del_ssl_from_config(name, manage); // let it throw.
1020 return;
1021 } // else:
1022
1023 del_ssl_directives_from(name); // let it throw.
1024
1025 for (const auto* ext : {".crt", ".key"}) {
1026 struct stat sb {};
1027
1028 auto path = std::string{CONF_DIR} + name + ext;
1029
1030 // managed version of add_ssl_if_needed created symlinks (if needed):
1031 // NOLINTNEXTLINE(hicpp-signed-bitwise) S_ISLNK macro:
1032 if (lstat(path.c_str(), &sb) == 0 && S_ISLNK(sb.st_mode)) {
1033 if (remove(path.c_str()) != 0) {
1034 auto errmsg = "del_ssl warning: cannot remove " + path;
1035 perror(errmsg.c_str());
1036 }
1037 }
1038 }
1039}
1040
1041auto check_ssl(const uci::package& pkg, bool is_enabled) -> bool
1042{
1043 auto are_valid = true;
1044 auto is_enabled_and_at_least_one_has_manage_ssl = false;
1045
1046 if (is_enabled) {
1047 for (auto sec : pkg) {
1048 if (sec.anonymous() || sec.type() != "server") {
1049 continue;
1050 } // else:
1051
1052 const auto legacypath = std::string{CONF_DIR} + sec.name() + ".conf";
1053 if (access(legacypath.c_str(), R_OK) == 0) {
1054 continue;
1055 } // else:
1056
1057 auto keypath = std::string{};
1058 auto crtpath = std::string{};
1059 auto self_signed = false;
1060
1061 for (auto opt : sec) {
1062 for (auto itm : opt) {
1063 if (opt.name() == "ssl_certificate_key") {
1064 keypath = itm.name();
1065 }
1066
1067 else if (opt.name() == "ssl_certificate") {
1068 crtpath = itm.name();
1069 }
1070
1071 else if (opt.name() == MANAGE_SSL) {
1072 if (itm.name() == "self-signed") {
1073 self_signed = true;
1074 }
1075
1076 // else if (itm.name()=="???") { /* manage other */ }
1077
1078 else {
1079 continue;
1080 } // no supported manage_ssl string.
1081
1082 is_enabled_and_at_least_one_has_manage_ssl = true;
1083 }
1084 }
1085 }
1086
1087 if (self_signed && !crtpath.empty() && !keypath.empty()) {
1088 try {
1089 if (!check_ssl_certificate(crtpath, keypath)) {
1090 are_valid = false;
1091 }
1092 }
1093 catch (...) {
1094 std::cerr << "check_ssl warning: cannot build certificate '";
1095 std::cerr << crtpath << "' or key '" << keypath << "'.\n";
1096 }
1097 }
1098 }
1099 }
1100
1101 auto suffix = std::string_view{" the cron job checking the managed SSL certificates.\n"};
1102
1103 if (is_enabled_and_at_least_one_has_manage_ssl) {
1104 try {
1105 install_cron_job(CRON_CHECK);
1106 }
1107 catch (...) {
1108 std::cerr << "check_ssl warning: cannot install" << suffix;
1109 }
1110 }
1111
1112 else if (access("/etc/crontabs/root", R_OK) == 0) {
1113 try {
1114 remove_cron_job(CRON_CHECK);
1115 }
1116 catch (...) {
1117 std::cerr << "check_ssl warning: cannot remove" << suffix;
1118 }
1119 } // else: do nothing
1120
1121 return are_valid;
1122}
1123
1124#endif