chore: 并入 iotlcm-upstream-chan 历史到 upstream-chan/(原提交 1e74497)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:02 +08:00
192 changed files with 33119 additions and 0 deletions
@@ -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, "认证失败")
}