package upstreamimpl import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" sharedtoken "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/token" ) // lanshuTokenPath is the fallback used when Path is empty. On the template // path this default is unreachable: parseSharedOneLinkConfig rejects an empty // token.path before any provider is built, so Path always carries the value // declared in temp_config. It only applies to direct construction (tests). // The same unreachable-fallback shape exists in every shared-token provider. const lanshuTokenPath = "/route/open/sharedToken" // LanShuTokenConfig contains the endpoint from the template and the // credentials from the channel configuration. BaseURL and Path come from // temp_config token.baseUrl/token.path; the credentials come from chan_config. // Sign is a fixed value issued by the upstream and is passed through // unchanged: the operator does not derive it from appId/password/timeStamp. type LanShuTokenConfig struct { BaseURL string Path string AppID string Password string Sign string TokenType string Timeout time.Duration } // LanShuTokenProvider fetches a shared token from the LanShu open route. type LanShuTokenProvider struct { baseURL string path string appID string password string sign string tokenType string client *http.Client } // lanshuTokenRequest is the documented request envelope. Note the capitalized // Param/Verify keys. type lanshuTokenRequest struct { Param lanshuTokenParam `json:"Param"` Verify lanshuTokenVerify `json:"verify"` } type lanshuTokenParam struct { TokenType string `json:"tokenType"` } type lanshuTokenVerify struct { TimeStamp int64 `json:"timeStamp"` Password string `json:"password"` AppID string `json:"appId"` Sign string `json:"sign"` } // lanshuTokenResponse mirrors the upstream envelope. The data object is // uppercase on the wire ("result.Data"), so the JSON tag must match exactly. type lanshuTokenResponse struct { Result struct { Data struct { ExpireTime int64 `json:"expireTime"` Token string `json:"token"` } `json:"Data"` } `json:"result"` Message string `json:"message"` Status string `json:"status"` } func NewLanShuTokenProvider(config LanShuTokenConfig) *LanShuTokenProvider { timeout := config.Timeout if timeout <= 0 { timeout = defaultUpstreamHTTPTimeout } return &LanShuTokenProvider{ baseURL: strings.TrimRight(strings.TrimSpace(config.BaseURL), "/"), path: normalizeProviderPath(config.Path, lanshuTokenPath), appID: strings.TrimSpace(config.AppID), password: config.Password, sign: strings.TrimSpace(config.Sign), tokenType: strings.TrimSpace(config.TokenType), client: &http.Client{Timeout: timeout}, } } func (p *LanShuTokenProvider) Fetch(ctx context.Context) (sharedtoken.OneLinkToken, error) { if p == nil || p.client == nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("lanshu token provider is not initialized") } if p.baseURL == "" || p.appID == "" || p.password == "" || p.sign == "" || p.tokenType == "" { return sharedtoken.OneLinkToken{}, fmt.Errorf("lanshu baseUrl, appId, password, sign and tokenType are required") } // The upstream does not validate timeStamp freshness, but it must be a // real millisecond timestamp for the request to be well-formed. payload, err := json.Marshal(lanshuTokenRequest{ Param: lanshuTokenParam{TokenType: p.tokenType}, Verify: lanshuTokenVerify{ TimeStamp: time.Now().UnixMilli(), Password: p.password, AppID: p.appID, Sign: p.sign, }, }) if err != nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("encode lanshu token request: %w", err) } req, err := http.NewRequestWithContext( ctx, http.MethodPost, joinProviderURL(p.baseURL, p.path), bytes.NewReader(payload), ) if err != nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("create lanshu 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 lanshu token: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 4*1024*1024+1)) if err != nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("read lanshu token response: %w", err) } if len(body) > 4*1024*1024 { return sharedtoken.OneLinkToken{}, fmt.Errorf("lanshu token response is too large") } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { return sharedtoken.OneLinkToken{}, fmt.Errorf("lanshu token HTTP status %d", resp.StatusCode) } var output lanshuTokenResponse if err := json.Unmarshal(body, &output); err != nil { return sharedtoken.OneLinkToken{}, fmt.Errorf("decode lanshu token response: %w", err) } if status := strings.TrimSpace(output.Status); status != "0" { return sharedtoken.OneLinkToken{}, fmt.Errorf( "lanshu token failed: status=%s message=%s", status, strings.TrimSpace(output.Message), ) } value := strings.TrimSpace(output.Result.Data.Token) if value == "" { return sharedtoken.OneLinkToken{}, fmt.Errorf("lanshu token is empty") } // expireTime is reported in seconds (observed 1162~1565s), not // milliseconds, so the refresh-ahead window in the factory can rely on it. now := time.Now() var expiresAt time.Time if output.Result.Data.ExpireTime > 0 { expiresAt = now.Add(time.Duration(output.Result.Data.ExpireTime) * time.Second) } return sharedtoken.OneLinkToken{Value: value, CreatedAt: now, ExpiresAt: expiresAt}, nil } func (p *LanShuTokenProvider) FetchToken(ctx context.Context, _ string) (sharedtoken.OneLinkToken, error) { return p.Fetch(ctx) } // lanshuFetcher adapts LanShuTokenProvider to the shared token TokenFetcher // contract, mirroring qianxunFetcher. type lanshuFetcher struct{ provider *LanShuTokenProvider } func (f lanshuFetcher) FetchToken(ctx context.Context, _ string) (sharedtoken.OneLinkToken, error) { return f.provider.Fetch(ctx) }