package auth import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "strings" "time" "git.misaka.ren/M1saka/zhanlu_proxy/internal/util" ) // ExchangeCode exchanges an SSO auth code for a user profile via the Zhanlu // gateway authToken endpoint (POST /api/acepilot/zhanlu/authToken). The // profile fields may be AES-ECB encrypted with the token decrypt key; each // field falls back to the raw value when decryption fails. func ExchangeCode(client *http.Client, endpoint string, code string, decryptKey string) (Profile, error) { if strings.TrimSpace(endpoint) == "" { return Profile{}, errors.New("exchange endpoint is required") } if strings.TrimSpace(code) == "" { return Profile{}, errors.New("code is required") } if client == nil { // HTTP/1.1 only: the Zhanlu gateway drops HTTP/2 negotiation. client = &http.Client{Timeout: 60 * time.Second, Transport: &http.Transport{ForceAttemptHTTP2: false}} } body, _ := json.Marshal(map[string]string{"deputyAccountNumber": code}) req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { return Profile{}, err } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return Profile{}, err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) return Profile{}, fmt.Errorf("exchange returned %d: %s", resp.StatusCode, strings.TrimSpace(string(b))) } var exchange ExchangeResponse if err := json.NewDecoder(resp.Body).Decode(&exchange); err != nil { return Profile{}, err } if exchange.ErrorCode != "" && exchange.ErrorCode != "Success" { msg := util.FirstNonEmpty(exchange.ErrorMsg, exchange.Message, exchange.ErrorCode) if msg == "" { msg = "unknown error" } return Profile{}, fmt.Errorf("exchange failed: %s", msg) } if exchange.State != "" && exchange.State != "OK" { msg := util.FirstNonEmpty(exchange.ErrorMsg, exchange.Message, exchange.State) if msg == "" { msg = "unknown error" } return Profile{}, fmt.Errorf("exchange failed: %s", msg) } profile := Profile{} for _, m := range []map[string]any{exchange.Body, exchange.Result, exchange.Data} { if m == nil { continue } profile.Email = decryptProfileField(m, "email", decryptKey) profile.Organization = decryptProfileField(m, "organization", decryptKey) profile.Team = decryptProfileField(m, "team", decryptKey) profile.UserName = decryptProfileField(m, "name", decryptKey) profile.Telephone = decryptProfileField(m, "telephone", decryptKey) if profile.Email != "" || profile.Organization != "" || profile.Team != "" { return profile, nil } } return Profile{}, errors.New("exchange response body missing profile fields") } func decryptProfileField(m map[string]any, key, decryptKey string) string { v, ok := m[key] if !ok { return "" } s, ok := v.(string) if !ok { return "" } return DecryptCredentialOrRaw(strings.TrimSpace(s), decryptKey) } type ExchangeResponse struct { ErrorCode string `json:"errorCode"` ErrorMsg string `json:"errorMsg"` Message string `json:"message"` State string `json:"state"` Body map[string]any `json:"body"` Result map[string]any `json:"result"` Data map[string]any `json:"data"` }