diff --git a/app/lib/DeployHelper.php b/app/lib/DeployHelper.php index 4adbf24..f8b3b06 100644 --- a/app/lib/DeployHelper.php +++ b/app/lib/DeployHelper.php @@ -1564,6 +1564,54 @@ ctrl+x 保存退出', ], ], ], + 'ksyun' => [ + 'name' => '金山云', + 'class' => 2, + 'icon' => 'ksyun.ico', + 'desc' => '支持部署到金山云CDN', + 'note' => '支持部署到金山云CDN', + 'inputs' => [ + 'AccessKeyId' => [ + 'name' => 'AccessKeyId', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'SecretAccessKey' => [ + 'name' => 'SecretAccessKey', + 'type' => 'input', + 'placeholder' => '', + 'required' => true, + ], + 'proxy' => [ + 'name' => '使用代理服务器', + 'type' => 'radio', + 'options' => [ + '0' => '否', + '1' => '是', + ], + 'value' => '0' + ], + ], + 'taskinputs' => [ + 'product' => [ + 'name' => '要部署的产品', + 'type' => 'select', + 'options' => [ + ['value'=>'cdn', 'label'=>'CDN'], + ], + 'value' => 'cdn', + 'required' => true, + ], + 'domain' => [ + 'name' => '绑定的域名', + 'type' => 'input', + 'placeholder' => '多个域名可使用,分隔', + 'show' => 'product==\'cdn\'', + 'required' => true, + ], + ], + ], 'huoshan' => [ 'name' => '火山引擎', 'class' => 2, diff --git a/app/lib/client/Ksyun.php b/app/lib/client/Ksyun.php new file mode 100644 index 0000000..4cea059 --- /dev/null +++ b/app/lib/client/Ksyun.php @@ -0,0 +1,209 @@ +AccessKeyId = $AccessKeyId; + $this->SecretAccessKey = $SecretAccessKey; + $this->endpoint = $endpoint; + $this->service = $service; + $this->region = $region; + $this->proxy = $proxy; + } + + /** + * @param string $method 请求方法 + * @param string $action 方法名称 + * @param array $params 请求参数 + * @return array + * @throws Exception + */ + public function request($method, $action, $version, $path = '/', $params = []) + { + if (!empty($params)) { + $params = array_filter($params, function ($a) { + return $a !== null; + }); + } + + $body = ''; + $query = []; + if ($method == 'GET') { + $query = $params; + } else { + $body = !empty($params) ? json_encode($params) : ''; + } + + $time = time(); + $headers = [ + 'Host' => $this->endpoint, + 'X-Amz-Date' => gmdate("Ymd\THis\Z", $time), + 'X-Version' => $version, + 'X-Action' => $action, + ]; + + $authorization = $this->generateSign($method, $path, $query, $headers, $body, $time); + $headers['Authorization'] = $authorization; + $headers['Accept'] = 'application/json'; + if ($body) { + $headers['Content-Type'] = 'application/json'; + } + + $url = 'https://' . $this->endpoint . $path; + if (!empty($query)) { + $url .= '?' . http_build_query($query); + } + $header = []; + foreach ($headers as $key => $value) { + $header[] = $key . ': ' . $value; + } + return $this->curl($method, $url, $body, $header); + } + + private function generateSign($method, $path, $query, $headers, $body, $time) + { + $algorithm = "AWS4-HMAC-SHA256"; + + // step 1: build canonical request string + $httpRequestMethod = $method; + $canonicalUri = $this->getCanonicalURI($path); + $canonicalQueryString = $this->getCanonicalQueryString($query); + [$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers); + $hashedRequestPayload = hash("sha256", $body); + $canonicalRequest = $httpRequestMethod . "\n" + . $canonicalUri . "\n" + . $canonicalQueryString . "\n" + . $canonicalHeaders . "\n" + . $signedHeaders . "\n" + . $hashedRequestPayload; + + // step 2: build string to sign + $date = gmdate("Ymd\THis\Z", $time); + $shortDate = substr($date, 0, 8); + $credentialScope = $shortDate . '/' . $this->region . '/' . $this->service . '/aws4_request'; + $hashedCanonicalRequest = hash("sha256", $canonicalRequest); + $stringToSign = $algorithm . "\n" + . $date . "\n" + . $credentialScope . "\n" + . $hashedCanonicalRequest; + + // step 3: sign string + $kDate = hash_hmac("sha256", $shortDate, 'AWS4' . $this->SecretAccessKey, true); + $kRegion = hash_hmac("sha256", $this->region, $kDate, true); + $kService = hash_hmac("sha256", $this->service, $kRegion, true); + $kSigning = hash_hmac("sha256", "aws4_request", $kService, true); + $signature = hash_hmac("sha256", $stringToSign, $kSigning); + + // step 4: build authorization + $credential = $this->AccessKeyId . '/' . $credentialScope; + $authorization = $algorithm . ' Credential=' . $credential . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature; + + return $authorization; + } + + private function escape($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + + private function getCanonicalURI($path) + { + if (empty($path)) return '/'; + $pattens = explode('/', $path); + $pattens = array_map(function ($item) { + return $this->escape($item); + }, $pattens); + $canonicalURI = implode('/', $pattens); + return $canonicalURI; + } + + private function getCanonicalQueryString($parameters) + { + if (empty($parameters)) return ''; + ksort($parameters); + $canonicalQueryString = ''; + foreach ($parameters as $key => $value) { + if (!is_array($value)) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($value); + } else { + sort($value); + foreach ($value as $v) { + $canonicalQueryString .= '&' . $this->escape($key) . '=' . $this->escape($v); + } + } + } + return substr($canonicalQueryString, 1); + } + + private function getCanonicalHeaders($oldheaders) + { + $headers = array(); + foreach ($oldheaders as $key => $value) { + $headers[strtolower($key)] = trim($value); + } + ksort($headers); + + $canonicalHeaders = ''; + $signedHeaders = ''; + foreach ($headers as $key => $value) { + $canonicalHeaders .= $key . ':' . $value . "\n"; + $signedHeaders .= $key . ';'; + } + $signedHeaders = substr($signedHeaders, 0, -1); + return [$canonicalHeaders, $signedHeaders]; + } + + 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); + if ($errno) { + $errmsg = curl_error($ch); + curl_close($ch); + throw new Exception('Curl error: ' . $errmsg); + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $arr = json_decode($response, true); + if ($httpCode == 200) { + return $arr; + } else { + if (isset($arr['Error']['Message'])) { + throw new Exception($arr['Error']['Message']); + } else { + throw new Exception('返回数据解析失败(http_code=' . $httpCode . ')'); + } + } + } +} diff --git a/app/lib/deploy/ksyun.php b/app/lib/deploy/ksyun.php new file mode 100644 index 0000000..0a559cc --- /dev/null +++ b/app/lib/deploy/ksyun.php @@ -0,0 +1,79 @@ +AccessKeyId = $config['AccessKeyId']; + $this->SecretAccessKey = $config['SecretAccessKey']; + $this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false; + } + + public function check() + { + if (empty($this->AccessKeyId) || empty($this->SecretAccessKey)) throw new Exception('必填参数不能为空'); + $client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy); + $client->request('GET', 'GetCertificates', '2016-09-01', '/2016-09-01/cert/GetCertificates'); + return true; + } + + public function deploy($fullchain, $privatekey, $config, &$info) + { + $this->deploy_cdn($fullchain, $privatekey, $config, $info); + } + + public function deploy_cdn($fullchain, $privatekey, $config, &$info) + { + if (empty($config['domain'])) throw new Exception('绑定的域名不能为空'); + $certInfo = openssl_x509_parse($fullchain, true); + if (!$certInfo) throw new Exception('证书解析失败'); + $config['cert_name'] = str_replace('*.', '', $certInfo['subject']['CN']) . '-' . $certInfo['validFrom_time_t']; + $domains = explode(',', $config['domain']); + + $client = new KsyunClient($this->AccessKeyId, $this->SecretAccessKey, 'cdn.api.ksyun.com', 'cdn', 'cn-shanghai-2', $this->proxy); + $param = [ + 'PageSize' => 100, + 'PageNumber' => 1, + ]; + $domain_ids = []; + $result = $client->request('GET', 'GetCdnDomains', '2019-06-01', '/2019-06-01/domain/GetCdnDomains', $param); + foreach ($result['Domains'] as $row) { + if (in_array($row['DomainName'], $domains)) { + $domain_ids[] = $row['DomainId']; + } + } + if (count($domain_ids) == 0) throw new Exception('未找到对应的CDN域名'); + $param = [ + 'Enable' => 'on', + 'DomainIds' => implode(',', $domain_ids), + 'CertificateName' => $config['cert_name'], + 'ServerCertificate' => $fullchain, + 'PrivateKey' => $privatekey, + ]; + $result = $client->request('POST', 'ConfigCertificate', '2016-09-01', '/2016-09-01/cert/ConfigCertificate', $param); + $this->log('CDN证书部署成功,证书ID:' . $result['CertificateId']); + } + + public function setLogger($func) + { + $this->logger = $func; + } + + private function log($txt) + { + if ($this->logger) { + call_user_func($this->logger, $txt); + } + } +} diff --git a/app/view/dmonitor/taskform.html b/app/view/dmonitor/taskform.html index 4f004ca..aa6807d 100644 --- a/app/view/dmonitor/taskform.html +++ b/app/view/dmonitor/taskform.html @@ -55,7 +55,7 @@