diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index e8300b3..325a74b 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -370,9 +370,8 @@ class DeployHelper 'id' => [ 'name' => '证书ID', 'type' => 'input', - 'placeholder' => '', + 'placeholder' => '留空则为添加证书', 'note' => '在网站管理->证书管理查看证书的ID,注意域名是否与证书匹配', - 'required' => true, ], ], ], @@ -435,9 +434,8 @@ class DeployHelper 'id' => [ 'name' => '证书ID', 'type' => 'input', - 'placeholder' => '', + 'placeholder' => '留空则为添加证书', 'note' => '在站点->证书管理查看证书的ID,注意域名是否与证书匹配', - 'required' => true, ], ], ], @@ -1060,6 +1058,7 @@ ctrl+x 保存退出', ['value'=>'vod', 'label'=>'视频点播'], ['value'=>'fc', 'label'=>'函数计算3.0'], ['value'=>'fc2', 'label'=>'函数计算2.0'], + ['value'=>'upload', 'label'=>'上传到证书管理'], ], 'value' => 'cdn', 'required' => true, @@ -1171,7 +1170,7 @@ ctrl+x 保存退出', 'name' => '绑定的域名', 'type' => 'input', 'placeholder' => '', - 'show' => 'product!=\'esa\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'', + 'show' => 'product!=\'esa\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'upload\'', 'required' => true, ], ], @@ -1224,6 +1223,7 @@ ctrl+x 保存退出', ['value'=>'tse', 'label'=>'云原生API网关TSE'], ['value'=>'tcb', 'label'=>'云开发TCB'], ['value'=>'lighthouse', 'label'=>'轻量应用服务器'], + ['value'=>'upload', 'label'=>'上传到证书管理'], ['value'=>'update', 'label'=>'更新证书内容(证书ID不变)'], ], 'value' => 'cdn', @@ -1324,7 +1324,7 @@ ctrl+x 保存退出', 'name' => '绑定的域名', 'type' => 'input', 'placeholder' => '', - 'show' => 'product!=\'clb\'&&product!=\'tke\'', + 'show' => 'product!=\'clb\'&&product!=\'tke\'&&product!=\'upload\'', 'note' => 'CDN、EO、WAF多个域名可用,隔开,其他只能填写1个域名', 'required' => true, ], @@ -1375,15 +1375,31 @@ ctrl+x 保存退出', ['value'=>'cdn', 'label'=>'内容分发网络CDN'], ['value'=>'elb', 'label'=>'弹性负载均衡ELB'], ['value'=>'waf', 'label'=>'Web应用防火墙WAF'], + ['value'=>'obs', 'label'=>'对象存储服务OBS'], + ['value'=>'upload', 'label'=>'上传到证书管理'], ], 'value' => 'cdn', 'required' => true, ], + 'obs_endpoint' => [ + 'name' => 'Endpoint地址', + 'type' => 'input', + 'placeholder' => '填写示例:obs.cn-north-4.myhuaweicloud.com', + 'show' => 'product==\'obs\'', + 'required' => true, + ], + 'obs_bucket' => [ + 'name' => '桶名称', + 'type' => 'input', + 'placeholder' => '', + 'show' => 'product==\'obs\'', + 'required' => true, + ], 'domain' => [ 'name' => '绑定的域名', 'type' => 'input', 'placeholder' => '多个域名可使用,分隔', - 'show' => 'product==\'cdn\'', + 'show' => 'product==\'cdn\'||product==\'obs\'', 'required' => true, ], 'project_id' => [ @@ -1478,6 +1494,7 @@ ctrl+x 保存退出', ['value'=>'cdn', 'label'=>'CDN'], ['value'=>'oss', 'label'=>'OSS'], ['value'=>'pili', 'label'=>'视频直播'], + ['value'=>'upload', 'label'=>'上传到证书管理'], ], 'value' => 'cdn', 'required' => true, @@ -1493,6 +1510,7 @@ ctrl+x 保存退出', 'name' => '绑定的域名', 'type' => 'input', 'placeholder' => '多个域名可使用,分隔', + 'show' => 'product!=\'upload\'', 'required' => true, ], ], @@ -1603,6 +1621,7 @@ ctrl+x 保存退出', ['value'=>'cdn', 'label'=>'CDN'], ['value'=>'blb', 'label'=>'普通型BLB'], ['value'=>'appblb', 'label'=>'应用型BLB'], + ['value'=>'upload', 'label'=>'上传到证书管理'], ], 'value' => 'cdn', 'required' => true, @@ -1737,6 +1756,7 @@ ctrl+x 保存退出', ['value'=>'tos', 'label'=>'对象存储TOS'], ['value'=>'live', 'label'=>'视频直播'], ['value'=>'imagex', 'label'=>'veImageX'], + ['value'=>'upload', 'label'=>'上传到证书管理'], ], 'value' => 'cdn', 'required' => true, @@ -1752,7 +1772,7 @@ ctrl+x 保存退出', 'name' => '绑定的域名', 'type' => 'input', 'placeholder' => '多个域名可使用,分隔', - 'show' => 'product!=\'clb\'&&product!=\'alb\'', + 'show' => 'product!=\'clb\'&&product!=\'alb\'&&product!=\'upload\'', 'required' => true, ], 'listener_id' => [ @@ -1948,10 +1968,22 @@ ctrl+x 保存退出', ['value'=>'cdn', 'label'=>'CDN加速'], ['value'=>'icdn', 'label'=>'全站加速'], ['value'=>'accessone', 'label'=>'边缘安全加速平台'], + ['value'=>'cf', 'label'=>'函数计算'], ], 'value' => 'cdn', 'required' => true, ], + 'region_id' => [ + 'name' => '所属地域', + 'type' => 'select', + 'options' => [ + ['value'=>'bb9fdb42056f11eda1610242ac110002', 'label'=>'华东1'], + ['value'=>'200000002368', 'label'=>'西南1'], + ], + 'value' => 'bb9fdb42056f11eda1610242ac110002', + 'show' => 'product==\'cf\'', + 'required' => true, + ], 'domain' => [ 'name' => '绑定的域名', 'type' => 'input', @@ -2034,9 +2066,8 @@ ctrl+x 保存退出', 'id' => [ 'name' => '证书ID', 'type' => 'input', - 'placeholder' => '', + 'placeholder' => '留空则为添加证书', 'note' => '在SSL证书->我的证书页面查看,注意域名是否与证书匹配', - 'required' => true, ], ], ], diff --git a/app/lib/client/Ctyun.php b/app/lib/client/Ctyun.php index 59b30f6..1be035e 100644 --- a/app/lib/client/Ctyun.php +++ b/app/lib/client/Ctyun.php @@ -30,7 +30,7 @@ class Ctyun * @return array * @throws Exception */ - public function request($method, $path, $query = null, $params = null) + public function request($method, $path, $query = null, $params = null, $header = null) { if (!empty($query)) { $query = array_filter($query, function ($a) { return $a !== null;}); @@ -50,6 +50,11 @@ class Ctyun if ($body) { $headers['Content-Type'] = 'application/json'; } + if (!empty($header)) { + foreach ($header as $key => $value) { + $headers[$key] = $value; + } + } $authorization = $this->generateSign($query, $headers, $body, $date); $headers['Eop-Authorization'] = $authorization; @@ -151,7 +156,7 @@ class Ctyun curl_close($ch); $arr = json_decode($response, true); - if (isset($arr['statusCode']) && $arr['statusCode'] == 100000) { + if (isset($arr['statusCode']) && ($arr['statusCode'] == 100000 || $arr['statusCode'] == 0 && $this->endpoint == 'cf-global.ctapi.ctyun.cn')) { return isset($arr['returnObj']) ? $arr['returnObj'] : true; } elseif (isset($arr['errorMessage'])) { throw new Exception($arr['errorMessage']); diff --git a/app/lib/client/HuaweiOBS.php b/app/lib/client/HuaweiOBS.php new file mode 100644 index 0000000..dbd7def --- /dev/null +++ b/app/lib/client/HuaweiOBS.php @@ -0,0 +1,232 @@ +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->Endpoint = $Endpoint; + $this->proxy = $proxy; + } + + public function setBucketCustomdomain($bucket, $domain, $cert_name, $fullchain, $privatekey) + { + $strXml = << + + EOF; + $xml = new \SimpleXMLElement($strXml); + $xml->addChild('Name', $cert_name); + $xml->addChild('Certificate', $fullchain); + $xml->addChild('PrivateKey', $privatekey); + $body = $xml->asXML(); + + $options = [ + 'bucket' => $bucket, + 'key' => '', + ]; + $query = [ + 'customdomain' => $domain + ]; + return $this->request('PUT', '/', $query, $body, $options); + } + + public function deleteBucketCustomdomain($bucket, $domain) + { + $options = [ + 'bucket' => $bucket, + 'key' => '', + ]; + $query = [ + 'customdomain' => $domain + ]; + return $this->request('DELETE', '/', $query, '', $options); + } + + public function getBucketCustomdomain($bucket) + { + $options = [ + 'bucket' => $bucket, + 'key' => '', + ]; + $query = [ + 'customdomain' => '', + ]; + return $this->request('GET', '/', $query, '', $options); + } + + private function request($method, $path, $query, $body, $options) + { + $hostname = $options['bucket'] . '.' . $this->Endpoint; + $query_string = $this->toQueryString($query); + $query_string = empty($query_string) ? '' : '?' . $query_string; + $requestUrl = 'https://' . $hostname . $path . $query_string; + $headers = [ + 'Content-Type' => 'application/xml', + 'Content-MD5' => base64_encode(md5($body, true)), + 'Date' => gmdate('D, d M Y H:i:s \G\M\T'), + ]; + $headers['Authorization'] = $this->getAuthorization($method, $path, $query, $headers, $options); + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $requestUrl, $body, $header); + } + + private function curl($method, $url, $body, $header) + { + $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, $header); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + if (!empty($body)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $errno = curl_errno($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + curl_close($ch); + + if ($httpCode >= 200 && $httpCode < 300) { + if (empty($response)) return true; + return $this->xml2array($response); + } + $arr = $this->xml2array($response); + if (isset($arr['Message'])) { + throw new Exception($arr['Message']); + } else { + throw new Exception('HTTP Code: ' . $httpCode); + } + } + + private function toQueryString($params = array()) + { + $temp = array(); + uksort($params, 'strnatcasecmp'); + foreach ($params as $key => $value) { + if (is_string($key) && !is_array($value)) { + if (strlen($value) > 0) { + $temp[] = rawurlencode($key) . '=' . rawurlencode($value); + } else { + $temp[] = rawurlencode($key); + } + } + } + return implode('&', $temp); + } + + private function xml2array($xml) + { + if (!$xml) { + return false; + } + LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true); + return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true); + } + + private function getAuthorization($method, $url, $query, $headers, $options) + { + $method = strtoupper($method); + $date = $headers['Date']; + $resourcePath = $this->getResourcePath($options); + $stringToSign = $this->calcStringToSign($method, $date, $headers, $resourcePath, $query); + $signature = base64_encode(hash_hmac('sha1', $stringToSign, $this->SecretAccessKey, true)); + return 'OBS ' . $this->AccessKeyId . ':' . $signature; + } + + private function getResourcePath(array $options) + { + $resourcePath = '/'; + if (strlen($options['bucket']) > 0) { + $resourcePath .= $options['bucket'] . '/'; + } + if (strlen($options['key']) > 0) { + $resourcePath .= $options['key']; + } + return $resourcePath; + } + + private function calcStringToSign($method, $date, array $headers, $resourcePath, array $query) + { + /* + SignToString = + VERB + "\n" + + Content-MD5 + "\n" + + Content-Type + "\n" + + Date + "\n" + + CanonicalizedOSSHeaders + + CanonicalizedResource + Signature = base64(hmac-sha1(AccessKeySecret, SignToString)) + */ + $contentMd5 = ''; + $contentType = ''; + // CanonicalizedOSSHeaders + $signheaders = array(); + foreach ($headers as $key => $value) { + $lowk = strtolower($key); + if (strncmp($lowk, "x-obs-", 6) == 0) { + $signheaders[$lowk] = $value; + } else if ($lowk === 'content-md5') { + $contentMd5 = $value; + } else if ($lowk === 'content-type') { + $contentType = $value; + } + } + ksort($signheaders); + $canonicalizedOSSHeaders = ''; + foreach ($signheaders as $key => $value) { + $canonicalizedOSSHeaders .= $key . ':' . $value . "\n"; + } + // CanonicalizedResource + $signquery = array(); + foreach ($query as $key => $value) { + if (in_array($key, $this->signKeyList)) { + $signquery[$key] = $value; + } + } + ksort($signquery); + $sortedQueryList = array(); + foreach ($signquery as $key => $value) { + if (strlen($value) > 0) { + $sortedQueryList[] = $key . '=' . $value; + } else { + $sortedQueryList[] = $key; + } + } + $queryStringSorted = implode('&', $sortedQueryList); + $canonicalizedResource = $resourcePath; + if (!empty($queryStringSorted)) { + $canonicalizedResource .= '?' . $queryStringSorted; + } + return $method . "\n" . $contentMd5 . "\n" . $contentType . "\n" . $date . "\n" . $canonicalizedOSSHeaders . $canonicalizedResource; + } + + private $signKeyList = array( + 'acl', 'policy', 'torrent', 'logging', 'location', 'storageinfo', 'quota', 'storagepolicy', 'requestpayment', 'versions', 'versioning', 'versionid', 'uploads', 'uploadid', 'partnumber', 'website', 'notification', 'lifecycle', 'deletebucket', 'delete', 'cors', 'restore', 'tagging', 'response-content-type', 'response-content-language', 'response-expires', 'response-cache-control', 'response-content-disposition', 'response-content-encoding', 'x-image-process', 'backtosource', 'storageclass', 'replication', 'append', 'position', 'x-oss-process', 'CDNNotifyConfiguration', 'attname', 'customdomain', 'directcoldaccess', 'encryption', 'inventory', 'length', 'metadata', 'modify', 'name', 'rename', 'truncate', 'x-image-save-bucket', 'x-image-save-object', 'x-obs-security-token', 'x-obs-callback' + ); +} \ No newline at end of file diff --git a/app/lib/deploy/aliyun.php b/app/lib/deploy/aliyun.php index f76eaf0..a23d6d0 100644 --- a/app/lib/deploy/aliyun.php +++ b/app/lib/deploy/aliyun.php @@ -66,6 +66,7 @@ class aliyun implements DeployInterface $this->deploy_alb($cert_id, $config); } elseif ($config['product'] == 'nlb') { $this->deploy_nlb($cert_id, $config); + } elseif ($config['product'] == 'upload') { } else { throw new Exception('未知的产品类型'); } diff --git a/app/lib/deploy/baidu.php b/app/lib/deploy/baidu.php index de66efd..927e0be 100644 --- a/app/lib/deploy/baidu.php +++ b/app/lib/deploy/baidu.php @@ -39,6 +39,7 @@ class baidu implements DeployInterface $this->deploy_blb($cert_id, $config); } elseif ($config['product'] == 'appblb') { $this->deploy_appblb($cert_id, $config); + } elseif ($config['product'] == 'upload') { } else { throw new Exception('不支持的产品类型'); } diff --git a/app/lib/deploy/cdnfly.php b/app/lib/deploy/cdnfly.php index 0904607..913fca7 100644 --- a/app/lib/deploy/cdnfly.php +++ b/app/lib/deploy/cdnfly.php @@ -43,7 +43,39 @@ class cdnfly implements DeployInterface public function deploy($fullchain, $privatekey, $config, &$info) { $id = $config['id']; - if (empty($id)) throw new Exception('证书ID不能为空'); + if (empty($id)) { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + $params = [ + 'type' => 'custom', + 'name' => $cert_name, + 'cert' => $fullchain, + 'key' => $privatekey, + ]; + if ($this->auth == 1) { + $access_token = $this->login(); + $url = $this->url . '/v1/certs'; + $body = json_encode($params); + $headers = [ + 'Access-Token' => $access_token, + ]; + $response = http_request($url, $body, null, null, $headers, $this->proxy, 'POST'); + $result = json_decode($response['body'], true); + if (isset($result['code']) && $result['code'] == 0) { + $id = $result['data']; + } elseif (isset($result['msg'])) { + throw new Exception('证书添加失败,' . $result['msg']); + } else { + throw new Exception('证书添加失败,返回数据解析失败'); + } + } else { + $id = $this->request('/v1/certs', $params, 'POST'); + } + $this->log("证书ID:{$id}添加成功!"); + $info['config']['id'] = $id; + return; + } $params = [ 'type' => 'custom', diff --git a/app/lib/deploy/ctyun.php b/app/lib/deploy/ctyun.php index 01552fa..6302888 100644 --- a/app/lib/deploy/ctyun.php +++ b/app/lib/deploy/ctyun.php @@ -39,6 +39,8 @@ class ctyun implements DeployInterface $this->deploy_icdn($fullchain, $privatekey, $config); } elseif ($config['product'] == 'accessone') { $this->deploy_accessone($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'cf') { + $this->deploy_cf($fullchain, $privatekey, $config); } } @@ -170,6 +172,44 @@ class ctyun implements DeployInterface $this->log('边缘安全加速域名 ' . $config['domain'] . ' 部署证书成功!'); } + private function deploy_cf($fullchain, $privatekey, $config) + { + $client = new CtyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cf-global.ctapi.ctyun.cn', $this->proxy); + try { + $data = $client->request('GET', '/openapi/v1/domains/customdomains/' . $config['domain'], null, null, ['regionId' => $config['region_id']]); + } catch (Exception $e) { + throw new Exception('获取自定义域名配置失败:' . $e->getMessage()); + } + + if (isset($data['certConfig']['certificate']) && trim($data['certConfig']['certificate']) == trim($fullchain)) { + $this->log('函数计算域名 ' . $config['domain'] . ' 证书已部署,无需重复操作!'); + return; + } + + if ($data['protocol'] == 'HTTP') $data['protocol'] = 'HTTP,HTTPS'; + $param = [ + 'domainName' => $config['domain'], + 'description' => $data['description'], + 'protocol' => $data['protocol'], + 'certConfig' => [ + 'certName' => 'cert' . substr($config['cert_name'], strpos($config['cert_name'], '-') + 1), + 'certificate' => $fullchain, + 'privateKey' => $privatekey, + ], + 'authConfig' => $data['authConfig'], + 'routeConfig' => $data['routeConfig'], + ]; + try { + $client->request('PUT', '/openapi/v1/domains/customdomains/' . $config['domain'], null, $param, ['regionId' => $config['region_id']]); + } catch (Exception $e) { + if (strpos($e->getMessage(), '请求已提交,请勿重复操作!') === false) { + throw new Exception($e->getMessage()); + } + } + + $this->log('函数计算域名 ' . $config['domain'] . ' 部署证书成功!'); + } + public function setLogger($func) { $this->logger = $func; diff --git a/app/lib/deploy/huawei.php b/app/lib/deploy/huawei.php index 7db5174..058eb86 100644 --- a/app/lib/deploy/huawei.php +++ b/app/lib/deploy/huawei.php @@ -4,6 +4,7 @@ namespace app\lib\deploy; use app\lib\DeployInterface; use app\lib\client\HuaweiCloud; +use app\lib\client\HuaweiOBS; use Exception; class huawei implements DeployInterface @@ -39,6 +40,11 @@ class huawei implements DeployInterface $this->deploy_elb($fullchain, $privatekey, $config); } elseif ($config['product'] == 'waf') { $this->deploy_waf($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'obs') { + $this->deploy_obs($fullchain, $privatekey, $config); + } elseif ($config['product'] == 'upload') { + $cert_id = $this->get_cert_id($fullchain, $privatekey); + $info['cert_id'] = $cert_id; } } @@ -117,6 +123,19 @@ class huawei implements DeployInterface $this->log('WAF证书ID ' . $config['cert_id'] . ' 更新证书成功!'); } + private function deploy_obs($fullchain, $privatekey, $config) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + if (empty($config['obs_endpoint'])) throw new Exception('OBS Endpoint不能为空'); + if (empty($config['obs_bucket'])) throw new Exception('OBS 桶名称不能为空'); + $obsClient = new HuaweiOBS($this->AccessKeyId, $this->SecretAccessKey, $config['obs_endpoint'], $this->proxy); + foreach (explode(',', $config['domain']) as $domain) { + if (empty($domain)) continue; + $obsClient->setBucketCustomdomain($config['obs_bucket'], $domain, $config['cert_name'], $fullchain, $privatekey); + $this->log('OSS域名 ' . $domain . ' 部署证书成功!'); + } + } + private function get_cert_id($fullchain, $privatekey) { $certInfo = openssl_x509_parse($fullchain, true); diff --git a/app/lib/deploy/huoshan.php b/app/lib/deploy/huoshan.php index 7a416ae..9cc5ce3 100644 --- a/app/lib/deploy/huoshan.php +++ b/app/lib/deploy/huoshan.php @@ -191,7 +191,7 @@ class huoshan implements DeployInterface if (!$certInfo) throw new Exception('证书解析失败'); $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; - $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'open.volcengineapi.com', 'certificate_service', '2024-10-01', 'cn-beijing', $this->proxy); + $client = new Volcengine($this->AccessKeyId, $this->SecretAccessKey, 'certificate-service.volcengineapi.com', 'certificate_service', '2024-10-01', 'cn-beijing', $this->proxy); $param = [ 'Tag' => $cert_name, 'Repeatable' => false, @@ -207,10 +207,20 @@ class huoshan implements DeployInterface } if (!empty($data['InstanceId'])) { $cert_id = $data['InstanceId']; + $this->log('上传证书成功 CertId=' . $cert_id); + + $param = [ + 'InstanceId' => $cert_id, + 'Options' => [ + 'ExpiredNotice' => 'Disabled', + ], + ]; + $client->request('POST', 'CertificateUpdateInstance', $param); + } else { $cert_id = $data['RepeatId']; + $this->log('找到已上传的证书 CertId=' . $cert_id); } - $this->log('上传证书成功 CertId=' . $cert_id); return $cert_id; } diff --git a/app/lib/deploy/lecdn.php b/app/lib/deploy/lecdn.php index 3a5b143..a4af274 100644 --- a/app/lib/deploy/lecdn.php +++ b/app/lib/deploy/lecdn.php @@ -41,13 +41,29 @@ class lecdn implements DeployInterface public function deploy($fullchain, $privatekey, $config, &$info) { - $id = $config['id']; - if (empty($id)) throw new Exception('证书ID不能为空'); - if ($this->auth == 0) { $this->login(); } + $id = $config['id']; + if (empty($id)) { + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + $params = [ + 'name' => $cert_name, + 'type' => 'upload', + 'ssl_pem' => base64_encode($fullchain), + 'ssl_key' => base64_encode($privatekey), + 'auto_renewal' => false, + ]; + $data = $this->request('/prod-api/certificate', $params, 'POST'); + $id = $data['id']; + $this->log("证书ID:{$id}添加成功!"); + $info['config']['id'] = $id; + return; + } + try { $data = $this->request('/prod-api/certificate/' . $id); } catch (Exception $e) { diff --git a/app/lib/deploy/opanel.php b/app/lib/deploy/opanel.php index 2091cd8..19cdc97 100644 --- a/app/lib/deploy/opanel.php +++ b/app/lib/deploy/opanel.php @@ -44,10 +44,10 @@ class opanel implements DeployInterface // 没有指定节点,只部署到主控节点 try { $this->request('/core/settings/ssl/update', $params); - $this->log("主节点面板证书更新成功!"); + $this->log("面板证书更新成功!"); return; } catch (Exception $e) { - throw new Exception("主节点面板证书更新失败:" . $e->getMessage()); + throw new Exception("面板证书更新失败:" . $e->getMessage()); } } else { // 同时部署到主节点和所有指定的子节点 @@ -131,11 +131,11 @@ class opanel implements DeployInterface ]; try { $this->request('/websites/ssl/upload', $params, $nodeName); - $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新成功!" : "主节点 证书ID:{$config['id']}更新成功!"; + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新成功!" : "证书ID:{$config['id']}更新成功!"; $this->log($logMsg); return; } catch (Exception $e) { - $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新失败:" : "主节点 证书ID:{$config['id']}更新失败:"; + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$config['id']}更新失败:" : "证书ID:{$config['id']}更新失败:"; throw new Exception($logMsg . $e->getMessage()); } } @@ -147,10 +147,10 @@ class opanel implements DeployInterface $params = ['page' => 1, 'pageSize' => 500]; try { $data = $this->request("/websites/ssl/search", $params, $nodeName); - $logMsg = $nodeName ? "节点 [{$nodeName}] " : "主节点 "; + $logMsg = $nodeName ? "节点 [{$nodeName}] " : ""; $this->log($logMsg . '获取证书列表成功(total=' . $data['total'] . ')'); } catch (Exception $e) { - $logMsg = $nodeName ? "节点 [{$nodeName}] " : "主节点 "; + $logMsg = $nodeName ? "节点 [{$nodeName}] " : ""; throw new Exception($logMsg . '获取证书列表失败:' . $e->getMessage()); } @@ -179,12 +179,12 @@ class opanel implements DeployInterface ]; try { $this->request('/websites/ssl/upload', $params, $nodeName); - $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新成功!" : "主节点 证书ID:{$row['id']}更新成功!"; + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新成功!" : "证书ID:{$row['id']}更新成功!"; $this->log($logMsg); $success++; } catch (Exception $e) { $errmsg = $e->getMessage(); - $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新失败:" : "主节点 证书ID:{$row['id']}更新失败:"; + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书ID:{$row['id']}更新失败:" : "证书ID:{$row['id']}更新失败:"; $this->log($logMsg . $errmsg); } } @@ -199,7 +199,7 @@ class opanel implements DeployInterface 'description' => '', ]; $this->request('/websites/ssl/upload', $params, $nodeName); - $logMsg = $nodeName ? "节点 [{$nodeName}] 证书上传成功!" : "主节点 证书上传成功!"; + $logMsg = $nodeName ? "节点 [{$nodeName}] 证书上传成功!" : "证书上传成功!"; $this->log($logMsg); } } diff --git a/app/lib/deploy/qiniu.php b/app/lib/deploy/qiniu.php index b023e0a..8fb1739 100644 --- a/app/lib/deploy/qiniu.php +++ b/app/lib/deploy/qiniu.php @@ -37,6 +37,9 @@ class qiniu implements DeployInterface $cert_name = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; $cert_id = $this->get_cert_id($fullchain, $privatekey, $certInfo['subject']['CN'], $cert_name); + $info['cert_id'] = $cert_id; + $info['cert_name'] = $cert_name; + if ($config['product'] == 'upload') return; foreach (explode(',', $domains) as $domain) { if (empty($domain)) continue; @@ -50,8 +53,6 @@ class qiniu implements DeployInterface throw new Exception('未知的产品类型'); } } - $info['cert_id'] = $cert_id; - $info['cert_name'] = $cert_name; } private function deploy_cdn($domain, $cert_id) diff --git a/app/lib/deploy/rainyun.php b/app/lib/deploy/rainyun.php index f204b21..fb591ee 100644 --- a/app/lib/deploy/rainyun.php +++ b/app/lib/deploy/rainyun.php @@ -26,19 +26,43 @@ class rainyun implements DeployInterface public function deploy($fullchain, $privatekey, $config, &$info) { - if (empty($config['id'])) throw new Exception('证书ID不能为空'); + if (empty($config['id'])) { + $params = [ + 'cert' => $fullchain, + 'key' => $privatekey, + ]; + try { + $this->request('/product/sslcenter/', $params, 'POST'); + } catch (Exception $e) { + throw new Exception('上传证书失败,' . $e->getMessage()); + } - $params = [ - 'cert' => $fullchain, - 'key' => $privatekey, - ]; - try { - $this->request('/product/sslcenter/' . $config['id'], $params, 'PUT'); - } catch (Exception $e) { - throw new Exception($e->getMessage()); + $params = [ + 'options' => '{"columnFilters":{"Domain":""},"sort":[],"page":1,"perPage":1}', + ]; + try { + $data = $this->request('/product/sslcenter/?' . http_build_query($params), null, 'GET'); + } catch (Exception $e) { + throw new Exception('获取证书列表失败,' . $e->getMessage()); + } + if (empty($data['Records'])) throw new Exception('未找到已上传的证书'); + $cert_id = $data['Records'][0]['ID']; + $info['config']['id'] = $cert_id; + + $this->log('证书ID:' . $cert_id . '添加成功!'); + } else { + $params = [ + 'cert' => $fullchain, + 'key' => $privatekey, + ]; + try { + $this->request('/product/sslcenter/' . $config['id'], $params, 'PUT'); + } catch (Exception $e) { + throw new Exception($e->getMessage()); + } + + $this->log('证书ID:' . $config['id'] . '更新成功!'); } - - $this->log('证书ID:' . $config['id'] . '更新成功!'); } private function request($path, $params = null, $method = null) @@ -55,7 +79,7 @@ class rainyun implements DeployInterface $response = http_request($url, $body, null, null, $headers, $this->proxy, $method); $result = json_decode($response['body'], true); if (isset($result['code']) && $result['code'] == 200) { - return $result; + return isset($result['data']) ? $result['data'] : null; } elseif (isset($result['message'])) { throw new Exception($result['message']); } else { diff --git a/app/lib/deploy/tencent.php b/app/lib/deploy/tencent.php index 91f2d7c..ddfca18 100644 --- a/app/lib/deploy/tencent.php +++ b/app/lib/deploy/tencent.php @@ -36,6 +36,7 @@ class tencent implements DeployInterface } $cert_id = $this->get_cert_id($fullchain, $privatekey); if (!$cert_id) throw new Exception('证书ID获取失败'); + $info['cert_id'] = $cert_id; if ($config['product'] == 'cos') { if (empty($config['regionid'])) throw new Exception('所属地域ID不能为空'); if (empty($config['cos_bucket'])) throw new Exception('存储桶名称不能为空'); @@ -65,6 +66,8 @@ class tencent implements DeployInterface return $this->deploy_scf($cert_id, $config); } elseif ($config['product'] == 'teo' && isset($config['site_id'])) { return $this->deploy_teo($cert_id, $config); + } elseif ($config['product'] == 'upload') { + return; } else { if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); if ($config['product'] == 'waf') { @@ -77,7 +80,6 @@ class tencent implements DeployInterface } try { $record_id = $this->deploy_common($config['product'], $cert_id, $instance_id); - $info['cert_id'] = $cert_id; $info['record_id'] = $record_id; } catch (Exception $e) { if (isset($info['record_id'])) { diff --git a/app/lib/deploy/upyun.php b/app/lib/deploy/upyun.php index 287689c..4c524f2 100644 --- a/app/lib/deploy/upyun.php +++ b/app/lib/deploy/upyun.php @@ -92,8 +92,11 @@ class upyun implements DeployInterface } } - if ($i == 0) throw new Exception('未找到可迁移的证书'); - $this->log('共迁移' . $i . '个证书,关联域名' . $d . '个'); + if ($i == 0) { + $this->log('未找到可迁移的证书'); + } else { + $this->log('共迁移' . $i . '个证书,关联域名' . $d . '个'); + } } private function login() diff --git a/app/service/CertDeployService.php b/app/service/CertDeployService.php index ead2254..275f165 100644 --- a/app/service/CertDeployService.php +++ b/app/service/CertDeployService.php @@ -70,8 +70,15 @@ class CertDeployService $this->saveResult(-1, $e->getMessage(), date('Y-m-d H:i:s', time() + (array_key_exists($this->task['retry'], self::$retry_interval) ? self::$retry_interval[$this->task['retry']] : 3600))); throw $e; } finally { - if($this->info){ - Db::name('cert_deploy')->where('id', $this->task['id'])->update(['info' => json_encode($this->info)]); + if ($this->info && is_array($this->info)) { + if (isset($this->info['config']) && is_array($this->info['config'])) { + $config = array_merge(json_decode($this->task['config'], true), $this->info['config']); + Db::name('cert_deploy')->where('id', $this->task['id'])->update(['config' => json_encode($config)]); + unset($this->info['config']); + } + if (!empty($this->info)) { + Db::name('cert_deploy')->where('id', $this->task['id'])->update(['info' => json_encode($this->info)]); + } } } }