Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

128 linhas
3.6 KiB

  1. package service
  2. import (
  3. "bytes"
  4. "crypto/hmac"
  5. "crypto/sha256"
  6. "encoding/hex"
  7. "encoding/json"
  8. "fmt"
  9. "net/http"
  10. "strings"
  11. "time"
  12. "github.com/QuantumNous/new-api/common"
  13. "github.com/QuantumNous/new-api/dto"
  14. "github.com/QuantumNous/new-api/setting/system_setting"
  15. )
  16. // WebhookPayload webhook 通知的负载数据
  17. type WebhookPayload struct {
  18. Type string `json:"type"`
  19. Title string `json:"title"`
  20. Content string `json:"content"`
  21. Values []interface{} `json:"values,omitempty"`
  22. Timestamp int64 `json:"timestamp"`
  23. }
  24. // generateSignature 生成 webhook 签名
  25. func generateSignature(secret string, payload []byte) string {
  26. h := hmac.New(sha256.New, []byte(secret))
  27. h.Write(payload)
  28. return hex.EncodeToString(h.Sum(nil))
  29. }
  30. // SendWebhookNotify 发送 webhook 通知
  31. func SendWebhookNotify(webhookURL string, secret string, data dto.Notify) error {
  32. // 处理占位符
  33. content := data.Content
  34. for _, value := range data.Values {
  35. content = strings.Replace(content, dto.ContentValueParam, fmt.Sprintf("%v", value), 1)
  36. }
  37. // 构建 webhook 负载
  38. payload := WebhookPayload{
  39. Type: data.Type,
  40. Title: data.Title,
  41. Content: content,
  42. Values: data.Values,
  43. Timestamp: time.Now().Unix(),
  44. }
  45. // 序列化负载
  46. payloadBytes, err := json.Marshal(payload)
  47. if err != nil {
  48. return fmt.Errorf("failed to marshal webhook payload: %v", err)
  49. }
  50. // 创建 HTTP 请求
  51. var req *http.Request
  52. var resp *http.Response
  53. if system_setting.EnableWorker() {
  54. // 构建worker请求数据
  55. workerReq := &WorkerRequest{
  56. URL: webhookURL,
  57. Key: system_setting.WorkerValidKey,
  58. Method: http.MethodPost,
  59. Headers: map[string]string{
  60. "Content-Type": "application/json",
  61. },
  62. Body: payloadBytes,
  63. }
  64. // 如果有secret,添加签名到headers
  65. if secret != "" {
  66. signature := generateSignature(secret, payloadBytes)
  67. workerReq.Headers["X-Webhook-Signature"] = signature
  68. workerReq.Headers["Authorization"] = "Bearer " + secret
  69. }
  70. resp, err = DoWorkerRequest(workerReq)
  71. if err != nil {
  72. return fmt.Errorf("failed to send webhook request through worker: %v", err)
  73. }
  74. defer resp.Body.Close()
  75. // 检查响应状态
  76. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  77. return fmt.Errorf("webhook request failed with status code: %d", resp.StatusCode)
  78. }
  79. } else {
  80. // SSRF防护:验证Webhook URL(非Worker模式)
  81. fetchSetting := system_setting.GetFetchSetting()
  82. if err := common.ValidateURLWithFetchSetting(webhookURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
  83. return fmt.Errorf("request reject: %v", err)
  84. }
  85. req, err = http.NewRequest(http.MethodPost, webhookURL, bytes.NewBuffer(payloadBytes))
  86. if err != nil {
  87. return fmt.Errorf("failed to create webhook request: %v", err)
  88. }
  89. // 设置请求头
  90. req.Header.Set("Content-Type", "application/json")
  91. // 如果有 secret,生成签名
  92. if secret != "" {
  93. signature := generateSignature(secret, payloadBytes)
  94. req.Header.Set("X-Webhook-Signature", signature)
  95. }
  96. // 发送请求
  97. client := GetHttpClient()
  98. resp, err = client.Do(req)
  99. if err != nil {
  100. return fmt.Errorf("failed to send webhook request: %v", err)
  101. }
  102. defer resp.Body.Close()
  103. // 检查响应状态
  104. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  105. return fmt.Errorf("webhook request failed with status code: %d", resp.StatusCode)
  106. }
  107. }
  108. return nil
  109. }