<?php

namespace app\lib\deploy;

use app\lib\CertHelper;
use app\lib\DeployInterface;
use Exception;

class ssh implements DeployInterface
{
    private $logger;
    private $config;

    public function __construct($config)
    {
        $this->config = $config;
    }

    public function check()
    {
        $this->connect();
    }

    public function deploy($fullchain, $privatekey, $config, &$info)
    {
        $connection = $this->connect();
        if (isset($config['cmd_pre']) && !empty($config['cmd_pre'])) {
            $cmds = explode("\n", $config['cmd_pre']);
            foreach ($cmds as $cmd) {
                $cmd = trim($cmd);
                if (empty($cmd)) continue;
                $this->exec($connection, $cmd);
            }
        }
        $sftp = ssh2_sftp($connection);
        if ($config['format'] == 'pem') {
            $stream = fopen("ssh2.sftp://$sftp{$config['pem_cert_file']}", 'w');
            if (!$stream) {
                throw new Exception("无法创建证书文件：{$config['pem_cert_file']}");
            }
            fwrite($stream, $fullchain);
            fclose($stream);
            $this->log('证书已保存到：' . $config['pem_cert_file']);

            $stream = fopen("ssh2.sftp://$sftp{$config['pem_key_file']}", 'w');
            if (!$stream) {
                throw new Exception("无法创建私钥文件：{$config['pem_key_file']}");
            }
            fwrite($stream, $privatekey);
            fclose($stream);
            $this->log('私钥已保存到：' . $config['pem_key_file']);
        } elseif ($config['format'] == 'pfx') {
            $pfx_pass = $config['pfx_pass'] ?? null;
            $pfx = CertHelper::getPfx($fullchain, $privatekey, $pfx_pass);

            $stream = fopen("ssh2.sftp://$sftp{$config['pfx_file']}", 'w');
            if (!$stream) {
                throw new Exception("无法创建PFX证书文件：{$config['pfx_file']}");
            }
            fwrite($stream, $pfx);
            fclose($stream);
            $this->log('PFX证书已保存到：' . $config['pfx_file']);

            if ($config['uptype'] == '1' && !empty($config['iis_domain'])) {
                $cert_hash = openssl_x509_fingerprint($fullchain, 'sha1');
                $this->deploy_iis($connection, $config['iis_domain'], $config['pfx_file'], $config['pfx_pass'], $cert_hash);
                $config['cmd'] = null;
            }
        }
        if (!empty($config['cmd'])) {
            $cmds = explode("\n", $config['cmd']);
            foreach ($cmds as $cmd) {
                $cmd = trim($cmd);
                if (empty($cmd)) continue;
                $this->exec($connection, $cmd);
            }
        }
    }

    private function deploy_iis($connection, $domain, $pfx_file, $pfx_pass, $cert_hash)
    {
        if (!strpos($domain, ':')) {
            $domain .= ':443';
        }
        $ret = $this->exec($connection, 'netsh http show sslcert hostnameport=' . $domain);
        if (preg_match('/:\s+(\w{40})/', $ret, $match)) {
            if ($match[1] == $cert_hash) {
                $this->log('IIS域名 ' . $domain . ' 证书已存在，无需更新');
                return;
            }
        }
        $p = '-p ""';
        if (!empty($pfx_pass)) $p = '-p ' . $pfx_pass;
        if (substr($pfx_file, 0, 1) == '/') $pfx_file = substr($pfx_file, 1);
        $this->exec($connection, 'certutil ' . $p . ' -importPFX ' . $pfx_file);
        $this->exec($connection, 'netsh http delete sslcert hostnameport=' . $domain);
        $this->exec($connection, 'netsh http add sslcert hostnameport=' . $domain . ' certhash=' . $cert_hash . ' certstorename=MY appid=\'{' . $this->uuid() . '}\'');
        $this->log('IIS域名 ' . $domain . ' 证书已更新');
    }

    private function uuid()
    {
        $guid = md5(uniqid(mt_rand(), true));
        return substr($guid, 0, 8) . '-' . substr($guid, 8, 4) . '-4' . substr($guid, 12, 3) . '-' . substr($guid, 16, 4) . '-' . substr($guid, 20, 12);
    }

