| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- package service
- // CN 供应商探测端点 URL 安全策略回归测试(review B4):
- // 配额/余额探测不得绕过 security.url_allowlist——base_url 衍生的探测端点
- // 必须先过运营者策略,被拒绝时不得发起任何上游请求(API key 不出站)。
- import (
- "context"
- "net/http"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
- "github.com/stretchr/testify/require"
- )
- func cnProbeAllowlistConfig(hosts ...string) *config.Config {
- return &config.Config{
- Security: config.SecurityConfig{
- URLAllowlist: config.URLAllowlistConfig{
- Enabled: true,
- UpstreamHosts: hosts,
- },
- },
- }
- }
- func TestCNValidateProbeURL_AllowlistPolicy(t *testing.T) {
- cfg := cnProbeAllowlistConfig("api.moonshot.cn", "api.deepseek.com")
- // 白名单内主机放行(保留完整路径)。
- ok, err := cnValidateProbeURL(cfg, "https://api.moonshot.cn/v1/users/me/balance")
- require.NoError(t, err)
- require.Equal(t, "https://api.moonshot.cn/v1/users/me/balance", ok)
- // 白名单外主机拒绝。
- _, err = cnValidateProbeURL(cfg, "https://relay.attacker.example/v1/usages")
- require.Error(t, err)
- require.Contains(t, err.Error(), "rejected by URL security policy")
- // 私网主机拒绝(内网探测面)。
- _, err = cnValidateProbeURL(cfg, "http://169.254.169.254/latest/meta-data")
- require.Error(t, err)
- // 白名单关闭:仅格式校验,任意 https 主机放行。
- formatOnly, err := cnValidateProbeURL(&config.Config{
- Security: config.SecurityConfig{URLAllowlist: config.URLAllowlistConfig{Enabled: false}},
- }, "https://relay.attacker.example/v1/usages")
- require.NoError(t, err)
- require.Equal(t, "https://relay.attacker.example/v1/usages", formatOnly)
- }
- // recordingHTTPUpstream 断言探测被策略拒绝时没有任何上游请求发出。
- type recordingHTTPUpstream struct{ calls int }
- func (u *recordingHTTPUpstream) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
- u.calls++
- return nil, context.DeadlineExceeded
- }
- func (u *recordingHTTPUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile) (*http.Response, error) {
- u.calls++
- return nil, context.DeadlineExceeded
- }
- type fakeCNProbeAccountRepo struct {
- AccountRepository
- account *Account
- }
- func (r *fakeCNProbeAccountRepo) GetByID(ctx context.Context, id int64) (*Account, error) {
- return r.account, nil
- }
- // kimi coding 账号的 base_url 指向中转(含 api.kimi.com/coding 路径段即可被识别
- // 为 kimi coding plan)→ 衍生额度端点落在中转主机上,白名单未列名必须拒绝。
- func TestCNProviderQuotaService_RejectsURLBlockedByPolicy(t *testing.T) {
- repo := &fakeCNProbeAccountRepo{account: &Account{
- ID: 1, Platform: PlatformKimi, Type: AccountTypeAPIKey, Status: StatusActive,
- Credentials: map[string]any{
- "account_mode": "coding",
- "api_key": "sk-test",
- "base_url": "https://relay.attacker.example/api.kimi.com/coding",
- },
- }}
- upstream := &recordingHTTPUpstream{}
- svc := NewCNProviderQuotaService(repo, nil, upstream, cnProbeAllowlistConfig("api.kimi.com"))
- _, err := svc.QueryUsage(context.Background(), 1)
- require.Error(t, err)
- require.Contains(t, err.Error(), "CN_QUOTA_URL_REJECTED")
- require.Zero(t, upstream.calls, "probe must not issue any upstream request when URL policy rejects the target")
- }
- // deepseek payg 账号自定义 base_url → 余额端点落在中转主机上,必须先过策略。
- func TestCNProviderBalanceService_RejectsURLBlockedByPolicy(t *testing.T) {
- repo := &fakeCNProbeAccountRepo{account: &Account{
- ID: 2, Platform: PlatformDeepseek, Type: AccountTypeAPIKey, Status: StatusActive,
- Credentials: map[string]any{
- "account_mode": "payg",
- "api_key": "sk-test",
- "base_url": "https://relay.attacker.example",
- },
- }}
- upstream := &recordingHTTPUpstream{}
- svc := NewCNProviderBalanceService(repo, nil, upstream, cnProbeAllowlistConfig("api.deepseek.com"))
- _, err := svc.QueryBalance(context.Background(), 2)
- require.Error(t, err)
- require.Contains(t, err.Error(), "CN_BALANCE_URL_REJECTED")
- require.Zero(t, upstream.calls, "probe must not issue any upstream request when URL policy rejects the target")
- }
- // 白名单包含官方主机的正常路径:URL 校验通过后才发出上游请求(此处允许到达
- // httpUpstream 层即视为通过校验,不发真实网络)。
- func TestCNProviderBalanceService_OfficialHostPassesValidation(t *testing.T) {
- repo := &fakeCNProbeAccountRepo{account: &Account{
- ID: 3, Platform: PlatformDeepseek, Type: AccountTypeAPIKey, Status: StatusActive,
- Credentials: map[string]any{
- "account_mode": "payg",
- "api_key": "sk-test",
- },
- }}
- upstream := &recordingHTTPUpstream{}
- svc := NewCNProviderBalanceService(repo, nil, upstream, cnProbeAllowlistConfig("api.deepseek.com"))
- _, _ = svc.QueryBalance(context.Background(), 3)
- require.Equal(t, 1, upstream.calls, "official host must pass URL policy and reach the upstream layer")
- }
|