mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(grok): harden OAuth routing and CLI version guard
This commit is contained in:
+1
-1
@@ -44,6 +44,7 @@ require (
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/image v0.39.0
|
||||
golang.org/x/mod v0.35.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.43.0
|
||||
@@ -176,7 +177,6 @@ require (
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -29,6 +28,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// 默认配置常量
|
||||
@@ -68,8 +68,6 @@ const (
|
||||
grokCLIVersionOverride = "XAI_GROK_CLI_VERSION"
|
||||
)
|
||||
|
||||
var grokCLIVersionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?$`)
|
||||
|
||||
const (
|
||||
upstreamProtocolModeDefault = "default"
|
||||
upstreamProtocolModeOpenAIH1 = "openai_h1"
|
||||
@@ -275,7 +273,7 @@ func applyGrokCLIProxyHeaders(req *http.Request) {
|
||||
req.Header = make(http.Header)
|
||||
}
|
||||
version := strings.TrimSpace(os.Getenv(grokCLIVersionOverride))
|
||||
if !grokCLIVersionPattern.MatchString(version) {
|
||||
if !isSupportedGrokCLIVersion(version) {
|
||||
version = grokCLIStableVersion
|
||||
}
|
||||
req.Header.Set("X-XAI-Token-Auth", "xai-grok-cli")
|
||||
@@ -283,6 +281,14 @@ func applyGrokCLIProxyHeaders(req *http.Request) {
|
||||
req.Header.Set("User-Agent", "xai-grok-workspace/"+version)
|
||||
}
|
||||
|
||||
func isSupportedGrokCLIVersion(version string) bool {
|
||||
canonical := "v" + version
|
||||
minimum := "v" + grokCLIStableVersion
|
||||
return semver.IsValid(canonical) &&
|
||||
semver.Canonical(canonical) == canonical &&
|
||||
semver.Compare(canonical, minimum) >= 0
|
||||
}
|
||||
|
||||
// acquireClientWithTLS 获取或创建带 TLS 指纹的客户端
|
||||
func (s *httpUpstreamService) acquireClientWithTLS(proxyURL string, accountID int64, accountConcurrency int, profile *tlsfingerprint.Profile, upstreamProfile service.HTTPUpstreamProfile) (*upstreamClientEntry, error) {
|
||||
return s.getClientEntryWithTLS(proxyURL, accountID, accountConcurrency, profile, upstreamProfile, true, true)
|
||||
|
||||
@@ -51,6 +51,47 @@ func TestApplyGrokCLIProxyHeaders(t *testing.T) {
|
||||
require.Empty(t, req.Header.Get("X-Injected"))
|
||||
})
|
||||
|
||||
t.Run("rejects an override below the supported minimum", func(t *testing.T) {
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.92")
|
||||
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
applyGrokCLIProxyHeaders(req)
|
||||
|
||||
require.Equal(t, "0.2.93", req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, "xai-grok-workspace/0.2.93", req.Header.Get("User-Agent"))
|
||||
})
|
||||
|
||||
t.Run("rejects a prerelease override at the minimum version", func(t *testing.T) {
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.93-beta.1")
|
||||
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
applyGrokCLIProxyHeaders(req)
|
||||
|
||||
require.Equal(t, "0.2.93", req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, "xai-grok-workspace/0.2.93", req.Header.Get("User-Agent"))
|
||||
})
|
||||
|
||||
for _, version := range []string{
|
||||
"0.2.093",
|
||||
"0.2.94-alpha..1",
|
||||
"0.3",
|
||||
"1",
|
||||
"0.2.95+build.1",
|
||||
} {
|
||||
t.Run("rejects invalid semver "+version, func(t *testing.T) {
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", version)
|
||||
req, err := http.NewRequest(http.MethodPost, "https://cli-chat-proxy.grok.com/v1/responses", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
applyGrokCLIProxyHeaders(req)
|
||||
|
||||
require.Equal(t, "0.2.93", req.Header.Get("x-grok-client-version"))
|
||||
require.Equal(t, "xai-grok-workspace/0.2.93", req.Header.Get("User-Agent"))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("leaves direct xAI API requests unchanged", func(t *testing.T) {
|
||||
t.Setenv("XAI_GROK_CLI_VERSION", "0.2.95")
|
||||
req, err := http.NewRequest(http.MethodPost, "https://api.x.ai/v1/responses", nil)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"hash/fnv"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -1256,8 +1257,7 @@ func (a *Account) GetGrokBaseURL() string {
|
||||
}
|
||||
baseURL := a.GetCredential("base_url")
|
||||
if a.IsGrokOAuth() {
|
||||
normalizedBaseURL := strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
if normalizedBaseURL == "" || strings.EqualFold(normalizedBaseURL, xai.DefaultBaseURL) {
|
||||
if strings.TrimSpace(baseURL) == "" || isOfficialGrokAPIBaseURL(baseURL) {
|
||||
return xai.DefaultCLIBaseURL
|
||||
}
|
||||
}
|
||||
@@ -1267,6 +1267,28 @@ func (a *Account) GetGrokBaseURL() string {
|
||||
return xai.DefaultBaseURL
|
||||
}
|
||||
|
||||
func isOfficialGrokAPIBaseURL(raw string) bool {
|
||||
parsed, err := url.Parse(strings.TrimSpace(raw))
|
||||
if err != nil || parsed == nil || parsed.Opaque != "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||
return false
|
||||
}
|
||||
defaultURL, err := url.Parse(xai.DefaultBaseURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !strings.EqualFold(parsed.Scheme, defaultURL.Scheme) || !strings.EqualFold(parsed.Hostname(), defaultURL.Hostname()) {
|
||||
return false
|
||||
}
|
||||
if port := parsed.Port(); port != "" {
|
||||
portNumber, err := strconv.Atoi(port)
|
||||
if err != nil || portNumber != 443 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
path := strings.TrimRight(parsed.Path, "/")
|
||||
return path == "" || path == strings.TrimRight(defaultURL.Path, "/")
|
||||
}
|
||||
|
||||
func (a *Account) GetGrokAccessToken() string {
|
||||
if !a.IsGrok() {
|
||||
return ""
|
||||
|
||||
@@ -199,6 +199,72 @@ func TestGetGrokBaseURLUsesSubscriptionProxyForOAuth(t *testing.T) {
|
||||
},
|
||||
expected: xai.DefaultCLIBaseURL,
|
||||
},
|
||||
{
|
||||
name: "oauth legacy API root is migrated at runtime",
|
||||
account: Account{
|
||||
Type: AccountTypeOAuth,
|
||||
Platform: PlatformGrok,
|
||||
Credentials: map[string]any{
|
||||
"base_url": "https://api.x.ai",
|
||||
},
|
||||
},
|
||||
expected: xai.DefaultCLIBaseURL,
|
||||
},
|
||||
{
|
||||
name: "oauth legacy API root with canonical HTTPS port is migrated at runtime",
|
||||
account: Account{
|
||||
Type: AccountTypeOAuth,
|
||||
Platform: PlatformGrok,
|
||||
Credentials: map[string]any{
|
||||
"base_url": "HTTPS://API.X.AI:443/",
|
||||
},
|
||||
},
|
||||
expected: xai.DefaultCLIBaseURL,
|
||||
},
|
||||
{
|
||||
name: "oauth legacy API canonical port with leading zeroes is migrated at runtime",
|
||||
account: Account{
|
||||
Type: AccountTypeOAuth,
|
||||
Platform: PlatformGrok,
|
||||
Credentials: map[string]any{
|
||||
"base_url": "https://api.x.ai:0443/v1",
|
||||
},
|
||||
},
|
||||
expected: xai.DefaultCLIBaseURL,
|
||||
},
|
||||
{
|
||||
name: "oauth legacy API encoded version path is migrated at runtime",
|
||||
account: Account{
|
||||
Type: AccountTypeOAuth,
|
||||
Platform: PlatformGrok,
|
||||
Credentials: map[string]any{
|
||||
"base_url": "https://api.x.ai/%76%31",
|
||||
},
|
||||
},
|
||||
expected: xai.DefaultCLIBaseURL,
|
||||
},
|
||||
{
|
||||
name: "oauth legacy API encoded trailing slash is migrated at runtime",
|
||||
account: Account{
|
||||
Type: AccountTypeOAuth,
|
||||
Platform: PlatformGrok,
|
||||
Credentials: map[string]any{
|
||||
"base_url": "https://api.x.ai/v1%2F",
|
||||
},
|
||||
},
|
||||
expected: xai.DefaultCLIBaseURL,
|
||||
},
|
||||
{
|
||||
name: "oauth non-default API port remains an explicit override",
|
||||
account: Account{
|
||||
Type: AccountTypeOAuth,
|
||||
Platform: PlatformGrok,
|
||||
Credentials: map[string]any{
|
||||
"base_url": "https://api.x.ai:8443/v1",
|
||||
},
|
||||
},
|
||||
expected: "https://api.x.ai:8443/v1",
|
||||
},
|
||||
{
|
||||
name: "oauth explicit custom base_url remains supported",
|
||||
account: Account{
|
||||
|
||||
Reference in New Issue
Block a user