"Fossies" - the Fresh Open Source Software Archive

Member "ispconfig3_install/server/lib/classes/letsencrypt.inc.php" (8 Jun 2021, 19385 Bytes) of package /linux/privat/ISPConfig-3.2.5.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "letsencrypt.inc.php": 3.2.4_vs_3.2.5.

    1 <?php
    2 
    3 /*
    4 Copyright (c) 2017, Marius Burkard, projektfarm Gmbh
    5 All rights reserved.
    6 
    7 Redistribution and use in source and binary forms, with or without modification,
    8 are permitted provided that the following conditions are met:
    9 
   10     * Redistributions of source code must retain the above copyright notice,
   11       this list of conditions and the following disclaimer.
   12     * Redistributions in binary form must reproduce the above copyright notice,
   13       this list of conditions and the following disclaimer in the documentation
   14       and/or other materials provided with the distribution.
   15     * Neither the name of ISPConfig nor the names of its contributors
   16       may be used to endorse or promote products derived from this software without
   17       specific prior written permission.
   18 
   19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
   20 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   21 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
   22 IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
   23 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   24 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
   25 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
   26 OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
   27 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
   28 EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   29 */
   30 
   31 class letsencrypt {
   32 
   33     /**
   34      * Construct for this class
   35      *
   36      * @return system
   37      */
   38     private $base_path = '/etc/letsencrypt';
   39     private $renew_config_path = '/etc/letsencrypt/renewal';
   40     private $certbot_use_certcommand = false;
   41 
   42     public function __construct(){
   43 
   44     }
   45 
   46     public function get_acme_script() {
   47         $acme = explode("\n", shell_exec('which /usr/local/ispconfig/server/scripts/acme.sh /root/.acme.sh/acme.sh'));
   48         $acme = reset($acme);
   49         if(is_executable($acme)) {
   50             return $acme;
   51         } else {
   52             return false;
   53         }
   54     }
   55 
   56     public function get_acme_command($domains, $key_file, $bundle_file, $cert_file, $server_type = 'apache') {
   57         global $app, $conf;
   58 
   59         $letsencrypt = $this->get_acme_script();
   60 
   61         $cmd = '';
   62         // generate cli format
   63         foreach($domains as $domain) {
   64             $cmd .= (string) " -d " . $domain;
   65         }
   66 
   67         if($cmd == '') {
   68             return false;
   69         }
   70 
   71         if($server_type != 'apache' || version_compare($app->system->getapacheversion(true), '2.4.8', '>=')) {
   72             $cert_arg = '--fullchain-file ' . escapeshellarg($cert_file);
   73         } else {
   74             $cert_arg = '--fullchain-file ' . escapeshellarg($bundle_file) . ' --cert-file ' . escapeshellarg($cert_file);
   75         }
   76 
   77         $cmd = 'R=0 ; C=0 ; ' . $letsencrypt . ' --issue ' . $cmd . ' -w /usr/local/ispconfig/interface/acme --always-force-new-domain-key --keylength 4096; R=$? ; if [[ $R -eq 0 || $R -eq 2 ]] ; then ' . $letsencrypt . ' --install-cert ' . $cmd . ' --key-file ' . escapeshellarg($key_file) . ' ' . $cert_arg . ' --reloadcmd ' . escapeshellarg($this->get_reload_command()) . ' --log ' . escapeshellarg($conf['ispconfig_log_dir'].'/acme.log') . '; C=$? ; fi ; if [[ $C -eq 0 ]] ; then exit $R ; else exit $C  ; fi';
   78 
   79         return $cmd;
   80     }
   81 
   82     public function get_certbot_script() {
   83         $letsencrypt = explode("\n", shell_exec('which certbot /root/.local/share/letsencrypt/bin/letsencrypt /opt/eff.org/certbot/venv/bin/certbot letsencrypt'));
   84         $letsencrypt = reset($letsencrypt);
   85         if(is_executable($letsencrypt)) {
   86             return $letsencrypt;
   87         } else {
   88             return false;
   89         }
   90     }
   91 
   92     private function install_acme() {
   93         $install_cmd = 'wget -O -  https://get.acme.sh | sh';
   94         $ret = null;
   95         $val = 0;
   96         exec($install_cmd . ' 2>&1', $ret, $val);
   97 
   98         return ($val == 0 ? true : false);
   99     }
  100 
  101     private function get_reload_command() {
  102         global $app, $conf;
  103 
  104         $web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
  105 
  106         $daemon = '';
  107         switch ($web_config['server_type']) {
  108             case 'nginx':
  109                 $daemon = $web_config['server_type'];
  110                 break;
  111             default:
  112                 if(is_file($conf['init_scripts'] . '/' . 'httpd24-httpd') || is_dir('/opt/rh/httpd24/root/etc/httpd')) {
  113                     $daemon = 'httpd24-httpd';
  114                 } elseif(is_file($conf['init_scripts'] . '/' . 'httpd') || is_dir('/etc/httpd')) {
  115                     $daemon = 'httpd';
  116                 } else {
  117                     $daemon = 'apache2';
  118                 }
  119         }
  120 
  121         $cmd = $app->system->getinitcommand($daemon, 'force-reload');
  122         return $cmd;
  123     }
  124 
  125     public function get_certbot_command($domains) {
  126         global $app;
  127 
  128         $letsencrypt = $this->get_certbot_script();
  129 
  130         $cmd = '';
  131         // generate cli format
  132         foreach($domains as $domain) {
  133             $cmd .= (string) " --domains " . $domain;
  134         }
  135 
  136         if($cmd == '') {
  137             return false;
  138         }
  139 
  140         $primary_domain = $domains[0];
  141         $matches = array();
  142         $ret = null;
  143         $val = 0;
  144 
  145         $letsencrypt_version = exec($letsencrypt . ' --version  2>&1', $ret, $val);
  146         if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $letsencrypt_version, $matches)) {
  147             $letsencrypt_version = $matches[2];
  148         }
  149         if (version_compare($letsencrypt_version, '0.22', '>=')) {
  150             $acme_version = 'https://acme-v02.api.letsencrypt.org/directory';
  151         } else {
  152             $acme_version = 'https://acme-v01.api.letsencrypt.org/directory';
  153         }
  154         if (version_compare($letsencrypt_version, '0.30', '>=')) {
  155             $app->log("LE version is " . $letsencrypt_version . ", so using certificates command and --cert-name instead of --expand", LOGLEVEL_DEBUG);
  156             $this->certbot_use_certcommand = true;
  157             $webroot_map = array();
  158             for($i = 0; $i < count($domains); $i++) {
  159                 $webroot_map[$domains[$i]] = '/usr/local/ispconfig/interface/acme';
  160             }
  161             $webroot_args = "--webroot-map " . escapeshellarg(str_replace(array("\r", "\n"), '', json_encode($webroot_map)));
  162             // --cert-name might be working with earlier versions of certbot, but there is no exact version documented
  163             // So for safety reasons we add it to the 0.30 version check as it is documented to work as expected in this version
  164             $cert_selection_command = "--cert-name $primary_domain";
  165         } else {
  166             $webroot_args = "$cmd --webroot-path /usr/local/ispconfig/interface/acme";
  167             $cert_selection_command = "--expand";
  168         }
  169 
  170         $cmd = $letsencrypt . " certonly -n --text --agree-tos $cert_selection_command --authenticator webroot --server $acme_version --rsa-key-size 4096 --email webmaster@$primary_domain $webroot_args";
  171 
  172         return $cmd;
  173     }
  174 
  175     public function get_letsencrypt_certificate_paths($domains = array()) {
  176         global $app;
  177 
  178         if($this->get_acme_script()) {
  179             return false;
  180         }
  181 
  182         if(empty($domains)) return false;
  183         if(!is_dir($this->renew_config_path)) return false;
  184 
  185         $dir = opendir($this->renew_config_path);
  186         if(!$dir) return false;
  187 
  188         $path_scores = array();
  189 
  190         $main_domain = reset($domains);
  191         sort($domains);
  192         $min_diff = false;
  193 
  194         while($file = readdir($dir)) {
  195             if($file === '.' || $file === '..' || substr($file, -5) !== '.conf')  continue;
  196             $file_path = $this->renew_config_path . '/' . $file;
  197             if(!is_file($file_path) || !is_readable($file_path)) continue;
  198 
  199             $fp = fopen($file_path, 'r');
  200             if(!$fp) continue;
  201 
  202             $path_scores[$file_path] = array(
  203                 'domains' => array(),
  204                 'diff' => 0,
  205                 'has_main_domain' => false,
  206                 'cert_paths' => array(
  207                     'cert' => '',
  208                     'privkey' => '',
  209                     'chain' => '',
  210                     'fullchain' => ''
  211                 )
  212             );
  213             $in_list = false;
  214             while(!feof($fp) && $line = fgets($fp)) {
  215                 $line = trim($line);
  216                 if($line === '') continue;
  217                 elseif(!$in_list) {
  218                     if($line == '[[webroot_map]]') $in_list = true;
  219 
  220                     $tmp = explode('=', $line, 2);
  221                     if(count($tmp) != 2) continue;
  222                     $key = trim($tmp[0]);
  223                     if($key == 'cert' || $key == 'privkey' || $key == 'chain' || $key == 'fullchain') {
  224                         $path_scores[$file_path]['cert_paths'][$key] = trim($tmp[1]);
  225                     }
  226 
  227                     continue;
  228                 }
  229 
  230                 $tmp = explode('=', $line, 2);
  231                 if(count($tmp) != 2) continue;
  232 
  233                 $domain = trim($tmp[0]);
  234                 if($domain == $main_domain) $path_scores[$file_path]['has_main_domain'] = true;
  235                 $path_scores[$file_path]['domains'][] = $domain;
  236             }
  237             fclose($fp);
  238 
  239             sort($path_scores[$file_path]['domains']);
  240             if(count(array_intersect($domains, $path_scores[$file_path]['domains'])) < 1) {
  241                 $path_scores[$file_path]['diff'] = false;
  242             } else {
  243                 // give higher diff value to missing domains than to those that are too much in there
  244                 $path_scores[$file_path]['diff'] = (count(array_diff($domains, $path_scores[$file_path]['domains'])) * 1.5) + count(array_diff($path_scores[$file_path]['domains'], $domains));
  245             }
  246 
  247             if($min_diff === false || $path_scores[$file_path]['diff'] < $min_diff) $min_diff = $path_scores[$file_path]['diff'];
  248         }
  249         closedir($dir);
  250 
  251         if($min_diff === false) return false;
  252 
  253         $cert_paths = false;
  254         $used_path = false;
  255         foreach($path_scores as $path => $data) {
  256             if($data['diff'] === $min_diff) {
  257                 $used_path = $path;
  258                 $cert_paths = $data['cert_paths'];
  259                 if($data['has_main_domain'] == true) break;
  260             }
  261         }
  262 
  263         $app->log("Let's Encrypt Cert config path is: " . ($used_path ? $used_path : "not found") . ".", LOGLEVEL_DEBUG);
  264 
  265         return $cert_paths;
  266     }
  267 
  268     private function get_ssl_domain($data) {
  269         global $app;
  270 
  271         $domain = $data['new']['ssl_domain'];
  272         if(!$domain) {
  273             $domain = $data['new']['domain'];
  274         }
  275 
  276         if($data['new']['ssl'] == 'y' && $data['new']['ssl_letsencrypt'] == 'y') {
  277             $domain = $data['new']['domain'];
  278             if(substr($domain, 0, 2) === '*.') {
  279                 // wildcard domain not yet supported by letsencrypt!
  280                 $app->log('Wildcard domains not yet supported by letsencrypt, so changing ' . $domain . ' to ' . substr($domain, 2), LOGLEVEL_WARN);
  281                 $domain = substr($domain, 2);
  282             }
  283         }
  284 
  285         return $domain;
  286     }
  287 
  288     public function get_website_certificate_paths($data) {
  289         $ssl_dir = $data['new']['document_root'].'/ssl';
  290         $domain = $this->get_ssl_domain($data);
  291 
  292         $cert_paths = array(
  293             'domain' => $domain,
  294             'key' => $ssl_dir.'/'.$domain.'.key',
  295             'key2' => $ssl_dir.'/'.$domain.'.key.org',
  296             'csr' => $ssl_dir.'/'.$domain.'.csr',
  297             'crt' => $ssl_dir.'/'.$domain.'.crt',
  298             'bundle' => $ssl_dir.'/'.$domain.'.bundle'
  299         );
  300 
  301         if($data['new']['ssl'] == 'y' && $data['new']['ssl_letsencrypt'] == 'y') {
  302             $cert_paths = array(
  303                 'domain' => $domain,
  304                 'key' => $ssl_dir.'/'.$domain.'-le.key',
  305                 'key2' => $ssl_dir.'/'.$domain.'-le.key.org',
  306                 'crt' => $ssl_dir.'/'.$domain.'-le.crt',
  307                 'bundle' => $ssl_dir.'/'.$domain.'-le.bundle'
  308             );
  309         }
  310 
  311         return $cert_paths;
  312     }
  313 
  314     public function request_certificates($data, $server_type = 'apache') {
  315         global $app, $conf;
  316 
  317         $app->uses('getconf');
  318         $web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
  319         $server_config = $app->getconf->get_server_config($conf['server_id'], 'server');
  320 
  321         $use_acme = false;
  322         if($this->get_acme_script()) {
  323             $use_acme = true;
  324         } elseif(!$this->get_certbot_script()) {
  325             $app->log("Unable to find Let's Encrypt client, installing acme.sh.", LOGLEVEL_DEBUG);
  326             // acme and le missing
  327             $this->install_acme();
  328             if($this->get_acme_script()) {
  329                 $use_acme = true;
  330             } else {
  331                 $app->log("Unable to install acme.sh.  Cannot proceed, no Let's Encrypt client found.", LOGLEVEL_WARN);
  332                 return false;
  333             }
  334         }
  335 
  336         $tmp = $app->letsencrypt->get_website_certificate_paths($data);
  337         $domain = $tmp['domain'];
  338         $key_file = $tmp['key'];
  339         $crt_file = $tmp['crt'];
  340         $bundle_file = $tmp['bundle'];
  341 
  342         // default values
  343         $temp_domains = array($domain);
  344         $cli_domain_arg = '';
  345         $subdomains = null;
  346         $aliasdomains = null;
  347 
  348         //* be sure to have good domain
  349         if(substr($domain,0,4) != 'www.' && ($data['new']['subdomain'] == "www" || $data['new']['subdomain'] == "*")) {
  350             $temp_domains[] = "www." . $domain;
  351         }
  352 
  353         //* then, add subdomain if we have
  354         $subdomains = $app->db->queryAllRecords('SELECT domain FROM web_domain WHERE parent_domain_id = '.intval($data['new']['domain_id'])." AND active = 'y' AND type = 'subdomain' AND ssl_letsencrypt_exclude != 'y'");
  355         if(is_array($subdomains)) {
  356             foreach($subdomains as $subdomain) {
  357                 $temp_domains[] = $subdomain['domain'];
  358             }
  359         }
  360 
  361         //* then, add alias domain if we have
  362         $aliasdomains = $app->db->queryAllRecords('SELECT domain,subdomain FROM web_domain WHERE parent_domain_id = '.intval($data['new']['domain_id'])." AND active = 'y' AND type = 'alias' AND ssl_letsencrypt_exclude != 'y'");
  363         if(is_array($aliasdomains)) {
  364             foreach($aliasdomains as $aliasdomain) {
  365                 $temp_domains[] = $aliasdomain['domain'];
  366                 if(isset($aliasdomain['subdomain']) && substr($aliasdomain['domain'],0,4) != 'www.' && ($aliasdomain['subdomain'] == "www" OR $aliasdomain['subdomain'] == "*")) {
  367                     $temp_domains[] = "www." . $aliasdomain['domain'];
  368                 }
  369             }
  370         }
  371 
  372         // prevent duplicate
  373         $temp_domains = array_unique($temp_domains);
  374 
  375         // check if domains are reachable to avoid letsencrypt verification errors
  376         $le_rnd_file = uniqid('le-') . '.txt';
  377         $le_rnd_hash = md5(uniqid('le-', true));
  378         if(!is_dir('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/')) {
  379             $app->system->mkdir('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/', false, 0755, true);
  380         }
  381         file_put_contents('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/' . $le_rnd_file, $le_rnd_hash);
  382 
  383         $le_domains = array();
  384         foreach($temp_domains as $temp_domain) {
  385             if((isset($web_config['skip_le_check']) && $web_config['skip_le_check'] == 'y') || (isset($server_config['migration_mode']) && $server_config['migration_mode'] == 'y')) {
  386                 $le_domains[] = $temp_domain;
  387             } else {
  388                 $le_hash_check = trim(@file_get_contents('http://' . $temp_domain . '/.well-known/acme-challenge/' . $le_rnd_file));
  389                 if($le_hash_check == $le_rnd_hash) {
  390                     $le_domains[] = $temp_domain;
  391                     $app->log("Verified domain " . $temp_domain . " should be reachable for letsencrypt.", LOGLEVEL_DEBUG);
  392                 } else {
  393                     $app->log("Could not verify domain " . $temp_domain . ", so excluding it from letsencrypt request.", LOGLEVEL_WARN);
  394                 }
  395             }
  396         }
  397         $temp_domains = $le_domains;
  398         unset($le_domains);
  399         @unlink('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/' . $le_rnd_file);
  400 
  401         $le_domain_count = count($temp_domains);
  402         if($le_domain_count > 100) {
  403             $temp_domains = array_slice($temp_domains, 0, 100);
  404             $app->log("There were " . $le_domain_count . " domains in the domain list. LE only supports 100, so we strip the rest.", LOGLEVEL_WARN);
  405         }
  406 
  407         // unset useless data
  408         unset($subdomains);
  409         unset($aliasdomains);
  410 
  411         $this->certbot_use_certcommand = false;
  412         $letsencrypt_cmd = '';
  413         $allow_return_codes = null;
  414         $old_umask = umask(0022);  # work around acme.sh permission bug, see #6015
  415         if($use_acme) {
  416             $letsencrypt_cmd = $this->get_acme_command($temp_domains, $key_file, $bundle_file, $crt_file, $server_type);
  417             $allow_return_codes = array(2);
  418         } else {
  419             $letsencrypt_cmd = $this->get_certbot_command($temp_domains);
  420             umask($old_umask);
  421         }
  422 
  423         $success = false;
  424         if($letsencrypt_cmd) {
  425             if(!isset($server_config['migration_mode']) || $server_config['migration_mode'] != 'y') {
  426                 $app->log("Create Let's Encrypt SSL Cert for: $domain", LOGLEVEL_DEBUG);
  427                 $app->log("Let's Encrypt SSL Cert domains: $cli_domain_arg", LOGLEVEL_DEBUG);
  428 
  429                 $success = $app->system->_exec($letsencrypt_cmd, $allow_return_codes);
  430             } else {
  431                 $app->log("Migration mode active, skipping Let's Encrypt SSL Cert creation for: $domain", LOGLEVEL_DEBUG);
  432                 $success = true;
  433             }
  434         }
  435 
  436         if($use_acme === true) {
  437             umask($old_umask);
  438             if(!$success) {
  439                 $app->log('Let\'s Encrypt SSL Cert for: ' . $domain . ' could not be issued.', LOGLEVEL_WARN);
  440                 $app->log($letsencrypt_cmd, LOGLEVEL_WARN);
  441                 return false;
  442             } else {
  443                 return true;
  444             }
  445         }
  446 
  447         $le_files = array();
  448         if($this->certbot_use_certcommand === true && $letsencrypt_cmd) {
  449             $cli_domain_arg = '';
  450             // generate cli format
  451             foreach($temp_domains as $temp_domain) {
  452                 $cli_domain_arg .= (string) " --domains " . $temp_domain;
  453             }
  454 
  455 
  456             $letsencrypt_cmd = $this->get_certbot_script() . " certificates " . $cli_domain_arg;
  457             $output = explode("\n", shell_exec($letsencrypt_cmd . " 2>/dev/null | grep -v '^\$'"));
  458             $le_path = '';
  459             $skip_to_next = true;
  460             $matches = null;
  461             foreach($output as $outline) {
  462                 $outline = trim($outline);
  463                 $app->log("LE CERT OUTPUT: " . $outline, LOGLEVEL_DEBUG);
  464 
  465                 if($skip_to_next === true && !preg_match('/^\s*Certificate Name/', $outline)) {
  466                     continue;
  467                 }
  468                 $skip_to_next = false;
  469 
  470                 if(preg_match('/^\s*Expiry.*?VALID:\s+\D/', $outline)) {
  471                     $app->log("Found LE path is expired or invalid: " . $matches[1], LOGLEVEL_DEBUG);
  472                     $skip_to_next = true;
  473                     continue;
  474                 }
  475 
  476                 if(preg_match('/^\s*Certificate Path:\s*(\/.*?)\s*$/', $outline, $matches)) {
  477                     $app->log("Found LE path: " . $matches[1], LOGLEVEL_DEBUG);
  478                     $le_path = dirname($matches[1]);
  479                     if(is_dir($le_path)) {
  480                         break;
  481                     } else {
  482                         $le_path = false;
  483                     }
  484                 }
  485             }
  486 
  487             if($le_path) {
  488                 $le_files = array(
  489                     'privkey' => $le_path . '/privkey.pem',
  490                     'chain' => $le_path . '/chain.pem',
  491                     'cert' => $le_path . '/cert.pem',
  492                     'fullchain' => $le_path . '/fullchain.pem'
  493                 );
  494             }
  495         }
  496         if(empty($le_files)) {
  497             $le_files = $this->get_letsencrypt_certificate_paths($temp_domains);
  498         }
  499         unset($temp_domains);
  500 
  501         if($server_type != 'apache' || version_compare($app->system->getapacheversion(true), '2.4.8', '>=')) {
  502             $crt_tmp_file = $le_files['fullchain'];
  503         } else {
  504             $crt_tmp_file = $le_files['cert'];
  505         }
  506 
  507         $key_tmp_file = $le_files['privkey'];
  508         $bundle_tmp_file = $le_files['chain'];
  509 
  510         if(!$success) {
  511             // error issuing cert
  512             $app->log('Let\'s Encrypt SSL Cert for: ' . $domain . ' could not be issued.', LOGLEVEL_WARN);
  513             $app->log($letsencrypt_cmd, LOGLEVEL_WARN);
  514 
  515             // if cert already exists, dont remove it. Ex. expired/misstyped/noDnsYet alias domain, api down...
  516             if(!file_exists($crt_tmp_file)) {
  517                 return false;
  518             }
  519         }
  520 
  521         //* check is been correctly created
  522         if(file_exists($crt_tmp_file)) {
  523             $app->log("Let's Encrypt Cert file: $crt_tmp_file exists.", LOGLEVEL_DEBUG);
  524             $date = date("YmdHis");
  525 
  526             //* TODO: check if is a symlink, if target same keep it, either remove it
  527             if(is_file($key_file)) {
  528                 $app->system->copy($key_file, $key_file.'.old.'.$date);
  529                 $app->system->chmod($key_file.'.old.'.$date, 0400);
  530                 $app->system->unlink($key_file);
  531             }
  532 
  533             if(@is_link($key_file)) $app->system->unlink($key_file);
  534             if(@file_exists($key_tmp_file)) $app->system->exec_safe("ln -s ? ?", $key_tmp_file, $key_file);
  535 
  536             if(is_file($crt_file)) {
  537                 $app->system->copy($crt_file, $crt_file.'.old.'.$date);
  538                 $app->system->chmod($crt_file.'.old.'.$date, 0400);
  539                 $app->system->unlink($crt_file);
  540             }
  541 
  542             if(@is_link($crt_file)) $app->system->unlink($crt_file);
  543             if(@file_exists($crt_tmp_file))$app->system->exec_safe("ln -s ? ?", $crt_tmp_file, $crt_file);
  544 
  545             if(is_file($bundle_file)) {
  546                 $app->system->copy($bundle_file, $bundle_file.'.old.'.$date);
  547                 $app->system->chmod($bundle_file.'.old.'.$date, 0400);
  548                 $app->system->unlink($bundle_file);
  549             }
  550 
  551             if(@is_link($bundle_file)) $app->system->unlink($bundle_file);
  552             if(@file_exists($bundle_tmp_file)) $app->system->exec_safe("ln -s ? ?", $bundle_tmp_file, $bundle_file);
  553 
  554             return true;
  555         } else {
  556             $app->log("Let's Encrypt Cert file: $crt_tmp_file does not exist.", LOGLEVEL_DEBUG);
  557             return false;
  558         }
  559     }
  560 }