feat(ImageProxy): 添加域名白名单和日志管理功能

- 在ImageProxy中添加了域名白名单配置,支持精确和子域名匹配,增强安全性。
- 实现了日志文件的自动清理和轮转功能,限制日志文件大小、保留时间和数量,优化日志管理。
- 更新README文档,详细说明了新功能和配置方法。
This commit is contained in:
2025-05-28 02:04:34 +08:00
parent 192fc2f45c
commit 39d21d3a6a
3 changed files with 156 additions and 9 deletions

View File

@@ -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}");
}