package upstreamimpl import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) const ( tengyuOneLinkTokenPath = "/prod-api/open/api/oneLinkToken" tengyuTokenTTL = time.Hour ) // OneLinkToken is the value returned by a shared-token fetcher. type OneLinkToken struct { Value string CreatedAt time.Time ExpiresAt time.Time } // TokenFetcher mirrors the token core fetch boundary. chanKey is the // protocol's c_no; it is never inferred from unrelated channel fields. type TokenFetcher interface { FetchToken(context.Context, string) (OneLinkToken, error) } // TokenProvider is the consumer-facing token core boundary. type TokenProvider interface { GetToken(context.Context, string) (string, error) } // TengyuTokenConfig contains all values required by the Tengyu protocol. // The provider intentionally does not infer these values from channel config. type TengyuTokenConfig struct { BaseURL string Path string PwdKey string ChannelNo string Timeout time.Duration } // TengyuTokenProvider fetches a Tengyu shared OneLink token. type TengyuTokenProvider struct { baseURL string path string pwdKey string channelNo string client *http.Client } type tengyuTokenResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data json.RawMessage `json:"data"` } func NewTengyuTokenProvider(config TengyuTokenConfig) *TengyuTokenProvider { return &TengyuTokenProvider{ baseURL: strings.TrimRight(strings.TrimSpace(config.BaseURL), "/"), path: normalizeProviderPath(config.Path, tengyuOneLinkTokenPath), pwdKey: config.PwdKey, channelNo: config.ChannelNo, client: newTengyuHTTPClient(config.Timeout), } } // Historical in-package names are retained as a small adapter for callers // that constructed the provider before the token-core interfaces were named. type tengyuTokenConfig = TengyuTokenConfig func newTengyuTokenProvider(config tengyuTokenConfig) *TengyuTokenProvider { return NewTengyuTokenProvider(config) } func newTengyuHTTPClient(timeout time.Duration) *http.Client { if timeout <= 0 { timeout = defaultUpstreamHTTPTimeout } return &http.Client{Timeout: timeout} } func (p *TengyuTokenProvider) Fetch(ctx context.Context) (OneLinkToken, error) { return p.fetch(ctx, p.channelNo) } func (p *TengyuTokenProvider) FetchToken(ctx context.Context, channelNo string) (OneLinkToken, error) { if strings.TrimSpace(channelNo) == "" { channelNo = p.channelNo } return p.fetch(ctx, channelNo) } func (p *TengyuTokenProvider) GetToken(ctx context.Context, channelNo string) (string, error) { token, err := p.FetchToken(ctx, channelNo) if err != nil { return "", err } return token.Value, nil } func (p *TengyuTokenProvider) fetch(ctx context.Context, channelNo string) (OneLinkToken, error) { if p == nil || p.client == nil { return OneLinkToken{}, fmt.Errorf("tengyu token provider is not initialized") } if p.baseURL == "" { return OneLinkToken{}, fmt.Errorf("tengyu token base URL is required") } if strings.TrimSpace(p.pwdKey) == "" { return OneLinkToken{}, fmt.Errorf("tengyu pwdkey is required") } if strings.TrimSpace(channelNo) == "" { return OneLinkToken{}, fmt.Errorf("tengyu channel number is required") } endpoint, err := url.Parse(joinProviderURL(p.baseURL, p.path)) if err != nil { return OneLinkToken{}, fmt.Errorf("parse Tengyu token URL: %w", err) } query := endpoint.Query() query.Set("pwdkey", p.pwdKey) query.Set("c_no", channelNo) endpoint.RawQuery = query.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) if err != nil { return OneLinkToken{}, fmt.Errorf("create Tengyu token request: %w", err) } req.Header.Set("Accept", "application/json") resp, err := p.client.Do(req) if err != nil { return OneLinkToken{}, fmt.Errorf("request Tengyu token: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return OneLinkToken{}, fmt.Errorf("read Tengyu token response: %w", err) } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { return OneLinkToken{}, fmt.Errorf("Tengyu token HTTP status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) } var output tengyuTokenResponse if err := json.Unmarshal(body, &output); err != nil { return OneLinkToken{}, fmt.Errorf("decode Tengyu token response: %w", err) } if output.Code != http.StatusOK { return OneLinkToken{}, fmt.Errorf("Tengyu token failed: code=%d, message=%s", output.Code, output.Msg) } var value string if err := json.Unmarshal(output.Data, &value); err != nil { return OneLinkToken{}, fmt.Errorf("decode Tengyu shared token: %w", err) } if strings.TrimSpace(value) == "" { return OneLinkToken{}, fmt.Errorf("Tengyu shared token is empty") } now := time.Now() return OneLinkToken{Value: value, CreatedAt: now, ExpiresAt: now.Add(tengyuTokenTTL)}, nil }