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