SelfSiteSsl.php 8.0 KB
<?php

namespace App\Console\Commands;

use App\Repositories\BtRepository;
use Illuminate\Console\Command;

class SelfSiteSsl extends Command
{
    protected $signature = 'self_site_ssl';
    protected $description = '自建站项目自动更新证书';

    protected $bt_repository;
    protected $bt;

    public function __construct()
    {
        parent::__construct();

        $this->bt_repository = new BtRepository();
        $this->bt = $this->bt_repository->getBtObject();
    }

    public function handle()
    {
        try {
            $this->checkDomainSsl();
        } catch (\Exception $e) {
            $this->output($e->getMessage());
        }
    }

    public function checkDomainSsl()
    {
        $end_day = date('Y-m-d H:i:s', time() + 3 * 24 * 3600);//3天后到期

        $site_domain = env('DOMAIN', '');
        $site_ip = env('SITE_IP', '');

        if (!$site_domain) {
            throw new \Exception('项目主站域名未配置');
        }

        if (!$site_ip) {
            throw new \Exception('项目主站IP未配置');
        }

        $ssl_time = $this->getDomainSslTime($site_domain);
        if ($ssl_time['to'] < $end_day) {
            //主站证书即将到期
            $site_list = $this->bt->WebSiteList($site_domain);
            if (isset($site_list['data']) && $site_list['data'] && $site_list['data'][0]['status'] == 1) {
                $site_id = $site_list['data'][0]['id'];
                $host = $site_list['data'][0]['name'];

                //获取站点可用于设置证书的域名
                $site_domain = $this->bt->WebDoaminList($site_id);
                $apply_ssl_domain_list = [];
                foreach ($site_domain as $val) {
                    if (strpos($val['name'], '*') === false && $this->check_domain_record($val['name'], ['ip' => $site_ip])) {
                        $apply_ssl_domain_list[] = $val['name'];
                    }
                }
                if (empty($apply_ssl_domain_list)) {
                    throw new \Exception('主站所有域名都未解析在当前服务器');
                }

                //申请证书之前,还原主站配置
                $config_before = file_get_contents(public_path('main_site_default.txt'));
                $re_config_before = $this->bt->SaveFileBody('/www/server/panel/vhost/nginx/' . $host . '.conf', $config_before, 'utf-8', 1);
                if (!($re_config_before['status'] ?? false)) {
                    throw new \Exception($re_config_before['msg'] ?? '还原主站nginx配置失败');
                }

                //设置站点证书
                $this->setDomainSsl($site_id, $host, $apply_ssl_domain_list);

                //申请证书之后,更新主站配置
                $config_after = file_get_contents(public_path('main_site_config.txt'));
                $re_config_after = $this->bt->SaveFileBody('/www/server/panel/vhost/nginx/' . $host . '.conf', $config_after, 'utf-8', 1);
                if (!($re_config_after['status'] ?? false)) {
                    throw new \Exception($re_config_after['msg'] ?? '更新主站nginx配置失败');
                }

                $this->output('主站证书更新成功');
            }
        }

        $amp_domain = env('AMP_DOMAIN', '');
        if ($amp_domain) {
            $amp_ssl_time = $this->getDomainSslTime($amp_domain);
            if ($amp_ssl_time['to'] < $end_day) {
                //AMP证书即将到期
                $amp_site_list = $this->bt->WebSiteList($amp_domain);
                if (isset($amp_site_list['data']) && $amp_site_list['data'] && $amp_site_list['data'][0]['status'] == 1) {
                    $amp_site_id = $amp_site_list['data'][0]['id'];
                    $amp_host = $amp_site_list['data'][0]['name'];

                    //设置站点证书
                    $this->setDomainSsl($amp_site_id, $amp_host, [$amp_host]);

                    $this->output('AMP站证书更新成功');
                }
            }
        }
    }

    /**
     * 检查域名解析师是否正确
     * @param $domain
     * @param $server_info
     * @return bool
     * @author Akun
     * @date 2025/01/13 14:53
     */
    public function check_domain_record($domain, $server_info)
    {
        try {
            $records = dns_get_record($domain, DNS_A);
            if (count($records) != 1) {
                return false;
            }

            $record = $records[0];
            if ($record['host'] == $server_info['domain'] || $record['ip'] == $server_info['ip']) {
                return $domain;
            } else {
                return false;
            }
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * 设置域名证书
     * @param $site_id
     * @param $host
     * @param $domain_list
     * @param string $key
     * @param string $cer
     * @throws \Exception
     * @author Akun
     * @date 2025/01/13 14:53
     */
    public function setDomainSsl($site_id, $host, $domain_list, $key = '', $cer = '')
    {
        if (empty($key) || empty($cer)) {
            $ssl = $this->bt->GetSSL($host);
            if (isset($ssl['cert_data']['notAfter']) && strtotime($ssl['cert_data']['notAfter']) - time() > 259200) {
                // 如果已经申请了ssl证书, 并且证书有效期超过3天, 那么就使用已经申请好的证书
                $key = $ssl['key'];
                $cer = $ssl['csr'];
                $is_set_status = !$ssl['status'];
            } else {
                $re_apply_cert = $this->bt->ApplyCert(json_encode($domain_list), $site_id);
                if (!($re_apply_cert['status'] ?? false)) {
                    $apply_error_msg = '申请免费证书失败';
                    if (isset($re_apply_cert['msg'])) {
                        if (is_array($re_apply_cert['msg'])) {
                            $apply_error_msg = json_encode($re_apply_cert['msg']);
                        } else {
                            $apply_error_msg = $re_apply_cert['msg'];
                        }
                    }
                    throw new \Exception($apply_error_msg);
                }

                $key = $re_apply_cert['private_key'];
                $cer = $re_apply_cert['cert'];
                $is_set_status = true;
            }
        } else {
            $is_set_status = true;
        }

        if ($key && $cer && $is_set_status) {
            $re_set_ssl = $this->bt->SetSSL(1, $host, $key, $cer);
            if (!($re_set_ssl['status'] ?? false)) {
                throw new \Exception($re_set_ssl['msg'] ?? '设置证书失败');
            }
        }
    }

    /**
     * 获取域名证书有效时间
     * @param $domain
     * @return string[]
     * @author Akun
     * @date 2024/08/29 9:59
     */
    public function getDomainSslTime($domain)
    {
        $valid_from = '';
        $valid_to = '';
        try {
            $context = stream_context_create([
                'ssl' => [
                    'capture_peer_cert' => true,
                    'capture_peer_cert_chain' => false,
                    'verify_peer' => false,
                    'verify_peer_name' => false
                ],
            ]);
            $stream = stream_socket_client('ssl://' . $domain . ':443', $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
            if ($stream) {
                $remote_cert = stream_context_get_params($stream)['options']['ssl']['peer_certificate'];
                if ($remote_cert) {
                    $valid_from = date('Y-m-d H:i:s', openssl_x509_parse($remote_cert)['validFrom_time_t']);
                    $valid_to = date('Y-m-d H:i:s', openssl_x509_parse($remote_cert)['validTo_time_t']);
                }
            }
            fclose($stream);
        } catch (\Exception $e) {
            $valid_from = '';
            $valid_to = '';
        }
        return ['from' => $valid_from, 'to' => $valid_to];
    }

    /**
     * 输出处理日志
     * @param $message
     */
    public function output($message)
    {
        echo date('Y-m-d H:i:s') . ' | ' . $message . PHP_EOL;
    }
}