No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 

201 líneas
7.3 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. // The body is always serialized from req.Body so platform-side rewrites
  68. // (managed group scoping, ownership checks) take effect; RawBody is
  69. // intentionally not used here. The V4 signature covers the sent payload.
  70. body, err := common.Marshal(req.Body)
  71. if err != nil {
  72. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  73. }
  74. if len(body) == 0 {
  75. body = []byte("{}")
  76. }
  77. fetchSetting := system_setting.GetFetchSetting()
  78. if err := common.ValidateURLWithFetchSetting(upstreamURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
  79. return nil, newAssetError(AssetErrorServer, fmt.Sprintf("request blocked: %v", err), http.StatusForbidden)
  80. }
  81. httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamURL, bytes.NewReader(body))
  82. if err != nil {
  83. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  84. }
  85. signDoubaoVideoAssetRequest(httpReq, credential.AccessKey, credential.SecretKey, body, rawQuery)
  86. if projectCode := strings.TrimSpace(credential.PoolID); projectCode != "" {
  87. httpReq.Header.Set("X-Project-Code", projectCode)
  88. }
  89. client, err := GetHttpClientWithProxy(channel.GetSetting().Proxy)
  90. if err != nil {
  91. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  92. }
  93. if client == nil {
  94. client = http.DefaultClient
  95. }
  96. resp, err := client.Do(httpReq)
  97. if err != nil {
  98. return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
  99. }
  100. defer resp.Body.Close()
  101. data, err := io.ReadAll(resp.Body)
  102. if err != nil {
  103. return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
  104. }
  105. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  106. return nil, newAssetError(AssetErrorUpstream, string(data), http.StatusBadGateway)
  107. }
  108. return &AssetUpstreamResponse{StatusCode: resp.StatusCode, Header: resp.Header, Body: data}, nil
  109. }
  110. func buildDoubaoVideoAssetURL(channel *model.Channel, action string, version string) (string, string, error) {
  111. baseURL := strings.TrimSpace(channel.GetBaseURL())
  112. if baseURL == "" {
  113. baseURL = "https://ark.cn-beijing.volces.com"
  114. }
  115. u, err := url.Parse(baseURL)
  116. if err != nil {
  117. return "", "", err
  118. }
  119. u.Path = strings.TrimRight(u.Path, "/") + "/openApi/portrait"
  120. if strings.TrimSpace(version) == "" {
  121. version = "2024-01-01"
  122. }
  123. // Action < Version lexicographically, so this raw query is already the
  124. // sorted canonical form required by the V4 signature.
  125. rawQuery := "Action=" + url.QueryEscape(action) + "&Version=" + url.QueryEscape(version)
  126. u.RawQuery = rawQuery
  127. return u.String(), rawQuery, nil
  128. }
  129. // signDoubaoVideoAssetRequest applies the Volcengine V4 HMAC-SHA256 signature
  130. // to the request: canonical request over (method, path, sorted query,
  131. // content-type/host/x-content-sha256/x-date headers, body hash) -> string to
  132. // sign with the {date}/{region}/ark/request scope -> chained HMAC keys.
  133. func signDoubaoVideoAssetRequest(req *http.Request, accessKey string, secretKey string, body []byte, rawQuery string) {
  134. now := time.Now().UTC()
  135. xDate := now.Format("20060102T150405Z")
  136. shortDate := now.Format("20060102")
  137. payloadHash := sha256.Sum256(body)
  138. payloadHashHex := hex.EncodeToString(payloadHash[:])
  139. canonicalHeaders := fmt.Sprintf(
  140. "content-type:application/json\nhost:%s\nx-content-sha256:%s\nx-date:%s\n",
  141. req.URL.Host, payloadHashHex, xDate,
  142. )
  143. signedHeaders := "content-type;host;x-content-sha256;x-date"
  144. canonicalRequest := strings.Join([]string{
  145. req.Method,
  146. req.URL.EscapedPath(),
  147. rawQuery,
  148. canonicalHeaders,
  149. signedHeaders,
  150. payloadHashHex,
  151. }, "\n")
  152. scope := shortDate + "/cn-beijing/ark/request"
  153. canonicalHash := sha256.Sum256([]byte(canonicalRequest))
  154. stringToSign := strings.Join([]string{
  155. "HMAC-SHA256",
  156. xDate,
  157. scope,
  158. hex.EncodeToString(canonicalHash[:]),
  159. }, "\n")
  160. signingKey := volcengineSigningKey(secretKey, shortDate)
  161. signature := hmac.New(sha256.New, signingKey)
  162. signature.Write([]byte(stringToSign))
  163. req.Header.Set("Content-Type", "application/json")
  164. req.Header.Set("Accept", "application/json")
  165. req.Header.Set("X-Date", xDate)
  166. req.Header.Set("X-Content-Sha256", payloadHashHex)
  167. req.Header.Set("Authorization", fmt.Sprintf(
  168. "HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s",
  169. accessKey, scope, signedHeaders, hex.EncodeToString(signature.Sum(nil)),
  170. ))
  171. }
  172. func volcengineSigningKey(secretKey string, shortDate string) []byte {
  173. key := hmacSHA256([]byte(secretKey), shortDate)
  174. key = hmacSHA256(key, "cn-beijing")
  175. key = hmacSHA256(key, "ark")
  176. return hmacSHA256(key, "request")
  177. }
  178. func hmacSHA256(key []byte, message string) []byte {
  179. mac := hmac.New(sha256.New, key)
  180. mac.Write([]byte(message))
  181. return mac.Sum(nil)
  182. }