Bearsampp 2026.9.23
Loading...
Searching...
No Matches
class.openssl.php
Go to the documentation of this file.
1<?php
2/*
3 *
4 * * Copyright (c) 2022-2025 Bearsampp
5 * * License: GNU General Public License version 3 or later; see LICENSE.txt
6 * * Website: https://bearsampp.com
7 * * Github: https://github.com/Bearsampp
8 *
9 */
10
19{
32
46 private $rootCaName = 'BearsamppRootCA';
48 private $trustPromptShownThisRun = false;
50 private $firefoxTrustDeclinedFile = 'firefoxTrustDeclined';
51
52
58 private function ensureMkcertExeExists()
59 {
60 $mkcertExe = Path::getMkcertExe();
61 if (file_exists($mkcertExe)) {
62 return true;
63 }
64
65 Log::error('mkcert.exe missing at: ' . $mkcertExe . '. It must be fetched during build time (prepareBase/buildFull/buildLite).');
66
67 return false;
68 }
69
75 private function ensureSslDirExists()
76 {
77 $sslPath = Path::getSslPath();
78 if (!is_dir($sslPath)) {
79 Log::info('SSL directory missing, creating: ' . $sslPath);
80 if (mkdir($sslPath, 0700, true)) {
81 @chmod($sslPath, 0700);
82 // Create .gitignore if the directory was just created
83 $gitignorePath = $sslPath . '/.gitignore';
84 if (!file_exists($gitignorePath)) {
85 file_put_contents($gitignorePath, '# git holder' . PHP_EOL);
86 }
87 } else {
88 Log::error('Failed to create SSL directory: ' . $sslPath);
89 }
90 } else {
91 // Even if directory exists, ensure .gitignore is present
92 $gitignorePath = $sslPath . '/.gitignore';
93 if (!file_exists($gitignorePath)) {
94 file_put_contents($gitignorePath, '# git holder' . PHP_EOL);
95 }
96 }
97
98 if (!is_readable($sslPath) || !is_writable($sslPath)) {
99 Log::warning('SSL directory is not fully accessible. Attempting to relax permissions: ' . $sslPath);
100
101 // Set permissive permissions for local dev environment (0755 allows owner RWX, group/others RX)
102 @chmod($sslPath, 0755);
103 clearstatcache(true, $sslPath);
104
105 if (!is_readable($sslPath) || !is_writable($sslPath)) {
106 Log::error('SSL directory is still not readable/writable after permission adjustment: ' . $sslPath);
107 }
108 }
109
110 return $sslPath;
111 }
112
118 public function makeRootCa()
119 {
120 if (!$this->ensureMkcertExeExists()) {
121 return false;
122 }
123 $destPath = Path::getSslPath();
124 $mkcertExe = Path::getMkcertExe();
125
126 Log::info('Creating new Root CA and installing it...');
127
128 $rootCaPath = Path::getSslPath() . '/' . Path::getMkcertRootCaName();
129 $batch = 'SET "CAROOT=' . Path::formatWindowsPath(Path::getSslPath()) . '"' . PHP_EOL;
130 $batch .= '"' . $mkcertExe . '" -uninstall' . PHP_EOL;
131 $batch .= '"' . $mkcertExe . '" -install' . PHP_EOL;
132 $batch .= 'IF EXIST "' . Path::formatWindowsPath($rootCaPath) . '" (ECHO OK)' . PHP_EOL;
133
134 // Wait for the Root CA file to appear or timeout
135 $result = Batch::exec('mkcertMakeRootCa', $batch);
136
137 if (!file_exists($rootCaPath)) {
138 Log::error('Failed to create Root CA file at: ' . $rootCaPath);
139
140 return false;
141 }
142
143 // Display info about the new Root CA
144 $batch = 'SET "CAROOT=' . Path::formatWindowsPath(Path::getSslPath()) . '"' . PHP_EOL;
145 $batch .= '"' . $mkcertExe . '" -CAROOT' . PHP_EOL;
146 $caRootInfo = Batch::exec('mkcertCaRootInfo', $batch);
147 if ($caRootInfo && isset($caRootInfo[0])) {
148 Log::info('mkcert CAROOT is set to: ' . $caRootInfo[0]);
149 }
150
151 // A brand new Root CA was created: ask the user whether to trust it
152 $this->maybePromptTrustRootCa($rootCaPath, true);
153
154 Log::info('Root CA created. Rebuilding all existing certificates and ensuring localhost exists...');
155
156 // Ensure localhost is created/rebuilt
157 $this->createCrt('localhost');
158
159 return $this->rebuildAllCerts();
160 }
161
167 public function rebuildAllCerts()
168 {
169 $certs = $this->getCrts();
170 $success = true;
171
172 foreach ($certs as $cert) {
173 Log::info('Rebuilding certificate: ' . $cert);
174 if (!$this->createCrt($cert)) {
175 Log::error('Failed to rebuild certificate: ' . $cert);
176 $success = false;
177 }
178 }
179
180 return $success;
181 }
182
193 private function validateCertificateName($name)
194 {
195 if (empty($name)) {
196 return false;
197 }
198
199 // Filesystem-safe whitelist with mandatory alphanumeric first character:
200 // - Prevents CLI flag injection (leading `-`)
201 // - Prevents relative path traversal (leading `.`)
202 // - Allows remaining chars to be alphanumeric, dots, dashes, underscores
203 if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/', $name)) {
204 return false;
205 }
206
207 return true;
208 }
209
218 public function createCrt($name, $destPath = null)
219 {
220 Log::trace('createCrt called for: ' . $name . ($destPath ? ' (dest: ' . $destPath . ')' : ''));
221
222 if (!$this->validateCertificateName($name)) {
223 Log::error('Invalid certificate name: ' . $name);
224
225 return false;
226 }
227
228 if (!$this->ensureMkcertExeExists()) {
229 return false;
230 }
231 if (empty($destPath)) {
232 $destPath = $this->ensureSslDirExists();
233 }
234 $mkcertExe = Path::getMkcertExe();
235
236 if (!$this->ensureRootCaExists($destPath)) {
237 Log::error('Failed to ensure Root CA exists for: ' . $name);
238
239 return false;
240 }
241
242 $crtPath = '"' . Path::formatWindowsPath($destPath . '/' . $name . '.crt') . '"';
243 $pubPath = '"' . Path::formatWindowsPath($destPath . '/' . $name . '.pub') . '"';
244 $keyPath = '"' . Path::formatWindowsPath($destPath . '/' . $name . '.ppk') . '"'; // Using .ppk as requested in previous tasks
245 $opensslExe = '"' . Path::formatWindowsPath(Path::getOpenSslExe()) . '"';
246
247 $batch = 'SET "CAROOT=' . Path::formatWindowsPath($destPath) . '"' . PHP_EOL;
248
249 $mkcertNames = '"' . $name . '"';
250 if ($name === 'localhost') {
251 $mkcertNames .= ' ' . APP_LOCALHOST . ' ::1';
252 } else {
253 $mkcertNames .= ' "*.' . $name . '" localhost ' . APP_LOCALHOST . ' ::1';
254 }
255
256 Log::trace('Executing mkcert for "' . $name . '"');
257 // Use -- to terminate flag parsing before hostname arguments as defense-in-depth
258 $batch .= '"' . $mkcertExe . '" -cert-file ' . $crtPath . ' -key-file ' . $keyPath . ' -- ' . $mkcertNames . PHP_EOL;
259 $batch .= $opensslExe . " rsa -in " . $keyPath . " -out " . $keyPath . " -passin pass:" . PHP_EOL;
260 $batch .= "COPY /Y " . $keyPath . " " . $pubPath . PHP_EOL;
261 $batch .= 'IF EXIST ' . $crtPath . ' IF EXIST ' . $keyPath . ' ECHO OK' . PHP_EOL;
262
263 Log::trace('Creating SSL Certificate for "' . $name . '" using mkcert. Batch content: ' . $batch);
264 $result = Batch::exec('createCertificateMkcert', $batch);
265
266 if ($result === false || !is_array($result)) {
267 Log::error('Batch execution failed for mkcert generation of "' . $name . '". Check logs for createCertificateMkcert.');
268
269 return false;
270 }
271
272 $success = false;
273 foreach ($result as $line) {
274 if (trim($line) === 'OK') {
275 $success = true;
276 break;
277 }
278 }
279
280 if (!$success) {
281 Log::error('mkcert generation for "' . $name . '" did not return OK. Output: ' . implode(' | ', $result));
282 }
283 Log::trace('mkcert generation for "' . $name . '": ' . ($success ? 'SUCCESS' : 'FAILURE'));
284
285 return $success;
286 }
287
295 private function ensureRootCaExists($destPath)
296 {
297 if (!$this->ensureMkcertExeExists()) {
298 return false;
299 }
300 $mkcertExe = Path::getMkcertExe();
301
302 $rootCaPath = $destPath . '/' . Path::getMkcertRootCaName(); // mkcert default root CA name
303 if (!file_exists($rootCaPath)) {
304 Log::info('Root CA missing at ' . $rootCaPath . '. Running mkcert -install');
305 $batch = 'SET "CAROOT=' . Path::formatWindowsPath($destPath) . '"' . PHP_EOL;
306 $batch .= '"' . $mkcertExe . '" -install' . PHP_EOL;
307 $batch .= 'IF EXIST "' . Path::formatWindowsPath($rootCaPath) . '" (ECHO OK)' . PHP_EOL;
308 $result = Batch::exec('mkcertInstall', $batch);
309
310 if ($result === false) {
311 Log::error('Batch execution failed for mkcert -install');
312
313 return false;
314 }
315
316 // Re-check after installation
317 if (!file_exists($rootCaPath)) {
318 Log::error('Root CA still missing after mkcert -install at: ' . $rootCaPath);
319
320 return false;
321 }
322 Log::info('Root CA successfully created and verified.');
323
324 // A brand new Root CA was created: ask the user whether to trust it
325 $this->maybePromptTrustRootCa($rootCaPath, true);
326 } else {
327 // Root CA already exists: if Firefox does not trust it yet, offer to configure it
328 $this->maybePromptTrustRootCa($rootCaPath, false);
329 }
330
331 return true;
332 }
333
341 public function ensureFirefoxTrustPrompt()
342 {
343 $destPath = Path::getSslPath();
344 $rootCaPath = $destPath . '/' . Path::getMkcertRootCaName();
345 if (!file_exists($rootCaPath)) {
346 return false;
347 }
348
349 $this->maybePromptTrustRootCa($rootCaPath, false);
350
351 return true;
352 }
353
363 private function maybePromptTrustRootCa($rootCaPath, $force)
364 {
365 global $bearsamppLang, $bearsamppWinbinder;
366
367 if ($this->trustPromptShownThisRun) {
368 return false;
369 }
370
371 $declinedFile = Path::getSslPath() . '/' . $this->firefoxTrustDeclinedFile;
372
373 if ($force) {
374 // A brand new Root CA was created: forget any previous decline and offer trust again
375 @unlink($declinedFile);
376 } else {
377 // Existing Root CA: only offer to configure Firefox if it is not configured yet
379 return false;
380 }
381 if (file_exists($declinedFile)) {
382 Log::info('Skipping SSL Root CA trust prompt (previously declined).');
383
384 return false;
385 }
386 }
387
388 $this->trustPromptShownThisRun = true;
389
390 $message = $force
394
395 $trusted = false;
396 if (is_object($bearsamppWinbinder) && method_exists($bearsamppWinbinder, 'messageBoxYesNo')) {
397 $trusted = (bool)$bearsamppWinbinder->messageBoxYesNo($message, $title);
398 } else {
399 Log::warning('WinBinder not available, cannot prompt to trust the SSL Root CA.');
400 }
401
402 if (!$trusted) {
403 Log::info('User declined to trust the SSL Root CA: ' . $rootCaPath);
404 file_put_contents($declinedFile, date('Y-m-d H:i:s') . PHP_EOL);
405
406 return false;
407 }
408
409 Log::info('User accepted to trust the SSL Root CA. Configuring browsers...');
410 $this->trustRootCaInFirefox($rootCaPath);
411 @unlink($declinedFile);
412
413 if (is_object($bearsamppWinbinder) && method_exists($bearsamppWinbinder, 'messageBoxInfo')) {
414 $bearsamppWinbinder->messageBoxInfo(
416 $title
417 );
418 }
419
420 return true;
421 }
422
428 private function getFirefoxProfileDirs()
429 {
430 $dirs = array();
431 $bases = array();
432 $appData = getenv('APPDATA');
433 $localAppData = getenv('LOCALAPPDATA');
434 if (!empty($appData)) {
435 $bases[] = $appData . '/Mozilla/Firefox/Profiles';
436 }
437 if (!empty($localAppData)) {
438 $bases[] = $localAppData . '/Mozilla/Firefox/Profiles';
439 }
440
441 foreach ($bases as $base) {
442 if (!is_dir($base)) {
443 continue;
444 }
445 $entries = @scandir($base);
446 if ($entries === false) {
447 continue;
448 }
449 foreach ($entries as $entry) {
450 if ($entry === '.' || $entry === '..') {
451 continue;
452 }
453 $dir = $base . '/' . $entry;
454 if (is_dir($dir) && file_exists($dir . '/prefs.js')) {
455 $dirs[] = $dir;
456 }
457 }
458 }
459
460 return array_values(array_unique($dirs));
461 }
462
470 {
471 $profiles = $this->getFirefoxProfileDirs();
472 if (empty($profiles)) {
473 return true;
474 }
475
476 foreach ($profiles as $profile) {
477 $userJs = $profile . '/user.js';
478 $content = file_exists($userJs) ? @file_get_contents($userJs) : '';
479 if ($content === false) {
480 $content = '';
481 }
482 if (!preg_match('/user_pref\‍(\s*["\']security\.enterprise_roots\.enabled["\']\s*,\s*true\s*\‍)/i', $content)) {
483 return false;
484 }
485 }
486
487 return true;
488 }
489
498 private function trustRootCaInFirefox($rootCaPath)
499 {
500 $profiles = $this->getFirefoxProfileDirs();
501 if (empty($profiles)) {
502 Log::info('No Firefox profiles found. Skipping Firefox SSL Root CA configuration.');
503
504 return;
505 }
506
507 foreach ($profiles as $profile) {
508 $userJs = $profile . '/user.js';
509 $content = file_exists($userJs) ? @file_get_contents($userJs) : '';
510 if ($content === false) {
511 $content = '';
512 }
513 $lines = $content === '' ? array() : explode("\n", str_replace("\r\n", "\n", $content));
514
515 // Remove any existing enterprise_roots pref so we only keep the trusted one
516 $lines = array_values(array_filter($lines, function ($line) {
517 return preg_match('/security\.enterprise_roots\.enabled/i', trim($line)) ? false : true;
518 }));
519
520 $lines[] = 'user_pref("security.enterprise_roots.enabled", true);';
521
522 if (@file_put_contents($userJs, implode("\n", $lines) . PHP_EOL) !== false) {
523 Log::info('Configured Firefox profile to trust system root certificates: ' . $profile);
524 } else {
525 Log::error('Failed to write Firefox user.js: ' . $userJs);
526 }
527 }
528 }
529
533 public function delSslCertificate()
534 {
535 global $bearsamppLang, $bearsamppWinbinder;
536
537 $initServerName = 'test.local';
538 $initDocumentRoot = Path::formatWindowsPath(Path::getSslPath());
539
540 $bearsamppWinbinder->reset();
541 $wbWindow = $bearsamppWinbinder->createAppWindow($bearsamppLang->getValue(Lang::DELSSL_TITLE), 490, 160, WBC_NOTIFY, WBC_KEYDOWN | WBC_KEYUP);
542
543 $wbLabelName = $bearsamppWinbinder->createLabel($wbWindow, $bearsamppLang->getValue(Lang::NAME) . ' :', 15, 15, 85, null, WBC_RIGHT);
544 $this->wbDelSslListCerts = $bearsamppWinbinder->createInputText($wbWindow, $initServerName, 105, 13, 150, null);
545
546 $wbLabelDest = $bearsamppWinbinder->createLabel($wbWindow, $bearsamppLang->getValue(Lang::TARGET) . ' :', 15, 45, 85, null, WBC_RIGHT);
547 $this->wbDelSslInputDest = $bearsamppWinbinder->createInputText($wbWindow, $initDocumentRoot, 105, 43, 190, null, null, WBC_READONLY);
548 $this->wbDelSslBtnDest = $bearsamppWinbinder->createButton($wbWindow, $bearsamppLang->getValue(Lang::BUTTON_BROWSE), 300, 43, 110);
549
550 $this->wbDelSslProgressBar = $bearsamppWinbinder->createProgressBar($wbWindow, 3, 15, 97, 275);
551 $this->wbDelSslBtnDelete = $bearsamppWinbinder->createButton($wbWindow, $bearsamppLang->getValue(Lang::BUTTON_DELETE), 300, 92);
552 $this->wbDelSslBtnCancel = $bearsamppWinbinder->createButton($wbWindow, $bearsamppLang->getValue(Lang::BUTTON_CANCEL), 387, 92);
553
554 $bearsamppWinbinder->setHandler($wbWindow, $this, 'delSslCertificateHandler');
555
556 $bearsamppWinbinder->mainLoop();
557 $bearsamppWinbinder->reset();
558 }
559
569 public function delSslCertificateHandler($window, $id, $ctrl, $param1, $param2)
570 {
571 global $bearsamppLang, $bearsamppOpenSsl, $bearsamppWinbinder;
572
573 switch ($id) {
574 case $this->wbDelSslBtnDest[WinBinder::CTRL_ID]:
575 $target = $bearsamppWinbinder->getText($this->wbDelSslInputDest[WinBinder::CTRL_OBJ]);
576 $target = $bearsamppWinbinder->sysDlgPath($window, $bearsamppLang->getValue(Lang::GENSSL_PATH), $target);
577 if ($target && is_dir($target)) {
578 $bearsamppWinbinder->setText($this->wbDelSslInputDest[WinBinder::CTRL_OBJ], $target . '\\');
579 }
580 break;
581 case $this->wbDelSslBtnDelete[WinBinder::CTRL_ID]:
582 $cert = $bearsamppWinbinder->getText($this->wbDelSslListCerts[WinBinder::CTRL_OBJ]);
583 $target = $bearsamppWinbinder->getText($this->wbDelSslInputDest[WinBinder::CTRL_OBJ]);
584
585 if ($cert) {
586 $existingCerts = $this->getCrts();
587 if (!in_array($cert, $existingCerts)) {
588 $bearsamppWinbinder->messageBoxError(
589 sprintf($bearsamppLang->getValue(Lang::ERROR_FILE_NOT_FOUND), $cert, $target),
591 );
592
593 return;
594 }
595
596 $bearsamppWinbinder->setProgressBarMax($this->wbDelSslProgressBar, 3);
597 $bearsamppWinbinder->incrProgressBar($this->wbDelSslProgressBar);
598
599 $target = Path::formatUnixPath($target);
600 if ($bearsamppOpenSsl->removeCrt($cert, $target)) {
601 $bearsamppWinbinder->incrProgressBar($this->wbDelSslProgressBar);
602 $bearsamppWinbinder->messageBoxInfo(
603 sprintf($bearsamppLang->getValue(Lang::DELSSL_DELETED), $cert),
605 );
606 $bearsamppWinbinder->destroyWindow($window);
607 } else {
608 $bearsamppWinbinder->messageBoxError($bearsamppLang->getValue(Lang::DELSSL_DELETED_ERROR), $bearsamppLang->getValue(Lang::DELSSL_TITLE));
609 $bearsamppWinbinder->resetProgressBar($this->wbDelSslProgressBar);
610 }
611 }
612 break;
613 case IDCLOSE:
614 case $this->wbDelSslBtnCancel[WinBinder::CTRL_ID]:
615 $bearsamppWinbinder->destroyWindow($window);
616 break;
617 }
618 }
619
627 public function existsCrt($name)
628 {
629 $ppkPath = Path::getSslPath() . '/' . $name . '.ppk';
630 $crtPath = Path::getSslPath() . '/' . $name . '.crt';
631
632 return is_file($ppkPath) && is_file($crtPath);
633 }
634
638 public function genSslCertificate()
639 {
640 global $bearsamppLang, $bearsamppWinbinder;
641
642 $initServerName = 'test.local';
643 $initDocumentRoot = Path::formatWindowsPath(Path::getSslPath());
644
645 $bearsamppWinbinder->reset();
646 $wbWindow = $bearsamppWinbinder->createAppWindow($bearsamppLang->getValue(Lang::GENSSL_TITLE), 490, 160, WBC_NOTIFY, WBC_KEYDOWN | WBC_KEYUP);
647
648 $wbLabelName = $bearsamppWinbinder->createLabel($wbWindow, $bearsamppLang->getValue(Lang::NAME) . ' :', 15, 15, 85, null, WBC_RIGHT);
649 $this->wbGenSslInputName = $bearsamppWinbinder->createInputText($wbWindow, $initServerName, 105, 13, 150, null);
650
651 $wbLabelDest = $bearsamppWinbinder->createLabel($wbWindow, $bearsamppLang->getValue(Lang::TARGET) . ' :', 15, 45, 85, null, WBC_RIGHT);
652 $this->wbGenSslInputDest = $bearsamppWinbinder->createInputText($wbWindow, $initDocumentRoot, 105, 43, 190, null, null, WBC_READONLY);
653 $this->wbGenSslBtnDest = $bearsamppWinbinder->createButton($wbWindow, $bearsamppLang->getValue(Lang::BUTTON_BROWSE), 300, 43, 110);
654
655 $this->wbGenSslProgressBar = $bearsamppWinbinder->createProgressBar($wbWindow, 3, 15, 97, 275);
656 $this->wbGenSslBtnSave = $bearsamppWinbinder->createButton($wbWindow, $bearsamppLang->getValue(Lang::BUTTON_SAVE), 300, 92);
657 $this->wbGenSslBtnCancel = $bearsamppWinbinder->createButton($wbWindow, $bearsamppLang->getValue(Lang::BUTTON_CANCEL), 387, 92);
658
659 $bearsamppWinbinder->setHandler($wbWindow, $this, 'genSslCertificateHandler');
660
661 $bearsamppWinbinder->mainLoop();
662 $bearsamppWinbinder->reset();
663 }
664
674 public function genSslCertificateHandler($window, $id, $ctrl, $param1, $param2)
675 {
676 global $bearsamppLang, $bearsamppOpenSsl, $bearsamppWinbinder;
677
678 switch ($id) {
679 case $this->wbGenSslBtnDest[WinBinder::CTRL_ID]:
680 $target = $bearsamppWinbinder->getText($this->wbGenSslInputDest[WinBinder::CTRL_OBJ]);
681 $target = $bearsamppWinbinder->sysDlgPath($window, $bearsamppLang->getValue(Lang::GENSSL_PATH), $target);
682 if ($target && is_dir($target)) {
683 $bearsamppWinbinder->setText($this->wbGenSslInputDest[WinBinder::CTRL_OBJ], $target . '\\');
684 }
685 break;
686 case $this->wbGenSslBtnSave[WinBinder::CTRL_ID]:
687 $name = $bearsamppWinbinder->getText($this->wbGenSslInputName[WinBinder::CTRL_OBJ]);
688 $target = $bearsamppWinbinder->getText($this->wbGenSslInputDest[WinBinder::CTRL_OBJ]);
689
690 $bearsamppWinbinder->setProgressBarMax($this->wbGenSslProgressBar, 3);
691 $bearsamppWinbinder->incrProgressBar($this->wbGenSslProgressBar);
692
693 $target = Path::formatUnixPath($target);
694 if ($bearsamppOpenSsl->createCrt($name, $target)) {
695 $bearsamppWinbinder->incrProgressBar($this->wbGenSslProgressBar);
696 $bearsamppWinbinder->messageBoxInfo(
697 sprintf($bearsamppLang->getValue(Lang::GENSSL_CREATED), $name),
699 );
700 $bearsamppWinbinder->destroyWindow($window);
701 } else {
702 $bearsamppWinbinder->messageBoxError($bearsamppLang->getValue(Lang::GENSSL_CREATED_ERROR), $bearsamppLang->getValue(Lang::GENSSL_TITLE));
703 $bearsamppWinbinder->resetProgressBar($this->wbGenSslProgressBar);
704 }
705 break;
706 case IDCLOSE:
707 case $this->wbGenSslBtnCancel[WinBinder::CTRL_ID]:
708 $bearsamppWinbinder->destroyWindow($window);
709 break;
710 }
711 }
712
718 public function getCrts()
719 {
720 $sslPath = $this->ensureSslDirExists();
721 $certs = [];
722 if (is_dir($sslPath)) {
723 $files = glob($sslPath . '/*.crt');
724 if ($files !== false) {
725 foreach ($files as $file) {
726 $certs[] = basename($file, '.crt');
727 }
728 }
729 }
730 sort($certs);
731
732 return $certs;
733 }
734
742 public function isExpired($name)
743 {
744 $crtPath = Path::getSslPath() . '/' . $name . '.crt';
745 $pubPath = Path::getSslPath() . '/' . $name . '.pub';
746
747 if (!is_file($crtPath)) {
748 Log::trace('SSL certificate file missing: ' . $crtPath);
749
750 return true;
751 }
752
753 if (!is_file($pubPath)) {
754 Log::trace('SSL public certificate file missing: ' . $pubPath);
755
756 return true;
757 }
758
759 if (!extension_loaded('openssl')) {
760 Log::warning('OpenSSL extension not loaded. Cannot parse certificate for expiry check. Assuming NOT expired if file exists.');
761
762 return false;
763 }
764
765 $crtContent = file_get_contents($crtPath);
766 if ($crtContent === false) {
767 Log::error('Could not read certificate file: ' . $crtPath);
768
769 return true;
770 }
771
772 $certInfo = openssl_x509_parse($crtContent);
773 if ($certInfo === false) {
774 Log::error('Could not parse certificate: ' . $crtPath . '. OpenSSL error: ' . openssl_error_string());
775
776 return true;
777 }
778
779 if (isset($certInfo['validTo_time_t'])) {
780 $isExpired = $certInfo['validTo_time_t'] < time();
781 if ($isExpired) {
782 Log::trace('SSL certificate expired: ' . $name . ' (Expired on ' . date('Y-m-d H:i:s', $certInfo['validTo_time_t']) . ')');
783 }
784
785 return $isExpired;
786 }
787
788 Log::error('Could not find expiry date in certificate: ' . $crtPath);
789
790 return true;
791 }
792
801 public function removeCrt($name, $destPath = null)
802 {
803 if ($name === 'localhost') {
804 Log::warning('Attempted to remove protected "localhost" certificate. Operation cancelled.');
805
806 return false;
807 }
808 $destPath = empty($destPath) ? $this->ensureSslDirExists() : $destPath;
809
810 // Basic validation for name to prevent arbitrary file deletion
811 if (!preg_match('/^[a-zA-Z0-9._-]+$/', $name)) {
812 Log::error('Invalid certificate name for removal: ' . $name);
813
814 return false;
815 }
816
817 $ppkPath = $destPath . '/' . $name . '.ppk';
818 $crtPath = $destPath . '/' . $name . '.crt';
819 $pubPath = $destPath . '/' . $name . '.pub';
820
821 Log::info('Removing SSL certificate: ' . $name . ' from ' . $destPath);
822
823 return @unlink($ppkPath) && @unlink($crtPath) && @unlink($pubPath);
824 }
825}
$result
global $bearsamppLang
static exec($basename, $content, $timeout=true, $catchOutput=true, $standalone=false, $silent=true, $rebuild=true)
const GENSSL_TITLE
const GENSSL_CREATED
const NAME
const BUTTON_BROWSE
const BUTTON_CANCEL
const GENSSL_PATH
const BUTTON_DELETE
const DELSSL_DELETED_ERROR
const SSL_TRUST_FIREFOX_MSG
const BUTTON_SAVE
const GENSSL_CREATED_ERROR
const SSL_TRUST_ROOT_CA_TITLE
const TARGET
const DELSSL_DELETED
const SSL_TRUST_ROOT_CA_DONE
const ERROR_FILE_NOT_FOUND
const DELSSL_TITLE
const SSL_TRUST_ROOT_CA_MSG
static info($data, $file=null)
static warning($data, $file=null)
static trace($data, $file=null)
static error($data, $file=null)
maybePromptTrustRootCa($rootCaPath, $force)
removeCrt($name, $destPath=null)
ensureRootCaExists($destPath)
ensureFirefoxTrustPrompt()
getFirefoxProfileDirs()
$trustPromptShownThisRun
createCrt($name, $destPath=null)
ensureMkcertExeExists()
isExpired($name)
delSslCertificateHandler($window, $id, $ctrl, $param1, $param2)
existsCrt($name)
validateCertificateName($name)
isFirefoxEnterpriseRootsConfigured()
$firefoxTrustDeclinedFile
genSslCertificateHandler($window, $id, $ctrl, $param1, $param2)
trustRootCaInFirefox($rootCaPath)
ensureSslDirExists()
static getSslPath($aetrayPath=false)
static getOpenSslExe($aetrayPath=false)
static getMkcertExe($aetrayPath=false)
static formatWindowsPath($path)
static formatUnixPath($path)
static getMkcertRootCaName()
const APP_LOCALHOST
Definition root.php:21