feat: support more content-type in webhook

This commit is contained in:
Fu Diwei 2025-04-27 00:01:00 +08:00
parent 609a252ee0
commit 193a19b79c
4 changed files with 180 additions and 35 deletions

View File

@ -7,6 +7,7 @@ import (
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
@ -68,14 +69,58 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to parse x509: %w", err)
}
// 处理 Webhook URL
webhookUrl, err := url.Parse(d.config.WebhookUrl)
if err != nil {
return nil, fmt.Errorf("failed to parse webhook url: %w", err)
} else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" {
return nil, fmt.Errorf("unsupported webhook url scheme: %s", webhookUrl.Scheme)
} else {
webhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, "${DOMAIN}", url.PathEscape(certX509.Subject.CommonName))
}
// 处理 Webhook 请求谓词
webhookMethod := strings.ToUpper(d.config.Method)
if webhookMethod == "" {
webhookMethod = http.MethodPost
} else if webhookMethod != http.MethodGet &&
webhookMethod != http.MethodPost &&
webhookMethod != http.MethodPut &&
webhookMethod != http.MethodPatch &&
webhookMethod != http.MethodDelete {
return nil, fmt.Errorf("unsupported webhook request method: %s", webhookMethod)
}
// 处理 Webhook 请求标头
webhookHeaders := make(http.Header)
for k, v := range d.config.Headers {
webhookHeaders.Set(k, v)
}
// 处理 Webhook 请求内容类型
const CONTENT_TYPE_JSON = "application/json"
const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"
const CONTENT_TYPE_MULTIPART = "multipart/form-data"
webhookContentType := webhookHeaders.Get("Content-Type")
if webhookContentType == "" {
webhookContentType = CONTENT_TYPE_JSON
webhookHeaders.Set("Content-Type", CONTENT_TYPE_JSON)
} else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) {
return nil, fmt.Errorf("unsupported webhook content type: %s", webhookContentType)
}
// 处理 Webhook 请求数据
var webhookData interface{}
if d.config.WebhookData == "" {
webhookData = map[string]any{
webhookData = map[string]string{
"name": strings.Join(certX509.DNSNames, ";"),
"cert": certPEM,
"privkey": privkeyPEM,
@ -83,29 +128,49 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
} else {
err = json.Unmarshal([]byte(d.config.WebhookData), &webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshall webhook data: %w", err)
return nil, fmt.Errorf("failed to unmarshal 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)
if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART {
temp := make(map[string]string)
jsonb, err := json.Marshal(webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else if err := json.Unmarshal(jsonb, &temp); err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else {
webhookData = temp
}
}
}
// 生成请求
// 其中 GET 请求需转换为查询参数
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
SetHeaderMultiValues(webhookHeaders)
req.URL = webhookUrl.String()
req.Method = webhookMethod
if webhookMethod == http.MethodGet {
req.SetQueryParams(webhookData.(map[string]string))
} else {
switch webhookContentType {
case CONTENT_TYPE_JSON:
req.SetBody(webhookData)
case CONTENT_TYPE_FORM:
req.SetFormData(webhookData.(map[string]string))
case CONTENT_TYPE_MULTIPART:
req.SetMultipartFormData(webhookData.(map[string]string))
}
}
resp, err := req.
SetHeader("Content-Type", "application/json").
SetBody(webhookData).
Send()
// 发送请求
resp, err := req.SetDebug(true).Send()
if err != nil {
return nil, fmt.Errorf("failed to send webhook request: %w", err)
} else if resp.IsError() {

View File

@ -15,6 +15,7 @@ var (
fInputCertPath string
fInputKeyPath string
fWebhookUrl string
fWebhookContentType string
fWebhookData string
)
@ -24,6 +25,7 @@ func init() {
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fWebhookUrl, argsPrefix+"URL", "", "")
flag.StringVar(&fWebhookContentType, argsPrefix+"CONTENTTYPE", "application/json", "")
flag.StringVar(&fWebhookData, argsPrefix+"DATA", "", "")
}
@ -34,7 +36,8 @@ Shell command to run this test:
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_WEBHOOK_URL="https://example.com/your-webhook-url" \
--CERTIMATE_DEPLOYER_WEBHOOK_DATA="{\"certificate\":\"${Certificate}\",\"privateKey\":\"${PrivateKey}\"}"
--CERTIMATE_DEPLOYER_WEBHOOK_CONTENTTYPE="application/json" \
--CERTIMATE_DEPLOYER_WEBHOOK_DATA="{\"certificate\":\"${CERTIFICATE}\",\"privateKey\":\"${PRIVATE_KEY}\"}"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
@ -45,12 +48,17 @@ func TestDeploy(t *testing.T) {
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
fmt.Sprintf("WEBHOOKCONTENTTYPE: %v", fWebhookContentType),
fmt.Sprintf("WEBHOOKDATA: %v", fWebhookData),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
WebhookUrl: fWebhookUrl,
WebhookData: fWebhookData,
Method: "POST",
Headers: map[string]string{
"Content-Type": fWebhookContentType,
},
AllowInsecureConnections: true,
})
if err != nil {

View File

@ -7,6 +7,7 @@ import (
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
@ -67,35 +68,97 @@ 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) {
// 处理 Webhook URL
webhookUrl, err := url.Parse(n.config.WebhookUrl)
if err != nil {
return nil, fmt.Errorf("failed to parse webhook url: %w", err)
} else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" {
return nil, fmt.Errorf("unsupported webhook url scheme: %s", webhookUrl.Scheme)
}
// 处理 Webhook 请求谓词
webhookMethod := strings.ToUpper(n.config.Method)
if webhookMethod == "" {
webhookMethod = http.MethodPost
} else if webhookMethod != http.MethodGet &&
webhookMethod != http.MethodPost &&
webhookMethod != http.MethodPut &&
webhookMethod != http.MethodPatch &&
webhookMethod != http.MethodDelete {
return nil, fmt.Errorf("unsupported webhook request method: %s", webhookMethod)
}
// 处理 Webhook 请求标头
webhookHeaders := make(http.Header)
for k, v := range n.config.Headers {
webhookHeaders.Set(k, v)
}
// 处理 Webhook 请求内容类型
const CONTENT_TYPE_JSON = "application/json"
const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"
const CONTENT_TYPE_MULTIPART = "multipart/form-data"
webhookContentType := webhookHeaders.Get("Content-Type")
if webhookContentType == "" {
webhookContentType = CONTENT_TYPE_JSON
webhookHeaders.Set("Content-Type", CONTENT_TYPE_JSON)
} else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) {
return nil, fmt.Errorf("unsupported webhook content type: %s", webhookContentType)
}
// 处理 Webhook 请求数据
var webhookData interface{}
if n.config.WebhookData == "" {
webhookData = map[string]any{
webhookData = map[string]string{
"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)
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
}
replaceJsonValueRecursively(webhookData, "${SUBJECT}", subject)
replaceJsonValueRecursively(webhookData, "${MESSAGE}", message)
if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART {
temp := make(map[string]string)
jsonb, err := json.Marshal(webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else if err := json.Unmarshal(jsonb, &temp); err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else {
webhookData = temp
}
}
}
// 生成请求
// 其中 GET 请求需转换为查询参数
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
SetHeaderMultiValues(webhookHeaders)
req.URL = webhookUrl.String()
req.Method = webhookMethod
if webhookMethod == http.MethodGet {
req.SetQueryParams(webhookData.(map[string]string))
} else {
switch webhookContentType {
case CONTENT_TYPE_JSON:
req.SetBody(webhookData)
case CONTENT_TYPE_FORM:
req.SetFormData(webhookData.(map[string]string))
case CONTENT_TYPE_MULTIPART:
req.SetMultipartFormData(webhookData.(map[string]string))
}
}
resp, err := req.
SetHeader("Content-Type", "application/json").
SetBody(webhookData).
Send()
// 发送请求
resp, err := req.Send()
if err != nil {
return nil, fmt.Errorf("failed to send webhook request: %w", err)
} else if resp.IsError() {

View File

@ -15,19 +15,24 @@ const (
mockMessage = "test_message"
)
var fUrl string
var (
fWebhookUrl string
fWebhookContentType string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_WEBHOOK_"
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
flag.StringVar(&fWebhookUrl, argsPrefix+"URL", "", "")
flag.StringVar(&fWebhookContentType, argsPrefix+"CONTENTTYPE", "application/json", "")
}
/*
Shell command to run this test:
go test -v ./webhook_test.go -args \
--CERTIMATE_NOTIFIER_WEBHOOK_URL="https://example.com/your-webhook-url"
--CERTIMATE_NOTIFIER_WEBHOOK_URL="https://example.com/your-webhook-url" \
--CERTIMATE_NOTIFIER_WEBHOOK_CONTENTTYPE="application/json"
*/
func TestNotify(t *testing.T) {
flag.Parse()
@ -35,11 +40,15 @@ func TestNotify(t *testing.T) {
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("URL: %v", fUrl),
fmt.Sprintf("URL: %v", fWebhookUrl),
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
WebhookUrl: fUrl,
WebhookUrl: fWebhookUrl,
Method: "POST",
Headers: map[string]string{
"Content-Type": fWebhookContentType,
},
AllowInsecureConnections: true,
})
if err != nil {