103 lines
3.7 KiB
Go
103 lines
3.7 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
cos "github.com/tencentyun/cos-go-sdk-v5"
|
||
)
|
||
|
||
const cvmCAMCredentialMetadataBaseURL = "http://metadata.tencentyun.com/latest/meta-data/cam/security-credentials"
|
||
|
||
type cvmCAMCredential struct {
|
||
TmpSecretID string `json:"TmpSecretId"`
|
||
TmpSecretKey string `json:"TmpSecretKey"`
|
||
Token string `json:"Token"`
|
||
ExpiredTime int64 `json:"ExpiredTime"`
|
||
Code string `json:"Code"`
|
||
}
|
||
|
||
// cvmCAMCredentialProvider 在凭证过期前 5 分钟刷新,整个结构不暴露凭证方法,避免误打日志。
|
||
type cvmCAMCredentialProvider struct {
|
||
roleName string
|
||
client *http.Client
|
||
mu sync.Mutex
|
||
cached cvmCAMCredential
|
||
}
|
||
|
||
func newCVMCAMCredentialProvider(roleName string) *cvmCAMCredentialProvider {
|
||
return &cvmCAMCredentialProvider{
|
||
roleName: strings.TrimSpace(roleName),
|
||
// metadata 是启动/请求链路的外部依赖,必须在 2 秒内失败,不能把 COS 归档 goroutine 长时间卡死。
|
||
client: &http.Client{Timeout: 2 * time.Second},
|
||
}
|
||
}
|
||
|
||
func (p *cvmCAMCredentialProvider) get(ctx context.Context) (string, string, string, error) {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
now := time.Now().UTC().Unix()
|
||
if p.cached.ExpiredTime > now+int64((5*time.Minute).Seconds()) {
|
||
return p.cached.TmpSecretID, p.cached.TmpSecretKey, p.cached.Token, nil
|
||
}
|
||
previous := p.cached
|
||
if p.roleName == "" {
|
||
return "", "", "", fmt.Errorf("wallet outbox archive CVM CAM role name is empty")
|
||
}
|
||
metadataURL := cvmCAMCredentialMetadataBaseURL + "/" + url.PathEscape(p.roleName)
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataURL, nil)
|
||
if err != nil {
|
||
return "", "", "", err
|
||
}
|
||
response, err := p.client.Do(request)
|
||
if err != nil {
|
||
if previous.ExpiredTime > now {
|
||
return previous.TmpSecretID, previous.TmpSecretKey, previous.Token, nil
|
||
}
|
||
return "", "", "", fmt.Errorf("load wallet outbox archive CVM role credential: %w", err)
|
||
}
|
||
defer response.Body.Close()
|
||
if response.StatusCode < 200 || response.StatusCode > 299 {
|
||
// 不读取/回传 metadata 错误体,避免平台异常响应携带临时凭证。
|
||
if previous.ExpiredTime > now {
|
||
return previous.TmpSecretID, previous.TmpSecretKey, previous.Token, nil
|
||
}
|
||
return "", "", "", fmt.Errorf("load wallet outbox archive CVM role credential: metadata status %d", response.StatusCode)
|
||
}
|
||
var credential cvmCAMCredential
|
||
if err := json.NewDecoder(response.Body).Decode(&credential); err != nil {
|
||
return "", "", "", fmt.Errorf("decode wallet outbox archive CVM role credential: %w", err)
|
||
}
|
||
if credential.Code != "Success" || strings.TrimSpace(credential.TmpSecretID) == "" || strings.TrimSpace(credential.TmpSecretKey) == "" || strings.TrimSpace(credential.Token) == "" || credential.ExpiredTime <= now+30 {
|
||
return "", "", "", fmt.Errorf("wallet outbox archive CVM role returned incomplete or expired credential")
|
||
}
|
||
p.cached = credential
|
||
return credential.TmpSecretID, credential.TmpSecretKey, credential.Token, nil
|
||
}
|
||
|
||
// cvmRoleAuthorizationTransport 每次签名前从缓存 provider 取 STS;只有刷新窗口才访问 metadata。
|
||
type cvmRoleAuthorizationTransport struct {
|
||
provider *cvmCAMCredentialProvider
|
||
transport http.RoundTripper
|
||
}
|
||
|
||
func (t *cvmRoleAuthorizationTransport) GetCredential() (string, string, string, error) {
|
||
return t.provider.get(context.Background())
|
||
}
|
||
|
||
func (t *cvmRoleAuthorizationTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||
secretID, secretKey, token, err := t.provider.get(request.Context())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return (&cos.AuthorizationTransport{
|
||
SecretID: secretID, SecretKey: secretKey, SessionToken: token, Transport: t.transport,
|
||
}).RoundTrip(request)
|
||
}
|