2025-12-20 11:56:11 +08:00
package repository
import (
"context"
"fmt"
"io"
2026-03-02 15:53:26 +08:00
"log/slog"
2025-12-20 11:56:11 +08:00
"net/http"
"strings"
"time"
2025-12-31 08:50:12 +08:00
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
2025-12-24 21:07:21 +08:00
"github.com/Wei-Shaw/sub2api/internal/service"
2025-12-20 11:56:11 +08:00
)
type pricingRemoteClient struct {
httpClient * http . Client
}
2026-03-02 15:53:26 +08:00
// pricingRemoteClientError 代理初始化失败时的错误占位客户端
// 所有请求直接返回初始化错误,禁止回退到直连
type pricingRemoteClientError struct {
err error
}
func ( c * pricingRemoteClientError ) FetchPricingJSON ( _ context . Context , _ string ) ( [ ] byte , error ) {
return nil , c . err
}
func ( c * pricingRemoteClientError ) FetchHashText ( _ context . Context , _ string ) ( string , error ) {
return "" , c . err
}
2026-01-06 15:55:36 +08:00
// NewPricingRemoteClient 创建定价数据远程客户端
// proxyURL 为空时直连,支持 http/https/socks5/socks5h 协议
2026-03-02 15:53:26 +08:00
// 代理配置失败时行为由 allowDirectOnProxyError 控制:
// - false( 默认) : 返回错误占位客户端, 禁止回退到直连
// - true: 回退到直连( 仅限管理员显式开启)
func NewPricingRemoteClient ( proxyURL string , allowDirectOnProxyError bool ) service . PricingRemoteClient {
// 安全说明: httpclient.GetClient 的错误链( url.Parse / proxyutil) 不含明文代理凭据,
// 但仍通过 slog 仅在服务端日志记录,不会暴露给 HTTP 响应。
2025-12-31 08:50:12 +08:00
sharedClient , err := httpclient . GetClient ( httpclient . Options {
2026-01-06 15:55:36 +08:00
Timeout : 30 * time . Second ,
ProxyURL : proxyURL ,
2025-12-31 08:50:12 +08:00
} )
if err != nil {
2026-03-02 15:53:26 +08:00
if strings . TrimSpace ( proxyURL ) != "" && ! allowDirectOnProxyError {
slog . Warn ( "proxy client init failed, all requests will fail" , "service" , "pricing" , "error" , err )
return & pricingRemoteClientError { err : fmt . Errorf ( "proxy client init failed and direct fallback is disabled; set security.proxy_fallback.allow_direct_on_error=true to allow fallback: %w" , err ) }
}
2025-12-31 08:50:12 +08:00
sharedClient = & http . Client { Timeout : 30 * time . Second }
}
2025-12-20 11:56:11 +08:00
return & pricingRemoteClient {
2025-12-31 08:50:12 +08:00
httpClient : sharedClient ,
2025-12-20 11:56:11 +08:00
}
}
func ( c * pricingRemoteClient ) FetchPricingJSON ( ctx context . Context , url string ) ( [ ] byte , error ) {
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , url , nil )
if err != nil {
return nil , err
}
resp , err := c . httpClient . Do ( req )
if err != nil {
return nil , err
}
2025-12-20 15:29:52 +08:00
defer func ( ) { _ = resp . Body . Close ( ) } ( )
2025-12-20 11:56:11 +08:00
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "HTTP %d" , resp . StatusCode )
}
return io . ReadAll ( resp . Body )
}
func ( c * pricingRemoteClient ) FetchHashText ( ctx context . Context , url string ) ( string , error ) {
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , url , nil )
if err != nil {
return "" , err
}
resp , err := c . httpClient . Do ( req )
if err != nil {
return "" , err
}
2025-12-20 15:29:52 +08:00
defer func ( ) { _ = resp . Body . Close ( ) } ( )
2025-12-20 11:56:11 +08:00
if resp . StatusCode != http . StatusOK {
return "" , fmt . Errorf ( "HTTP %d" , resp . StatusCode )
}
body , err := io . ReadAll ( resp . Body )
if err != nil {
return "" , err
}
// 哈希文件格式: hash filename 或者纯 hash
hash := strings . TrimSpace ( string ( body ) )
parts := strings . Fields ( hash )
if len ( parts ) > 0 {
return parts [ 0 ] , nil
}
return hash , nil
}