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(); });