    private function exec($connection, $cmd)
    {
        $this->log('执行命令：' . $cmd);
        $stream = ssh2_exec($connection, $cmd);
        $errorStream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR);
        if (!$stream || !$errorStream) {
            throw new Exception('执行命令失败');
        }
        stream_set_blocking($stream, true);
        stream_set_blocking($errorStream, true);
        $output = stream_get_contents($stream);
        $errorOutput = stream_get_contents($errorStream);
        fclose($stream);
        fclose($errorStream);
        if (trim($errorOutput)) {
            if ($this->config['windows'] == '1' && $this->containsGBKChinese($errorOutput)) {
                $errorOutput = mb_convert_encoding($errorOutput, 'UTF-8', 'GBK');
            }
            throw new Exception('执行命令失败：' . trim($errorOutput));
        } else {
            if ($this->config['windows'] == '1' && $this->containsGBKChinese($output)) {
                $output = mb_convert_encoding($output, 'UTF-8', 'GBK');
            }
            $this->log('执行命令成功：' . trim($output));
            return $output;
        }
    }

    private function connect()
    {
        if (!function_exists('ssh2_connect')) {
            throw new Exception('ssh2扩展未安装');
        }
        if (empty($this->config['host']) || empty($this->config['port']) || empty($this->config['username']) || $this->config['auth'] == '0' && empty($this->config['password']) || $this->config['auth'] == '1' && empty($this->config['privatekey'])) {
            throw new Exception('必填参数不能为空');
        }
        if (!filter_var($this->config['host'], FILTER_VALIDATE_IP) && !filter_var($this->config['host'], FILTER_VALIDATE_DOMAIN)) {
            throw new Exception('主机地址不合法');
        }
        if (!is_numeric($this->config['port']) || $this->config['port'] < 1 || $this->config['port'] > 65535) {
            throw new Exception('端口不合法');
        }

        $connection = ssh2_connect($this->config['host'], intval($this->config['port']));
        if (!$connection) {
            throw new Exception('SSH连接失败');
        }
        if ($this->config['auth'] == '1') {
            $publicKey = $this->getPublicKey($this->config['privatekey']);
            $publicKeyPath = app()->getRuntimePath() . $this->config['host'] . '.pub';
            $privateKeyPath = app()->getRuntimePath() . $this->config['host'] . '.key';
            $umask = umask(0066);
            file_put_contents($privateKeyPath, $this->config['privatekey']);
            file_put_contents($publicKeyPath, $publicKey);
            umask($umask);
            
            try {
                if (!empty($this->config['passphrase'])) {
                    if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $this->config['passphrase'])) {
                        throw new Exception('私钥认证失败');
                    }
                } else {
                    if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) {
                        throw new Exception('私钥认证失败');
                    }
                }
            } finally {
                unlink($publicKeyPath);
                unlink($privateKeyPath);
            }
        } else {
            if (!ssh2_auth_password($connection, $this->config['username'], $this->config['password'])) {
                throw new Exception('用户名或密码错误');
            }
        }
        return $connection;
    }

    private function getPublicKey($privateKey)
    {
        $res = openssl_pkey_get_private($privateKey);
        if (!$res) {
            throw new Exception('加载私钥失败');
        }
        $details = openssl_pkey_get_details($res);
        if (!$details || !isset($details['key'])) {
            throw new Exception('从私钥导出公钥失败');
        }
        $buffer = pack("N", 7) . "ssh-rsa" .
            $this->sshEncodeBuffer($details['rsa']['e']) .
            $this->sshEncodeBuffer($details['rsa']['n']);
        return "ssh-rsa " . base64_encode($buffer);
    }

    private function sshEncodeBuffer($buffer)
    {
        $len = strlen($buffer);
        if (ord($buffer[0]) & 0x80) {
            $len++;
            $buffer = "\x00" . $buffer;
        }
        return pack("Na*", $len, $buffer);
    }

    private function containsGBKChinese($string)
    {
        return preg_match('/[\x81-\xFE][\x40-\xFE]/', $string) === 1;
    }

    private function log($txt)
    {
        if ($this->logger) {
            call_user_func($this->logger, $txt);
        }
    }

    public function setLogger($logger)
    {
        $this->logger = $logger;
    }
}
