package upstreamimpl import ( "context" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" sharedtoken "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/token" ) const mobilePublicTokenPath = "/api/get/yd/api/msg" // MobilePublicTokenConfig contains the credentials used to request a shared // China Mobile OneLink token. Secret is sourced from t_sim_channels.app_secret. type MobilePublicTokenConfig struct { BaseURL string Path string UserID string Secret string Timeout time.Duration } // MobilePublicTokenProvider fetches a shared OneLink token from the mobile // public-token upstream. type MobilePublicTokenProvider struct { baseURL string path string userID string secret string client *http.Client now func() time.Time } type mobilePublicTokenResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data struct { Token string `json:"token"` ExpireTime int64 `json:"expire_time"` } `json:"data"` } func NewMobilePublicTokenProvider(config MobilePublicTokenConfig) *MobilePublicTokenProvider { timeout := config.Timeout if timeout <= 0 { timeout = defaultUpstreamHTTPTimeout } return &MobilePublicTokenProvider{ baseURL: strings.TrimRight(strings.TrimSpace(config.BaseURL), "/"), path: normalizeProviderPath(config.Path, mobilePublicTokenPath), userID: strings.TrimSpace(config.UserID), secret: config.Secret, client: &http.Client{Timeout: timeout}, now: time.Now, } } func mobilePublicSign(secret, userID string, timestamp int64) string { source := secret + userID + strconv.FormatInt(timestamp, 10) + secret sum := md5.Sum([]byte(source)) return hex.EncodeToString(sum[:]) } func (p *MobilePublicTokenProvider) Fetch(ctx context.Context) (sharedtoken.OneLinkToken, error) { if p == nil || p.client == nil || p.now == nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("mobile public token provider is not initialized") } if p.baseURL == "" { return sharedtoken.OneLinkToken{}, fmt.Errorf("mobile public token base URL is required") } if p.userID == "" || p.secret == "" { return sharedtoken.OneLinkToken{}, fmt.Errorf("mobile public token userId and secret are required") } userID, err := strconv.ParseInt(p.userID, 10, 64) if err != nil || userID <= 0 { return sharedtoken.OneLinkToken{}, fmt.Errorf("mobile public token userId must be a positive integer") } now := p.now() payload := struct { UserID int64 `json:"userid"` Time int64 `json:"time"` Sign string `json:"sign"` }{ UserID: userID, Time: now.Unix(), Sign: mobilePublicSign(p.secret, strconv.FormatInt(userID, 10), now.Unix()), } body, err := json.Marshal(payload) if err != nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("encode mobile public token request: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinProviderURL(p.baseURL, p.path), strings.NewReader(string(body))) if err != nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("create mobile public 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 mobile public token: %w", err) } defer resp.Body.Close() responseBody, err := io.ReadAll(resp.Body) if err != nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("read mobile public token response: %w", err) } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { return sharedtoken.OneLinkToken{}, fmt.Errorf("mobile public token HTTP status %d", resp.StatusCode) } var output mobilePublicTokenResponse if err := json.Unmarshal(responseBody, &output); err != nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("decode mobile public token response: %w", err) } if output.Code != http.StatusOK { return sharedtoken.OneLinkToken{}, fmt.Errorf("mobile public token failed: code=%d msg=%s", output.Code, strings.TrimSpace(output.Msg)) } value := strings.TrimSpace(output.Data.Token) if value == "" { return sharedtoken.OneLinkToken{}, fmt.Errorf("mobile public token is empty") } expiresAt := time.Unix(output.Data.ExpireTime, 0) if output.Data.ExpireTime <= 0 || !expiresAt.After(now) { return sharedtoken.OneLinkToken{}, fmt.Errorf("mobile public token expire_time must be in the future: %d", output.Data.ExpireTime) } return sharedtoken.OneLinkToken{Value: value, CreatedAt: now, ExpiresAt: expiresAt}, nil } func (p *MobilePublicTokenProvider) FetchToken(ctx context.Context, _ string) (sharedtoken.OneLinkToken, error) { return p.Fetch(ctx) } type mobilePublicFetcher struct{ provider *MobilePublicTokenProvider } func (f mobilePublicFetcher) FetchToken(ctx context.Context, _ string) (sharedtoken.OneLinkToken, error) { return f.provider.Fetch(ctx) }