Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 

191 řádky
6.9 KiB

  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/hmac"
  6. "crypto/sha256"
  7. "encoding/hex"
  8. "fmt"
  9. "io"
  10. "net/http"
  11. "net/url"
  12. "strings"
  13. "time"
  14. "github.com/QuantumNous/new-api/common"
  15. "github.com/QuantumNous/new-api/model"
  16. "github.com/QuantumNous/new-api/setting/system_setting"
  17. )
  18. // DoubaoVideoAssetAdapter serves the Ark-compatible asset API on official
  19. // Volcengine (DoubaoVideo) channels. Unlike the aiping adapter (Bearer key),
  20. // the official endpoint authenticates every request with a Volcengine V4
  21. // HMAC-SHA256 signature built from an AccessKey/SecretKey pair stored in
  22. // channel_asset_credentials (PoolID holds the optional project code).
  23. type DoubaoVideoAssetAdapter struct {
  24. operation map[AssetOperation]struct{}
  25. }
  26. func NewDoubaoVideoAssetAdapter() AssetAdapter {
  27. operations := []AssetOperation{
  28. AssetOperationAssetCreate,
  29. AssetOperationAssetList,
  30. AssetOperationAssetGet,
  31. AssetOperationAssetUpdate,
  32. AssetOperationAssetDelete,
  33. AssetOperationAssetGroupCreate,
  34. AssetOperationAssetGroupList,
  35. AssetOperationAssetGroupGet,
  36. AssetOperationAssetGroupUpdate,
  37. AssetOperationAssetGroupDelete,
  38. }
  39. supported := make(map[AssetOperation]struct{}, len(operations))
  40. for _, op := range operations {
  41. supported[op] = struct{}{}
  42. }
  43. return &DoubaoVideoAssetAdapter{operation: supported}
  44. }
  45. func (a *DoubaoVideoAssetAdapter) Name() string {
  46. return "doubao_video_asset"
  47. }
  48. func (a *DoubaoVideoAssetAdapter) Supports(operation AssetOperation) bool {
  49. _, ok := a.operation[operation]
  50. return ok
  51. }
  52. func (a *DoubaoVideoAssetAdapter) DoAssetRequest(ctx context.Context, channel *model.Channel, req AssetRequest) (*AssetUpstreamResponse, *AssetError) {
  53. if !a.Supports(req.Action.Operation) {
  54. return nil, newAssetError(AssetErrorOperationNotSupported, fmt.Sprintf("asset operation %s is not supported", req.Action.Operation), http.StatusBadRequest)
  55. }
  56. credential, err := model.GetChannelAssetCredential(channel.Id)
  57. if err != nil {
  58. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  59. }
  60. if credential == nil || strings.TrimSpace(credential.AccessKey) == "" || strings.TrimSpace(credential.SecretKey) == "" {
  61. return nil, newAssetError(AssetErrorServer, "Volcengine asset AccessKey and SecretKey are required (configure the channel asset credential)", http.StatusBadRequest)
  62. }
  63. upstreamURL, rawQuery, err := buildDoubaoVideoAssetURL(channel, req.Action.Action, req.Version)
  64. if err != nil {
  65. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  66. }
  67. fetchSetting := system_setting.GetFetchSetting()
  68. if err := common.ValidateURLWithFetchSetting(upstreamURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
  69. return nil, newAssetError(AssetErrorServer, fmt.Sprintf("request blocked: %v", err), http.StatusForbidden)
  70. }
  71. httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamURL, bytes.NewReader(req.RawBody))
  72. if err != nil {
  73. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  74. }
  75. signDoubaoVideoAssetRequest(httpReq, credential.AccessKey, credential.SecretKey, req.RawBody, rawQuery)
  76. if projectCode := strings.TrimSpace(credential.PoolID); projectCode != "" {
  77. httpReq.Header.Set("X-Project-Code", projectCode)
  78. }
  79. client, err := GetHttpClientWithProxy(channel.GetSetting().Proxy)
  80. if err != nil {
  81. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  82. }
  83. if client == nil {
  84. client = http.DefaultClient
  85. }
  86. resp, err := client.Do(httpReq)
  87. if err != nil {
  88. return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
  89. }
  90. defer resp.Body.Close()
  91. data, err := io.ReadAll(resp.Body)
  92. if err != nil {
  93. return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
  94. }
  95. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  96. return nil, newAssetError(AssetErrorUpstream, string(data), http.StatusBadGateway)
  97. }
  98. return &AssetUpstreamResponse{StatusCode: resp.StatusCode, Header: resp.Header, Body: data}, nil
  99. }
  100. func buildDoubaoVideoAssetURL(channel *model.Channel, action string, version string) (string, string, error) {
  101. baseURL := strings.TrimSpace(channel.GetBaseURL())
  102. if baseURL == "" {
  103. baseURL = "https://ark.cn-beijing.volces.com"
  104. }
  105. u, err := url.Parse(baseURL)
  106. if err != nil {
  107. return "", "", err
  108. }
  109. u.Path = strings.TrimRight(u.Path, "/") + "/openApi/portrait"
  110. if strings.TrimSpace(version) == "" {
  111. version = "2024-01-01"
  112. }
  113. // Action < Version lexicographically, so this raw query is already the
  114. // sorted canonical form required by the V4 signature.
  115. rawQuery := "Action=" + url.QueryEscape(action) + "&Version=" + url.QueryEscape(version)
  116. u.RawQuery = rawQuery
  117. return u.String(), rawQuery, nil
  118. }
  119. // signDoubaoVideoAssetRequest applies the Volcengine V4 HMAC-SHA256 signature
  120. // to the request: canonical request over (method, path, sorted query,
  121. // content-type/host/x-content-sha256/x-date headers, body hash) -> string to
  122. // sign with the {date}/{region}/ark/request scope -> chained HMAC keys.
  123. func signDoubaoVideoAssetRequest(req *http.Request, accessKey string, secretKey string, body []byte, rawQuery string) {
  124. now := time.Now().UTC()
  125. xDate := now.Format("20060102T150405Z")
  126. shortDate := now.Format("20060102")
  127. payloadHash := sha256.Sum256(body)
  128. payloadHashHex := hex.EncodeToString(payloadHash[:])
  129. canonicalHeaders := fmt.Sprintf(
  130. "content-type:application/json\nhost:%s\nx-content-sha256:%s\nx-date:%s\n",
  131. req.URL.Host, payloadHashHex, xDate,
  132. )
  133. signedHeaders := "content-type;host;x-content-sha256;x-date"
  134. canonicalRequest := strings.Join([]string{
  135. req.Method,
  136. req.URL.EscapedPath(),
  137. rawQuery,
  138. canonicalHeaders,
  139. signedHeaders,
  140. payloadHashHex,
  141. }, "\n")
  142. scope := shortDate + "/cn-beijing/ark/request"
  143. canonicalHash := sha256.Sum256([]byte(canonicalRequest))
  144. stringToSign := strings.Join([]string{
  145. "HMAC-SHA256",
  146. xDate,
  147. scope,
  148. hex.EncodeToString(canonicalHash[:]),
  149. }, "\n")
  150. signingKey := volcengineSigningKey(secretKey, shortDate)
  151. signature := hmac.New(sha256.New, signingKey)
  152. signature.Write([]byte(stringToSign))
  153. req.Header.Set("Content-Type", "application/json")
  154. req.Header.Set("Accept", "application/json")
  155. req.Header.Set("X-Date", xDate)
  156. req.Header.Set("X-Content-Sha256", payloadHashHex)
  157. req.Header.Set("Authorization", fmt.Sprintf(
  158. "HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s",
  159. accessKey, scope, signedHeaders, hex.EncodeToString(signature.Sum(nil)),
  160. ))
  161. }
  162. func volcengineSigningKey(secretKey string, shortDate string) []byte {
  163. key := hmacSHA256([]byte(secretKey), shortDate)
  164. key = hmacSHA256(key, "cn-beijing")
  165. key = hmacSHA256(key, "ark")
  166. return hmacSHA256(key, "request")
  167. }
  168. func hmacSHA256(key []byte, message string) []byte {
  169. mac := hmac.New(sha256.New, key)
  170. mac.Write([]byte(message))
  171. return mac.Sum(nil)
  172. }