mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
ad8384182c
24 of the 96 nolint directives were dead: they covered findings that no longer exist. A stale suppression is not inert — it silently claims any future finding for that linter in that scope, so a real problem raised there would vanish without anyone noticing. Explanatory prose was kept as ordinary comments. Two directives proved load-bearing under the project gate even though nolintlint reported them unused, and removing them exposed verified contextcheck false positives: App.Run does forward a sigCtx derived from the caller's context to Start, and the migration lock renewal must keep its own deadline because the task context may already be canceled. Both were restored, narrowed to the live linter, and given the reason the originals lacked.
98 lines
2.9 KiB
Go
98 lines
2.9 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package push
|
|
|
|
import (
|
|
"Wavelet/pkg/httppool"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
func init() {
|
|
Register("custom", &CustomPusher{})
|
|
}
|
|
|
|
// maxCustomResponseBytes 限制读取 Webhook 响应体的最大字节数,防止无界读取。
|
|
const maxCustomResponseBytes = 4096
|
|
|
|
// CustomPusher 自定义 Webhook 发送实现
|
|
type CustomPusher struct{}
|
|
|
|
// Send 发送自定义 webhook
|
|
func (p *CustomPusher) Send(ctx context.Context, cfg Config, _ string, body map[string]any, template string, _ map[string]any) (string, error) {
|
|
if cfg.URL == "" {
|
|
return "", errors.New("custom: URL is required")
|
|
}
|
|
|
|
var reqBody []byte
|
|
|
|
if template != "" {
|
|
// 替换模板中的 {{key}} 占位符
|
|
rendered := ParseTemplate(template, body)
|
|
reqBody = []byte(rendered)
|
|
} else {
|
|
// 兜底:直接把 body 转为 JSON 字符串发送
|
|
var err error
|
|
reqBody, err = json.Marshal(body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("custom: marshal body failed: %w", err)
|
|
}
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cfg.URL, bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return "", fmt.Errorf("custom: create http request failed: %w", err)
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
|
// 如果配置了 Key 且格式为 "HeaderName:HeaderValue",我们可以附加测试用 Header
|
|
if cfg.Key != "" && strings.Contains(cfg.Key, ":") {
|
|
parts := strings.SplitN(cfg.Key, ":", 2) //nolint:mnd
|
|
httpReq.Header.Set(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
|
|
}
|
|
|
|
client := httppool.NewClient(defaultHTTPClientTimeout)
|
|
resp, err := client.Do(httpReq)
|
|
if err != nil {
|
|
return "", fmt.Errorf("custom: http request failed: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, maxCustomResponseBytes))
|
|
upstreamResp := strings.TrimSpace(string(bodyBytes))
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return upstreamResp, fmt.Errorf("custom: http status %s", resp.Status)
|
|
}
|
|
|
|
// 部分 Webhook(如企业微信、钉钉)即使业务失败也返回 HTTP 200,
|
|
// 仅当响应体包含非零 errcode 时才判定为发送失败,避免审计记录误报成功。
|
|
var apiResp struct {
|
|
ErrCode int `json:"errcode"`
|
|
ErrMsg string `json:"errmsg"`
|
|
}
|
|
if err := json.Unmarshal(bodyBytes, &apiResp); err == nil && apiResp.ErrCode != 0 {
|
|
return upstreamResp, fmt.Errorf("custom: webhook rejected: errcode=%d errmsg=%q", apiResp.ErrCode, apiResp.ErrMsg)
|
|
}
|
|
|
|
return upstreamResp, nil
|
|
}
|
|
|
|
// ValidateConfig 校验自定义配置
|
|
func (p *CustomPusher) ValidateConfig(cfg Config) error {
|
|
if cfg.URL == "" {
|
|
return errors.New("webhook URL is required")
|
|
}
|
|
if !strings.HasPrefix(cfg.URL, "http://") && !strings.HasPrefix(cfg.URL, "https://") {
|
|
return errors.New("webhook URL must start with http:// or https://")
|
|
}
|
|
return nil
|
|
}
|