From 39d21d3a6a631209cd8a080060374fd14d6d996a Mon Sep 17 00:00:00 2001 From: Snowz <372492339@qq.com> Date: Wed, 28 May 2025 02:04:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(ImageProxy):=20=E6=B7=BB=E5=8A=A0=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E7=99=BD=E5=90=8D=E5=8D=95=E5=92=8C=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ImageProxy中添加了域名白名单配置,支持精确和子域名匹配,增强安全性。 - 实现了日志文件的自动清理和轮转功能,限制日志文件大小、保留时间和数量,优化日志管理。 - 更新README文档,详细说明了新功能和配置方法。 --- ImageProxy.php | 90 +++++++++++++++++++++++++++++++++++++++++++++++--- README.md | 67 +++++++++++++++++++++++++++++++++++-- index.php | 8 ++++- 3 files changed, 156 insertions(+), 9 deletions(-) diff --git a/ImageProxy.php b/ImageProxy.php index 9995ea3..daf5bf8 100644 --- a/ImageProxy.php +++ b/ImageProxy.php @@ -23,7 +23,8 @@ class ImageProxy { 'timeout' => 30, 'connect_timeout' => 15, 'max_redirects' => 5, - 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + 'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'allowed_domains' => [] // 添加白名单域名配置 ]; private $config; @@ -115,8 +116,29 @@ class ImageProxy { * @return bool */ private function validateUrl(string $url): bool { - return filter_var($url, FILTER_VALIDATE_URL) !== false && - preg_match('/^https?:\/\//i', $url); + if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('/^https?:\/\//i', $url)) { + return false; + } + + // 检查域名是否在白名单中 + $parsedUrl = parse_url($url); + $host = $parsedUrl['host'] ?? ''; + + // 如果白名单为空,则允许所有域名 + if (empty($this->config['allowed_domains'])) { + return true; + } + + // 检查域名是否在白名单中 + foreach ($this->config['allowed_domains'] as $allowedDomain) { + if (strcasecmp($host, $allowedDomain) === 0 || + preg_match('/\.' . preg_quote($allowedDomain, '/') . '$/', $host)) { + return true; + } + } + + $this->logger->error("Domain not in whitelist: $host"); + return false; } /** @@ -406,6 +428,9 @@ class ImageProxy { */ class Logger { private $logDir; + private const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB + private const MAX_LOG_AGE = 30 * 24 * 60 * 60; // 30天 + private const MAX_LOGS = 10; // 最多保留10个日志文件 public function __construct(string $logDir) { $this->logDir = $logDir; @@ -413,6 +438,9 @@ class Logger { if (!file_exists($this->logDir)) { mkdir($this->logDir, 0777, true); } + + // 清理旧日志 + $this->cleanupOldLogs(); } public function error(string $message) { @@ -423,19 +451,71 @@ class Logger { $this->log('INFO', $message); } + private function cleanupOldLogs() { + try { + $files = glob($this->logDir . '/*.log'); + if (!$files) { + return; + } + + // 按修改时间排序 + usort($files, function($a, $b) { + return filemtime($b) - filemtime($a); + }); + + // 删除超过保留时间的日志 + foreach ($files as $file) { + if (time() - filemtime($file) > self::MAX_LOG_AGE) { + unlink($file); + } + } + + // 如果日志文件数量超过限制,删除最旧的 + $files = glob($this->logDir . '/*.log'); + if (count($files) > self::MAX_LOGS) { + usort($files, function($a, $b) { + return filemtime($a) - filemtime($b); + }); + $filesToDelete = array_slice($files, 0, count($files) - self::MAX_LOGS); + foreach ($filesToDelete as $file) { + unlink($file); + } + } + } catch (Exception $e) { + error_log("Logger cleanup error: " . $e->getMessage()); + } + } + + private function rotateLogIfNeeded(string $logFile) { + if (!file_exists($logFile)) { + return; + } + + if (filesize($logFile) >= self::MAX_LOG_SIZE) { + $timestamp = date('Y-m-d_H-i-s'); + $newName = $logFile . '.' . $timestamp; + rename($logFile, $newName); + } + } + private function log(string $level, string $message) { try { $date = date('Y-m-d'); $time = date('Y-m-d H:i:s'); $logFile = $this->logDir . "/{$date}.log"; + // 检查并轮转日志 + $this->rotateLogIfNeeded($logFile); + $logMessage = "[{$time}] [{$level}] {$message}\n"; - // Ensure log directory is writable, though constructor should handle creation + + // 确保日志目录可写 if (!is_writable($this->logDir) && !mkdir($this->logDir, 0777, true) && !is_dir($this->logDir)) { - // Cannot write to log, perhaps output to error_log as fallback error_log("Logger Error: Directory {$this->logDir} is not writable. Message: {$logMessage}"); return; } + + // 写入日志 if (file_put_contents($logFile, $logMessage, FILE_APPEND) === false) { error_log("Logger Error: Failed to write to log file {$logFile}. Message: {$logMessage}"); } diff --git a/README.md b/README.md index b3a4f5c..ef25751 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ /index.php?url=https://example.com/image.jpg ``` +使用伪静态后的访问方式: +``` +/image/https://example.com/image.jpg +``` + ## 参数说明 - url: 必填参数,需要代理的远程图片URL @@ -36,6 +41,7 @@ - SSL验证:支持HTTPS图片获取 - 图片数据验证:验证下载的图片数据是否有效 - 错误处理:完善的错误处理和日志记录 +- 域名白名单:支持配置允许访问的域名列表,防止服务被滥用 ## 性能优化 @@ -61,17 +67,72 @@ $proxy = new ImageProxy([ 'cache_dir' => 'cache', // 缓存目录 'timeout' => 30, // 请求超时时间(秒) 'connect_timeout' => 15, // 连接超时时间(秒) - 'max_redirects' => 5 // 最大重定向次数 + 'max_redirects' => 5, // 最大重定向次数 + 'allowed_domains' => [ // 允许访问的域名白名单 + 'example.com', + 'trusted-site.com', + 'images.example.org' + // 添加更多允许的域名 + ] ]); ``` +白名单配置说明: +- 如果 `allowed_domains` 为空数组,则允许访问所有域名 +- 支持精确域名匹配(如 `example.com`) +- 支持子域名匹配(如 `images.example.com` 会匹配 `example.com`) +- 域名比较不区分大小写 +- 不在白名单中的域名请求将被拒绝,并记录到日志中 + ## 错误处理 -系统会自动记录所有错误到日志文件中,日志文件位于`cache/logs/`目录下,按日期命名。 +系统会自动记录所有错误到日志文件中,日志文件位于`cache/logs/`目录下,按日期命名。系统实现了自动日志轮转功能: + +- 单个日志文件大小限制:10MB +- 日志文件保留时间:30天 +- 最大保留日志文件数:10个 +- 自动清理过期日志 +- 自动轮转超大日志文件 + +日志文件命名规则: +- 当前日志:`YYYY-MM-DD.log` +- 轮转后的日志:`YYYY-MM-DD.log.YYYY-MM-DD_HH-mm-ss` ## 注意事项 1. 确保服务器有足够的磁盘空间用于缓存 2. 确保缓存目录有写入权限 3. 建议定期清理缓存文件 -4. 建议监控日志文件大小 \ No newline at end of file +4. 日志系统会自动管理日志文件大小和数量,无需手动干预 + +## 伪静态配置 + +### Apache 配置 +在网站根目录创建或编辑 `.htaccess` 文件: + +```apache +RewriteEngine On +RewriteBase / + +# 图片代理伪静态规则 +RewriteRule ^image/(.*)$ index.php?url=$1 [L,QSA] + +# 禁止直接访问 index.php +RewriteCond %{THE_REQUEST} ^[A-Z]{3,}\s/+index\.php [NC] +RewriteRule ^ - [F] +``` + +### Nginx 配置 +在网站配置文件中添加: + +```nginx +location / { + # 图片代理伪静态规则 + rewrite ^/image/(.*)$ /index.php?url=$1 last; + + # 禁止直接访问 index.php + location = /index.php { + return 403; + } +} +``` \ No newline at end of file diff --git a/index.php b/index.php index 6a9cfbd..5eef686 100644 --- a/index.php +++ b/index.php @@ -18,7 +18,13 @@ $proxy = new ImageProxy([ 'cache_dir' => 'cache', 'timeout' => 30, 'connect_timeout' => 15, - 'max_redirects' => 5 + 'max_redirects' => 5, + 'allowed_domains' => [ + 'doubanio.com' + // 'trusted-site.com', + // 'images.example.org' + // 在这里添加更多允许的域名 + ] ]); // 处理请求