chore: 并入 iotlcm-upstream-chan 历史到 upstream-chan/(原提交 1e74497)
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user