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: | if ($strict) { |
329: | return false; |
330: | } |
331: | continue; |
332: | } |
333: | |
334: | $host = ltrim($subdomain . '.' . $domain, '.'); |
335: | if (!preg_match(Regex::HTTP_HOST, $host)) { |
336: | error("invalid server name `%s' specified", $c); |
337: | if ($strict) { |
338: | return false; |
339: | } |
340: | continue; |
341: | } |
342: | |
343: | if ($verifyip && self::INCLUDE_ALT_FORM && !$isWildcard) { |
344: | $altform = null; |
345: | if (0 !== strpos($host, 'www.')) { |
346: | |
347: | $altform = 'www.' . $host; |
348: | } else { |
349: | |
350: | $altform = substr($host, 4); |
351: | } |
352: | |
353: | $resolved = null; |
354: | if ($this->_verifyIP($altform, (array)$myip, $resolved)) { |
355: | $cnreq[] = $altform; |
356: | } else if ($strict) { |
357: | return error("Domain `%s' would be dropped from renewal", $altform); |
358: | } else { |
359: | info("skipping alternative hostname form `%(hostname)s', IP %(actual)s does not resolve to `%(expected)s'", [ |
360: | 'hostname' => $altform, |
361: | 'expected' => $myip, |
362: | 'actual' => $resolved |
363: | ]); |
364: | } |
365: | } |
366: | |
367: | $resolved = null; |
368: | if ($verifyip && !$this->_verifyIP($host, (array)$myip, $resolved)) { |
369: | $msg = [ |
370: | "hostname `%(hostname)s' IP `%(actual)s' doesn't match hosting IP `%(expected)s', " |
371: | . '%(what)s request', [ |
372: | 'hostname' => $host, |
373: | 'actual' => $resolved, |
374: | 'expected' => implode(',', (array)$myip), |
375: | 'what' =>$strict ? 'aborting' : 'skipping' |
376: | ] |
377: | ]; |
378: | if ($strict) { |
379: | return error(...$msg); |
380: | } |
381: | warn(...$msg); |
382: | continue; |
383: | } |
384: | if ($isWildcard) { |
385: | $host = '*.' . $host; |
386: | } |
387: | $cnreq[] = $host; |
388: | } |
389: | if (!$cnreq) { |
390: | error('no hostnames to register'); |
391: | |
392: | return null; |
393: | } |
394: | |
395: | $storageMarker = ($this->permission_level & PRIVILEGE_ADMIN) ? LetsencryptAlias::SYSCERT_NAME : $this->site; |
396: | if (! ($ret = $this->requestReal($cnreq, $storageMarker, $strict)) ) { |
397: | return $ret; |
398: | } |
399: | |
400: | if ($strict && ($hosts = $this->filterMissingHostnames($cnames))) { |
401: | return error('Failed to append hostnames. Hostnames missing from new certificate: %s', |
402: | implode(', ', $hosts) |
403: | ); |
404: | } |
405: | |
406: | if (!$this->_moveCertificates($storageMarker)) { |
407: | return false; |
408: | } |
409: | |
410: | return info(':letsencrypt_issuance_limit', |
411: | 'reminder: only 5 duplicate certificates and ' . |
412: | '50 unique certificates may be issued per week per account' |
413: | ); |
414: | } |
415: | |
416: | |
417: | |
418: | |
419: | |
420: | |
421: | |
422: | |
423: | |
424: | |
425: | |
426: | private function _verifyIP($hostname, array $myip, string &$resolved = null) |
427: | { |
428: | for ($i = 0; $i < 2; $i++) { |
429: | $time = microtime(true); |
430: | if ($resolved = (string)$this->dns_gethostbyname_t($hostname)) { |
431: | break; |
432: | } |
433: | $now = microtime(true) - $time; |
434: | warn('DNS resolver failed to return answer in %dms', $now * 1000); |
435: | usleep(500000); |
436: | } |
437: | |
438: | if (!$resolved) { |
439: | return false; |
440: | } |
441: | |
442: | foreach ($myip as $chkip) { |
443: | if ($chkip === $resolved) { |
444: | return true; |
445: | } |
446: | } |
447: | return false; |
448: | } |
449: | |
450: | private function _moveCertificates($site) |
451: | { |
452: | if ($site === LetsencryptAlias::SYSCERT_NAME) { |
453: | return $this->installSystemCertificate(); |
454: | } |
455: | $files = LetsencryptAlias::getCertificateComponentData($site); |
456: | if (!$files) { |
457: | return false; |
458: | } |
459: | |
460: | return $this->ssl_install($files['key'], $files['crt'], $files['chain']); |
461: | } |
462: | |
463: | |
464: | |
465: | |
466: | |
467: | |
468: | |
469: | public function is_ca($crt) |
470: | { |
471: | $cert = $this->ssl_parse_certificate($crt); |
472: | if (!$cert) { |
473: | return error('invalid ssl certificate'); |
474: | } |
475: | |
476: | if (!isset($cert['extensions']['authorityKeyIdentifier'])) { |
477: | return false; |
478: | } |
479: | $authority = $cert['extensions']['authorityKeyIdentifier']; |
480: | $prefix = 'keyid:'; |
481: | if (!strncmp($authority, $prefix, strlen($prefix))) { |
482: | $authority = substr($authority, strlen($prefix)); |
483: | } |
484: | $authority = trim($authority); |
485: | |
486: | return in_array($authority, self::LE_AUTHORITY_FINGERPRINT, true) || |
487: | in_array($authority, self::LE_STAGING_AUTHORITY_FINGERPRINT, true); |
488: | } |
489: | |
490: | |
491: | |
492: | |
493: | |
494: | |
495: | |
496: | |
497: | public function append(string|array $cnames, bool $verifyip = null): bool |
498: | { |
499: | $cnames = array_flip((array)$cnames); |
500: | if ($this->ssl_cert_exists() && null === ($old = $this->getNonOrphanedDomainsFromCertificate())) { |
501: | return warn("Cannot append hostnames to non-Let's Encrypt certificate"); |
502: | } |
503: | $old = $old ?? []; |
504: | $new = self::filterDomainSet(array_keys($cnames + array_flip($old))); |
505: | |
506: | if (!array_diff($new, $old)) { |
507: | |
508: | return true; |
509: | } |
510: | |
511: | return (bool)$this->request($new, $verifyip); |
512: | } |
513: | |
514: | private function getNonOrphanedDomainsFromCertificate(): ?array |
515: | { |
516: | if (null === ($cns = $this->getSanFromCertificate())) { |
517: | |
518: | return null; |
519: | } |
520: | |
521: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
522: | return $cns; |
523: | } |
524: | |
525: | $strict = (bool)array_get( |
526: | \Preferences::factory($this->getAuthContext()), |
527: | LetsencryptAlias\Preferences::SENSITIVITY, |
528: | LETSENCRYPT_STRICT_MODE |
529: | ); |
530: | |
531: | return array_filter($cns, function ($hostname) use ($strict) { |
532: | if (0 === strncmp($hostname, '*.', 2)) { |
533: | $hostname = substr($hostname, 2); |
534: | } |
535: | $components = $this->web_split_host($hostname); |
536: | $domain = $components['domain']; |
537: | |
538: | if (!$strict && !$this->web_domain_exists($domain)) { |
539: | warn("Domain %s missing from account, removing from certificate", $domain); |
540: | |
541: | return false; |
542: | } |
543: | |
544: | return true; |
545: | }); |
546: | } |
547: | |
548: | |
549: | |
550: | |
551: | |
552: | |
553: | public function revoke(): bool |
554: | { |
555: | if (!IS_CLI) { |
556: | return $this->query('letsencrypt_revoke'); |
557: | } |
558: | |
559: | if (!$this->certificateIssued()) { |
560: | return error('no certificate issued to revoke'); |
561: | } |
562: | |
563: | $cert = ($this->permission_level & PRIVILEGE_ADMIN) ? LetsencryptAlias::SYSCERT_NAME : $this->site; |
564: | $ret = LetsencryptAlias\AcmeDispatcher::instantiateContexted($this->getAuthContext(), [$cert, $this->activeServer]) |
565: | ->revoke(); |
566: | |
567: | if (!$ret) { |
568: | return error('revocation failed'); |
569: | } |
570: | |
571: | $this->_deleteAcmeCertificate($cert); |
572: | |
573: | return true; |
574: | } |
575: | |
576: | private function _deleteAcmeCertificate($account) |
577: | { |
578: | $acmeDir = LetsencryptAlias::acmeSiteStorageDirectory($account); |
579: | if (!file_exists($acmeDir)) { |
580: | return; |
581: | } |
582: | $dir = opendir($acmeDir); |
583: | while (false !== ($f = readdir($dir))) { |
584: | if ($f === '..' || $f === '.') { |
585: | continue; |
586: | } |
587: | unlink($acmeDir . '/' . $f); |
588: | |
589: | } |
590: | closedir($dir); |
591: | rmdir($acmeDir); |
592: | |
593: | return; |
594: | |
595: | } |
596: | |
597: | public function exists(): bool |
598: | { |
599: | $path = LetsencryptAlias::acmeSiteStorageDirectory($this->site); |
600: | |
601: | return file_exists($path); |
602: | } |
603: | |
604: | |
605: | |
606: | |
607: | |
608: | |
609: | |
610: | public function storage_path(string $site): string |
611: | { |
612: | return LetsencryptAlias::acmeSiteStorageDirectory($site); |
613: | } |
614: | |
615: | |
616: | |
617: | |
618: | |
619: | |
620: | |
621: | |
622: | |
623: | public function bootstrap(?int $attempt = null): ?bool |
624: | { |
625: | if ($attempt !== null && ($attempt > 10 || $attempt < 0)) { |
626: | return error('Invalid attempt count provided: %d', $attempt); |
627: | } |
628: | |
629: | $domains = \Opcenter\Crypto\Ssl::generateHostnames($this->getAuthContext(), true); |
630: | if (\count($domains) > 100) { |
631: | warn('Hostname count exceeds 100 (%d hostnames). Taking first 100 hostnames', \count($domains)); |
632: | $domains = array_slice($domains, 0, 100); |
633: | } |
634: | if ($attempt !== null && $this->request($domains, true)) { |
635: | |
636: | return true; |
637: | } |
638: | |
639: | $delay = 43200; |
640: | if ($attempt === null) { |
641: | $attempt = static::BOOTSTRAP_ATTEMPTS; |
642: | $delay = 0; |
643: | } else if (--$attempt < 0) { |
644: | return error('Failed to bootstrap SSL'); |
645: | } |
646: | $er = \Error_Reporter::flush_buffer(); |
647: | $job = \Lararia\Jobs\Job::create( |
648: | \Lararia\Jobs\SimpleCommandJob::class, |
649: | $this->getAuthContext(), |
650: | 'letsencrypt_bootstrap', |
651: | $attempt |
652: | ); |
653: | $job->setTags([$this->site, 'letsencrypt_bootstrap']); |
654: | $job->delayedDispatch($delay); |
655: | \Error_Reporter::merge_buffer($er); |
656: | info('Scheduled letsencrypt:bootstrap job'); |
657: | return null; |
658: | } |
659: | |
660: | public function _housekeeping() |
661: | { |
662: | |
663: | if (!$this->supported()) { |
664: | return; |
665: | } |
666: | if (!$this->_registered() && !$this->_register()) { |
667: | return error("failed to register with Let's Encrypt"); |
668: | } |
669: | |
670: | $this->renew_expiring(); |
671: | } |
672: | |
673: | private function _registered() |
674: | { |
675: | $key = str_replace(['/'], '.', $this->activeServer) . '.pem'; |
676: | $storageDir = LetsencryptAlias::acmeDataDirectory(); |
677: | |
678: | return file_exists($storageDir . '/accounts/' . $key); |
679: | } |
680: | |
681: | private function _register($email = null) |
682: | { |
683: | $acctdir = LetsencryptAlias::acmeDataDirectory(); |
684: | if (!file_exists($acctdir)) { |
685: | mkdir($acctdir, 0700, true); |
686: | } |
687: | |
688: | $email = $this->admin_get_email() ?: \Crm_Module::FROM_ADDRESS; |
689: | |
690: | if (!$email) { |
691: | return error("Cannot register Let's Encrypt without an email address. Run 'cpcmd common_set_email newemail' from command-line."); |
692: | } |
693: | |
694: | $marker = $this->site ?? Opcenter\Crypto\Letsencrypt::SYSCERT_NAME; |
695: | $ret = LetsencryptAlias\AcmeDispatcher::instantiateContexted($this->getAuthContext(), [$marker, $this->activeServer]) |
696: | ->register($email); |
697: | if (!$ret) { |
698: | return error("Let's Encrypt registration failed"); |
699: | } |
700: | |
701: | return true; |
702: | } |
703: | |
704: | public function _edit() |
705: | { |
706: | $conf_new = $this->getAuthContext()->getAccount()->new; |
707: | $conf_cur = $this->getAuthContext()->getAccount()->old; |
708: | $ssl = \Opcenter\SiteConfiguration::getModuleRemap('openssl'); |
709: | if (!$conf_new[$ssl]['enabled']) { |
710: | $this->_delete(); |
711: | } |
712: | } |
713: | |
714: | public function _delete() |
715: | { |
716: | $this->_deleteAcmeCertificate($this->site); |
717: | } |
718: | |
719: | public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool |
720: | { |
721: | return true; |
722: | } |
723: | |
724: | public function _create() |
725: | { |
726: | return; |
727: | } |
728: | |
729: | public function _create_user(string $user) |
730: | { |
731: | return; |
732: | } |
733: | |
734: | public function _delete_user(string $user) |
735: | { |
736: | return; |
737: | } |
738: | |
739: | public function _edit_user(string $userold, string $usernew, array $oldpwd) |
740: | { |
741: | return; |
742: | } |
743: | |
744: | public function _reload(string $why = '', array $args = []) |
745: | { |
746: | if ($why === \Ssl_Module::USER_RHOOK) { |
747: | $crt = $this->ssl_get_certificate(); |
748: | if ($this->letsencrypt_is_ca($crt)) { |
749: | return; |
750: | } |
751: | |
752: | |
753: | if (is_dir($path = LEService::acmeSiteStorageDirectory($this->site))) { |
754: | Opcenter\Filesystem::rmdir($path); |
755: | } |
756: | } |
757: | } |
758: | } |