fwknop: init script improvements
[feed/packages.git] / net / nginx-util / src / nginx-ssl-util.hpp
1 #ifndef __NGINX_SSL_UTIL_HPP
2 #define __NGINX_SSL_UTIL_HPP
3
4 #include <thread>
5
6 #ifdef NO_PCRE
7 #include <regex>
8 namespace rgx = std;
9 #else
10 #include "regex-pcre.hpp"
11 #endif
12
13 #include "nginx-util.hpp"
14 #include "px5g-openssl.hpp"
15
16
17 #ifndef NO_UBUS
18 static constexpr auto UBUS_TIMEOUT = 1000;
19 #endif
20
21 // once a year:
22 static constexpr auto CRON_INTERVAL = std::string_view{"3 3 12 12 *"};
23
24 static constexpr auto LAN_SSL_LISTEN =
25 std::string_view{"/var/lib/nginx/lan_ssl.listen"};
26
27 static constexpr auto LAN_SSL_LISTEN_DEFAULT =
28 std::string_view{"/var/lib/nginx/lan_ssl.listen.default"};
29
30 static constexpr auto ADD_SSL_FCT = std::string_view{"add_ssl"};
31
32 static constexpr auto SSL_SESSION_CACHE_ARG =
33 [](const std::string_view & /*name*/) -> std::string
34 { return "shared:SSL:32k"; };
35
36 static constexpr auto SSL_SESSION_TIMEOUT_ARG = std::string_view{"64m"};
37
38
39 using _Line =
40 std::array< std::string (*)(const std::string &, const std::string &), 2 >;
41
42 class Line {
43
44 private:
45
46 _Line _line;
47
48 public:
49
50 explicit Line(const _Line & line) noexcept : _line{line} {}
51
52 template<const _Line & ...xn>
53 static auto build() noexcept -> Line
54 {
55 return Line{_Line{
56 [](const std::string & p, const std::string & b) -> std::string
57 { return (... + xn[0](p, b)); },
58 [](const std::string & p, const std::string & b) -> std::string
59 { return (... + xn[1](p, b)); }
60 }};
61 }
62
63
64 [[nodiscard]] auto STR(const std::string & param, const std::string & begin)
65 const -> std::string
66 { return _line[0](param, begin); }
67
68
69 [[nodiscard]] auto RGX() const -> rgx::regex
70 { return rgx::regex{_line[1]("", "")}; }
71
72 };
73
74
75 auto get_if_missed(const std::string & conf, const Line & LINE,
76 const std::string & val,
77 const std::string & indent="\n ", bool compare=true)
78 -> std::string;
79
80
81 auto delete_if(const std::string & conf, const rgx::regex & rgx,
82 const std::string & val="", bool compare=false)
83 -> std::string;
84
85
86 void add_ssl_directives_to(const std::string & name, bool isdefault);
87
88
89 void create_ssl_certificate(const std::string & crtpath,
90 const std::string & keypath,
91 int days=792);
92
93
94 void use_cron_to_recreate_certificate(const std::string & name);
95
96
97 void add_ssl_if_needed(const std::string & name);
98
99
100 void del_ssl_directives_from(const std::string & name, bool isdefault);
101
102
103 void del_ssl(const std::string & name);
104
105
106 constexpr auto _begin = _Line{
107 [](const std::string & /*param*/, const std::string & begin) -> std::string
108 { return begin; },
109
110 [](const std::string & /*param*/, const std::string & /*begin*/)
111 -> std::string
112 { return R"([{;](?:\s*#[^\n]*(?=\n))*(\s*))"; }
113 };
114
115
116 constexpr auto _space = _Line{
117 [](const std::string & /*param*/, const std::string & /*begin*/)
118 -> std::string
119 { return std::string{" "}; },
120
121 [](const std::string & /*param*/, const std::string & /*begin*/)
122 -> std::string
123 { return R"(\s+)"; }
124 };
125
126
127 constexpr auto _newline = _Line{
128 [](const std::string & /*param*/, const std::string & /*begin*/)
129 -> std::string
130 { return std::string{"\n"}; },
131
132 [](const std::string & /*param*/, const std::string & /*begin*/)
133 -> std::string
134 { return std::string{"\n"}; }
135 };
136
137
138 constexpr auto _end = _Line{
139 [](const std::string & /*param*/, const std::string & /*begin*/)
140 -> std::string
141 { return std::string{";"}; },
142
143 [](const std::string & /*param*/, const std::string & /*begin*/)
144 -> std::string
145 { return std::string{R"(\s*;(?:[\t ]*#[^\n]*)?)"}; }
146 };
147
148
149 template<char clim='\0'>
150 constexpr auto _capture = _Line{
151 [](const std::string & param, const std::string & /*begin*/) -> std::string
152 { return '\'' + param + '\''; },
153
154 [](const std::string & /*param*/, const std::string & /*begin*/)
155 -> std::string
156 {
157 const auto lim = clim=='\0' ? std::string{"\\s"} : std::string{clim};
158 return std::string{R"(((?:(?:"[^"]*")|(?:[^'")"} +
159 lim + "][^" + lim + "]*)|(?:'[^']*'))+)";
160 }
161 };
162
163
164 template<const std::string_view & strptr, char clim='\0'>
165 constexpr auto _escape = _Line{
166 [](const std::string & /*param*/, const std::string & /*begin*/)
167 -> std::string
168 {
169 return clim=='\0' ?
170 std::string{strptr.data()} :
171 clim + std::string{strptr.data()} + clim;
172 },
173
174 [](const std::string & /*param*/, const std::string & /*begin*/)
175 -> std::string
176 {
177 std::string ret{};
178 for (char c : strptr) {
179 switch(c) {
180 case '^': ret += '\\'; [[fallthrough]];
181 case '_': [[fallthrough]];
182 case '-': ret += c;
183 break;
184 default:
185 if ((isalpha(c)!=0) || (isdigit(c)!=0)) { ret += c; }
186 else { ret += std::string{"["}+c+"]"; }
187 }
188 }
189 return "(?:"+ret+"|'"+ret+"'"+"|\""+ret+"\""+")";
190 }
191 };
192
193
194 constexpr std::string_view _server_name = "server_name";
195
196 constexpr std::string_view _include = "include";
197
198 constexpr std::string_view _ssl_certificate = "ssl_certificate";
199
200 constexpr std::string_view _ssl_certificate_key = "ssl_certificate_key";
201
202 constexpr std::string_view _ssl_session_cache = "ssl_session_cache";
203
204 constexpr std::string_view _ssl_session_timeout = "ssl_session_timeout";
205
206
207 // For a compile time regex lib, this must be fixed, use one of these options:
208 // * Hand craft or macro concat them (loosing more or less flexibility).
209 // * Use Macro concatenation of __VA_ARGS__ with the help of:
210 // https://p99.gforge.inria.fr/p99-html/group__preprocessor__for.html
211 // * Use constexpr---not available for strings or char * for now---look at lib.
212
213 static const auto CRON_CMD = Line::build
214 < _space, _escape<NGINX_UTIL>, _space, _escape<ADD_SSL_FCT,'\''>, _space,
215 _capture<>, _newline >();
216
217 static const auto NGX_SERVER_NAME =
218 Line::build<_begin, _escape<_server_name>, _space, _capture<';'>, _end>();
219
220 static const auto NGX_INCLUDE_LAN_LISTEN = Line::build
221 <_begin, _escape<_include>, _space, _escape<LAN_LISTEN,'\''>, _end>();
222
223 static const auto NGX_INCLUDE_LAN_LISTEN_DEFAULT = Line::build
224 < _begin, _escape<_include>, _space,
225 _escape<LAN_LISTEN_DEFAULT, '\''>, _end >();
226
227 static const auto NGX_INCLUDE_LAN_SSL_LISTEN = Line::build
228 <_begin, _escape<_include>, _space, _escape<LAN_SSL_LISTEN, '\''>, _end>();
229
230 static const auto NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT = Line::build
231 < _begin, _escape<_include>, _space,
232 _escape<LAN_SSL_LISTEN_DEFAULT, '\''>, _end >();
233
234 static const auto NGX_SSL_CRT = Line::build
235 <_begin, _escape<_ssl_certificate>, _space, _capture<';'>, _end>();
236
237 static const auto NGX_SSL_KEY = Line::build
238 <_begin, _escape<_ssl_certificate_key>, _space, _capture<';'>, _end>();
239
240 static const auto NGX_SSL_SESSION_CACHE = Line::build
241 <_begin, _escape<_ssl_session_cache>, _space, _capture<';'>, _end>();
242
243 static const auto NGX_SSL_SESSION_TIMEOUT = Line::build
244 <_begin, _escape<_ssl_session_timeout>, _space, _capture<';'>, _end>();
245
246
247 auto get_if_missed(const std::string & conf, const Line & LINE,
248 const std::string & val,
249 const std::string & indent, bool compare)
250 -> std::string
251 {
252 if (!compare || val.empty()) {
253 return rgx::regex_search(conf, LINE.RGX()) ? "" : LINE.STR(val, indent);
254 }
255
256 rgx::smatch match; // assuming last capture has the value!
257
258 for (auto pos = conf.begin();
259 rgx::regex_search(pos, conf.end(), match, LINE.RGX());
260 pos += match.position(0) + match.length(0))
261 {
262 const std::string value = match.str(match.size() - 1);
263
264 if (value==val || value=="'"+val+"'" || value=='"'+val+'"') {
265 return "";
266 }
267 }
268
269 return LINE.STR(val, indent);
270 }
271
272
273 auto delete_if(const std::string & conf, const rgx::regex & rgx,
274 const std::string & val, const bool compare)
275 -> std::string
276 {
277 std::string ret{};
278 auto pos = conf.begin();
279
280 for (rgx::smatch match;
281 rgx::regex_search(pos, conf.end(), match, rgx);
282 pos += match.position(0) + match.length(0))
283 {
284 const std::string value = match.str(match.size() - 1);
285 auto len = match.position(1);
286 if (compare && value!=val && value!="'"+val+"'" && value!='"'+val+'"') {
287 len = match.position(0) + match.length(0);
288 }
289 ret.append(pos, pos + len);
290 }
291
292 ret.append(pos, conf.end());
293 return ret;
294 }
295
296
297 void add_ssl_directives_to(const std::string & name, const bool isdefault)
298 {
299 const std::string prefix = std::string{CONF_DIR} + name;
300
301 std::string conf = read_file(prefix+".conf");
302
303 const std::string & const_conf = conf; // iteration needs const string.
304 rgx::smatch match; // captures str(1)=indentation spaces, str(2)=server name
305 for (auto pos = const_conf.begin();
306 rgx::regex_search(pos, const_conf.end(), match, NGX_SERVER_NAME.RGX());
307 pos += match.position(0) + match.length(0))
308 {
309 if (match.str(2).find(name) == std::string::npos) { continue; }
310
311 const std::string indent = match.str(1);
312
313 std::string adds = isdefault ?
314 get_if_missed(conf, NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT,"",indent) :
315 get_if_missed(conf, NGX_INCLUDE_LAN_SSL_LISTEN, "", indent);
316
317 adds += get_if_missed(conf, NGX_SSL_CRT, prefix+".crt", indent);
318
319 adds += get_if_missed(conf, NGX_SSL_KEY, prefix+".key", indent);
320
321 adds += get_if_missed(conf, NGX_SSL_SESSION_CACHE,
322 SSL_SESSION_CACHE_ARG(name), indent, false);
323
324 adds += get_if_missed(conf, NGX_SSL_SESSION_TIMEOUT,
325 std::string{SSL_SESSION_TIMEOUT_ARG}, indent, false);
326
327 if (adds.length() > 0) {
328 pos += match.position(0) + match.length(0);
329
330 conf = std::string(const_conf.begin(), pos) + adds +
331 std::string(pos, const_conf.end());
332
333 conf = isdefault ?
334 delete_if(conf, NGX_INCLUDE_LAN_LISTEN_DEFAULT.RGX()) :
335 delete_if(conf, NGX_INCLUDE_LAN_LISTEN.RGX());
336
337 write_file(prefix+".conf", conf);
338
339 std::cerr<<"Added SSL directives to "<<prefix<<".conf: ";
340 std::cerr<<adds<<std::endl;
341 }
342
343 return;
344 }
345
346 auto errmsg = std::string{"add_ssl_directives_to error: "};
347 errmsg += "cannot add SSL directives to " + name + ".conf, missing: ";
348 errmsg += NGX_SERVER_NAME.STR(name, "\n ") + "\n";
349 throw std::runtime_error(errmsg.c_str());
350 }
351
352
353 template<typename T>
354 inline auto num2hex(T bytes) -> std::array<char, 2*sizeof(bytes)+1>
355 {
356 constexpr auto n = 2*sizeof(bytes);
357 std::array<char, n+1> str{};
358
359 for (size_t i=0; i<n; ++i) {
360 static const std::array<char, 17> hex{"0123456789ABCDEF"};
361 static constexpr auto get = 0x0fU;
362 str.at(i) = hex.at(bytes & get);
363
364 static constexpr auto move = 4U;
365 bytes >>= move;
366 }
367
368 str[n] = '\0';
369 return str;
370 }
371
372
373 template<typename T>
374 inline auto get_nonce(const T salt=0) -> T
375 {
376 T nonce = 0;
377
378 std::ifstream urandom{"/dev/urandom"};
379
380 static constexpr auto move = 6U;
381
382 constexpr size_t steps = (sizeof(nonce)*8 - 1)/move + 1;
383
384 for (size_t i=0; i<steps; ++i) {
385 if (!urandom.good()) { throw std::runtime_error("get_nonce error"); }
386 nonce = (nonce << move) + static_cast<unsigned>(urandom.get());
387 }
388
389 nonce ^= salt;
390
391 return nonce;
392 }
393
394
395 void create_ssl_certificate(const std::string & crtpath,
396 const std::string & keypath,
397 const int days)
398 {
399 size_t nonce = 0;
400
401 try { nonce = get_nonce(nonce); }
402
403 catch (...) { // the address of a variable should be random enough:
404 auto addr = &crtpath;
405 auto addrptr = static_cast<const size_t *>(
406 static_cast<const void *>(&addr) );
407 nonce += *addrptr;
408 }
409
410 auto noncestr = num2hex(nonce);
411
412 const auto tmpcrtpath = crtpath + ".new-" + noncestr.data();
413 const auto tmpkeypath = keypath + ".new-" + noncestr.data();
414
415 try {
416 auto pkey = gen_eckey(NID_secp384r1);
417
418 write_key(pkey, tmpkeypath);
419
420 std::string subject {"/C=ZZ/ST=Somewhere/L=None/CN=OpenWrt/O=OpenWrt"};
421 subject += noncestr.data();
422
423 selfsigned(pkey, days, subject, tmpcrtpath);
424
425 static constexpr auto to_seconds = 24*60*60;
426 static constexpr auto leeway = 42;
427 if (!checkend(tmpcrtpath, days*to_seconds - leeway)) {
428 throw std::runtime_error("bug: created certificate is not valid!!");
429 }
430
431 } catch (...) {
432 std::cerr<<"create_ssl_certificate error: ";
433 std::cerr<<"cannot create selfsigned certificate, ";
434 std::cerr<<"removing temporary files ..."<<std::endl;
435
436 if (remove(tmpcrtpath.c_str())!=0) {
437 auto errmsg = "\t cannot remove "+tmpcrtpath;
438 perror(errmsg.c_str());
439 }
440
441 if (remove(tmpkeypath.c_str())!=0) {
442 auto errmsg = "\t cannot remove "+tmpkeypath;
443 perror(errmsg.c_str());
444 }
445
446 throw;
447 }
448
449 if ( rename(tmpcrtpath.c_str(), crtpath.c_str())!=0 ||
450 rename(tmpkeypath.c_str(), keypath.c_str())!=0 )
451 {
452 auto errmsg = std::string{"create_ssl_certificate warning: "};
453 errmsg += "cannot move "+tmpcrtpath+" to "+crtpath;
454 errmsg += " or "+tmpkeypath+" to "+keypath+", continuing ... ";
455 perror(errmsg.c_str());
456 }
457
458 }
459
460
461 void use_cron_to_recreate_certificate(const std::string & name)
462 {
463 static const char * filename = "/etc/crontabs/root";
464
465 std::string conf{};
466 try { conf = read_file(filename); }
467 catch (const std::ifstream::failure &) { /* is ok if not found, create. */ }
468
469 const std::string add = get_if_missed(conf, CRON_CMD, name);
470
471 if (add.length() > 0) {
472 #ifndef NO_UBUS
473 auto service = ubus::call("service","list",UBUS_TIMEOUT).filter("cron");
474
475 if (!service) {
476 std::string errmsg{"use_cron_to_recreate_certificate error: "};
477 errmsg += "Cron unavailable to re-create the ssl certificate for ";
478 errmsg += name + "\n";
479 throw std::runtime_error(errmsg.c_str());
480 } // else active with or without instances:
481 #endif
482
483 write_file(filename, std::string{CRON_INTERVAL}+add, std::ios::app);
484
485 #ifndef NO_UBUS
486 call("/etc/init.d/cron", "reload");
487 #endif
488
489 std::cerr<<"Rebuild the ssl certificate for '";
490 std::cerr<<name<<"' annually with cron."<<std::endl;
491 }
492 }
493
494
495 void add_ssl_if_needed(const std::string & name)
496 {
497 add_ssl_directives_to(name, name==LAN_NAME); // let it throw.
498
499 const auto crtpath = std::string{CONF_DIR} + name + ".crt";
500 const auto keypath = std::string{CONF_DIR} + name + ".key";
501 constexpr auto remaining_seconds = (365 + 32)*24*60*60;
502 constexpr auto validity_days = 3*(365 + 31);
503
504 bool is_valid = true;
505
506 if (access(keypath.c_str(), R_OK) != 0 ||
507 access(crtpath.c_str(), R_OK) != 0)
508 { is_valid = false; }
509
510 else {
511 try {
512 if (!checkend(crtpath, remaining_seconds)) {
513 is_valid = false;
514 }
515 }
516 catch (...) { // something went wrong, maybe it is in DER format:
517 try {
518 if (!checkend(crtpath, remaining_seconds, false)) {
519 is_valid = false;
520 }
521 }
522 catch (...) { // it has neither DER nor PEM format, rebuild.
523 is_valid = false;
524 }
525 }
526 }
527
528 if (!is_valid) { create_ssl_certificate(crtpath, keypath, validity_days); }
529
530 try { use_cron_to_recreate_certificate(name); }
531 catch (...) {
532 std::cerr<<"add_ssl_if_needed warning: ";
533 std::cerr<<"cannot use cron to rebuild certificate for "<<name<<"\n";
534 }
535 }
536
537
538 void del_ssl_directives_from(const std::string & name, const bool isdefault)
539 {
540 const std::string prefix = std::string{CONF_DIR} + name;
541
542 std::string conf = read_file(prefix+".conf");
543
544 const std::string & const_conf = conf; // iteration needs const string.
545 rgx::smatch match; // captures str(1)=indentation spaces, str(2)=server name
546 for (auto pos = const_conf.begin();
547 rgx::regex_search(pos, const_conf.end(), match, NGX_SERVER_NAME.RGX());
548 pos += match.position(0) + match.length(0))
549 {
550 if (match.str(2).find(name) == std::string::npos) { continue; }
551
552 const std::string indent = match.str(1);
553
554 std::string adds = isdefault ?
555 get_if_missed(conf, NGX_INCLUDE_LAN_LISTEN_DEFAULT,"",indent) :
556 get_if_missed(conf, NGX_INCLUDE_LAN_LISTEN, "", indent);
557
558 if (adds.length() > 0) {
559 pos += match.position(1);
560
561 conf = std::string(const_conf.begin(), pos) + adds
562 + std::string(pos, const_conf.end());
563
564 conf = isdefault ?
565 delete_if(conf, NGX_INCLUDE_LAN_SSL_LISTEN_DEFAULT.RGX())
566 : delete_if(conf, NGX_INCLUDE_LAN_SSL_LISTEN.RGX());
567
568 const auto crtpath = prefix+".crt";
569 conf = delete_if(conf, NGX_SSL_CRT.RGX(), crtpath, true);
570
571 const auto keypath = prefix+".key";
572 conf = delete_if(conf, NGX_SSL_KEY.RGX(), keypath, true);
573
574 conf = delete_if(conf, NGX_SSL_SESSION_CACHE.RGX());
575
576 conf = delete_if(conf, NGX_SSL_SESSION_TIMEOUT.RGX());
577
578 write_file(prefix+".conf", conf);
579
580 std::cerr<<"Deleted SSL directives from "<<prefix<<".conf\n";
581 }
582
583 return;
584 }
585
586 auto errmsg = std::string{"del_ssl_directives_from error: "};
587 errmsg += "cannot delete SSL directives from " + name + ".conf, missing: ";
588 errmsg += NGX_SERVER_NAME.STR(name, "\n ") + "\n";
589 throw std::runtime_error(errmsg.c_str());
590 }
591
592
593 void del_ssl(const std::string & name)
594 {
595 static const char * filename = "/etc/crontabs/root";
596
597 try {
598 const auto const_conf = read_file(filename);
599
600 bool changed = false;
601 auto conf = std::string{};
602
603 size_t prev = 0;
604 size_t curr = 0;
605 while ((curr=const_conf.find('\n', prev)) != std::string::npos) {
606
607 auto line = const_conf.substr(prev, curr-prev+1);
608
609 if (line==delete_if(line,CRON_CMD.RGX(),std::string{name},true)) {
610 conf += line;
611 } else { changed = true; }
612
613 prev = curr + 1;
614 }
615
616 if (changed) {
617 write_file(filename, conf);
618
619 std::cerr<<"Do not rebuild the ssl certificate for '";
620 std::cerr<<name<<"' annually with cron anymore."<<std::endl;
621
622 #ifndef NO_UBUS
623 if (ubus::call("service", "list", UBUS_TIMEOUT).filter("cron"))
624 { call("/etc/init.d/cron", "reload"); }
625 #endif
626 }
627
628 } catch (...) {
629 std::cerr<<"del_ssl warning: ";
630 std::cerr<<"cannot delete cron job for "<<name<<" in "<<filename<<"\n";
631 }
632
633 try { del_ssl_directives_from(name, name==LAN_NAME); }
634 catch (...) {
635 std::cerr<<"del_ssl error: ";
636 std::cerr<<"cannot delete SSL directives from "<<name<<".conf\n";
637 throw;
638 }
639
640 const auto crtpath = std::string{CONF_DIR} + name + ".crt";
641
642 if (remove(crtpath.c_str())!=0) {
643 auto errmsg = "del_ssl warning: cannot remove "+crtpath;
644 perror(errmsg.c_str());
645 }
646
647 const auto keypath = std::string{CONF_DIR} + name + ".key";
648
649 if (remove(keypath.c_str())!=0) {
650 auto errmsg = "del_ssl warning: cannot remove "+keypath;
651 perror(errmsg.c_str());
652 }
653 }
654
655
656 #endif