commit 309b0eddf19f5d3c43494dc277e4729d12a0ff36 Author: Snowz <372492339@qq.com> Date: Mon Apr 14 17:13:00 2025 +0800 first commit diff --git a/ImageProxy.php b/ImageProxy.php new file mode 100644 index 0000000..7dd4572 --- /dev/null +++ b/ImageProxy.php @@ -0,0 +1,286 @@ + 'image/gif', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'png' => 'image/png', + 'webp' => 'image/webp', + 'bmp' => 'image/bmp', + 'svg' => 'image/svg+xml' + ]; + + // 默认配置 + private const DEFAULT_CONFIG = [ + 'cache_dir' => 'cache', + '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' + ]; + + private $config; + private $logger; + + /** + * 构造函数 + * + * @param array $config 配置参数 + */ + public function __construct(array $config = []) { + $this->config = array_merge(self::DEFAULT_CONFIG, $config); + $this->initCacheDir(); + $this->initLogger(); + } + + /** + * 初始化缓存目录 + */ + private function initCacheDir() { + if (!file_exists($this->config['cache_dir'])) { + mkdir($this->config['cache_dir'], 0755, true); + } + } + + /** + * 初始化日志记录器 + */ + private function initLogger() { + $logDir = $this->config['cache_dir'] . '/logs'; + if (!file_exists($logDir)) { + mkdir($logDir, 0755, true); + } + $this->logger = new Logger($logDir); + } + + /** + * 处理图片请求 + * + * @param string $url 图片URL + * @return void + */ + public function processRequest(string $url) { + try { + if (!$this->validateUrl($url)) { + throw new Exception('Invalid URL format'); + } + + $cachePath = $this->getCachePath($url); + + // 检查缓存 + if ($this->isCached($cachePath)) { + $this->serveFromCache($cachePath); + return; + } + + // 获取远程图片 + $imageData = $this->fetchRemoteImage($url); + + // 验证图片数据 + if (!$this->validateImageData($imageData)) { + throw new Exception('Invalid image data'); + } + + // 保存到缓存 + $this->saveToCache($cachePath, $imageData); + + // 输出图片 + $this->outputImage($imageData, $this->getImageType($url)); + } catch (Exception $e) { + $this->logger->error('Image processing error: ' . $e->getMessage()); + $this->outputError(); + } + } + + /** + * 验证URL格式 + * + * @param string $url URL地址 + * @return bool + */ + private function validateUrl(string $url): bool { + return filter_var($url, FILTER_VALIDATE_URL) !== false && + preg_match('/^https?:\/\//i', $url); + } + + /** + * 获取缓存路径 + * + * @param string $url 图片URL + * @return string + */ + private function getCachePath(string $url): string { + $parsedUrl = parse_url($url); + $domain = $parsedUrl['host']; + $path = $parsedUrl['path']; + + $cacheSubDir = $this->config['cache_dir'] . '/' . $domain; + if (!file_exists($cacheSubDir)) { + mkdir($cacheSubDir, 0755, true); + } + + return $cacheSubDir . '/' . md5($url) . '.' . pathinfo($path, PATHINFO_EXTENSION); + } + + /** + * 检查图片是否已缓存 + * + * @param string $cachePath 缓存路径 + * @return bool + */ + private function isCached(string $cachePath): bool { + return file_exists($cachePath) && + (time() - filemtime($cachePath)) < 86400; // 缓存24小时 + } + + /** + * 从缓存输出图片 + * + * @param string $cachePath 缓存路径 + */ + private function serveFromCache(string $cachePath) { + $type = $this->getImageType($cachePath); + header("Content-type: " . $type); + readfile($cachePath); + } + + /** + * 获取远程图片 + * + * @param string $url 图片URL + * @return string + */ + private function fetchRemoteImage(string $url): string { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => $this->config['max_redirects'], + CURLOPT_CONNECTTIMEOUT => $this->config['connect_timeout'], + CURLOPT_TIMEOUT => $this->config['timeout'], + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_USERAGENT => $this->config['user_agent'], + CURLOPT_REFERER => parse_url($url, PHP_URL_SCHEME) . '://' . parse_url($url, PHP_URL_HOST) . '/', + CURLOPT_HTTPHEADER => ['Accept: image/*'] + ]); + + $data = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200 || empty($data)) { + throw new Exception('Failed to fetch image: HTTP ' . $httpCode); + } + + return $data; + } + + /** + * 验证图片数据 + * + * @param string $data 图片数据 + * @return bool + */ + private function validateImageData(string $data): bool { + if (empty($data)) { + return false; + } + + // 检查文件头 + $headers = [ + 'image/jpeg' => "\xFF\xD8\xFF", + 'image/png' => "\x89PNG\r\n\x1a\n", + 'image/gif' => "GIF", + 'image/webp' => "RIFF....WEBP", + 'image/bmp' => "BM", + 'image/svg+xml' => ' $header) { + if (strpos($data, $header) === 0) { + return true; + } + } + + return false; + } + + /** + * 保存到缓存 + * + * @param string $cachePath 缓存路径 + * @param string $data 图片数据 + */ + private function saveToCache(string $cachePath, string $data) { + file_put_contents($cachePath, $data); + } + + /** + * 输出图片 + * + * @param string $data 图片数据 + * @param string $type 图片类型 + */ + private function outputImage(string $data, string $type) { + header("Content-type: " . $type); + header("Cache-Control: public, max-age=86400"); + echo $data; + } + + /** + * 获取图片类型 + * + * @param string $url 图片URL + * @return string + */ + private function getImageType(string $url): string { + $ext = strtolower(pathinfo($url, PATHINFO_EXTENSION)); + return self::SUPPORTED_TYPES[$ext] ?? 'image/jpeg'; + } + + /** + * 输出错误信息 + */ + private function outputError() { + header("HTTP/1.1 500 Internal Server Error"); + header("Content-type: image/jpeg"); + // 输出一个1x1的透明图片 + echo base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'); + } +} + +/** + * 简单的日志记录类 + */ +class Logger { + private $logDir; + + public function __construct(string $logDir) { + $this->logDir = $logDir; + } + + public function error(string $message) { + $this->log('ERROR', $message); + } + + public function info(string $message) { + $this->log('INFO', $message); + } + + private function log(string $level, string $message) { + $date = date('Y-m-d'); + $time = date('Y-m-d H:i:s'); + $logFile = $this->logDir . "/{$date}.log"; + + $logMessage = "[{$time}] [{$level}] {$message}\n"; + file_put_contents($logFile, $logMessage, FILE_APPEND); + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3a4f5c --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# 图片代理与缓存系统 + +这是一个用于代理和缓存远程图片的系统,主要功能包括: + +1. 图片代理:通过URL参数获取远程图片并返回 +2. 图片缓存:自动缓存已访问的图片,提高访问速度 +3. 防盗链支持:支持设置Referer,解决部分网站的防盗链问题 + +## 使用方法 + +在浏览器中访问: +``` +/index.php?url=https://example.com/image.jpg +``` + +## 参数说明 + +- url: 必填参数,需要代理的远程图片URL + +## 技术特点 + +- 支持多种图片格式(GIF、JPEG、PNG、WebP、BMP、SVG) +- 自动创建缓存目录结构 +- 支持HTTPS图片获取 +- 自动处理重定向 +- 合理的超时设置 +- 完善的错误处理和日志记录 +- 图片数据验证 +- 缓存过期控制 + +## 安全特性 + +- 输入验证:确保URL以http或https开头 +- 文件类型验证:只允许特定图片格式 +- 目录权限控制:缓存目录权限设置 +- SSL验证:支持HTTPS图片获取 +- 图片数据验证:验证下载的图片数据是否有效 +- 错误处理:完善的错误处理和日志记录 + +## 性能优化 + +- 图片缓存机制:24小时缓存 +- 合理的超时设置 +- 内存使用优化 +- 并发处理能力 +- 缓存控制头:支持浏览器缓存 + +## 文件说明 + +- `index.php`: 入口文件,处理请求 +- `ImageProxy.php`: 核心类文件,实现图片代理和缓存功能 +- `cache/`: 缓存目录 +- `cache/logs/`: 日志目录 + +## 配置说明 + +可以通过修改`index.php`中的配置参数来调整系统行为: + +```php +$proxy = new ImageProxy([ + 'cache_dir' => 'cache', // 缓存目录 + 'timeout' => 30, // 请求超时时间(秒) + 'connect_timeout' => 15, // 连接超时时间(秒) + 'max_redirects' => 5 // 最大重定向次数 +]); +``` + +## 错误处理 + +系统会自动记录所有错误到日志文件中,日志文件位于`cache/logs/`目录下,按日期命名。 + +## 注意事项 + +1. 确保服务器有足够的磁盘空间用于缓存 +2. 确保缓存目录有写入权限 +3. 建议定期清理缓存文件 +4. 建议监控日志文件大小 \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..6a9cfbd --- /dev/null +++ b/index.php @@ -0,0 +1,32 @@ + 'cache', + 'timeout' => 30, + 'connect_timeout' => 15, + 'max_redirects' => 5 +]); + +// 处理请求 +if (!empty($url)) { + $proxy->processRequest($url); +} else { + // 输出错误信息 + header("HTTP/1.1 400 Bad Request"); + header("Content-type: image/jpeg"); + echo base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'); +} \ No newline at end of file