diff --git a/internal/config/config.go b/internal/config/config.go index 889d6e9..8024a67 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -86,12 +86,21 @@ func (cm *ConfigManager) createDefaultConfig() error { Target: "https://img1.example.com", SizeThreshold: 500 * 1024, // 500KB MaxSize: 2 * 1024 * 1024, // 2MB + Domains: "a.com,b.com", // 只对a.com和b.com域名生效 }, { Extensions: "jpg,png,webp", Target: "https://img2.example.com", SizeThreshold: 2 * 1024 * 1024, // 2MB MaxSize: 5 * 1024 * 1024, // 5MB + Domains: "b.com", // 只对b.com域名生效 + }, + { + Extensions: "mp4,avi", + Target: "https://video.example.com", + SizeThreshold: 1024 * 1024, // 1MB + MaxSize: 50 * 1024 * 1024, // 50MB + // 不指定Domains,对所有域名生效 }, }, }, diff --git a/internal/config/types.go b/internal/config/types.go index a422dc6..59df448 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -23,6 +23,7 @@ type ExtensionRule struct { SizeThreshold int64 // 最小阈值 MaxSize int64 // 最大阈值 RedirectMode bool // 是否使用302跳转模式 + Domains []string // 支持的域名列表,为空表示匹配所有域名 } type CompressionConfig struct { @@ -42,6 +43,7 @@ type ExtRuleConfig struct { SizeThreshold int64 `json:"SizeThreshold"` // 最小阈值 MaxSize int64 `json:"MaxSize"` // 最大阈值 RedirectMode bool `json:"RedirectMode"` // 是否使用302跳转模式 + Domains string `json:"Domains"` // 逗号分隔的域名列表,为空表示匹配所有域名 } // 处理扩展名映射的方法 @@ -69,6 +71,16 @@ func (p *PathConfig) ProcessExtensionMap() { } } + // 处理域名列表 + if rule.Domains != "" { + for _, domain := range strings.Split(rule.Domains, ",") { + domain = strings.TrimSpace(domain) + if domain != "" { + extRule.Domains = append(extRule.Domains, domain) + } + } + } + if len(extRule.Extensions) > 0 { p.ExtRules = append(p.ExtRules, extRule) } diff --git a/internal/handler/redirect.go b/internal/handler/redirect.go index 08d1fa9..df0b6c3 100644 --- a/internal/handler/redirect.go +++ b/internal/handler/redirect.go @@ -38,17 +38,17 @@ func (rh *RedirectHandler) HandleRedirect(w http.ResponseWriter, r *http.Request // shouldRedirect 判断是否应该进行302跳转,并返回目标URL(优化版本) func (rh *RedirectHandler) shouldRedirect(r *http.Request, pathConfig config.PathConfig, targetPath string, client *http.Client) (bool, string) { - // 使用service包的规则选择函数 - result := rh.ruleService.SelectRuleForRedirect(client, pathConfig, targetPath) + // 使用service包的规则选择函数,传递请求的域名 + result := rh.ruleService.SelectRuleForRedirect(client, pathConfig, targetPath, r.Host) if result.ShouldRedirect { // 构建完整的目标URL targetURL := rh.buildTargetURL(result.TargetURL, targetPath, r.URL.RawQuery) if result.Rule != nil { - log.Printf("[Redirect] %s -> 使用选中规则进行302跳转: %s", targetPath, targetURL) + log.Printf("[Redirect] %s -> 使用选中规则进行302跳转 (域名: %s): %s", targetPath, r.Host, targetURL) } else { - log.Printf("[Redirect] %s -> 使用默认目标进行302跳转: %s", targetPath, targetURL) + log.Printf("[Redirect] %s -> 使用默认目标进行302跳转 (域名: %s): %s", targetPath, r.Host, targetURL) } return true, targetURL diff --git a/internal/service/rule_service.go b/internal/service/rule_service.go index 8d49ec1..a795139 100644 --- a/internal/service/rule_service.go +++ b/internal/service/rule_service.go @@ -27,7 +27,7 @@ func NewRuleService(cacheManager CacheManager) *RuleService { } // SelectBestRule 选择最合适的规则 -func (rs *RuleService) SelectBestRule(client *http.Client, pathConfig config.PathConfig, path string) (*config.ExtensionRule, bool, bool) { +func (rs *RuleService) SelectBestRule(client *http.Client, pathConfig config.PathConfig, path string, requestHost string) (*config.ExtensionRule, bool, bool) { // 如果没有扩展名规则,返回nil if len(pathConfig.ExtRules) == 0 { return nil, false, false @@ -53,6 +53,20 @@ func (rs *RuleService) SelectBestRule(client *http.Client, pathConfig config.Pat return nil, false, false } + // 过滤符合域名条件的规则 + var domainMatchingRules []*config.ExtensionRule + for _, rule := range matchingRules { + if rs.isDomainMatching(rule, requestHost) { + domainMatchingRules = append(domainMatchingRules, rule) + } + } + + // 如果没有域名匹配的规则,返回nil + if len(domainMatchingRules) == 0 { + log.Printf("[SelectRule] %s -> 没有找到匹配域名 %s 的扩展名规则", path, requestHost) + return nil, false, false + } + // 获取文件大小 contentLength, err := utils.GetFileSize(client, pathConfig.DefaultTarget+path) if err != nil { @@ -62,12 +76,12 @@ func (rs *RuleService) SelectBestRule(client *http.Client, pathConfig config.Pat } // 根据文件大小找出最匹配的规则(规则已经预排序) - for _, rule := range matchingRules { + for _, rule := range domainMatchingRules { // 检查文件大小是否在阈值范围内 if contentLength >= rule.SizeThreshold && contentLength <= rule.MaxSize { // 找到匹配的规则 - log.Printf("[SelectRule] %s -> 选中规则 (文件大小: %s, 在区间 %s 到 %s 之间)", - path, utils.FormatBytes(contentLength), + log.Printf("[SelectRule] %s -> 选中规则 (域名: %s, 文件大小: %s, 在区间 %s 到 %s 之间)", + path, requestHost, utils.FormatBytes(contentLength), utils.FormatBytes(rule.SizeThreshold), utils.FormatBytes(rule.MaxSize)) // 检查目标是否可访问 @@ -85,6 +99,29 @@ func (rs *RuleService) SelectBestRule(client *http.Client, pathConfig config.Pat return nil, false, false } +// isDomainMatching 检查规则的域名是否匹配请求的域名 +func (rs *RuleService) isDomainMatching(rule *config.ExtensionRule, requestHost string) bool { + // 如果规则没有指定域名,则匹配所有域名 + if len(rule.Domains) == 0 { + return true + } + + // 提取请求域名(去除端口号) + host := requestHost + if colonIndex := strings.Index(host, ":"); colonIndex != -1 { + host = host[:colonIndex] + } + + // 检查是否匹配任一指定的域名 + for _, domain := range rule.Domains { + if strings.EqualFold(host, domain) { + return true + } + } + + return false +} + // RuleSelectionResult 规则选择结果 type RuleSelectionResult struct { Rule *config.ExtensionRule @@ -95,7 +132,7 @@ type RuleSelectionResult struct { } // SelectRuleForRedirect 专门为302跳转优化的规则选择函数 -func (rs *RuleService) SelectRuleForRedirect(client *http.Client, pathConfig config.PathConfig, path string) *RuleSelectionResult { +func (rs *RuleService) SelectRuleForRedirect(client *http.Client, pathConfig config.PathConfig, path string, requestHost string) *RuleSelectionResult { result := &RuleSelectionResult{} // 快速检查:如果没有任何302跳转配置,直接返回 @@ -106,7 +143,7 @@ func (rs *RuleService) SelectRuleForRedirect(client *http.Client, pathConfig con // 优先检查扩展名规则,即使根级别配置了302跳转 if len(pathConfig.ExtRules) > 0 { // 尝试选择最佳规则(包括文件大小检测) - if rule, found, usedAlt := rs.SelectBestRule(client, pathConfig, path); found && rule != nil && rule.RedirectMode { + if rule, found, usedAlt := rs.SelectBestRule(client, pathConfig, path, requestHost); found && rule != nil && rule.RedirectMode { result.Rule = rule result.Found = found result.UsedAltTarget = usedAlt @@ -120,7 +157,8 @@ func (rs *RuleService) SelectRuleForRedirect(client *http.Client, pathConfig con // 1. 扩展名不匹配,或者 // 2. 扩展名匹配但文件大小不在配置范围内,或者 // 3. 无法获取文件大小,或者 - // 4. 目标服务器不可访问 + // 4. 目标服务器不可访问,或者 + // 5. 域名不匹配 // 在这些情况下,我们不应该强制使用扩展名规则 } @@ -151,7 +189,7 @@ func (rs *RuleService) GetTargetURL(client *http.Client, r *http.Request, pathCo } // 使用严格的规则选择逻辑 - rule, found, usedAlt := rs.SelectBestRule(client, pathConfig, path) + rule, found, usedAlt := rs.SelectBestRule(client, pathConfig, path, r.Host) if found && rule != nil { targetBase = rule.Target usedAltTarget = usedAlt diff --git a/readme.md b/readme.md index ce3ec35..61af5ae 100644 --- a/readme.md +++ b/readme.md @@ -4,16 +4,12 @@ A 'simple' reverse proxy server written in Go. 使用方法: https://www.q58.club/t/topic/165 -``` 最新镜像地址: woodchen/proxy-go:latest -``` ## 新版统计仪表盘 ![image](https://github.com/user-attachments/assets/0b87863e-5566-4ee6-a3b7-94a994cdd572) - - ## 图片 ![image](https://github.com/user-attachments/assets/99b1767f-9470-4838-a4eb-3ce70bbe2094) @@ -22,19 +18,14 @@ A 'simple' reverse proxy server written in Go. ![image](https://github.com/user-attachments/assets/e09d0eb1-e1bb-435b-8f90-b04bc474477b) - ### 配置页 ![image](https://github.com/user-attachments/assets/5acddc06-57f5-417c-9fec-87e906dc22af) - - ### 缓存页 ![image](https://github.com/user-attachments/assets/6225b909-c5ff-4374-bb07-c472fbec791d) - - ## 说明 1. 支持gzip和brotli压缩 @@ -45,5 +36,135 @@ A 'simple' reverse proxy server written in Go. 6. 适配Cloudflare Images的图片自适应功能, 透传`Accept`头, 支持`format=auto` 7. 支持网页端监控和管理 +## 功能特性 + +- 🚀 **多路径代理**: 根据不同路径代理到不同的目标服务器 +- 🔄 **扩展名规则**: 根据文件扩展名和大小智能选择目标服务器 +- 🌐 **域名过滤**: 支持根据请求域名应用不同的扩展规则 +- 📦 **压缩支持**: 支持Gzip和Brotli压缩 +- 🎯 **302跳转**: 支持302跳转模式 +- 📊 **缓存管理**: 智能缓存机制提升性能 +- 📈 **监控指标**: 内置监控和指标收集 + +## 域名过滤功能 + +### 功能介绍 + +新增的域名过滤功能允许你为不同的请求域名配置不同的扩展规则。这在以下场景中非常有用: + +1. **多域名服务**: 一个代理服务绑定多个域名(如 a.com 和 b.com) +2. **差异化配置**: 不同域名使用不同的CDN或存储服务 +3. **精细化控制**: 根据域名和文件类型组合进行精确路由 + +### 配置示例 + +```json +{ + "MAP": { + "/images": { + "DefaultTarget": "https://default-cdn.com", + "ExtensionMap": [ + { + "Extensions": "jpg,png,webp", + "Target": "https://a-domain-cdn.com", + "SizeThreshold": 1024, + "MaxSize": 2097152, + "Domains": "a.com", + "RedirectMode": false + }, + { + "Extensions": "jpg,png,webp", + "Target": "https://b-domain-cdn.com", + "SizeThreshold": 1024, + "MaxSize": 2097152, + "Domains": "b.com", + "RedirectMode": true + }, + { + "Extensions": "mp4,avi", + "Target": "https://video-cdn.com", + "SizeThreshold": 1048576, + "MaxSize": 52428800 + // 不指定Domains,对所有域名生效 + } + ] + } + } +} +``` + +### 使用场景 + +#### 场景1: 多域名图片CDN +``` +请求: https://a.com/images/photo.jpg (1MB) +结果: 代理到 https://a-domain-cdn.com/photo.jpg + +请求: https://b.com/images/photo.jpg (1MB) +结果: 302跳转到 https://b-domain-cdn.com/photo.jpg + +请求: https://c.com/images/photo.jpg (1MB) +结果: 代理到 https://default-cdn.com/photo.jpg (使用默认目标) +``` + +#### 场景2: 域名+扩展名组合规则 +``` +请求: https://a.com/files/video.mp4 (10MB) +结果: 代理到 https://video-cdn.com/video.mp4 (匹配通用视频规则) + +请求: https://b.com/files/video.mp4 (10MB) +结果: 代理到 https://video-cdn.com/video.mp4 (匹配通用视频规则) +``` + +### 配置字段说明 + +- **Domains**: 逗号分隔的域名列表,指定该规则适用的域名 + - 为空或不设置:匹配所有域名 + - 单个域名:`"a.com"` + - 多个域名:`"a.com,b.com,c.com"` +- **Extensions**: 文件扩展名(与之前相同) +- **Target**: 目标服务器(与之前相同) +- **SizeThreshold/MaxSize**: 文件大小范围(与之前相同) +- **RedirectMode**: 是否使用302跳转(与之前相同) + +### 匹配优先级 + +1. **域名匹配**: 首先筛选出匹配请求域名的规则 +2. **扩展名匹配**: 在域名匹配的规则中筛选扩展名匹配的规则 +3. **文件大小匹配**: 根据文件大小选择最合适的规则 +4. **目标可用性**: 检查目标服务器是否可访问 +5. **默认回退**: 如果没有匹配的规则,使用默认目标 + +### 日志输出 + +启用域名过滤后,日志会包含域名信息: + +``` +[SelectRule] /image.jpg -> 选中规则 (域名: a.com, 文件大小: 1.2MB, 在区间 1KB 到 2MB 之间) +[Redirect] /image.jpg -> 使用选中规则进行302跳转 (域名: b.com): https://b-domain-cdn.com/image.jpg +``` + +## 原有功能 + +### 功能作用 + +主要是最好有一台国外服务器, 回国又不慢的, 可以反代国外资源, 然后在proxy-go外面套个cloudfront或者Edgeone, 方便国内访问. + +config里MAP的功能 + +目前我的主要使用是反代B2, R2, Oracle存储桶之类的. 也可以反代网站静态资源, 可以一并在CDN环节做缓存. + +根据config示例作示范 + +访问https://proxy-go/path1/123.jpg, 实际是访问 https://path1.com/path/path/path/123.jpg +访问https://proxy-go/path2/749.movie, 实际是访问https://path2.com/749.movie + +### mirror 固定路由 +比较适合接口类的CORS问题 + +访问https://proxy-go/mirror/https://example.com/path/to/resource + +会实际访问https://example.com/path/to/resource + diff --git a/web/app/dashboard/config/page.tsx b/web/app/dashboard/config/page.tsx index 48c6b32..6517aaa 100644 --- a/web/app/dashboard/config/page.tsx +++ b/web/app/dashboard/config/page.tsx @@ -35,6 +35,7 @@ interface ExtRuleConfig { SizeThreshold: number; // 最小阈值(字节) MaxSize: number; // 最大阈值(字节) RedirectMode?: boolean; // 是否使用302跳转模式 + Domains?: string; // 逗号分隔的域名列表,为空表示匹配所有域名 } interface PathMapping { @@ -110,6 +111,7 @@ export default function ConfigPage() { maxSize: number; sizeThresholdUnit: 'B' | 'KB' | 'MB' | 'GB'; maxSizeUnit: 'B' | 'KB' | 'MB' | 'GB'; + domains: string; }>({ extensions: "", target: "", @@ -118,6 +120,7 @@ export default function ConfigPage() { maxSize: 0, sizeThresholdUnit: 'MB', maxSizeUnit: 'MB', + domains: "", }); const [editingExtensionRule, setEditingExtensionRule] = useState<{ @@ -128,6 +131,7 @@ export default function ConfigPage() { maxSize: number; sizeThresholdUnit: 'B' | 'KB' | 'MB' | 'GB'; maxSizeUnit: 'B' | 'KB' | 'MB' | 'GB'; + domains: string; } | null>(null); // 添加扩展名规则对话框状态 @@ -527,13 +531,14 @@ export default function ConfigPage() { maxSize: 0, sizeThresholdUnit: 'MB', maxSizeUnit: 'MB', + domains: "", }); } }); }, [handleDialogOpenChange]); // 处理扩展名规则的编辑 - const handleExtensionRuleEdit = (path: string, index?: number, rule?: { Extensions: string; Target: string; SizeThreshold?: number; MaxSize?: number; RedirectMode?: boolean }) => { + const handleExtensionRuleEdit = (path: string, index?: number, rule?: { Extensions: string; Target: string; SizeThreshold?: number; MaxSize?: number; RedirectMode?: boolean; Domains?: string }) => { setEditingPath(path); if (index !== undefined && rule) { @@ -549,6 +554,7 @@ export default function ConfigPage() { maxSize: maxValue, sizeThresholdUnit: thresholdUnit, maxSizeUnit: maxUnit, + domains: rule.Domains || "", }); // 同时更新表单显示数据 @@ -560,6 +566,7 @@ export default function ConfigPage() { maxSize: maxValue, sizeThresholdUnit: thresholdUnit, maxSizeUnit: maxUnit, + domains: rule.Domains || "", }); } else { setEditingExtensionRule(null); @@ -572,6 +579,7 @@ export default function ConfigPage() { maxSize: 0, sizeThresholdUnit: 'MB', maxSizeUnit: 'MB', + domains: "", }); } @@ -582,7 +590,7 @@ export default function ConfigPage() { const addOrUpdateExtensionRule = () => { if (!config || !editingPath) return; - const { extensions, target, redirectMode, sizeThreshold, maxSize, sizeThresholdUnit, maxSizeUnit } = newExtensionRule; + const { extensions, target, redirectMode, sizeThreshold, maxSize, sizeThresholdUnit, maxSizeUnit, domains } = newExtensionRule; // 验证输入 if (!extensions.trim() || !target.trim()) { @@ -617,6 +625,23 @@ export default function ConfigPage() { return; } + // 验证域名格式(如果提供) + if (domains.trim()) { + const domainList = domains.split(',').map(d => d.trim()); + const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + + for (const domain of domainList) { + if (domain && !domainRegex.test(domain)) { + toast({ + title: "错误", + description: `域名格式不正确: ${domain}`, + variant: "destructive", + }); + return; + } + } + } + // 转换大小为字节 const sizeThresholdBytes = convertToBytes(sizeThreshold, sizeThresholdUnit); const maxSizeBytes = convertToBytes(maxSize, maxSizeUnit); @@ -642,7 +667,9 @@ export default function ConfigPage() { Extensions: extensions, Target: target, SizeThreshold: sizeThresholdBytes, - MaxSize: maxSizeBytes + MaxSize: maxSizeBytes, + RedirectMode: redirectMode, + Domains: domains.trim() || undefined }] }; } else { @@ -659,7 +686,8 @@ export default function ConfigPage() { Target: target, SizeThreshold: sizeThresholdBytes, MaxSize: maxSizeBytes, - RedirectMode: redirectMode + RedirectMode: redirectMode, + Domains: domains.trim() || undefined }; } else { // 添加新规则 @@ -668,7 +696,8 @@ export default function ConfigPage() { Target: target, SizeThreshold: sizeThresholdBytes, MaxSize: maxSizeBytes, - RedirectMode: redirectMode + RedirectMode: redirectMode, + Domains: domains.trim() || undefined }); } } @@ -684,6 +713,7 @@ export default function ConfigPage() { maxSize: 0, sizeThresholdUnit: 'MB', maxSizeUnit: 'MB', + domains: "", }); }; @@ -886,6 +916,11 @@ export default function ConfigPage() { 302 )} + {rule.Domains && ( + + 域名 + + )}
+ {rule.Domains && ( +
+ 域名: {rule.Domains} +
+ )}
阈值: {formatBytes(rule.SizeThreshold || 0)}
最大: {formatBytes(rule.MaxSize || 0)}
@@ -1024,6 +1064,17 @@ export default function ConfigPage() { placeholder="https://example.com" />
+
+ + setNewExtensionRule({ ...newExtensionRule, domains: e.target.value })} + placeholder="a.com,b.com" + /> +

+ 指定该规则适用的域名,多个域名用逗号分隔。留空表示适用于所有域名。 +

+