7 Commits
2.11.0 ... main

Author SHA1 Message Date
耗子
96ff262333 feat: ssh私钥支持设置密码 (#346) 2025-11-11 10:36:19 +08:00
net909
17ffe5704f 1panel支持子节点部署 2025-11-08 16:05:56 +08:00
mrdong916
d4c11b520f 新增Spaceship DNS (#335)
Co-authored-by: mrdong916 <mrdong916@gmail.com>
2025-11-08 16:03:15 +08:00
net909
ba418da84c fix: 证书私钥 EC 指示 2025-11-04 20:42:52 +08:00
TomyJan
b58db855ca fix: 证书私钥 EC 指示 2025-11-04 18:57:55 +08:00
dependabot[bot]
3bd45367b0 Bump symfony/yaml from 7.3.3 to 7.3.5 (#339)
Bumps [symfony/yaml](https://github.com/symfony/yaml) from 7.3.3 to 7.3.5.
- [Release notes](https://github.com/symfony/yaml/releases)
- [Changelog](https://github.com/symfony/yaml/blob/7.3/CHANGELOG.md)
- [Commits](https://github.com/symfony/yaml/compare/v7.3.3...v7.3.5)

---
updated-dependencies:
- dependency-name: symfony/yaml
  dependency-version: 7.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 20:40:10 +08:00
dependabot[bot]
a22bc4fa37 Bump symfony/var-dumper from 7.3.4 to 7.3.5 (#340)
Bumps [symfony/var-dumper](https://github.com/symfony/var-dumper) from 7.3.4 to 7.3.5.
- [Release notes](https://github.com/symfony/var-dumper/releases)
- [Changelog](https://github.com/symfony/var-dumper/blob/7.3/CHANGELOG.md)
- [Commits](https://github.com/symfony/var-dumper/compare/v7.3.4...v7.3.5)

---
updated-dependencies:
- dependency-name: symfony/var-dumper
  dependency-version: 7.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 20:39:46 +08:00
11 changed files with 507 additions and 22 deletions

View File

@@ -304,6 +304,10 @@ class Cert extends BaseController
}
}
if ($certInfo['keytype'] == 'ECC') {
$privatekey = CertHelper::ensureECPrivateKeyFormat($privatekey);
}
$order = [
'aid' => 0,
'keytype' => $certInfo['keytype'],
@@ -367,6 +371,10 @@ class Cert extends BaseController
if ($certInfo['code'] == -1) return json($certInfo);
$domains = $certInfo['domains'];
if ($certInfo['keytype'] == 'ECC') {
$privatekey = CertHelper::ensureECPrivateKeyFormat($privatekey);
}
$order = [
'aid' => 0,
'keytype' => $certInfo['keytype'],

View File

@@ -407,6 +407,24 @@ location / {
return false;
}
/**
* 确保ECC私钥使用EC专用格式标识
* 某些程序需要EC标识才能正确识别ECC私钥
*/
public static function ensureECPrivateKeyFormat($private_key)
{
if (strpos($private_key, '-----BEGIN EC PRIVATE KEY-----') !== false) {
return $private_key;
}
if (strpos($private_key, '-----BEGIN PRIVATE KEY-----') !== false) {
$private_key = preg_replace('/^-----BEGIN PRIVATE KEY-----$/m', '-----BEGIN EC PRIVATE KEY-----', $private_key);
$private_key = preg_replace('/^-----END PRIVATE KEY-----$/m', '-----END EC PRIVATE KEY-----', $private_key);
}
return $private_key;
}
public static function getPfx($fullchain, $privatekey, $pwd = '123456')
{
openssl_pkcs12_export($fullchain, $pfx, $privatekey, $pwd);

View File

@@ -559,7 +559,7 @@ class DeployHelper
'icon' => 'opanel.png',
'desc' => '更新面板证书管理内的SSL证书',
'note' => null,
'tasknote' => '系统会根据关联SSL证书的域名自动更新对应证书',
'tasknote' => '',
'inputs' => [
'url' => [
'name' => '面板地址',
@@ -594,7 +594,32 @@ class DeployHelper
'value' => '0'
],
],
'taskinputs' => [],
'taskinputs' => [
'type' => [
'name' => '部署类型',
'type' => 'radio',
'options' => [
'0' => '更新已有证书',
'3' => '面板本身的证书',
],
'value' => '0',
'required' => true,
],
'id' => [
'name' => '证书ID',
'type' => 'input',
'placeholder' => '在证书列表查看ID',
'note' => '留空为根据关联SSL证书的域名自动更新对应证书',
'show' => 'type==0',
],
'node_name' => [
'name' => '子节点名称',
'type' => 'input',
'placeholder' => '',
'note' => '不填写时,将替换主控节点证书;否则,将替换被控节点证书',
'show' => 'type==0',
],
],
],
'mwpanel' => [
'name' => 'MW面板',
@@ -2248,6 +2273,12 @@ ctrl+x 保存退出',
'required' => true,
'show' => 'auth==1',
],
'passphrase' => [
'name' => '私钥密码',
'type' => 'input',
'placeholder' => '若私钥有设置密码,请填写此项',
'show' => 'auth==1',
],
'windows' => [
'name' => '是否Windows',
'type' => 'radio',

View File

@@ -162,6 +162,20 @@ class DnsHelper
'page' => true,
'add' => true,
],
'spaceship' => [
'name' => 'Spaceship',
'config' => [
'ak' => 'AccessKey',
'sk' => 'SecretKey',
],
'remark' => 0,
'status' => false,
'redirect' => true,
'log' => false,
'weight' => false,
'page' => false,
'add' => true,
],
];
public static $line_name = [
@@ -176,6 +190,7 @@ class DnsHelper
'cloudflare' => ['DEF' => '0'],
'namesilo' => ['DEF' => 'default'],
'powerdns' => ['DEF' => 'default'],
'spaceship' => ['DEF' => 'default'],
];
public static function getList()

View File

@@ -4,6 +4,7 @@ namespace app\lib\acme;
use Exception;
use stdClass;
use app\lib\CertHelper;
/**
* ACMECert
@@ -368,10 +369,12 @@ class ACMECert extends ACMEv2
if (version_compare(PHP_VERSION, '7.1.0') < 0) throw new Exception('PHP >= 7.1.0 required for EC keys !');
$map = array('256' => 'prime256v1', '384' => 'secp384r1', '521' => 'secp521r1');
if (isset($map[$curve_name])) $curve_name = $map[$curve_name];
return $this->generateKey(array(
$pem = $this->generateKey(array(
'curve_name' => $curve_name,
'private_key_type' => OPENSSL_KEYTYPE_EC
));
return CertHelper::ensureECPrivateKeyFormat($pem);
}
public function parseCertificate($cert_pem)

View File

@@ -11,6 +11,7 @@ class opanel implements DeployInterface
private $url;
private $key;
private $proxy;
private $nodeName;
public function __construct($config)
{
@@ -27,6 +28,42 @@ class opanel implements DeployInterface
public function deploy($fullchain, $privatekey, $config, &$info)
{
if (isset($config['type']) && $config['type'] == '3') {
$params = [
'cert' => $fullchain,
'key' => $privatekey,
'ssl' => 'Enable',
'sslID' => null,
'sslType' => 'import-paste',
];
try {
$this->request('/core/settings/ssl/update', $params);
$this->log("面板证书更新成功!");
return;
} catch (Exception $e) {
throw new Exception("面板证书更新失败:" . $e->getMessage());
}
}
if (isset($config['node_name'])) $this->nodeName = $config['node_name'];
if (!empty($config['id'])) {
$params = [
'sslID' => intval($config['id']),
'type' => 'paste',
'certificate' => $fullchain,
'privateKey' => $privatekey,
'description' => '',
];
try {
$this->request('/websites/ssl/upload', $params);
$this->log("证书ID:{$config['id']}更新成功!");
return;
} catch (Exception $e) {
throw new Exception("证书ID:{$config['id']}更新失败:" . $e->getMessage());
}
}
$domains = $config['domainList'];
if (empty($domains)) throw new Exception('没有设置要部署的域名');
@@ -73,7 +110,15 @@ class opanel implements DeployInterface
}
}
if ($success == 0) {
throw new Exception($errmsg ? $errmsg : '没有要更新的证书');
$params = [
'sslID' => 0,
'type' => 'paste',
'certificate' => $fullchain,
'privateKey' => $privatekey,
'description' => '',
];
$this->request('/websites/ssl/upload', $params);
$this->log("证书上传成功!");
}
}
@@ -97,8 +142,11 @@ class opanel implements DeployInterface
$token = md5('1panel' . $this->key . $timestamp);
$headers = [
'1Panel-Token' => $token,
'1Panel-Timestamp' => $timestamp
'1Panel-Timestamp' => $timestamp,
];
if (!empty($this->nodeName)) {
$headers['CurrentNode'] = $this->nodeName;
}
$body = $params ? json_encode($params) : '{}';
if ($body) $headers['Content-Type'] = 'application/json';
$response = http_request($url, $body, null, null, $headers, $this->proxy);

View File

@@ -2,6 +2,7 @@
namespace app\lib\deploy;
use app\lib\CertHelper;
use app\lib\DeployInterface;
use Exception;
@@ -49,7 +50,8 @@ class ssh implements DeployInterface
fclose($stream);
$this->log('私钥已保存到:' . $config['pem_key_file']);
} elseif ($config['format'] == 'pfx') {
$pfx = \app\lib\CertHelper::getPfx($fullchain, $privatekey, $config['pfx_pass'] ? $config['pfx_pass'] : null);
$pfx_pass = $config['pfx_pass'] ?? null;
$pfx = CertHelper::getPfx($fullchain, $privatekey, $pfx_pass);
$stream = fopen("ssh2.sftp://$sftp{$config['pfx_file']}", 'w');
if (!$stream) {
@@ -157,7 +159,8 @@ class ssh implements DeployInterface
file_put_contents($privateKeyPath, $this->config['privatekey']);
file_put_contents($publicKeyPath, $publicKey);
umask($umask);
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath)) {
$passphrase = $this->config['passphrase'] ?? null; // 私钥密码
if (!ssh2_auth_pubkey_file($connection, $this->config['username'], $publicKeyPath, $privateKeyPath, $passphrase)) {
throw new Exception('私钥认证失败');
}
} else {

View File

@@ -78,6 +78,9 @@ class cloudflare implements DnsInterface
$name = $this->domain == $row['name'] ? '@' : str_replace('.'.$this->domain, '', $row['name']);
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $status == '0' ? substr($name, 0, -6) : $name;
if ($row['type'] == 'SRV' && isset($row['priority'])) {
$row['content'] = $row['priority'] . ' ' . $row['content'];
}
$list[] = [
'RecordId' => $row['id'],
'Domain' => $this->domain,
@@ -112,6 +115,9 @@ class cloudflare implements DnsInterface
$name = $this->domain == $data['result']['name'] ? '@' : str_replace('.' . $this->domain, '', $data['result']['name']);
$status = str_ends_with($name, '_pause') ? '0' : '1';
$name = $status == '0' ? substr($name, 0, -6) : $name;
if ($data['result']['type'] == 'SRV' && isset($data['result']['priority'])) {
$data['result']['content'] = $data['result']['priority'] . ' ' . $data['result']['content'];
}
return [
'RecordId' => $data['result']['id'],
'Domain' => $this->domain,
@@ -257,7 +263,7 @@ class cloudflare implements DnsInterface
{
$url = $this->baseUrl . $path;
if (preg_match('/^[0-9a-z]+$/i', $this->ApiKey)) {
if (preg_match('/^[0-9a-f]+$/i', $this->ApiKey)) {
$headers = [
'X-Auth-Email: ' . $this->Email,
'X-Auth-Key: ' . $this->ApiKey,

353
app/lib/dns/spaceship.php Normal file
View File

@@ -0,0 +1,353 @@
<?php
namespace app\lib\dns;
use app\lib\DnsInterface;
/**
* @see https://docs.spaceship.dev/
*/
class spaceship implements DnsInterface
{
private $apiKey;
private $apiSecret;
private $baseUrl = 'https://spaceship.dev/api/v1';
private $error;
private $domain;
private $proxy;
public function __construct($config)
{
$this->apiKey = $config['ak'];
$this->apiSecret = $config['sk'];
$this->domain = $config['domain'];
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function getError()
{
return $this->error;
}
public function check()
{
if ($this->getDomainList() != false) {
return true;
}
return false;
}
//获取域名列表
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 100)
{
$param = ['take' => $PageSize, 'skip' => ($PageNumber - 1) * $PageSize];
$data = $this->send_reuqest('GET', '/domains', $param);
if ($data) {
$list = [];
foreach ($data['items'] as $row) {
$list[] = [
'DomainId' => $row['name'],
'Domain' => $row['name'],
'RecordCount' => 0,
];
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
//获取解析记录列表
private function send_reuqest($method, $path, $params = null)
{
$url = $this->baseUrl . $path;
$headers = [
'X-API-Key: ' . $this->apiKey,
'X-API-Secret: ' . $this->apiSecret,
];
$body = '';
if ($method == 'GET') {
if ($params) {
$url .= '?' . http_build_query($params);
}
} else {
$body = json_encode($params);
$headers[] = 'Content-Type: application/json';
}
$ch = curl_init($url);
if ($this->proxy) {
curl_set_proxy($ch);
}
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PATCH') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
if ($errno) {
$this->setError('Curl error: ' . curl_error($ch));
}
curl_close($ch);
if ($errno) return false;
$arr = json_decode($response, true);
if (!isset($arr['detail'])) {
return $arr;
} else {
$this->setError($response['detail']);
return false;
}
}
//获取子域名解析记录列表
private function setError($message)
{
$this->error = $message;
//file_put_contents('logs.txt',date('H:i:s').' '.$message."\r\n", FILE_APPEND);
}
//获取解析记录详细信息
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
{
if ($SubDomain == '') $SubDomain = '@';
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
}
//添加解析记录
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
{
$param = ['take' => $PageSize, 'skip' => ($PageNumber - 1) * $PageSize];
if (!isNullOrEmpty(($SubDomain))) {
$param['host'] = $SubDomain;
}
$data = $this->send_reuqest('GET', '/dns/records/' . $this->domain, $param);
if ($data) {
$list = [];
foreach ($data['items'] as $row) {
$type = $row['type'];
$name = $row['name'];
if ('MX' == $type) {
$address = $row['exchange'];
$mx = $row['preference'];
} else if ('CNAME' == $type) {
$address = $row['cname'];
$mx = 0;
} else if ('TXT' == $type) {
$address = $row['value'];
$mx = 0;
} else if ('PTR' == $type) {
$address = $row['pointer'];
$mx = 0;
} else if ('NS' == $type) {
$address = $row['nameserver'];
$mx = 0;
} else if ('HTTPS' == $type) {
$address = $row['targetName'] . $row['svcParams'] . '|' . $row['svcPriority'];
$mx = 0;
} else if ('CAA' == $type) {
$address = $row['value'];
$mx = 0;
} else if ('TLSA' == $type) {
$address = $row['associationData'];
$mx = 0;
} else if ('SVRB' == $type) {
$address = $row['targetName'] . $row['svcParams'] . '|' . $row['svcPriority'];
$mx = 0;
} else if ('ALIAS' == $type) {
$address = $row['aliasName'];
$mx = 0;
} else {
$address = $row['address'];
$mx = 0;
}
$list[] = [
'RecordId' => $row['type'] . '|' . $name . '|' . $address . '|' . $mx,
'Domain' => $this->domain,
'Name' => $row['name'],
'Type' => $row['type'],
'Value' => $address,
'TTL' => $row['ttl'],
'Line' => 'default',
'MX' => $mx,
'Status' => '1',
'Weight' => null,
'Remark' => null,
'UpdateTime' => null,
];
}
return ['total' => $data['total'], 'list' => $list];
}
return false;
}
//修改解析记录
public function getDomainRecordInfo($RecordId)
{
return false;
}
//修改解析记录备注
public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$param = [
'force' => true,
'items' => [
[
'type' => $this->convertType($Type),
'name' => $Name,
'address' => $Value,
'ttl' => $TTL,
]
]
];
$data = $this->send_reuqest('PUT', '/dns/records/' . $this->domain, $param);
return !isset($data);
}
//删除解析记录
private function convertType($type)
{
return $type;
}
//设置解析记录状态
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
{
$param = [
'force' => true,
'items' => [
[
'type' => $this->convertType($Type),
'name' => $Name,
'address' => $Value,
'ttl' => $TTL,
]
]
];
$data = $this->send_reuqest('PUT', '/dns/records/' . $this->domain, $param);
return !isset($data);
}
//获取解析记录操作日志
public function updateDomainRecordRemark($RecordId, $Remark)
{
return false;
}
//获取解析线路列表
public function deleteDomainRecord($RecordId)
{
$array = explode("|", $RecordId);
$type = $array[0];
$name = $array[1];
$address = $array[2];
$mx = $array[3];
if ('MX' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'exchange' => $address,
'preference' => (int)$mx,
]
];
} else if ('TXT' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'value' => $address,
]
];
} else if ('CNAME' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'cname' => $address,
]
];
} else if ('ALIAS' == $type) {
$param = [
[
'type' => $type,
'name' => $name,
'aliasName' => $address,
]
];
} else {
$param = [
[
'type' => $type,
'name' => $name,
'address' => $address,
]
];
}
$data = $this->send_reuqest('DELETE', '/dns/records/' . $this->domain, $param);
return !isset($data);
}
//获取域名信息
public function setDomainRecordStatus($RecordId, $Status)
{
return false;
}
//获取域名最低TTL
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
{
return false;
}
public function getRecordLine()
{
return ['default' => ['name' => '默认', 'parent' => null]];
}
public function getDomainInfo()
{
return false;
}
public function getMinTTL()
{
return false;
}
public function addDomain($Domain)
{
return false;
}
}

28
composer.lock generated
View File

@@ -1452,16 +1452,16 @@
},
{
"name": "symfony/yaml",
"version": "v7.3.3",
"version": "v7.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d"
"reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d",
"reference": "d4f4a66866fe2451f61296924767280ab5732d9d",
"url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc",
"reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc",
"shasum": ""
},
"require": {
@@ -1504,7 +1504,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.3.3"
"source": "https://github.com/symfony/yaml/tree/v7.3.5"
},
"funding": [
{
@@ -1524,7 +1524,7 @@
"type": "tidelift"
}
],
"time": "2025-08-27T11:34:33+00:00"
"time": "2025-09-27T09:00:46+00:00"
},
{
"name": "topthink/framework",
@@ -1907,16 +1907,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v7.3.4",
"version": "v7.3.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb"
"reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
"reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d",
"shasum": ""
},
"require": {
@@ -1970,7 +1970,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.4"
"source": "https://github.com/symfony/var-dumper/tree/v7.3.5"
},
"funding": [
{
@@ -1990,7 +1990,7 @@
"type": "tidelift"
}
],
"time": "2025-09-11T10:12:26+00:00"
"time": "2025-09-27T09:00:46+00:00"
},
{
"name": "topthink/think-trace",
@@ -2046,7 +2046,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@@ -2060,6 +2060,6 @@
"ext-sockets": "*",
"ext-ssh2": "*"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB