Files
dnsmgr/app/lib/deploy/ssh.php
T
2026-02-02 19:27:31 +08:00

228 lines
8.7 KiB
PHP

<?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;
}
}