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 }