Files
2026-06-10 05:12:15 +00:00

330 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/gin-gonic/gin"
)
// ── ProxyOptions ────────────────────────────────────────────
// ProxyOptions 代理行为配置,便于未来扩展不同资源类型的代理端点
type ProxyOptions struct {
// 上游请求头配置
UserAgent string // 自定义 UA空 = 使用默认 Chrome UA
Referer string // 自定义 Referer空 = 不发送 Referer
Origin string // 自定义 Origin空 = 不发送)
ExtraHeader map[string]string
// 上游请求行为
Timeout time.Duration // 请求超时0 = 无超时,适用于流式传输)
FollowRedirect bool // 是否跟随重定向(默认 true
// 客户端请求头转发(从客户端透传到上游)
ForwardRange bool // 转发 Range 头(视频拖动进度)
ForwardAccept bool // 转发 Accept 头
// 响应处理
Streaming bool // 流式传输(边读边写,适用大文件)
BufferSize int // 流式缓冲区大小0 = 32KB
DefaultContentType string // 上游无 Content-Type 时的回退值
ResponseHeaders map[string]string // 额外响应头(如 Cache-Control
// 安全
MaxResponseBytes int64 // 响应体大小上限0 = 不限制)
}
func defaultOptions() ProxyOptions {
return ProxyOptions{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36",
FollowRedirect: true,
Streaming: true,
BufferSize: 32 * 1024,
}
}
// ── ProxyHandler ────────────────────────────────────────────
// ProxyHandler 通用代理处理器
// 路由:
//
// GET /api/proxy/video?url=... 视频流代理(无超时,转发 Range
// GET /api/proxy/resource?url=... 通用资源代理(有超时,可配置缓存头)
type ProxyHandler struct{}
// NewProxyHandler 创建代理处理器
func NewProxyHandler() *ProxyHandler {
return &ProxyHandler{}
}
// RegisterRoutes 注册路由
func (h *ProxyHandler) RegisterRoutes(r *gin.RouterGroup) {
r.GET("/proxy/video", h.ProxyVideo)
r.GET("/proxy/resource", h.ProxyResource)
}
// ── 视频流代理 ──────────────────────────────────────────────
// ProxyVideo GET /api/proxy/video?url=...
// @Summary 视频代理
// @Description 服务端代理获取视频流,绕过 CDN Referer/CORS 限制。
// @Description 前端播放器通过此端点加载视频,上游请求不带 Referer。
// @Tags proxy
// @Produce octet-stream
// @Param url query string true "视频源 URL需 URL 编码)"
// @Success 200 {file} binary
// @Failure 400 {object} map[string]interface{}
// @Failure 502 {object} map[string]interface{}
// @Router /proxy/video [get]
func (h *ProxyHandler) ProxyVideo(c *gin.Context) {
opts := defaultOptions()
opts.ForwardRange = true // 支持拖动进度条
opts.DefaultContentType = "video/mp4"
// Timeout 保持 0 —— 视频播放不设超时
h.baseProxy(c, "video", opts)
}
// ── 通用资源代理 ────────────────────────────────────────────
// ProxyResource GET /api/proxy/resource?url=...
// @Summary 资源代理
// @Description 通用资源代理端点,用于代理图片、音频、文件等。
// @Description 支持通过查询参数配置行为referer, origin, timeout 等)。
// @Tags proxy
// @Produce octet-stream
// @Param url query string true "资源 URL需 URL 编码)"
// @Param referer query string false "自定义 Referer空 = 不发送)"
// @Param timeout query int false "超时秒数0 = 不超时,默认 30"
// @Param streaming query string false "是否流式传输true/false默认 true"
// @Success 200 {file} binary
// @Failure 400 {object} map[string]interface{}
// @Failure 502 {object} map[string]interface{}
// @Router /proxy/resource [get]
func (h *ProxyHandler) ProxyResource(c *gin.Context) {
opts := defaultOptions()
// 从查询参数覆盖配置
if ref := c.Query("referer"); ref != "" {
opts.Referer = ref
}
if c.Query("timeout") != "" {
var sec int
if _, err := fmt.Sscanf(c.Query("timeout"), "%d", &sec); err == nil && sec > 0 {
opts.Timeout = time.Duration(sec) * time.Second
}
} else {
opts.Timeout = 30 * time.Second // 默认 30s
}
opts.Streaming = c.Query("streaming") != "false"
// 资源代理默认 1h 浏览器缓存
opts.ResponseHeaders = map[string]string{
"Cache-Control": "public, max-age=3600",
}
h.baseProxy(c, "resource", opts)
}
// ── 核心代理逻辑 ────────────────────────────────────────────
// baseProxy 可复用的代理核心,所有代理端点共享。
//
// 流程:
// 1. 解析 & 验证上游 URL
// 2. 构建上游请求(配置化 Header
// 3. 执行请求(可选超时、重定向跟随)
// 4. 流式透传响应体到客户端
// 5. 错误统一返回 JSON
func (h *ProxyHandler) baseProxy(c *gin.Context, proxyType string, opts ProxyOptions) {
rawURL := c.Query("url")
if rawURL == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "缺少 url 参数"})
return
}
// 解码 URL支持已编码和未编码输入
decoded, err := url.QueryUnescape(rawURL)
if err != nil {
decoded = rawURL
}
// 创建上游请求
req, err := http.NewRequest("GET", decoded, nil)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的 URL"})
return
}
// ── 上游请求头 ──────────────────────────────────────
req.Header.Set("User-Agent", opts.UserAgent)
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
if opts.Referer != "" {
req.Header.Set("Referer", opts.Referer)
}
// 未设置 Referer = 不发送,模仿地址栏直接访问
if opts.Origin != "" {
req.Header.Set("Origin", opts.Origin)
}
for k, v := range opts.ExtraHeader {
req.Header.Set(k, v)
}
// ── 客户端请求头转发 ────────────────────────────────
if opts.ForwardRange {
if rh := c.GetHeader("Range"); rh != "" {
req.Header.Set("Range", rh)
}
}
if opts.ForwardAccept {
if ah := c.GetHeader("Accept"); ah != "" {
req.Header.Set("Accept", ah)
}
} else {
req.Header.Set("Accept", "*/*")
}
// ── HTTP Client ─────────────────────────────────────
client := &http.Client{
Timeout: opts.Timeout,
}
if opts.FollowRedirect {
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return nil // 跟随重定向,不携带 Referer
}
} else {
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}
// ── 执行请求 ────────────────────────────────────────
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{
"code": 1,
"msg": fmt.Sprintf("%s 代理请求失败: %s", proxyType, err.Error()),
"proxy_type": proxyType,
})
return
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
c.JSON(http.StatusBadGateway, gin.H{
"code": 1,
"msg": fmt.Sprintf("上游返回 %d", resp.StatusCode),
"upstream_status": resp.StatusCode,
"proxy_type": proxyType,
})
return
}
// ── 响应头透传 ──────────────────────────────────────
contentType := resp.Header.Get("Content-Type")
if contentType == "" && opts.DefaultContentType != "" {
contentType = opts.DefaultContentType
}
if contentType != "" {
c.Header("Content-Type", contentType)
}
copyHeader := func(key string) {
if v := resp.Header.Get(key); v != "" {
c.Header(key, v)
}
}
copyHeader("Content-Length")
copyHeader("Content-Range")
copyHeader("Accept-Ranges")
copyHeader("ETag")
copyHeader("Last-Modified")
copyHeader("Content-Disposition")
// 确保 Accept-Ranges 存在(视频 seek 需要)
if resp.Header.Get("Accept-Ranges") == "" && opts.ForwardRange {
c.Header("Accept-Ranges", "bytes")
}
// 额外响应头
for k, v := range opts.ResponseHeaders {
c.Header(k, v)
}
c.Status(resp.StatusCode)
// ── 响应体传输 ──────────────────────────────────────
if !opts.Streaming {
// 非流式:一次性读取全部
body, err := io.ReadAll(resp.Body)
if err != nil {
return
}
c.Writer.Write(body)
return
}
// 流式:边读边写
bufSize := opts.BufferSize
if bufSize <= 0 {
bufSize = 32 * 1024
}
buf := make([]byte, bufSize)
written := int64(0)
for {
n, readErr := resp.Body.Read(buf)
if n > 0 {
if opts.MaxResponseBytes > 0 && written+int64(n) > opts.MaxResponseBytes {
// 截断(安全上限)
remaining := opts.MaxResponseBytes - written
if remaining > 0 {
c.Writer.Write(buf[:remaining])
}
return
}
if _, writeErr := c.Writer.Write(buf[:n]); writeErr != nil {
return // 客户端断开
}
written += int64(n)
// 每 128KB flush 一次
if written%(int64(bufSize)*4) == 0 {
c.Writer.Flush()
}
}
if readErr == io.EOF {
break
}
if readErr != nil {
return
}
}
}
// ── 辅助函数 ────────────────────────────────────────────────
// ProxyVideoURL 构建视频代理 URL
func ProxyVideoURL(videoURL string) string {
return "/api/proxy/video?url=" + url.QueryEscape(videoURL)
}
// ProxyResourceURL 构建资源代理 URL
func ProxyResourceURL(resourceURL string, referer string) string {
if referer != "" {
return "/api/proxy/resource?url=" + url.QueryEscape(resourceURL) +
"&referer=" + url.QueryEscape(referer)
}
return "/api/proxy/resource?url=" + url.QueryEscape(resourceURL)
}
// ProxyURL 构建代理 URL向后兼容等同于 ProxyVideoURL
func ProxyURL(videoURL string) string {
return ProxyVideoURL(videoURL)
}