chore: 并入 iotlcm-upstream-chan 历史到 upstream-chan/(原提交 1e74497)
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
henanLelianLoginPath = "/api/v2/loginVerify"
|
||||
henanLelianSharePath = "/api/v2/getShareToken"
|
||||
henanLelianPlatformTTL = 23 * time.Hour
|
||||
henanLelianShareInterval = time.Minute
|
||||
)
|
||||
|
||||
// HenanLelianTokenConfig contains the public provider endpoints and the
|
||||
// per-channel credentials required to obtain a shared OneLink token.
|
||||
type HenanLelianTokenConfig struct {
|
||||
BaseURL string
|
||||
LoginPath string
|
||||
Path string
|
||||
UserName string
|
||||
Password string
|
||||
PasswayUID string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// HenanLelianTokenProvider first obtains a platform access token and then
|
||||
// exchanges it for the carrier-issued OneLink token.
|
||||
type HenanLelianTokenProvider struct {
|
||||
baseURL string
|
||||
loginPath string
|
||||
path string
|
||||
userName string
|
||||
password string
|
||||
passwayUID string
|
||||
client *http.Client
|
||||
now func() time.Time
|
||||
|
||||
platformMu sync.Mutex
|
||||
platformToken string
|
||||
platformTokenExp time.Time
|
||||
|
||||
shareMu sync.Mutex
|
||||
nextShareAt time.Time
|
||||
shareInterval time.Duration
|
||||
}
|
||||
|
||||
type henanLelianResponse struct {
|
||||
Code json.RawMessage `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Payload struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Token string `json:"token"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
func NewHenanLelianTokenProvider(config HenanLelianTokenConfig) *HenanLelianTokenProvider {
|
||||
timeout := config.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultUpstreamHTTPTimeout
|
||||
}
|
||||
return &HenanLelianTokenProvider{
|
||||
baseURL: strings.TrimRight(strings.TrimSpace(config.BaseURL), "/"),
|
||||
loginPath: normalizeProviderPath(config.LoginPath, henanLelianLoginPath),
|
||||
path: normalizeProviderPath(config.Path, henanLelianSharePath),
|
||||
userName: strings.TrimSpace(config.UserName),
|
||||
password: config.Password,
|
||||
passwayUID: strings.TrimSpace(config.PasswayUID),
|
||||
client: &http.Client{Timeout: timeout},
|
||||
now: time.Now,
|
||||
shareInterval: henanLelianShareInterval,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HenanLelianTokenProvider) Fetch(ctx context.Context) (OneLinkToken, error) {
|
||||
if p == nil || p.client == nil || p.now == nil {
|
||||
return OneLinkToken{}, fmt.Errorf("Henan Lelian token provider is not initialized")
|
||||
}
|
||||
if p.baseURL == "" {
|
||||
return OneLinkToken{}, fmt.Errorf("Henan Lelian token base URL is required")
|
||||
}
|
||||
if p.userName == "" || strings.TrimSpace(p.password) == "" {
|
||||
return OneLinkToken{}, fmt.Errorf("Henan Lelian username and password are required")
|
||||
}
|
||||
if p.passwayUID == "" {
|
||||
return OneLinkToken{}, fmt.Errorf("Henan Lelian passwayUid is required")
|
||||
}
|
||||
|
||||
platformToken, err := p.getPlatformToken(ctx)
|
||||
if err != nil {
|
||||
return OneLinkToken{}, err
|
||||
}
|
||||
value, authFailed, err := p.fetchSharedToken(ctx, platformToken)
|
||||
if authFailed {
|
||||
p.clearPlatformToken(platformToken)
|
||||
}
|
||||
if err != nil {
|
||||
return OneLinkToken{}, err
|
||||
}
|
||||
now := p.now()
|
||||
return OneLinkToken{Value: value, CreatedAt: now}, nil
|
||||
}
|
||||
|
||||
func (p *HenanLelianTokenProvider) getPlatformToken(ctx context.Context) (string, error) {
|
||||
p.platformMu.Lock()
|
||||
defer p.platformMu.Unlock()
|
||||
|
||||
now := p.now()
|
||||
if p.platformToken != "" && now.Before(p.platformTokenExp) {
|
||||
return p.platformToken, nil
|
||||
}
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"userName": p.userName,
|
||||
"password": p.password,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encode Henan Lelian login request: %w", err)
|
||||
}
|
||||
responseBody, status, err := p.postJSON(ctx, p.loginPath, body, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request Henan Lelian login: %w", err)
|
||||
}
|
||||
if status < http.StatusOK || status >= http.StatusMultipleChoices {
|
||||
return "", fmt.Errorf("Henan Lelian login HTTP status %d: %s", status, strings.TrimSpace(string(responseBody)))
|
||||
}
|
||||
var output henanLelianResponse
|
||||
if err = json.Unmarshal(responseBody, &output); err != nil {
|
||||
return "", fmt.Errorf("decode Henan Lelian login response: %w", err)
|
||||
}
|
||||
code, err := parseHenanLelianCode(output.Code)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode Henan Lelian login code: %w", err)
|
||||
}
|
||||
if code != "0" {
|
||||
return "", fmt.Errorf("Henan Lelian login failed: code=%s, message=%s", code, output.Message)
|
||||
}
|
||||
token := strings.TrimSpace(output.Payload.AccessToken)
|
||||
if token == "" {
|
||||
return "", fmt.Errorf("Henan Lelian login access_token is empty")
|
||||
}
|
||||
p.platformToken = token
|
||||
p.platformTokenExp = now.Add(henanLelianPlatformTTL)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (p *HenanLelianTokenProvider) fetchSharedToken(ctx context.Context, platformToken string) (string, bool, error) {
|
||||
if err := p.reserveShareAttempt(); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
body, err := json.Marshal(map[string]string{"passwayUid": p.passwayUID})
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("encode Henan Lelian shared token request: %w", err)
|
||||
}
|
||||
responseBody, status, err := p.postJSON(ctx, p.path, body, platformToken)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("request Henan Lelian shared token: %w", err)
|
||||
}
|
||||
if status < http.StatusOK || status >= http.StatusMultipleChoices {
|
||||
authFailed := status == http.StatusUnauthorized || status == http.StatusForbidden
|
||||
return "", authFailed, fmt.Errorf("Henan Lelian shared token HTTP status %d: %s", status, strings.TrimSpace(string(responseBody)))
|
||||
}
|
||||
var output henanLelianResponse
|
||||
if err = json.Unmarshal(responseBody, &output); err != nil {
|
||||
return "", false, fmt.Errorf("decode Henan Lelian shared token response: %w", err)
|
||||
}
|
||||
code, err := parseHenanLelianCode(output.Code)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("decode Henan Lelian shared token code: %w", err)
|
||||
}
|
||||
if code != "0" {
|
||||
authFailed := isHenanLelianAuthFailure(code, output.Message)
|
||||
return "", authFailed, fmt.Errorf("Henan Lelian shared token failed: code=%s, message=%s", code, output.Message)
|
||||
}
|
||||
token := strings.TrimSpace(output.Payload.Token)
|
||||
if token == "" {
|
||||
return "", false, fmt.Errorf("Henan Lelian shared token is empty")
|
||||
}
|
||||
return token, false, nil
|
||||
}
|
||||
|
||||
func (p *HenanLelianTokenProvider) reserveShareAttempt() error {
|
||||
p.shareMu.Lock()
|
||||
defer p.shareMu.Unlock()
|
||||
|
||||
now := p.now()
|
||||
if now.Before(p.nextShareAt) {
|
||||
return fmt.Errorf("Henan Lelian shared token is rate limited until %s", p.nextShareAt.Format(time.RFC3339))
|
||||
}
|
||||
p.nextShareAt = now.Add(p.shareInterval)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HenanLelianTokenProvider) clearPlatformToken(rejected string) {
|
||||
p.platformMu.Lock()
|
||||
if p.platformToken == rejected {
|
||||
p.platformToken = ""
|
||||
p.platformTokenExp = time.Time{}
|
||||
}
|
||||
p.platformMu.Unlock()
|
||||
}
|
||||
|
||||
func (p *HenanLelianTokenProvider) postJSON(ctx context.Context, path string, body []byte, platformToken string) ([]byte, int, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinProviderURL(p.baseURL, path), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if platformToken != "" {
|
||||
req.Header.Set("access_token", platformToken)
|
||||
}
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
return responseBody, resp.StatusCode, err
|
||||
}
|
||||
|
||||
func parseHenanLelianCode(raw json.RawMessage) (string, error) {
|
||||
var value string
|
||||
if err := json.Unmarshal(raw, &value); err == nil {
|
||||
return strings.TrimSpace(value), nil
|
||||
}
|
||||
var number json.Number
|
||||
if err := json.Unmarshal(raw, &number); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := strconv.ParseInt(number.String(), 10, 64); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return number.String(), nil
|
||||
}
|
||||
|
||||
func isHenanLelianAuthFailure(code, message string) bool {
|
||||
switch strings.TrimSpace(code) {
|
||||
case "401", "40101", "40102", "403":
|
||||
return true
|
||||
}
|
||||
message = strings.ToLower(strings.TrimSpace(message))
|
||||
return strings.Contains(message, "access_token") ||
|
||||
strings.Contains(message, "access token") ||
|
||||
strings.Contains(message, "token expired") ||
|
||||
strings.Contains(message, "token invalid") ||
|
||||
strings.Contains(message, "鉴权") ||
|
||||
strings.Contains(message, "认证失败")
|
||||
}
|
||||
Reference in New Issue
Block a user