feat: support configuring method and headers in webhook

This commit is contained in:
Fu Diwei
2025-04-26 21:17:09 +08:00
parent 3c2fbd720f
commit 3be70c3696
16 changed files with 482 additions and 243 deletions

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
@@ -18,8 +19,13 @@ import (
type DeployerConfig struct {
// Webhook URL。
WebhookUrl string `json:"webhookUrl"`
// Webhook 回调数据(JSON 格式)。
// Webhook 回调数据(application/json 或 application/x-www-form-urlencoded 格式)。
WebhookData string `json:"webhookData,omitempty"`
// 请求谓词。
// 零值时默认为 "POST"。
Method string `json:"method,omitempty"`
// 请求标头。
Headers map[string]string `json:"headers,omitempty"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
@@ -68,25 +74,41 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
var webhookData interface{}
err = json.Unmarshal([]byte(d.config.WebhookData), &webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshall webhook data: %w", err)
if d.config.WebhookData == "" {
webhookData = map[string]any{
"name": strings.Join(certX509.DNSNames, ";"),
"cert": certPEM,
"privkey": privkeyPEM,
}
} else {
err = json.Unmarshal([]byte(d.config.WebhookData), &webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshall webhook data: %w", err)
}
replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName)
replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${SUBJECT_ALT_NAMES}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM)
replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPEM)
}
replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName)
replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${SUBJECT_ALT_NAMES}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM)
replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPEM)
resp, err := d.httpClient.R().
req := d.httpClient.R().
SetContext(ctx).
SetHeaders(d.config.Headers)
req.URL = d.config.WebhookUrl
req.Method = d.config.Method
if req.Method == "" {
req.Method = http.MethodPost
}
resp, err := req.
SetHeader("Content-Type", "application/json").
SetBody(webhookData).
Post(d.config.WebhookUrl)
Send()
if err != nil {
return nil, fmt.Errorf("failed to send webhook request: %w", err)
} else if resp.StatusCode() != 200 {
} else if resp.IsError() {
return nil, fmt.Errorf("unexpected webhook response status code: %d", resp.StatusCode())
}

View File

@@ -3,24 +3,36 @@ package webhook
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
webhook "github.com/nikoksr/notify/service/http"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
// Webhook URL。
Url string `json:"url"`
WebhookUrl string `json:"webhookUrl"`
// Webhook 回调数据application/json 或 application/x-www-form-urlencoded 格式)。
WebhookData string `json:"webhookData,omitempty"`
// 请求谓词。
// 零值时默认为 "POST"。
Method string `json:"method,omitempty"`
// 请求标头。
Headers map[string]string `json:"headers,omitempty"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
config *NotifierConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@@ -30,8 +42,18 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New().
SetTimeout(30 * time.Second).
SetRetryCount(3).
SetRetryWaitTime(5 * time.Second)
if config.AllowInsecureConnections {
client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}
return &NotifierProvider{
config: config,
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
@@ -45,20 +67,58 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := webhook.New()
srv.AddReceiversURLs(n.config.Url)
var webhookData interface{}
if n.config.WebhookData == "" {
webhookData = map[string]any{
"subject": subject,
"message": message,
}
} else {
err = json.Unmarshal([]byte(n.config.WebhookData), &webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshall webhook data: %w", err)
}
if n.config.AllowInsecureConnections {
tlsConfig := &tls.Config{InsecureSkipVerify: true}
transport := &http.Transport{TLSClientConfig: tlsConfig}
client := &http.Client{Transport: transport}
srv.WithClient(client)
replaceJsonValueRecursively(webhookData, "${SUBJECT}", subject)
replaceJsonValueRecursively(webhookData, "${MESSAGE}", message)
}
err = srv.Send(ctx, subject, message)
req := n.httpClient.R().
SetContext(ctx).
SetHeaders(n.config.Headers)
req.URL = n.config.WebhookUrl
req.Method = n.config.Method
if req.Method == "" {
req.Method = http.MethodPost
}
resp, err := req.
SetHeader("Content-Type", "application/json").
SetBody(webhookData).
Send()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to send webhook request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("unexpected webhook response status code: %d", resp.StatusCode())
}
n.logger.Debug("webhook responded", slog.Any("response", resp.String()))
return &notifier.NotifyResult{}, nil
}
func replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} {
switch v := data.(type) {
case map[string]any:
for k, val := range v {
v[k] = replaceJsonValueRecursively(val, oldStr, newStr)
}
case []any:
for i, val := range v {
v[i] = replaceJsonValueRecursively(val, oldStr, newStr)
}
case string:
return strings.ReplaceAll(v, oldStr, newStr)
}
return data
}

View File

@@ -39,7 +39,7 @@ func TestNotify(t *testing.T) {
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
Url: fUrl,
WebhookUrl: fUrl,
AllowInsecureConnections: true,
})
if err != nil {