200 lines
5.9 KiB
Go
200 lines
5.9 KiB
Go
package upstreamimpl
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
sharedtoken "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/token"
|
|
)
|
|
|
|
const (
|
|
yunzeTokenPath = "/openapi/v3/query/operator-token"
|
|
yunzeTokenExpiryLayout = "2006-01-02 15:04:05"
|
|
)
|
|
|
|
// YunzeTokenConfig contains the platform endpoint and the per-channel app id.
|
|
// The Yunze operator-token API does not require app_secret.
|
|
type YunzeTokenConfig struct {
|
|
BaseURL string
|
|
Path string
|
|
AppID string
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// YunzeTokenProvider fetches the shared token from the Yunze platform.
|
|
type YunzeTokenProvider struct {
|
|
baseURL string
|
|
path string
|
|
appID string
|
|
client *http.Client
|
|
}
|
|
|
|
type yunzeTokenResponse struct {
|
|
Code json.RawMessage `json:"code"`
|
|
Message string `json:"message"`
|
|
Data struct {
|
|
ChanAlias string `json:"chanAlias"`
|
|
ChanKey string `json:"chanKey"`
|
|
AccessToken string `json:"accessToken"`
|
|
// The live endpoint returns the expiry as tokenExpire. The published
|
|
// documentation incorrectly lists accessTokenExpire; it is being
|
|
// corrected to match the implementation.
|
|
TokenExpire string `json:"tokenExpire"`
|
|
} `json:"data"`
|
|
Error json.RawMessage `json:"error"`
|
|
}
|
|
|
|
// yunzeTokenRequest is the operator-token request body.
|
|
type yunzeTokenRequest struct {
|
|
AppID string `json:"appId"`
|
|
ChanKey string `json:"chanKey"`
|
|
}
|
|
|
|
// yunzeFetcher adapts YunzeTokenProvider to the shared token TokenFetcher
|
|
// contract, mirroring ziboFetcher and apikaFetcher.
|
|
type yunzeFetcher struct{ provider *YunzeTokenProvider }
|
|
|
|
func (f yunzeFetcher) FetchToken(ctx context.Context, chanKey string) (sharedtoken.OneLinkToken, error) {
|
|
return f.provider.Fetch(ctx, chanKey)
|
|
}
|
|
|
|
func NewYunzeTokenProvider(config YunzeTokenConfig) *YunzeTokenProvider {
|
|
timeout := config.Timeout
|
|
if timeout <= 0 {
|
|
timeout = defaultUpstreamHTTPTimeout
|
|
}
|
|
return &YunzeTokenProvider{
|
|
baseURL: strings.TrimRight(strings.TrimSpace(config.BaseURL), "/"),
|
|
path: normalizeProviderPath(config.Path, yunzeTokenPath),
|
|
appID: strings.TrimSpace(config.AppID),
|
|
client: &http.Client{Timeout: timeout},
|
|
}
|
|
}
|
|
|
|
func (p *YunzeTokenProvider) Fetch(ctx context.Context, chanKey string) (sharedtoken.OneLinkToken, error) {
|
|
if p == nil || p.client == nil {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("yunze token provider is not initialized")
|
|
}
|
|
if p.baseURL == "" {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("yunze token base URL is required")
|
|
}
|
|
if p.appID == "" {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("yunze appId is required")
|
|
}
|
|
chanKey = strings.TrimSpace(chanKey)
|
|
if chanKey == "" {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("yunze chanKey is required")
|
|
}
|
|
|
|
payload, err := json.Marshal(yunzeTokenRequest{AppID: p.appID, ChanKey: chanKey})
|
|
if err != nil {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("encode yunze token request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(
|
|
ctx,
|
|
http.MethodPost,
|
|
joinProviderURL(p.baseURL, p.path),
|
|
bytes.NewReader(payload),
|
|
)
|
|
if err != nil {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("create yunze token request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("request yunze token: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 4*1024*1024+1))
|
|
if err != nil {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("read yunze token response: %w", err)
|
|
}
|
|
if len(body) > 4*1024*1024 {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("yunze token response is too large")
|
|
}
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("yunze token HTTP status %d", resp.StatusCode)
|
|
}
|
|
|
|
var output yunzeTokenResponse
|
|
if err := json.Unmarshal(body, &output); err != nil {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("decode yunze token response: %w", err)
|
|
}
|
|
code, err := decodeYunzeCode(output.Code)
|
|
if err != nil {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("decode yunze token code: %w", err)
|
|
}
|
|
if code != 0 {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf(
|
|
"yunze token failed: code=%d message=%s",
|
|
code,
|
|
strings.TrimSpace(output.Message),
|
|
)
|
|
}
|
|
if len(output.Error) > 0 && string(output.Error) != "null" {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("yunze token error: %s", string(output.Error))
|
|
}
|
|
|
|
value := strings.TrimSpace(output.Data.AccessToken)
|
|
if value == "" {
|
|
return sharedtoken.OneLinkToken{}, fmt.Errorf("yunze accessToken is empty")
|
|
}
|
|
expiresAt, err := parseYunzeTokenExpiry(output.Data.TokenExpire)
|
|
if err != nil {
|
|
return sharedtoken.OneLinkToken{}, err
|
|
}
|
|
|
|
return sharedtoken.OneLinkToken{
|
|
Value: value,
|
|
CreatedAt: time.Now(),
|
|
ExpiresAt: expiresAt,
|
|
}, nil
|
|
}
|
|
|
|
func decodeYunzeCode(raw json.RawMessage) (int, error) {
|
|
var code int
|
|
if err := json.Unmarshal(raw, &code); err == nil {
|
|
return code, nil
|
|
}
|
|
var value string
|
|
if err := json.Unmarshal(raw, &value); err != nil {
|
|
return 0, err
|
|
}
|
|
var parsed int
|
|
if _, err := fmt.Sscanf(strings.TrimSpace(value), "%d", &parsed); err != nil {
|
|
return 0, err
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
// yunzeTokenLocation is the timezone used by the Yunze operator-token API.
|
|
// It is resolved once so the hot refresh path does not re-read tzdata.
|
|
var yunzeTokenLocation = func() *time.Location {
|
|
if loc, err := time.LoadLocation("Asia/Shanghai"); err == nil {
|
|
return loc
|
|
}
|
|
return time.Local
|
|
}()
|
|
|
|
func parseYunzeTokenExpiry(raw string) (time.Time, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return time.Time{}, fmt.Errorf("yunze tokenExpire is empty")
|
|
}
|
|
expiresAt, err := time.ParseInLocation(yunzeTokenExpiryLayout, raw, yunzeTokenLocation)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("parse yunze tokenExpire %q: %w", raw, err)
|
|
}
|
|
return expiresAt, nil
|
|
}
|