chore: 初始开源发布
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。 - 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件), yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方 gogf/gf/contrib/registry/consul/v2(API 兼容) - Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan - 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除, 测试断言按当前实现语义重写(不改产品行为) - 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、 内网域名、真实手机号、注释里的真实 token/transid - 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档 - Dockerfile 改用公共基础镜像,移除写死的 registry 凭据 - 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model/entity"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const ydTokenInvalidCode = "12021"
|
||||
|
||||
var errYdTokenUnchanged = errors.New("mobile token recovery returned the rejected token")
|
||||
|
||||
type ydResponseMeta struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type ydHTTPResponse struct{ body string }
|
||||
|
||||
func (r *ydHTTPResponse) ReadAllString() string { return r.body }
|
||||
|
||||
type ydRequestFunc func(params g.Map) (string, error)
|
||||
|
||||
// ydTokenProvider is the construction seam for channels whose credential is
|
||||
// shared outside t_sim_channels. Implementations own storage and refresh;
|
||||
// RecoverToken must use compare-and-swap semantics for rejectedToken.
|
||||
type ydTokenProvider interface {
|
||||
GetToken(ctx context.Context) (string, error)
|
||||
RecoverToken(ctx context.Context, rejectedToken string) (string, error)
|
||||
}
|
||||
|
||||
const (
|
||||
// expired_only reuses the current token until it actually expires.
|
||||
ydTokenRefreshModeExpiredOnly = "expired_only"
|
||||
// proactive enters the configured advance window before requesting a token.
|
||||
ydTokenRefreshModeProactive = "proactive"
|
||||
ydTokenDefaultSkewSeconds = int64(600)
|
||||
)
|
||||
|
||||
type ydTokenRefreshPolicy struct {
|
||||
mode string
|
||||
skewSecond int64
|
||||
}
|
||||
|
||||
func loadYdTokenRefreshPolicy(ctx context.Context) ydTokenRefreshPolicy {
|
||||
mode := g.Cfg().MustGet(ctx, "dependencies.token_refresh.mode", ydTokenRefreshModeExpiredOnly).String()
|
||||
if mode != ydTokenRefreshModeExpiredOnly && mode != ydTokenRefreshModeProactive {
|
||||
g.Log().Warningf(ctx, "unknown token refresh mode=%s, fallback to %s", mode, ydTokenRefreshModeExpiredOnly)
|
||||
mode = ydTokenRefreshModeExpiredOnly
|
||||
}
|
||||
skew := g.Cfg().MustGet(ctx, "dependencies.token_refresh.skew_seconds", ydTokenDefaultSkewSeconds).Int64()
|
||||
if skew < 0 {
|
||||
g.Log().Warningf(ctx, "invalid token refresh skew_seconds=%d, fallback to 0", skew)
|
||||
skew = 0
|
||||
}
|
||||
return ydTokenRefreshPolicy{mode: mode, skewSecond: skew}
|
||||
}
|
||||
|
||||
func (p ydTokenRefreshPolicy) shouldRefresh(channel *entity.TSimChannels, now time.Time) bool {
|
||||
if channel == nil || channel.AccessToken == "" || channel.AccessTokenExpire == nil {
|
||||
return true
|
||||
}
|
||||
expire := channel.AccessTokenExpire.Time
|
||||
if p.mode == ydTokenRefreshModeProactive {
|
||||
return !now.Before(expire.Add(-time.Duration(p.skewSecond) * time.Second))
|
||||
}
|
||||
return !now.Before(expire)
|
||||
}
|
||||
|
||||
var (
|
||||
ydTokenGuards sync.Map
|
||||
ydTokenStates sync.Map // map[string]*ydTokenState
|
||||
)
|
||||
|
||||
type ydTokenState struct {
|
||||
mu sync.RWMutex
|
||||
token string
|
||||
createdAt time.Time
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func ydTokenStateFor(chanKey string) *ydTokenState {
|
||||
state, _ := ydTokenStates.LoadOrStore(chanKey, &ydTokenState{})
|
||||
return state.(*ydTokenState)
|
||||
}
|
||||
|
||||
func seedYdTokenState(channel *entity.TSimChannels) {
|
||||
if channel == nil || channel.ChanKey == "" || channel.AccessToken == "" || channel.AccessTokenExpire == nil {
|
||||
return
|
||||
}
|
||||
state := ydTokenStateFor(channel.ChanKey)
|
||||
state.mu.Lock()
|
||||
createdAt := time.Time{}
|
||||
if channel.AccessTokenCreated != nil {
|
||||
createdAt = channel.AccessTokenCreated.Time
|
||||
}
|
||||
if state.token == "" || createdAt.After(state.createdAt) {
|
||||
state.token = channel.AccessToken
|
||||
state.createdAt = createdAt
|
||||
state.expiresAt = channel.AccessTokenExpire.Time
|
||||
}
|
||||
state.mu.Unlock()
|
||||
}
|
||||
|
||||
func currentYdToken(channel *entity.TSimChannels) (string, time.Time) {
|
||||
if channel == nil {
|
||||
return "", time.Time{}
|
||||
}
|
||||
seedYdTokenState(channel)
|
||||
state := ydTokenStateFor(channel.ChanKey)
|
||||
state.mu.RLock()
|
||||
token, expiresAt := state.token, state.expiresAt
|
||||
state.mu.RUnlock()
|
||||
if token == "" || expiresAt.IsZero() || !time.Now().Before(expiresAt) {
|
||||
return "", expiresAt
|
||||
}
|
||||
return token, expiresAt
|
||||
}
|
||||
|
||||
func updateYdTokenState(chanKey, token string, createdAt, expiresAt time.Time) {
|
||||
state := ydTokenStateFor(chanKey)
|
||||
state.mu.Lock()
|
||||
state.token = token
|
||||
state.createdAt = createdAt
|
||||
state.expiresAt = expiresAt
|
||||
state.mu.Unlock()
|
||||
}
|
||||
|
||||
func clearYdTokenStateIfCurrent(chanKey, rejected string) {
|
||||
state := ydTokenStateFor(chanKey)
|
||||
state.mu.Lock()
|
||||
if state.token == rejected {
|
||||
state.token = ""
|
||||
state.createdAt = time.Time{}
|
||||
state.expiresAt = time.Time{}
|
||||
}
|
||||
state.mu.Unlock()
|
||||
}
|
||||
|
||||
func withYdTokenGuard(chanKey string, fn func() string) string {
|
||||
guard, _ := ydTokenGuards.LoadOrStore(chanKey, &sync.Mutex{})
|
||||
mu := guard.(*sync.Mutex)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return fn()
|
||||
}
|
||||
|
||||
// validYdToken only checks actual expiry. Proactive refresh decisions belong
|
||||
// to ydTokenRefreshPolicy so refresh=0 integrations can reuse the token until
|
||||
// it really expires.
|
||||
func validYdToken(channel *entity.TSimChannels, now time.Time) string {
|
||||
if channel == nil || channel.AccessToken == "" || channel.AccessTokenExpire == nil {
|
||||
return ""
|
||||
}
|
||||
if !now.Before(channel.AccessTokenExpire.Time) {
|
||||
return ""
|
||||
}
|
||||
return channel.AccessToken
|
||||
}
|
||||
|
||||
func recoverYdToken(ctx context.Context, chanKey string, channel *entity.TSimChannels, rejectedToken string, refresh func(context.Context) string) (token string, err error) {
|
||||
withYdTokenGuard(chanKey, func() string {
|
||||
if channel == nil {
|
||||
err = errors.New("mobile token recovery has no channel")
|
||||
return ""
|
||||
}
|
||||
seedYdTokenState(channel)
|
||||
if current, _ := currentYdToken(channel); current != "" && current != rejectedToken {
|
||||
channel.AccessToken = current
|
||||
token = current
|
||||
return ""
|
||||
}
|
||||
clearYdTokenStateIfCurrent(chanKey, rejectedToken)
|
||||
channel.AccessToken = ""
|
||||
channel.AccessTokenExpire = nil
|
||||
token = refresh(ctx)
|
||||
if token == "" {
|
||||
err = errors.New("mobile token recovery failed to acquire a token")
|
||||
} else if token == rejectedToken {
|
||||
err = errYdTokenUnchanged
|
||||
}
|
||||
return ""
|
||||
})
|
||||
return token, err
|
||||
}
|
||||
|
||||
func doYdRequest(ctx context.Context, operation, chanKey string, getToken func(context.Context) string, recoverToken func(context.Context, string) (string, error), params g.Map, request ydRequestFunc) (*ydHTTPResponse, error) {
|
||||
var lastBody string
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
token := getToken(ctx)
|
||||
requestParams := make(g.Map, len(params)+1)
|
||||
for key, value := range params {
|
||||
requestParams[key] = value
|
||||
}
|
||||
requestParams["token"] = token
|
||||
body, err := request(requestParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastBody = body
|
||||
var meta ydResponseMeta
|
||||
if err = json.Unmarshal([]byte(body), &meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if meta.Status != ydTokenInvalidCode || attempt == 1 {
|
||||
return &ydHTTPResponse{body: body}, nil
|
||||
}
|
||||
g.Log().Warningf(ctx, "%s token rejected, invalidate and retry once, chanKey:%s", operation, chanKey)
|
||||
if _, err = recoverToken(ctx, token); err != nil {
|
||||
if errors.Is(err, errYdTokenUnchanged) {
|
||||
g.Log().Warningf(ctx, "%s token recovery returned the rejected token, stop retrying, chanKey:%s", operation, chanKey)
|
||||
return &ydHTTPResponse{body: body}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s token recovery failed: %w", operation, err)
|
||||
}
|
||||
}
|
||||
return &ydHTTPResponse{body: lastBody}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user