1: | <?php |
2: | declare(strict_types=1); |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | use AcmePhp\Core\Protocol\AuthorizationChallenge; |
16: | use Module\Support\Letsencrypt; |
17: | use Opcenter\Crypto\Letsencrypt as LEService; |
18: | use Opcenter\Crypto\Letsencrypt as LetsencryptAlias; |
19: | use Opcenter\Crypto\Ssl\Certificate; |
20: | |
21: | |
22: | |
23: | |
24: | |
25: | |
26: | class Letsencrypt_Module extends Letsencrypt implements \Module\Skeleton\Contracts\Hookable, \Module\Skeleton\Contracts\Reactive |
27: | { |
28: | const DEPENDENCY_MAP = [ |
29: | 'ssl' |
30: | ]; |
31: | |
32: | const BOOTSTRAP_ATTEMPTS = LETSENCRYPT_BOOTSTRAP_ATTEMPTS; |
33: | |
34: | const LETSENCRYPT_SERVER = 'acme-v02.api.letsencrypt.org/directory'; |
35: | |
36: | const LETSENCRYPT_TESTING_SERVER = 'acme-staging-v02.api.letsencrypt.org/directory'; |
37: | protected const LE_AUTHORITY_FINGERPRINT = LETSENCRYPT_KEYID; |
38: | protected const LE_STAGING_AUTHORITY_FINGERPRINT = LETSENCRYPT_STAGING_KEYID; |
39: | |
40: | const INCLUDE_ALT_FORM = LETSENCRYPT_ALTERNATIVE_FORM; |
41: | protected $activeServer; |
42: | |
43: | |
44: | |
45: | |
46: | |
47: | |
48: | |
49: | public function __construct() |
50: | { |
51: | parent::__construct(); |
52: | |
53: | $this->activeServer = \Opcenter\Crypto\Letsencrypt::activeServer(); |
54: | if ($this->supported()) { |
55: | $fns = array( |
56: | '*' => PRIVILEGE_SITE, |
57: | 'request' => PRIVILEGE_SITE|PRIVILEGE_ADMIN, |
58: | 'append' => PRIVILEGE_SITE|PRIVILEGE_ADMIN, |
59: | 'renew_expiring' => PRIVILEGE_ADMIN, |
60: | 'revoke' => PRIVILEGE_SITE|PRIVILEGE_ADMIN, |
61: | 'challenges' => PRIVILEGE_SITE|PRIVILEGE_ADMIN, |
62: | 'solve' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
63: | 'renew' => PRIVILEGE_SITE|PRIVILEGE_ADMIN |
64: | ); |
65: | } else { |
66: | $fns = array( |
67: | 'supported' => PRIVILEGE_SITE, |
68: | 'permitted' => PRIVILEGE_SITE, |
69: | 'is_ca' => PRIVILEGE_SITE, |
70: | '*' => PRIVILEGE_NONE |
71: | ); |
72: | } |
73: | $this->exportedFunctions = $fns; |
74: | } |
75: | |
76: | |
77: | |
78: | |
79: | |
80: | |
81: | public function supported() |
82: | { |
83: | return true; |
84: | } |
85: | |
86: | |
87: | |
88: | |
89: | |
90: | |
91: | public function permitted() |
92: | { |
93: | return $this->supported() && $this->ssl_permitted(); |
94: | } |
95: | |
96: | |
97: | |
98: | |
99: | |
100: | |
101: | public function debug(): bool |
102: | { |
103: | return (bool)LETSENCRYPT_DEBUG; |
104: | } |
105: | |
106: | |
107: | |
108: | |
109: | |
110: | |
111: | |
112: | public function renew(bool $verifyip = null) |
113: | { |
114: | if ($this->permission_level & PRIVILEGE_SITE && $this->auth_is_inactive()) { |
115: | return error("account `%s' is inactive - not renewing SSL", $this->domain); |
116: | } |
117: | |
118: | if (null === ($cns = $this->getNonOrphanedDomainsFromCertificate())) { |
119: | return warn("Certificate is not Let's Encrypt"); |
120: | } |
121: | if ($cns === []) { |
122: | return error('no certificates installed on account'); |
123: | } |
124: | if ($cns === null) { |
125: | return warn("certificate for `%s' is not provided by LE", $this->domain); |
126: | } |
127: | $ret = $this->request($cns, $verifyip); |
128: | if (null === $ret) { |
129: | |
130: | |
131: | return warn('request failed, lack of valid hostnames to renew'); |
132: | } |
133: | if (!$ret) { |
134: | return error('failed to renew certificate'); |
135: | } |
136: | |
137: | return info('successfully renewed certificate for 90 days'); |
138: | } |
139: | |
140: | |
141: | |
142: | |
143: | |
144: | |
145: | public function renew_expiring(): void { |
146: | $this->renewExpiringCertificates(); |
147: | |
148: | if (!$this->systemNeedsIssuance()) { |
149: | return; |
150: | } |
151: | |
152: | debug("System certificate require issuance"); |
153: | |
154: | $cns = \Opcenter\Crypto\Ssl::generateHostnames($this->getAuthContext(), true); |
155: | if ($this->requestReal($cns, LetsencryptAlias::SYSCERT_NAME)) { |
156: | $this->installSystemCertificate(); |
157: | } |
158: | } |
159: | |
160: | |
161: | |
162: | |
163: | |
164: | |
165: | |
166: | |
167: | |
168: | public function challenges($hostnames): array |
169: | { |
170: | $hostnames = (array)$hostnames; |
171: | $sans = []; |
172: | foreach ($hostnames as $host) { |
173: | $chk = $host; |
174: | if (0 === strncmp($host, '*.', 2)) { |
175: | $chk = substr($chk, 2); |
176: | } |
177: | if (($this->permission_level & PRIVILEGE_SITE) && !$this->web_split_host($chk)) { |
178: | error("Invalid hostname `%s'", $chk); |
179: | continue; |
180: | } |
181: | $sans[$host] = null; |
182: | } |
183: | $dispatch = LetsencryptAlias\AcmeDispatcher::instantiateContexted($this->getAuthContext(), [$this->site]); |
184: | if ( !($challenges = $dispatch->challenges(array_keys($sans))) ) { |
185: | |
186: | return []; |
187: | } |
188: | foreach (array_get($challenges->toArray(), 'authorizationsChallenges', []) as $domain => $challengeTypes) { |
189: | $sans[$domain] = array_map(static function (AuthorizationChallenge $c) { |
190: | return $c->toArray(); |
191: | }, $challengeTypes); |
192: | } |
193: | |
194: | return $sans; |
195: | } |
196: | |
197: | |
198: | |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | public function solve($hostname, string $solver = null): bool |
206: | { |
207: | $dispatch = LetsencryptAlias\AcmeDispatcher::instantiateContexted($this->getAuthContext(), [$this->site]); |
208: | $dispatch->setStagingOnly(true); |
209: | if ($solver) { |
210: | array_fill_keys((array)$hostname, $solver); |
211: | } else if (!\is_array($hostname) || isset($hostname[0])) { |
212: | return error('$hostname parameter is not a map of hostnames => solvers'); |
213: | } |
214: | $order = $dispatch->challenges((array)array_keys($hostname)); |
215: | |
216: | $challengeSet = $order->getAuthorizationsChallenges(); |
217: | foreach ($challengeSet as $host => &$challenges) { |
218: | foreach ($challenges as &$challenge) { |
219: | if (!isset($hostname[$host]) || 0 !== strpos($challenge->getType(), $hostname[$host])) { |
220: | $challenge = null; |
221: | continue; |
222: | } |
223: | } |
224: | unset($challenge); |
225: | $challenges = array_filter($challenges); |
226: | } |
227: | unset($challenges); |
228: | $revised = new \AcmePhp\Core\Protocol\CertificateOrder($challengeSet); |
229: | |
230: | return null === $dispatch->solve($revised); |
231: | } |
232: | |
233: | |
234: | |
235: | |
236: | |
237: | |
238: | |
239: | |
240: | public function use_mechanism(string $domain, ?string $mechanism): bool |
241: | { |
242: | if (!preg_match(Regex::DOMAIN_WC, $domain)) { |
243: | return error("Invalid hostname `%s'", $domain); |
244: | } |
245: | $prefs = \Preferences::factory($this->getAuthContext()); |
246: | $mechanisms = array_get($prefs, LetsencryptAlias\Preferences::MECHANISM_PREFERENCE, []); |
247: | if (null === $mechanism) { |
248: | unset($prefs[$domain]); |
249: | } |
250: | if (!in_array($mechanism, LetsencryptAlias\AcmeDispatcher::VALIDATION_MODES, true)) { |
251: | return error("Unknown mechanism `%s'", $mechanism); |
252: | } |
253: | $mechanisms[$domain] = $mechanism; |
254: | array_set($prefs, LetsencryptAlias\Preferences::MECHANISM_PREFERENCE, $mechanisms); |
255: | return true; |
256: | } |
257: | |
258: | |
259: | |
260: | |
261: | |
262: | |
263: | |
264: | public function mechanism(?string $domain) |
265: | { |
266: | $prefs = array_get( |
267: | \Preferences::factory($this->getAuthContext()), |
268: | LetsencryptAlias\Preferences::MECHANISM_PREFERENCE, |
269: | [] |
270: | ); |
271: | if (null === $domain) { |
272: | return $prefs; |
273: | } |
274: | return $prefs[$domain] ?? null; |
275: | } |
276: | |
277: | |
278: | |
279: | |
280: | |
281: | |
282: | |
283: | |
284: | |
285: | |
286: | |
287: | |
288: | public function request($cnames, ?bool $verifyip = null, ?bool $strict = null): ?bool |
289: | { |
290: | |
291: | if (posix_geteuid() && !IS_CLI) { |
292: | return $this->query('letsencrypt_request', $cnames, $verifyip, $strict); |
293: | } |
294: | |
295: | if (null === $verifyip) { |
296: | $verifyip = (bool)array_get(\Preferences::factory($this->getAuthContext()), |
297: | LetsencryptAlias\Preferences::VERIFY_IP, LETSENCRYPT_VERIFY_IP); |
298: | } |
299: | |
300: | if (null === $strict) { |
301: | $strict = (bool)array_get(\Preferences::factory($this->getAuthContext()), |
302: | LetsencryptAlias\Preferences::SENSITIVITY, LETSENCRYPT_STRICT_MODE); |
303: | } |
304: | |
305: | $cnreq = array(); |
306: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
307: | |
308: | |
309: | |
310: | return $this->requestReal((array)$cnames, LetsencryptAlias::SYSCERT_NAME, $strict) && $this->_moveCertificates(LetsencryptAlias::SYSCERT_NAME); |
311: | } |
312: | $myip = ($this->permission_level & PRIVILEGE_ADMIN) ? \Opcenter\Net\Ip4::my_ip() : $this->dns_get_public_ip(); |
313: | |
314: | foreach ((array)$cnames as $c) { |
315: | $isWildcard = false; |
316: | if (!is_string($c)) { |
317: | error('Skipping garbled input - hostname not presented as string, is %s', gettype($c)); |
318: | } |
319: | if (0 === strncmp($c, '*.', 2)) { |
320: | $c = substr($c, 2); |
321: | $isWildcard = true; |
322: | } |
323: | $c = $this->web_split_host($c); |
324: | $domain = $c['domain']; |
325: | $subdomain = $c['subdomain']; |
326: | |
327: | if (!$this->web_domain_exists($domain)) { |
328: | error("cannot process Let's Encrypt: domain `%s' not a valid domain on this account", |
329: | $domain); |
330: | continue; |
331: | } |
332: | |
333: | $host = ltrim($subdomain . '.' . $domain, '.'); |
334: | if (!preg_match(Regex::HTTP_HOST, $host)) { |
335: | error("invalid server name `%s' specified", $c); |
336: | continue; |
337: | } |
338: | |
339: | if ($verifyip && self::INCLUDE_ALT_FORM && !$isWildcard) { |
340: | $altform = null; |
341: | if (0 !== strpos($host, 'www.')) { |
342: | |
343: | $altform = 'www.' . $host; |
344: | } else { |
345: | |
346: | $altform = substr($host, 4); |
347: | } |
348: | |
349: | $resolved = null; |
350: | if ($this->_verifyIP($altform, (array)$myip, $resolved)) { |
351: | $cnreq[] = $altform; |
352: | } else if ($strict) { |
353: | return error("Domain `%s' would be dropped from renewal", $altform); |
354: | } else { |
355: | info("skipping alternative hostname form `%(hostname)s', IP %(actual)s does not resolve to `%(expected)s'", [ |
356: | 'hostname' => $altform, |
357: | 'expected' => $myip, |
358: | 'actual' => $resolved |
359: | ]); |
360: | } |
361: | } |
362: | |
363: | $resolved = null; |
364: | if ($verifyip && !$this->_verifyIP($host, (array)$myip, $resolved)) { |
365: | $msg = [ |
366: | "hostname `%(hostname)s' IP `%(actual)s' doesn't match hosting IP `%(expected)s', " |
367: | . '%(what)s request', [ |
368: | 'hostname' => $host, |
369: | 'actual' => $resolved, |
370: | 'expected' => implode(',', (array)$myip), |
371: | 'what' =>$strict ? 'aborting' : 'skipping' |
372: | ] |
373: | ]; |
374: | if ($strict) { |
375: | return error(...$msg); |
376: | } |
377: | warn(...$msg); |
378: | continue; |
379: | } |
380: | if ($isWildcard) { |
381: | $host = '*.' . $host; |
382: | } |
383: | $cnreq[] = $host; |
384: | } |
385: | if (!$cnreq) { |
386: | error('no hostnames to register'); |
387: | |
388: | return null; |
389: | } |
390: | |
391: | $storageMarker = ($this->permission_level & PRIVILEGE_ADMIN) ? LetsencryptAlias::SYSCERT_NAME : $this->site; |
392: | if (! ($ret = $this->requestReal($cnreq, $storageMarker, $strict)) ) { |
393: | return $ret; |
394: | } |
395: | |
396: | if ($strict && ($hosts = $this->filterMissingHostnames($cnames))) { |
397: | return error('Failed to append hostnames. Hostnames missing from new certificate: %s', |
398: | implode(', ', $hosts) |
399: | ); |
400: | } |
401: | |
402: | if (!$this->_moveCertificates($storageMarker)) { |
403: | return false; |
404: | } |
405: | |
406: | return info(':letsencrypt_issuance_limit', |
407: | 'reminder: only 5 duplicate certificates and ' . |
408: | '50 unique certificates may be issued per week per account' |
409: | ); |
410: | } |
411: | |
412: | |
413: | |
414: | |
415: | |
416: | |
417: | |
418: | |
419: | |
420: | |
421: | |
422: | private function _verifyIP($hostname, array $myip, string &$resolved = null) |
423: | { |
424: | for ($i = 0; $i < 2; $i++) { |
425: | $time = microtime(true); |
426: | if ($resolved = (string)$this->dns_gethostbyname_t($hostname)) { |
427: | break; |
428: | } |
429: | $now = microtime(true) - $time; |
430: | warn('DNS resolver failed to return answer in %dms', $now * 1000); |
431: | usleep(500000); |
432: | } |
433: | |
434: | if (!$resolved) { |
435: | return false; |
436: | } |
437: | |
438: | foreach ($myip as $chkip) { |
439: | if ($chkip === $resolved) { |
440: | return true; |
441: | } |
442: | } |
443: | return false; |
444: | } |
445: | |
446: | private function _moveCertificates($site) |
447: | { |
448: | if ($site === LetsencryptAlias::SYSCERT_NAME) { |
449: | return $this->installSystemCertificate(); |
450: | } |
451: | $files = LetsencryptAlias::getCertificateComponentData($site); |
452: | if (!$files) { |
453: | return false; |
454: | } |
455: | |
456: | return $this->ssl_install($files['key'], $files['crt'], $files['chain']); |
457: | } |
458: | |
459: | |
460: | |
461: | |
462: | |
463: | |
464: | |
465: | public function is_ca(string $crt) |
466: | { |
467: | $info = $this->ssl_parse_certificate($crt); |
468: | if (!$info) { |
469: | return error('invalid ssl certificate'); |
470: | } |
471: | |
472: | $certificate = new Certificate($crt); |
473: | |
474: | if (!$authority = $certificate->authority()) { |
475: | debug("No authorityKeyIdentifier detected"); |
476: | return false; |
477: | } |
478: | |
479: | return in_array($authority, self::LE_AUTHORITY_FINGERPRINT, true) || |
480: | in_array($authority, self::LE_STAGING_AUTHORITY_FINGERPRINT, true); |
481: | } |
482: | |
483: | |
484: | |
485: | |
486: | |
487: | |
488: | |
489: | |
490: | public function append(string|array $cnames, bool $verifyip = null): bool |
491: | { |
492: | $cnames = array_flip((array)$cnames); |
493: | if ($this->ssl_cert_exists() && null === ($old = $this->getNonOrphanedDomainsFromCertificate())) { |
494: | return warn("Cannot append hostnames to non-Let's Encrypt certificate"); |
495: | } |
496: | $old = $old ?? []; |
497: | $new = self::filterDomainSet(array_keys($cnames + array_flip($old))); |
498: | |
499: | if (!array_diff($new, $old)) { |
500: | |
501: | return true; |
502: | } |
503: | |
504: | return (bool)$this->request($new, $verifyip); |
505: | } |
506: | |
507: | private function getNonOrphanedDomainsFromCertificate(): ?array |
508: | { |
509: | if (null === ($cns = $this->getSanFromCertificate())) { |
510: | |
511: | return null; |
512: | } |
513: | |
514: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
515: | return $cns; |
516: | } |
517: | |
518: | $strict = (bool)array_get( |
519: | \Preferences::factory($this->getAuthContext()), |
520: | LetsencryptAlias\Preferences::SENSITIVITY, |
521: | LETSENCRYPT_STRICT_MODE |
522: | ); |
523: | |
524: | return array_filter($cns, function ($hostname) use ($strict) { |
525: | if (0 === strncmp($hostname, '*.', 2)) { |
526: | $hostname = substr($hostname, 2); |
527: | } |
528: | $components = $this->web_split_host($hostname); |
529: | $domain = $components['domain']; |
530: | |
531: | if (!$strict && !$this->web_domain_exists($domain)) { |
532: | warn("Domain %s missing from account, removing from certificate", $domain); |
533: | |
534: | return false; |
535: | } |
536: | |
537: | return true; |
538: | }); |
539: | } |
540: | |
541: | |
542: | |
543: | |
544: | |
545: | |
546: | public function revoke(): bool |
547: | { |
548: | if (!IS_CLI) { |
549: | return $this->query('letsencrypt_revoke'); |
550: | } |
551: | |
552: | if (!$this->certificateIssued()) { |
553: | return error('no certificate issued to revoke'); |
554: | } |
555: | |
556: | $cert = ($this->permission_level & PRIVILEGE_ADMIN) ? LetsencryptAlias::SYSCERT_NAME : $this->site; |
557: | $ret = LetsencryptAlias\AcmeDispatcher::instantiateContexted($this->getAuthContext(), [$cert, $this->activeServer]) |
558: | ->revoke(); |
559: | |
560: | if (!$ret) { |
561: | return error('revocation failed'); |
562: | } |
563: | |
564: | $this->_deleteAcmeCertificate($cert); |
565: | |
566: | return true; |
567: | } |
568: | |
569: | private function _deleteAcmeCertificate($account) |
570: | { |
571: | $acmeDir = LetsencryptAlias::acmeSiteStorageDirectory($account); |
572: | if (!file_exists($acmeDir)) { |
573: | return; |
574: | } |
575: | $dir = opendir($acmeDir); |
576: | while (false !== ($f = readdir($dir))) { |
577: | if ($f === '..' || $f === '.') { |
578: | continue; |
579: | } |
580: | unlink($acmeDir . '/' . $f); |
581: | |
582: | } |
583: | closedir($dir); |
584: | rmdir($acmeDir); |
585: | |
586: | return; |
587: | |
588: | } |
589: | |
590: | public function exists(): bool |
591: | { |
592: | $path = LetsencryptAlias::acmeSiteStorageDirectory($this->site); |
593: | |
594: | return file_exists($path); |
595: | } |
596: | |
597: | |
598: | |
599: | |
600: | |
601: | |
602: | |
603: | public function storage_path(string $site): string |
604: | { |
605: | return LetsencryptAlias::acmeSiteStorageDirectory($site); |
606: | } |
607: | |
608: | |
609: | |
610: | |
611: | |
612: | |
613: | |
614: | |
615: | |
616: | public function bootstrap(?int $attempt = null): ?bool |
617: | { |
618: | if ($attempt !== null && ($attempt > 10 || $attempt < 0)) { |
619: | return error('Invalid attempt count provided: %d', $attempt); |
620: | } |
621: | |
622: | $domains = \Opcenter\Crypto\Ssl::generateHostnames($this->getAuthContext(), true); |
623: | if (\count($domains) > 100) { |
624: | warn('Hostname count exceeds 100 (%d hostnames). Taking first 100 hostnames', \count($domains)); |
625: | $domains = array_slice($domains, 0, 100); |
626: | } |
627: | if ($attempt !== null && $this->request($domains, true)) { |
628: | |
629: | return true; |
630: | } |
631: | |
632: | $delay = 43200; |
633: | if ($attempt === null) { |
634: | $attempt = static::BOOTSTRAP_ATTEMPTS; |
635: | $delay = 0; |
636: | } else if (--$attempt < 0) { |
637: | return error('Failed to bootstrap SSL'); |
638: | } |
639: | $er = \Error_Reporter::flush_buffer(); |
640: | $job = \Lararia\Jobs\Job::create( |
641: | \Lararia\Jobs\SimpleCommandJob::class, |
642: | $this->getAuthContext(), |
643: | 'letsencrypt_bootstrap', |
644: | $attempt |
645: | ); |
646: | $job->setTags([$this->site, 'letsencrypt_bootstrap']); |
647: | $job->delayedDispatch($delay); |
648: | \Error_Reporter::merge_buffer($er); |
649: | info('Scheduled letsencrypt:bootstrap job'); |
650: | return null; |
651: | } |
652: | |
653: | public function _housekeeping() |
654: | { |
655: | |
656: | if (!$this->supported()) { |
657: | return; |
658: | } |
659: | if (!$this->_registered() && !$this->_register()) { |
660: | return error("failed to register with Let's Encrypt"); |
661: | } |
662: | |
663: | $this->renew_expiring(); |
664: | } |
665: | |
666: | private function _registered() |
667: | { |
668: | $key = str_replace(['/'], '.', $this->activeServer) . '.pem'; |
669: | $storageDir = LetsencryptAlias::acmeDataDirectory(); |
670: | |
671: | return file_exists($storageDir . '/accounts/' . $key); |
672: | } |
673: | |
674: | private function _register($email = null) |
675: | { |
676: | $acctdir = LetsencryptAlias::acmeDataDirectory(); |
677: | if (!file_exists($acctdir)) { |
678: | mkdir($acctdir, 0700, true); |
679: | } |
680: | |
681: | $email = $this->admin_get_email() ?: \Crm_Module::FROM_ADDRESS; |
682: | |
683: | if (!$email) { |
684: | return error("Cannot register Let's Encrypt without an email address. Run 'cpcmd common_set_email newemail' from command-line."); |
685: | } |
686: | |
687: | $marker = $this->site ?? Opcenter\Crypto\Letsencrypt::SYSCERT_NAME; |
688: | $ret = LetsencryptAlias\AcmeDispatcher::instantiateContexted($this->getAuthContext(), [$marker, $this->activeServer]) |
689: | ->register($email); |
690: | if (!$ret) { |
691: | return error("Let's Encrypt registration failed"); |
692: | } |
693: | |
694: | return true; |
695: | } |
696: | |
697: | public function _edit() |
698: | { |
699: | $conf_new = $this->getAuthContext()->getAccount()->new; |
700: | $conf_cur = $this->getAuthContext()->getAccount()->old; |
701: | $ssl = \Opcenter\SiteConfiguration::getModuleRemap('openssl'); |
702: | if (!$conf_new[$ssl]['enabled']) { |
703: | $this->_delete(); |
704: | } |
705: | } |
706: | |
707: | public function _delete() |
708: | { |
709: | $this->_deleteAcmeCertificate($this->site); |
710: | } |
711: | |
712: | public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool |
713: | { |
714: | return true; |
715: | } |
716: | |
717: | public function _create() |
718: | { |
719: | return; |
720: | } |
721: | |
722: | public function _create_user(string $user) |
723: | { |
724: | return; |
725: | } |
726: | |
727: | public function _delete_user(string $user) |
728: | { |
729: | return; |
730: | } |
731: | |
732: | public function _edit_user(string $userold, string $usernew, array $oldpwd) |
733: | { |
734: | return; |
735: | } |
736: | |
737: | public function _reload(string $why = '', array $args = []) |
738: | { |
739: | if ($why === \Ssl_Module::USER_RHOOK) { |
740: | $crt = $this->ssl_get_certificate(); |
741: | if ($this->letsencrypt_is_ca($crt)) { |
742: | return; |
743: | } |
744: | |
745: | |
746: | if (is_dir($path = LEService::acmeSiteStorageDirectory($this->site))) { |
747: | Opcenter\Filesystem::rmdir($path); |
748: | } |
749: | } |
750: | } |
751: | } |