feat: support configuring method and headers in webhook
This commit is contained in:
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ¬ifier.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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
33
internal/pkg/utils/http/parser.go
Normal file
33
internal/pkg/utils/http/parser.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 从表示 HTTP 标头的字符串解析并返回一个 http.Header 对象。
|
||||
//
|
||||
// 入参:
|
||||
// - headers: 表示 HTTP 标头的字符串。
|
||||
//
|
||||
// 出参:
|
||||
// - header: http.Header 对象。
|
||||
// - err: 错误。
|
||||
func ParseHeaders(headers string) (http.Header, error) {
|
||||
str := strings.TrimSpace(headers) + "\r\n\r\n"
|
||||
if len(str) == 4 {
|
||||
return make(http.Header), nil
|
||||
}
|
||||
|
||||
br := bufio.NewReader(strings.NewReader(str))
|
||||
tp := textproto.NewReader(br)
|
||||
|
||||
mimeHeader, err := tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.Header(mimeHeader), err
|
||||
}
|
||||
@@ -199,6 +199,28 @@ func GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 以 `map[string]V` 形式从字典中获取指定键的值。
|
||||
//
|
||||
// 入参:
|
||||
// - dict: 字典。
|
||||
// - key: 键。
|
||||
//
|
||||
// 出参:
|
||||
// - 字典中键对应的 `map[string]V` 对象。
|
||||
func GetKVMap[V any](dict map[string]any, key string) map[string]V {
|
||||
if dict == nil {
|
||||
return make(map[string]V)
|
||||
}
|
||||
|
||||
if val, ok := dict[key]; ok {
|
||||
if result, ok := val.(map[string]V); ok {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return make(map[string]V)
|
||||
}
|
||||
|
||||
// 以 `map[string]any` 形式从字典中获取指定键的值。
|
||||
//
|
||||
// 入参:
|
||||
@@ -207,16 +229,6 @@ func GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool {
|
||||
//
|
||||
// 出参:
|
||||
// - 字典中键对应的 `map[string]any` 对象。
|
||||
func GetMap(dict map[string]any, key string) map[string]any {
|
||||
if dict == nil {
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
if val, ok := dict[key]; ok {
|
||||
if result, ok := val.(map[string]any); ok {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return make(map[string]any)
|
||||
func GetKVMapAny(dict map[string]any, key string) map[string]any {
|
||||
return GetKVMap[any](dict, key)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user