Files
alist-proxy/index.php
XiaoMo 95737ecab8 refactor: 移除未使用的依赖和功能,优化代码结构
移除sharp模块及相关缩略图功能
删除webpack配置文件和安装脚本
清理未使用的依赖项
重构PHP服务端代码,优化缓存处理逻辑
2025-09-01 17:06:35 +08:00

543 lines
20 KiB
PHP
Raw 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
use Swoole\Coroutine\Http\Server;
use Swoole\Coroutine\Http\Client;
use function Swoole\Coroutine\run;
// 常量定义
const CACHE_DIR_NAME = '.cache';
const DEFAULT_PORT = 9001;
const DEFAULT_API_ENDPOINT = 'http://183.6.121.121:9519/api';
const CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24小时
const CACHE_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1小时
const API_TIMEOUT_MS = 5000;
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36';
// HTTP状态码
const HTTP_STATUS = [
'OK' => 200,
'NO_CONTENT' => 204,
'REDIRECT' => 302,
'NOT_MODIFIED' => 304,
'BAD_REQUEST' => 400,
'NOT_FOUND' => 404,
'INTERNAL_SERVER_ERROR' => 500,
'BAD_GATEWAY' => 502,
];
// 初始化变量
$cacheDir = __DIR__ . '/' . CACHE_DIR_NAME;
$pathIndex = [];
$port = DEFAULT_PORT;
$apiEndpoint = DEFAULT_API_ENDPOINT;
// 访问计数器
$viewsInfo = [
'request' => 0,
'cacheHit' => 0,
'apiCall' => 0,
'cacheCall' => 0,
'cacheReadError' => 0,
'fetchApiError' => 0,
'fetchApiWarning' => 0,
];
// 解析命令行参数
function parseArguments() {
global $port, $apiEndpoint;
$options = getopt('', ['port:', 'api:']);
if (isset($options['port'])) {
$parsedPort = intval($options['port']);
if ($parsedPort > 0) {
$port = $parsedPort;
}
}
if (isset($options['api'])) {
$apiEndpoint = $options['api'];
}
}
// 初始化应用
function initializeApp() {
global $cacheDir;
parseArguments();
if (!file_exists($cacheDir)) {
try {
mkdir($cacheDir, 0777, true);
echo "Cache directory created: {$cacheDir}\n";
} catch (Exception $e) {
echo "Error creating cache directory {$cacheDir}: " . $e->getMessage() . "\n";
exit(1);
}
}
}
// 发送错误响应
function sendErrorResponse($res, int $statusCode, string $message) {
if (!$res->isWritable()) {
return;
}
$res->status($statusCode);
$res->header('Content-Type', 'text/plain;charset=UTF-8');
$res->end($message);
}
// 处理favicon请求
function handleFavicon($req, $res) {
$res->status(HTTP_STATUS['NO_CONTENT']);
$res->end();
}
// 处理endpoint请求
function handleEndpoint($req, $res, array $queryParams) {
global $apiEndpoint, $port, $cacheDir, $pathIndex, $viewsInfo;
if (isset($queryParams['api'])) {
$urlRegex = '/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w.-]*)*\/?$/';
if (preg_match($urlRegex, $queryParams['api'])) {
$apiEndpoint = $queryParams['api'];
echo "API endpoint updated to: {$apiEndpoint}\n";
}
}
$res->status(HTTP_STATUS['OK']);
$res->header('Content-Type', 'application/json; charset=utf-8');
$res->end(json_encode([
'code' => HTTP_STATUS['OK'],
'data' => [
'api' => $apiEndpoint,
'port' => $port,
'cacheDir' => $cacheDir,
'pathIndexCount' => count($pathIndex),
'viewsInfo' => $viewsInfo
]
]));
}
// 处理API重定向
function handleApiRedirect($res, array $apiData) {
$res->status(HTTP_STATUS['REDIRECT']);
$res->header('Location', $apiData['data']['url']);
$res->end();
}
// 检查缓存头并返回是否为304
function checkCacheHeaders($req, string $cacheMetaFile) {
try {
$metaContent = file_get_contents($cacheMetaFile);
$cacheData = json_decode($metaContent, true);
$ifNoneMatch = isset($req->header['if-none-match']) ? $req->header['if-none-match'] : null;
$ifModifiedSince = isset($req->header['if-modified-since']) ? $req->header['if-modified-since'] : null;
// 检查ETag
if ($ifNoneMatch && isset($cacheData['uniqid']) && $ifNoneMatch === $cacheData['uniqid']) {
return ['cacheData' => $cacheData, 'isNotModified' => true];
}
// 检查If-Modified-Since
if ($ifModifiedSince && isset($cacheData['headers']['last-modified'])) {
try {
$lastModifiedDate = strtotime($cacheData['headers']['last-modified']);
$ifModifiedSinceDate = strtotime($ifModifiedSince);
if ($lastModifiedDate <= $ifModifiedSinceDate) {
return ['cacheData' => $cacheData, 'isNotModified' => true];
}
} catch (Exception $e) {
echo "Error parsing date for cache header check ({$cacheMetaFile}): " . $e->getMessage() . "\n";
}
}
return ['cacheData' => $cacheData, 'isNotModified' => false];
} catch (Exception $e) {
echo "Error reading or parsing cache meta file {$cacheMetaFile} in checkCacheHeaders: " . $e->getMessage() . "\n";
return ['cacheData' => null, 'isNotModified' => false];
}
}
// 检查缓存是否有效
function isCacheValid(string $cacheMetaFile, string $cacheContentFile) {
if (!file_exists($cacheMetaFile) || !file_exists($cacheContentFile)) {
return false;
}
try {
$metaContent = file_get_contents($cacheMetaFile);
$cacheData = json_decode($metaContent, true);
return isset($cacheData['expiration']) && is_numeric($cacheData['expiration']) && $cacheData['expiration'] > time() * 1000;
} catch (Exception $e) {
echo "Error reading or parsing cache meta file {$cacheMetaFile} for validation: " . $e->getMessage() . "\n";
return false;
}
}
// 从API获取数据
function fetchApiData(string $reqPath, string $token, string $sign) {
global $apiEndpoint;
$queryParams = http_build_query([
'type' => $reqPath,
'sign' => $sign
]);
$apiUrl = "{$apiEndpoint}?{$queryParams}";
$parsedApiUrl = parse_url($apiUrl);
$client = new Client($parsedApiUrl['host'], $parsedApiUrl['port'] ?? ($parsedApiUrl['scheme'] === 'https' ? 443 : 80), $parsedApiUrl['scheme'] === 'https');
$client->setHeaders([
'Accept' => 'application/json; charset=utf-8',
'User-Agent' => USER_AGENT,
'token' => $token
]);
$client->set(['timeout' => API_TIMEOUT_MS / 1000]);
$path = isset($parsedApiUrl['path']) ? $parsedApiUrl['path'] : '/';
if (isset($parsedApiUrl['query'])) {
$path .= '?' . $parsedApiUrl['query'];
}
$client->get($path);
if ($client->statusCode >= 400) {
echo "API request to {$apiUrl} failed with status {$client->statusCode}: {$client->body}\n";
$errorPayload = ['code' => $client->statusCode, 'message' => "API Error: {$client->statusCode}"];
try {
$parsedError = json_decode($client->body, true);
if ($parsedError && isset($parsedError['message'])) {
$errorPayload['message'] = $parsedError['message'];
}
} catch (Exception $e) {
// Ignore if response is not JSON
}
return $errorPayload;
}
try {
return json_decode($client->body, true);
} catch (Exception $e) {
echo "Error parsing JSON response from {$apiUrl}: " . $e->getMessage() . ", {$client->body}\n";
throw new Exception("Failed to parse API response: " . $e->getMessage());
}
}
// 从缓存中读取数据并返回
function serveFromCache(array $cacheData, string $cacheContentFile, string $cacheMetaFile, $res) {
global $viewsInfo;
if (!$cacheData) {
echo "serveFromCache called with null cacheData for {$cacheContentFile}\n";
sendErrorResponse($res, HTTP_STATUS['INTERNAL_SERVER_ERROR'], 'Cache metadata unavailable.');
return;
}
$viewsInfo['cacheCall']++;
try {
$fileContent = file_get_contents($cacheContentFile);
if ($fileContent === false) {
throw new Exception("Failed to read cache file");
}
$baseHeaders = [
'Cloud-Type' => $cacheData['cloudtype'] ?? 'unknown',
'Cloud-Expiration' => $cacheData['expiration'] ?? 0,
'ETag' => $cacheData['uniqid'] ?? '',
'Cache-Control' => 'public, max-age=31536000',
'Expires' => gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT',
'Accept-Ranges' => 'bytes',
'Connection' => 'keep-alive',
'Date' => gmdate('D, d M Y H:i:s') . ' GMT',
'Last-Modified' => isset($cacheData['headers']['last-modified']) ? $cacheData['headers']['last-modified'] : gmdate('D, d M Y H:i:s', filemtime($cacheMetaFile)) . ' GMT',
];
$isVideo = isset($cacheData['path']) && is_string($cacheData['path']) && strpos($cacheData['path'], '.mp4') !== false;
$contentType = isset($cacheData['headers']['Content-Type']) ? $cacheData['headers']['Content-Type'] : ($isVideo ? 'video/mp4' : 'application/octet-stream');
$responseHeaders = array_merge($baseHeaders, [
'Content-Type' => $contentType,
]);
foreach ($responseHeaders as $key => $value) {
$res->header($key, $value);
}
$res->status(HTTP_STATUS['OK']);
$res->end($fileContent);
} catch (Exception $e) {
$viewsInfo['cacheReadError']++;
echo "Error reading from cache {$cacheContentFile}: " . $e->getMessage() . "\n";
sendErrorResponse($res, HTTP_STATUS['INTERNAL_SERVER_ERROR'], "Failed to read from cache: " . $e->getMessage());
}
}
// 从真实URL获取数据并写入缓存
function fetchAndServe(array $data, string $tempCacheContentFile, string $cacheContentFile, string $cacheMetaFile, $res) {
global $viewsInfo;
$parsedUrl = parse_url($data['realUrl']);
$client = new Client($parsedUrl['host'], $parsedUrl['port'] ?? ($parsedUrl['scheme'] === 'https' ? 443 : 80), $parsedUrl['scheme'] === 'https');
$client->setHeaders([
'User-Agent' => USER_AGENT
]);
$path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/';
if (isset($parsedUrl['query'])) {
$path .= '?' . $parsedUrl['query'];
}
$client->get($path);
if ($client->statusCode !== 200) {
echo "Error fetching from {$data['realUrl']}: HTTP status {$client->statusCode}\n";
sendErrorResponse($res, HTTP_STATUS['BAD_GATEWAY'], "Bad Gateway: Failed to fetch content from source");
return;
}
$isVideo = isset($data['path']) && is_string($data['path']) && strpos($data['path'], '.mp4') !== false;
// 检查content-length
$contentLength = isset($client->headers['content-length']) ? $client->headers['content-length'] : null;
if ($contentLength) {
// contentLength小于2KB且与缓存文件大小不一致时重新获取
if ($contentLength < 2048 && isset($data['headers']['content-length']) && $data['headers']['content-length'] !== $contentLength) {
echo "Warning: content-length is different for the response from: {$data['realUrl']}\n";
sendErrorResponse($res, HTTP_STATUS['BAD_GATEWAY'], "Bad Gateway: Content-Length mismatch for {$data['realUrl']}");
return;
}
// 更新data到缓存cacheMetaFile
file_put_contents($cacheMetaFile, json_encode($data));
} else {
echo "Warning: content-length is undefined for the response from: {$data['realUrl']}\n";
}
// 写入临时缓存文件
file_put_contents($tempCacheContentFile, $client->body);
// 重命名临时文件为正式缓存文件
try {
$targetDir = dirname($cacheContentFile);
if (!file_exists($targetDir)) {
mkdir($targetDir, 0777, true);
}
rename($tempCacheContentFile, $cacheContentFile);
echo "Successfully cached: {$cacheContentFile}\n";
} catch (Exception $e) {
echo "Error renaming temp cache file {$tempCacheContentFile} to {$cacheContentFile}: " . $e->getMessage() . "\n";
// 如果重命名失败,尝试删除临时文件以避免混乱
if (file_exists($tempCacheContentFile)) {
unlink($tempCacheContentFile);
}
}
$baseHeaders = [
'Cloud-Type' => $data['cloudtype'] ?? 'unknown',
'Cloud-Expiration' => $data['expiration'] ?? 0,
'ETag' => $data['uniqid'] ?? '',
'Cache-Control' => 'public, max-age=31536000',
'Expires' => gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT',
'Accept-Ranges' => 'bytes',
'Connection' => 'keep-alive',
'Date' => gmdate('D, d M Y H:i:s') . ' GMT',
'Last-Modified' => isset($data['headers']['last-modified']) ?
$data['headers']['last-modified'] :
gmdate('D, d M Y H:i:s', filemtime($cacheMetaFile)) . ' GMT',
];
$responseHeaders = array_merge($baseHeaders, [
'Content-Type' => isset($client->headers['content-type']) ? $client->headers['content-type'] : ($isVideo ? 'video/mp4' : 'application/octet-stream'),
], $data['headers']);
foreach ($responseHeaders as $key => $value) {
$res->header($key, $value);
}
$res->status(HTTP_STATUS['OK']);
$res->end($client->body);
}
// 尝试从过期缓存提供服务或返回错误
function tryServeFromStaleCacheOrError(string $uniqidhex, $res, string $errorMessage = null) {
global $pathIndex, $cacheDir;
if (isset($pathIndex[$uniqidhex])) {
$cacheMetaFile = $cacheDir . '/' . $uniqidhex . '.meta';
$cacheContentFile = $cacheDir . '/' . $pathIndex[$uniqidhex]['uniqid'] . '.content';
if (file_exists($cacheMetaFile) && file_exists($cacheContentFile)) {
echo "API call failed or returned non-200. Serving stale cache for {$uniqidhex}\n";
try {
$cacheData = json_decode(file_get_contents($cacheMetaFile), true);
serveFromCache($cacheData, $cacheContentFile, $cacheMetaFile, $res);
return;
} catch (Exception $e) {
echo "Error parsing stale meta file {$cacheMetaFile}: " . $e->getMessage() . "\n";
// 如果过期缓存也损坏,则返回通用错误
}
}
}
sendErrorResponse($res, HTTP_STATUS['BAD_GATEWAY'], $errorMessage ?: 'Bad Gateway');
}
// 处理主请求
function handleMainRequest($req, $res) {
global $pathIndex, $cacheDir, $viewsInfo;
$url = preg_replace('/\/{2,}/', '/', $req->server['request_uri']);
$parsedUrl = parse_url($url);
$queryParams = [];
if (isset($parsedUrl['query'])) {
parse_str($parsedUrl['query'], $queryParams);
}
$sign = $queryParams['sign'] ?? '';
$pathParts = explode('/', trim($parsedUrl['path'], '/'));
$reqPath = $pathParts[0] ?? '';
$token = implode('/', array_slice($pathParts, 1));
if ($reqPath === 'favicon.ico') {
return handleFavicon($req, $res);
}
if ($reqPath === 'endpoint') {
return handleEndpoint($req, $res, $queryParams);
}
if (!$token && $reqPath) {
$token = $reqPath;
$reqPath = 'app'; // 默认为'app',如果只提供了一个路径段
}
$allowedPaths = ['avatar', 'go', 'bbs', 'www', 'url', 'thumb', 'app'];
if (!in_array($reqPath, $allowedPaths) || !$token) {
return sendErrorResponse($res, HTTP_STATUS['BAD_REQUEST'], "Bad Request: Invalid path or missing token.");
}
$viewsInfo['request']++;
$uniqidhex = md5($reqPath . $token . $sign);
$cacheMetaFile = '';
$cacheContentFile = '';
if (isset($pathIndex[$uniqidhex])) {
$cacheMetaFile = $cacheDir . '/' . $uniqidhex . '.meta';
$cacheContentFile = $cacheDir . '/' . $pathIndex[$uniqidhex]['uniqid'] . '.content';
}
if (isset($pathIndex[$uniqidhex]) && isCacheValid($cacheMetaFile, $cacheContentFile)) {
$cacheResult = checkCacheHeaders($req, $cacheMetaFile);
if ($cacheResult['isNotModified']) {
$res->status(HTTP_STATUS['NOT_MODIFIED']);
$res->end();
} else {
$viewsInfo['cacheHit']++;
serveFromCache($cacheResult['cacheData'], $cacheContentFile, $cacheMetaFile, $res);
}
} else {
try {
$viewsInfo['apiCall']++;
$apiData = fetchApiData($reqPath, $token, $sign);
if (isset($apiData['code']) && ($apiData['code'] === HTTP_STATUS['REDIRECT'] || $apiData['code'] === 301)) {
return handleApiRedirect($res, $apiData);
}
if (isset($apiData['code']) && $apiData['code'] === HTTP_STATUS['OK'] && isset($apiData['data']) && isset($apiData['data']['url'])) {
$data = [
'realUrl' => $apiData['data']['url'],
'cloudtype' => $apiData['data']['cloudtype'] ?? '',
'expiration' => isset($apiData['data']['expiration']) ? $apiData['data']['expiration'] * 1000 : 0,
'path' => $apiData['data']['path'] ?? '',
'headers' => $apiData['data']['headers'] ?? [],
'uniqid' => $apiData['data']['uniqid'] ?? '',
'thumb' => $apiData['data']['thumb'] ?? ''
];
$pathIndex[$uniqidhex] = ['uniqid' => $data['uniqid'], 'timestamp' => time() * 1000];
$cacheMetaFile = $cacheDir . '/' . $uniqidhex . '.meta';
$cacheContentFile = $cacheDir . '/' . $data['uniqid'] . '.content';
$tempCacheContentFile = $cacheDir . '/' . $data['uniqid'] . '_' . bin2hex(random_bytes(16)) . '.temp';
try {
file_put_contents($cacheMetaFile, json_encode($data));
} catch (Exception $e) {
echo "Error writing meta file {$cacheMetaFile}: " . $e->getMessage() . "\n";
sendErrorResponse($res, HTTP_STATUS['INTERNAL_SERVER_ERROR'], 'Failed to write cache metadata.');
return;
}
if (file_exists($cacheContentFile)) {
$contentLength = filesize($cacheContentFile);
if ($contentLength < 2048 && isset($data['headers']['content-length']) && intval($data['headers']['content-length']) !== $contentLength) {
echo "Content length mismatch for {$cacheContentFile}. API: {$data['headers']['content-length']}, Cache: {$contentLength}. Re-fetching.\n";
fetchAndServe($data, $tempCacheContentFile, $cacheContentFile, $cacheMetaFile, $res);
} else {
serveFromCache($data, $cacheContentFile, $cacheMetaFile, $res);
}
} else {
fetchAndServe($data, $tempCacheContentFile, $cacheContentFile, $cacheMetaFile, $res);
}
} else {
$viewsInfo['fetchApiWarning']++;
tryServeFromStaleCacheOrError($uniqidhex, $res, $apiData['message'] ?? null);
}
} catch (Exception $e) {
$viewsInfo['fetchApiError']++;
echo "Error in API call or processing: " . $e->getMessage() . "\n";
tryServeFromStaleCacheOrError($uniqidhex, $res, "Bad Gateway: API request failed. " . $e->getMessage());
}
}
}
// 定时清理过期缓存数据
function cleanupExpiredCache() {
global $pathIndex;
$currentTime = time() * 1000;
foreach ($pathIndex as $key => $value) {
if ($currentTime - $value['timestamp'] > CACHE_EXPIRY_MS) {
unset($pathIndex[$key]);
}
}
}
// 主函数
function main() {
global $port;
initializeApp();
// 创建服务器
$server = new Server('0.0.0.0', $port);
echo "Server started at http://0.0.0.0:{$port}\n";
// 设置定时器清理过期缓存
Swoole\Timer::tick(CACHE_CLEANUP_INTERVAL_MS, function () {
cleanupExpiredCache();
});
// 处理请求
$server->handle('/', function ($req, $res) {
handleMainRequest($req, $res);
});
$server->start();
}
// 启动服务器
run(function () {
main();
});