Add media proxy feature for resource downloading
- Introduced a new API endpoint `GET /api/v1/video/proxy` to facilitate media resource downloads, allowing users to bypass domain restrictions imposed by WeChat. - Updated configuration to include proxy settings such as `SHORT_VIDEO_PROXY_ENABLED`, `SHORT_VIDEO_PROXY_ALLOWED_DOMAINS`, `SHORT_VIDEO_PROXY_MAX_SIZE_MB`, and `SHORT_VIDEO_PROXY_TIMEOUT_SECONDS`. - Enhanced the `ShortVideoConfig` struct to accommodate new proxy-related fields. - Improved error handling for proxy requests, including checks for allowed domains and file size limits. - Updated documentation to reflect the new proxy functionality and its configuration options, ensuring clarity for users and developers.
This commit is contained in:
@@ -27,6 +27,10 @@ var (
|
||||
ErrURLNotFound = errors.New("no valid url found in content")
|
||||
ErrShortVideoAPIKey = errors.New("short video api key is not configured")
|
||||
ErrDailyQuotaExceeded = errors.New("daily free quota exceeded, please watch an ad to continue")
|
||||
ErrProxyDisabled = errors.New("media proxy is disabled")
|
||||
ErrProxyDomainBlocked = errors.New("domain is not in the allowed list")
|
||||
ErrProxyFileTooLarge = errors.New("file size exceeds the limit")
|
||||
ErrProxyInvalidURL = errors.New("invalid proxy url")
|
||||
)
|
||||
|
||||
type VideoService struct {
|
||||
@@ -292,3 +296,131 @@ func (s *VideoService) ReportDownloadFailure(ctx context.Context, report Downloa
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProxyMediaResult 代理媒体请求的结果
|
||||
type ProxyMediaResult struct {
|
||||
Body io.ReadCloser
|
||||
ContentType string
|
||||
ContentLength int64
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
// ProxyMedia 代理媒体资源下载,用于绕过微信小程序的域名限制
|
||||
// 该方法会验证目标URL是否在允许的域名白名单内,并流式转发响应
|
||||
func (s *VideoService) ProxyMedia(ctx context.Context, targetURL string) (*ProxyMediaResult, error) {
|
||||
// 检查是否启用代理功能
|
||||
if !s.cfg.ProxyEnabled {
|
||||
return nil, ErrProxyDisabled
|
||||
}
|
||||
|
||||
// 验证并解析 URL
|
||||
parsed, err := url.Parse(targetURL)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return nil, ErrProxyInvalidURL
|
||||
}
|
||||
|
||||
// 只允许 http/https 协议
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return nil, ErrProxyInvalidURL
|
||||
}
|
||||
|
||||
// 检查域名白名单(如果配置了的话)
|
||||
if len(s.cfg.ProxyAllowedDomains) > 0 {
|
||||
allowed := false
|
||||
for _, domain := range s.cfg.ProxyAllowedDomains {
|
||||
if strings.HasSuffix(parsed.Host, domain) || parsed.Host == domain {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return nil, ErrProxyDomainBlocked
|
||||
}
|
||||
}
|
||||
|
||||
// 创建带超时的 HTTP 客户端(代理请求可能需要更长时间)
|
||||
proxyTimeout := s.cfg.ProxyTimeout
|
||||
if proxyTimeout <= 0 {
|
||||
proxyTimeout = 60 * time.Second
|
||||
}
|
||||
proxyClient := &http.Client{
|
||||
Timeout: proxyTimeout,
|
||||
}
|
||||
|
||||
// 构建代理请求
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build proxy request: %w", err)
|
||||
}
|
||||
|
||||
// 设置常见的请求头,模拟正常浏览器行为
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
req.Header.Set("Referer", fmt.Sprintf("%s://%s/", parsed.Scheme, parsed.Host))
|
||||
|
||||
// 发起请求
|
||||
resp, err := proxyClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proxy request failed: %w", err)
|
||||
}
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||
resp.Body.Close()
|
||||
return nil, &ThirdPartyError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Message: fmt.Sprintf("upstream returned status %d", resp.StatusCode),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件大小限制
|
||||
if s.cfg.ProxyMaxSize > 0 && resp.ContentLength > s.cfg.ProxyMaxSize {
|
||||
resp.Body.Close()
|
||||
return nil, ErrProxyFileTooLarge
|
||||
}
|
||||
|
||||
// 获取 Content-Type
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
return &ProxyMediaResult{
|
||||
Body: resp.Body,
|
||||
ContentType: contentType,
|
||||
ContentLength: resp.ContentLength,
|
||||
StatusCode: resp.StatusCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateProxyURL 验证代理URL是否有效(不实际请求,只做格式和白名单检查)
|
||||
func (s *VideoService) ValidateProxyURL(targetURL string) error {
|
||||
if !s.cfg.ProxyEnabled {
|
||||
return ErrProxyDisabled
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(targetURL)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return ErrProxyInvalidURL
|
||||
}
|
||||
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return ErrProxyInvalidURL
|
||||
}
|
||||
|
||||
if len(s.cfg.ProxyAllowedDomains) > 0 {
|
||||
allowed := false
|
||||
for _, domain := range s.cfg.ProxyAllowedDomains {
|
||||
if strings.HasSuffix(parsed.Host, domain) || parsed.Host == domain {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return ErrProxyDomainBlocked
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user