146 lines
5.3 KiB
PHP
146 lines
5.3 KiB
PHP
<?php
|
||||
|
|
// ============================================
|
|||
|
|
// 文件: lib/storage/http.php
|
|||
|
|
// 说明: 对象存储驱动共用的 HTTP 客户端与签名工具函数
|
|||
|
|
// ============================================
|
|||
|
|
|
|||
|
|
// 该路径是否允许当前请求访问。
|
|||
|
|
//
|
|||
|
|
// 宝塔等环境会给站点加 .user.ini:open_basedir=/站点目录/:/tmp/。
|
|||
|
|
// 此时对 /etc/ssl/... 这类"站外路径"调用 is_file() 会抛
|
|||
|
|
// Warning: is_file(): open_basedir restriction in effect
|
|||
|
|
// 而站点开着 display_errors,警告就会被打进响应体——对返回 JSON 的接口
|
|||
|
|
// 是致命的(前端 JSON.parse 直接失败)。所以探测前先自己判断一遍。
|
|||
|
|
function storagePathAllowed($path) {
|
|||
|
|
$path = str_replace('\\', '/', trim((string)$path));
|
|||
|
|
if ($path === '') return false;
|
|||
|
|
|
|||
|
|
$open = (string)ini_get('open_basedir');
|
|||
|
|
if ($open !== '') {
|
|||
|
|
$allowed = false;
|
|||
|
|
foreach (explode(PATH_SEPARATOR, $open) as $dir) {
|
|||
|
|
$dir = rtrim(str_replace('\\', '/', trim($dir)), '/');
|
|||
|
|
if ($dir === '') continue;
|
|||
|
|
if ($path === $dir || strpos($path, $dir . '/') === 0) {
|
|||
|
|
$allowed = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (!$allowed) return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 仍在允许目录内,也压掉可能的名权限/符号链接警告
|
|||
|
|
return @is_file($path) && @is_readable($path);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定位系统 CA 证书,供 SSL 校验使用(找不到时返回 null,交由 curl 自行处理)
|
|||
|
|
function storageCaBundle() {
|
|||
|
|
static $ca = false;
|
|||
|
|
if ($ca !== false) return $ca;
|
|||
|
|
$candidates = [
|
|||
|
|
ini_get('curl.cainfo'),
|
|||
|
|
ini_get('openssl.cafile'),
|
|||
|
|
'/etc/ssl/certs/ca-certificates.crt',
|
|||
|
|
'/etc/pki/tls/certs/ca-bundle.crt',
|
|||
|
|
'/etc/ssl/cert.pem',
|
|||
|
|
];
|
|||
|
|
foreach ($candidates as $path) {
|
|||
|
|
if (storagePathAllowed($path)) {
|
|||
|
|
$ca = (string)$path;
|
|||
|
|
return $ca;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$ca = null;
|
|||
|
|
return $ca;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 发起 HTTP 请求。
|
|||
|
|
*
|
|||
|
|
* @param string $method GET / PUT / POST / DELETE / HEAD
|
|||
|
|
* @param string $url 完整 URL
|
|||
|
|
* @param array $headers 形如 ['Content-Type: image/jpeg', ...]
|
|||
|
|
* @param string|null $body 请求体
|
|||
|
|
* @param int $timeout 总超时(秒)
|
|||
|
|
* @param bool $insecure 是否跳过 SSL 证书校验(默认否)
|
|||
|
|
* @param int|null $connectTimeout 连接阶段超时(秒)。默认由总超时推算;
|
|||
|
|
* 想"快速失败"的地方(比如探测内网 endpoint)显式给一个很小的值。
|
|||
|
|
*
|
|||
|
|
* @return array ['ok'=>bool,'status'=>int,'headers'=>array,'body'=>string,'error'=>string]
|
|||
|
|
*/
|
|||
|
|
function storageHttp($method, $url, array $headers = [], $body = null, $timeout = 30, $insecure = false, $connectTimeout = null) {
|
|||
|
|
$respHeaders = [];
|
|||
|
|
$ch = curl_init();
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
CURLOPT_URL => $url,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_CUSTOMREQUEST => strtoupper($method),
|
|||
|
|
CURLOPT_HTTPHEADER => $headers,
|
|||
|
|
CURLOPT_CONNECTTIMEOUT => $connectTimeout === null
|
|||
|
|
? min(10, max(3, $timeout))
|
|||
|
|
: max(1, (int)$connectTimeout),
|
|||
|
|
CURLOPT_TIMEOUT => $timeout,
|
|||
|
|
CURLOPT_FOLLOWLOCATION => false,
|
|||
|
|
CURLOPT_SSL_VERIFYPEER => !$insecure,
|
|||
|
|
CURLOPT_SSL_VERIFYHOST => $insecure ? 0 : 2,
|
|||
|
|
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$respHeaders) {
|
|||
|
|
$pos = strpos($line, ':');
|
|||
|
|
if ($pos !== false) {
|
|||
|
|
$name = strtolower(trim(substr($line, 0, $pos)));
|
|||
|
|
$respHeaders[$name] = trim(substr($line, $pos + 1));
|
|||
|
|
}
|
|||
|
|
return strlen($line);
|
|||
|
|
},
|
|||
|
|
]);
|
|||
|
|
$ca = storageCaBundle();
|
|||
|
|
if ($ca && !$insecure) {
|
|||
|
|
curl_setopt($ch, CURLOPT_CAINFO, $ca);
|
|||
|
|
}
|
|||
|
|
if ($body !== null) {
|
|||
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$out = curl_exec($ch);
|
|||
|
|
$err = curl_error($ch);
|
|||
|
|
$errno = curl_errno($ch);
|
|||
|
|
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|||
|
|
curl_close($ch);
|
|||
|
|
|
|||
|
|
if ($out === false) {
|
|||
|
|
return [
|
|||
|
|
'ok' => false, 'status' => 0, 'headers' => [],
|
|||
|
|
'body' => '',
|
|||
|
|
'error' => $errno === CURLE_OPERATION_TIMEDOUT
|
|||
|
|
? "请求超时({$timeout}s)"
|
|||
|
|
: "网络错误({$errno}): {$err}",
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
'ok' => $status >= 200 && $status < 300,
|
|||
|
|
'status' => $status,
|
|||
|
|
'headers' => $respHeaders,
|
|||
|
|
'body' => $out,
|
|||
|
|
'error' => $status >= 200 && $status < 300 ? '' : "HTTP {$status}",
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成对象的访问 URL(路径部分逐段编码,保留斜杠)
|
|||
|
|
function storageEncodeKey($key) {
|
|||
|
|
$key = ltrim((string)$key, '/');
|
|||
|
|
$segments = explode('/', $key);
|
|||
|
|
return implode('/', array_map('rawurlencode', $segments));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析 host 形式:bucket.endpoint 或自定义域名
|
|||
|
|
function storageHostOf($domain, $useHttps = true) {
|
|||
|
|
$domain = preg_replace('#^https?://#i', '', trim((string)$domain));
|
|||
|
|
return ($useHttps ? 'https://' : 'http://') . rtrim($domain, '/');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 统一把失败信息压缩成一行,便于写入设置页展示
|
|||
|
|
function storageShortError($text, $max = 200) {
|
|||
|
|
$text = preg_replace('/\s+/', ' ', trim((string)$text));
|
|||
|
|
return mb_strlen($text, 'UTF-8') > $max ? mb_substr($text, 0, $max, 'UTF-8') . '…' : $text;
|
|||
|
|
}
|