win 9156585a23 chore: gofmt/goimports 后处理
合并上游后统一运行 gofmt/goimports,消除排序差异与空行不一致。
2026-04-24 11:52:53 +08:00

437 lines
13 KiB
Go

package windsurf
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"net/url"
"strings"
"time"
"github.com/imroc/req/v3"
)
type AuthClient struct {
Auth1BaseURL string
SeatServiceBaseURL string
CodeiumRegisterURL string
FirebaseAPIKey string
RequestTimeout time.Duration
}
type LoginResult struct {
APIKey string `json:"api_key"`
Name string `json:"name"`
Email string `json:"email"`
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
SessionToken string `json:"session_token,omitempty"`
Auth1Token string `json:"auth1_token,omitempty"`
APIServerURL string `json:"api_server_url,omitempty"`
AuthMethod string `json:"auth_method"`
ExpiresIn int `json:"expires_in,omitempty"`
}
type RefreshResult struct {
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
type RegisterResult struct {
APIKey string `json:"api_key"`
Name string `json:"name"`
APIServerURL string `json:"api_server_url"`
}
type AuthError struct {
Message string
IsAuthFail bool
FirebaseCode string
}
func (e *AuthError) Error() string { return e.Message }
var (
osVersions = []string{
"Windows NT 10.0; Win64; x64",
"Macintosh; Intel Mac OS X 10_15_7",
"Macintosh; Intel Mac OS X 13_4_1",
"Macintosh; Intel Mac OS X 14_2_1",
"X11; Linux x86_64",
}
chromeVersions = []string{
"120.0.0.0", "122.0.0.0", "124.0.0.0", "126.0.0.0",
"128.0.0.0", "130.0.0.0", "132.0.0.0", "134.0.0.0",
}
acceptLanguages = []string{
"en-US,en;q=0.9", "zh-CN,zh;q=0.9,en;q=0.8",
"ja,en-US;q=0.9,en;q=0.8", "de,en-US;q=0.9,en;q=0.8",
}
)
func pick(arr []string) string { return arr[rand.Intn(len(arr))] }
func generateFingerprint() http.Header {
os := pick(osVersions)
cv := pick(chromeVersions)
major := strings.Split(cv, ".")[0]
ua := fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", os, cv)
h := http.Header{}
h.Set("User-Agent", ua)
h.Set("Accept-Language", pick(acceptLanguages))
h.Set("Accept", "application/json, text/plain, */*")
h.Set("Accept-Encoding", "identity")
h.Set("sec-ch-ua", fmt.Sprintf(`"Chromium";v="%s", "Google Chrome";v="%s", "Not-A.Brand";v="99"`, major, major))
h.Set("sec-ch-ua-mobile", "?0")
if strings.Contains(os, "Windows") {
h.Set("sec-ch-ua-platform", `"Windows"`)
} else if strings.Contains(os, "Mac") {
h.Set("sec-ch-ua-platform", `"macOS"`)
} else {
h.Set("sec-ch-ua-platform", `"Linux"`)
}
h.Set("Sec-Fetch-Dest", "empty")
h.Set("Sec-Fetch-Mode", "cors")
h.Set("Sec-Fetch-Site", "cross-site")
h.Set("Origin", "https://windsurf.com")
h.Set("Referer", "https://windsurf.com/")
return h
}
func newClient(timeout time.Duration, proxyURL string) *req.Client {
c := req.C().SetTimeout(timeout).ImpersonateChrome()
if proxyURL != "" {
c.SetProxyURL(proxyURL)
}
return c
}
func (a *AuthClient) Login(ctx context.Context, email, password, proxyURL string) (*LoginResult, error) {
fp := generateFingerprint()
connData, _ := a.fetchAuth1Connections(ctx, email, fp, proxyURL)
authMethod, _ := extractString(connData, "auth_method", "method")
if authMethod == "auth1" {
hasPassword, _ := extractBool(connData, "auth_method", "has_password")
if !hasPassword {
return nil, &AuthError{
Message: "该账号未设置密码登录方式",
IsAuthFail: true,
}
}
return a.loginViaAuth1(ctx, email, password, fp, proxyURL)
}
result, fbErr := a.loginViaFirebase(ctx, email, password, fp, proxyURL)
if fbErr == nil {
return result, nil
}
if ae, ok := fbErr.(*AuthError); ok && ae.IsAuthFail {
result2, a1Err := a.loginViaAuth1(ctx, email, password, fp, proxyURL)
if a1Err == nil {
return result2, nil
}
if ae2, ok2 := a1Err.(*AuthError); ok2 && ae2.IsAuthFail {
return nil, fbErr
}
return nil, a1Err
}
return nil, fbErr
}
func (a *AuthClient) fetchAuth1Connections(ctx context.Context, email string, fp http.Header, proxyURL string) (map[string]any, error) {
body := map[string]string{"product": "windsurf", "email": email}
var result map[string]any
c := newClient(a.RequestTimeout, proxyURL)
resp, err := c.R().SetContext(ctx).SetHeaders(headerMap(fp)).SetBody(body).SetSuccessResult(&result).Post(a.Auth1BaseURL + "/_devin-auth/connections")
if err != nil {
return nil, err
}
if resp.IsErrorState() {
return nil, fmt.Errorf("auth1 connections: status %d", resp.StatusCode)
}
return result, nil
}
func (a *AuthClient) loginViaAuth1(ctx context.Context, email, password string, fp http.Header, proxyURL string) (*LoginResult, error) {
c := newClient(a.RequestTimeout, proxyURL)
var loginResp map[string]any
resp, err := c.R().SetContext(ctx).SetHeaders(headerMap(fp)).
SetBody(map[string]string{"email": email, "password": password}).
SetSuccessResult(&loginResp).
Post(a.Auth1BaseURL + "/_devin-auth/password/login")
if err != nil {
return nil, fmt.Errorf("auth1 login: %w", err)
}
if resp.IsErrorState() || loginResp["detail"] != nil {
detail, _ := loginResp["detail"].(string)
return nil, classifyAuthError("Auth1 登录失败", detail)
}
auth1Token, _ := loginResp["token"].(string)
if auth1Token == "" {
return nil, fmt.Errorf("auth1 login: no token in response")
}
hdrs := headerMap(fp)
hdrs["Connect-Protocol-Version"] = "1"
var bridgeResp map[string]any
resp, err = c.R().SetContext(ctx).SetHeaders(hdrs).
SetBody(map[string]string{"auth1Token": auth1Token, "orgId": ""}).
SetSuccessResult(&bridgeResp).
Post(a.SeatServiceBaseURL + "/WindsurfPostAuth")
if err != nil {
return nil, fmt.Errorf("windsurf post auth: %w", err)
}
if resp.IsErrorState() {
return nil, fmt.Errorf("windsurf post auth: status %d", resp.StatusCode)
}
sessionToken, _ := bridgeResp["sessionToken"].(string)
if sessionToken == "" {
return nil, fmt.Errorf("windsurf post auth: no sessionToken")
}
var ottResp map[string]any
resp, err = c.R().SetContext(ctx).SetHeaders(hdrs).
SetBody(map[string]string{"authToken": sessionToken}).
SetSuccessResult(&ottResp).
Post(a.SeatServiceBaseURL + "/GetOneTimeAuthToken")
if err != nil {
return nil, fmt.Errorf("get one-time token: %w", err)
}
if resp.IsErrorState() {
return nil, fmt.Errorf("get one-time token: status %d", resp.StatusCode)
}
oneTimeToken, _ := ottResp["authToken"].(string)
if oneTimeToken == "" {
return nil, fmt.Errorf("get one-time token: no authToken")
}
reg, err := a.RegisterWithCodeium(ctx, oneTimeToken, fp, proxyURL)
if err != nil {
return nil, fmt.Errorf("codeium register (auth1): %w", err)
}
return &LoginResult{
APIKey: reg.APIKey,
Name: reg.Name,
Email: email,
APIServerURL: reg.APIServerURL,
SessionToken: sessionToken,
Auth1Token: auth1Token,
AuthMethod: "auth1",
}, nil
}
func (a *AuthClient) loginViaFirebase(ctx context.Context, email, password string, fp http.Header, proxyURL string) (*LoginResult, error) {
c := newClient(a.RequestTimeout, proxyURL)
firebaseURL := fmt.Sprintf("https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=%s", a.FirebaseAPIKey)
body := map[string]any{"email": email, "password": password, "returnSecureToken": true}
var fbResp map[string]any
resp, err := c.R().SetContext(ctx).SetHeaders(headerMap(fp)).SetBody(body).SetSuccessResult(&fbResp).Post(firebaseURL)
if err != nil {
return nil, fmt.Errorf("firebase login: %w", err)
}
if errObj, ok := fbResp["error"].(map[string]any); ok {
msg, _ := errObj["message"].(string)
return nil, classifyAuthError("Firebase 登录失败", msg)
}
if resp.IsErrorState() {
return nil, fmt.Errorf("firebase login: status %d", resp.StatusCode)
}
idToken, _ := fbResp["idToken"].(string)
if idToken == "" {
return nil, fmt.Errorf("firebase login: no idToken")
}
refreshToken, _ := fbResp["refreshToken"].(string)
reg, err := a.RegisterWithCodeium(ctx, idToken, fp, proxyURL)
if err != nil {
return nil, fmt.Errorf("codeium register (firebase): %w", err)
}
return &LoginResult{
APIKey: reg.APIKey,
Name: reg.Name,
Email: email,
IDToken: idToken,
RefreshToken: refreshToken,
APIServerURL: reg.APIServerURL,
AuthMethod: "firebase",
}, nil
}
func (a *AuthClient) RegisterWithCodeium(ctx context.Context, token string, fp http.Header, proxyURL string) (*RegisterResult, error) {
c := newClient(a.RequestTimeout, proxyURL)
body := map[string]string{"firebase_id_token": token}
var regResp map[string]any
resp, err := c.R().SetContext(ctx).SetHeaders(headerMap(fp)).SetBody(body).SetSuccessResult(&regResp).Post(a.CodeiumRegisterURL)
if err != nil {
return nil, err
}
if resp.IsErrorState() {
data, _ := json.Marshal(regResp)
return nil, fmt.Errorf("codeium register: status %d: %s", resp.StatusCode, string(data))
}
apiKey, _ := regResp["api_key"].(string)
if apiKey == "" {
return nil, fmt.Errorf("codeium register: no api_key in response")
}
name, _ := regResp["name"].(string)
apiServerURL, _ := regResp["api_server_url"].(string)
return &RegisterResult{APIKey: apiKey, Name: name, APIServerURL: apiServerURL}, nil
}
func (a *AuthClient) RefreshFirebaseToken(ctx context.Context, refreshToken, proxyURL string) (*RefreshResult, error) {
if refreshToken == "" {
return nil, fmt.Errorf("no refresh token available")
}
refreshURL := fmt.Sprintf("https://securetoken.googleapis.com/v1/token?key=%s", a.FirebaseAPIKey)
postBody := fmt.Sprintf("grant_type=refresh_token&refresh_token=%s", url.QueryEscape(refreshToken))
c := newClient(a.RequestTimeout, proxyURL)
var result map[string]any
resp, err := c.R().SetContext(ctx).
SetHeader("Content-Type", "application/x-www-form-urlencoded").
SetHeader("Referer", "https://windsurf.com/").
SetHeader("Origin", "https://windsurf.com").
SetBodyString(postBody).
SetSuccessResult(&result).
Post(refreshURL)
if err != nil {
return nil, fmt.Errorf("firebase refresh: %w", err)
}
if resp.IsErrorState() {
if errObj, ok := result["error"].(map[string]any); ok {
msg, _ := errObj["message"].(string)
return nil, fmt.Errorf("firebase refresh: %s", msg)
}
return nil, fmt.Errorf("firebase refresh: status %d", resp.StatusCode)
}
idToken := firstString(result, "id_token", "idToken")
if idToken == "" {
return nil, fmt.Errorf("firebase refresh: no idToken in response")
}
newRefresh := firstString(result, "refresh_token", "refreshToken")
if newRefresh == "" {
newRefresh = refreshToken
}
expiresIn := 3600
if v, ok := result["expires_in"].(string); ok {
fmt.Sscanf(v, "%d", &expiresIn)
} else if v, ok := result["expiresIn"].(string); ok {
fmt.Sscanf(v, "%d", &expiresIn)
}
return &RefreshResult{IDToken: idToken, RefreshToken: newRefresh, ExpiresIn: expiresIn}, nil
}
func (a *AuthClient) ReRegisterWithCodeium(ctx context.Context, idToken, proxyURL string) (*RegisterResult, error) {
fp := generateFingerprint()
return a.RegisterWithCodeium(ctx, idToken, fp, proxyURL)
}
func classifyAuthError(prefix, detail string) *AuthError {
authFails := map[string]bool{
"EMAIL_NOT_FOUND": true,
"INVALID_PASSWORD": true,
"INVALID_LOGIN_CREDENTIALS": true,
"Invalid email or password": true,
"No password set. Please log in with Google or GitHub.": true,
"No password set": true,
}
friendly := map[string]string{
"EMAIL_NOT_FOUND": "该邮箱未注册",
"INVALID_PASSWORD": "密码错误",
"INVALID_LOGIN_CREDENTIALS": "邮箱或密码错误",
"Invalid email or password": "邮箱或密码错误",
"USER_DISABLED": "账号已被停用",
"TOO_MANY_ATTEMPTS_TRY_LATER": "尝试太多次,请稍后再试",
"INVALID_EMAIL": "邮箱格式错误",
}
msg := detail
if f, ok := friendly[detail]; ok {
msg = f
}
return &AuthError{
Message: fmt.Sprintf("%s: %s", prefix, msg),
IsAuthFail: authFails[detail],
FirebaseCode: detail,
}
}
func headerMap(h http.Header) map[string]string {
m := make(map[string]string, len(h))
for k := range h {
m[k] = h.Get(k)
}
return m
}
func extractString(data map[string]any, keys ...string) (string, bool) {
current := data
for i, k := range keys {
if i == len(keys)-1 {
v, ok := current[k].(string)
return v, ok
}
next, ok := current[k].(map[string]any)
if !ok {
return "", false
}
current = next
}
return "", false
}
func extractBool(data map[string]any, keys ...string) (bool, bool) {
current := data
for i, k := range keys {
if i == len(keys)-1 {
v, ok := current[k].(bool)
return v, ok
}
next, ok := current[k].(map[string]any)
if !ok {
return false, false
}
current = next
}
return false, false
}
func firstString(m map[string]any, keys ...string) string {
for _, k := range keys {
if v, ok := m[k].(string); ok && v != "" {
return v
}
}
return ""
}