dnsmgr/app/lib/deploy/s3storage.php
2026-04-11 21:13:52 +08:00

221 lines
7.1 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Exception;
class s3storage implements DeployInterface
{
private $logger;
private $AccessKeyId;
private $SecretAccessKey;
private $endpoint;
private $region;
private $proxy;
public function __construct($config)
{
$this->AccessKeyId = $config['AccessKeyId'];
$this->SecretAccessKey = $config['SecretAccessKey'];
$this->endpoint = rtrim($config['endpoint'], '/');
$this->region = !empty($config['region']) ? $config['region'] : 'us-east-1';
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
}
public function check()
{
if (empty($this->AccessKeyId) || empty($this->SecretAccessKey) || empty($this->endpoint)) {
throw new Exception('必填参数不能为空');
}
$this->s3Request('GET', '/', '', null);
return true;
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$bucket = $config['bucket'];
if (empty($bucket)) throw new Exception('存储桶名称不能为空');
$certPath = trim($config['cert_path'], '/');
$keyPath = trim($config['key_path'], '/');
if (empty($certPath) || empty($keyPath)) throw new Exception('证书和私钥保存路径不能为空');
$this->putObject($bucket, $certPath, $fullchain);
$this->log("证书已上传到s3://{$bucket}/{$certPath}");
$this->putObject($bucket, $keyPath, $privatekey);
$this->log("私钥已上传到s3://{$bucket}/{$keyPath}");
}
private function putObject($bucket, $key, $content)
{
$path = '/' . $bucket . '/' . $key;
$this->s3Request('PUT', $path, $content, 'application/x-pem-file');
}
private function s3Request($method, $path, $body, $contentType)
{
$time = time();
$date = gmdate("Ymd\THis\Z", $time);
$shortDate = gmdate("Ymd", $time);
$host = preg_replace('#^https?://#', '', $this->endpoint);
$scheme = (strpos($this->endpoint, 'https://') === 0) ? 'https' : 'http';
if (strpos($this->endpoint, '://') === false) {
$scheme = 'https';
}
$payloadHash = hash('sha256', $body ?? '');
$headers = [
'Host' => $host,
'X-Amz-Date' => $date,
'X-Amz-Content-Sha256' => $payloadHash,
];
if ($contentType) {
$headers['Content-Type'] = $contentType;
}
$authorization = $this->generateSign($method, $path, [], $headers, $body ?? '', $date, $shortDate);
$headers['Authorization'] = $authorization;
$url = $scheme . '://' . $host . $path;
$headerArr = [];
foreach ($headers as $k => $v) {
$headerArr[] = $k . ': ' . $v;
}
$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, $headerArr);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body !== null && $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);
if ($httpCode >= 200 && $httpCode < 300) {
return $response;
}
$errmsg = 'HTTP Code: ' . $httpCode;
if ($response) {
LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true);
$xml = @simplexml_load_string($response);
if ($xml && isset($xml->Message)) {
$errmsg = (string)$xml->Message;
} elseif ($xml && isset($xml->Error->Message)) {
$errmsg = (string)$xml->Error->Message;
}
}
throw new Exception($errmsg);
}
private function generateSign($method, $path, $query, $headers, $body, $date, $shortDate)
{
$algorithm = 'AWS4-HMAC-SHA256';
$canonicalUri = $this->getCanonicalURI($path);
$canonicalQueryString = $this->getCanonicalQueryString($query);
[$canonicalHeaders, $signedHeaders] = $this->getCanonicalHeaders($headers);
$hashedPayload = hash('sha256', $body);
$canonicalRequest = $method . "\n"
. $canonicalUri . "\n"
. $canonicalQueryString . "\n"
. $canonicalHeaders . "\n"
. $signedHeaders . "\n"
. $hashedPayload;
$credentialScope = $shortDate . '/' . $this->region . '/s3/aws4_request';
$stringToSign = $algorithm . "\n"
. $date . "\n"
. $credentialScope . "\n"
. hash('sha256', $canonicalRequest);
$kDate = hash_hmac('sha256', $shortDate, 'AWS4' . $this->SecretAccessKey, true);
$kRegion = hash_hmac('sha256', $this->region, $kDate, true);
$kService = hash_hmac('sha256', 's3', $kRegion, true);
$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
$signature = hash_hmac('sha256', $stringToSign, $kSigning);
return $algorithm . ' Credential=' . $this->AccessKeyId . '/' . $credentialScope
. ', SignedHeaders=' . $signedHeaders
. ', Signature=' . $signature;
}
private function escape($str)
{
$search = ['+', '*', '%7E'];
$replace = ['%20', '%2A', '~'];
return str_replace($search, $replace, urlencode($str));
}
private function getCanonicalURI($path)
{
if (empty($path)) return '/';
$parts = explode('/', $path);
$parts = array_map(function ($item) {
return $this->escape($item);
}, $parts);
return implode('/', $parts);
}
private function getCanonicalQueryString($parameters)
{
if (empty($parameters)) return '';
ksort($parameters);
$pairs = [];
foreach ($parameters as $key => $value) {
$pairs[] = $this->escape($key) . '=' . $this->escape($value);
}
return implode('&', $pairs);
}
private function getCanonicalHeaders($oldHeaders)
{
$headers = [];
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];
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
}