diff --git a/internal/pkg/core/deployer/providers/webhook/webhook.go b/internal/pkg/core/deployer/providers/webhook/webhook.go index f405ff96..3464e411 100644 --- a/internal/pkg/core/deployer/providers/webhook/webhook.go +++ b/internal/pkg/core/deployer/providers/webhook/webhook.go @@ -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() { diff --git a/internal/pkg/core/deployer/providers/webhook/webhook_test.go b/internal/pkg/core/deployer/providers/webhook/webhook_test.go index 0bd670b3..8642ef14 100644 --- a/internal/pkg/core/deployer/providers/webhook/webhook_test.go +++ b/internal/pkg/core/deployer/providers/webhook/webhook_test.go @@ -12,10 +12,11 @@ import ( ) var ( - fInputCertPath string - fInputKeyPath string - fWebhookUrl string - fWebhookData string + fInputCertPath string + fInputKeyPath string + fWebhookUrl string + fWebhookContentType string + fWebhookData string ) func init() { @@ -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, + WebhookUrl: fWebhookUrl, + WebhookData: fWebhookData, + Method: "POST", + Headers: map[string]string{ + "Content-Type": fWebhookContentType, + }, AllowInsecureConnections: true, }) if err != nil { diff --git a/internal/pkg/core/notifier/providers/webhook/webhook.go b/internal/pkg/core/notifier/providers/webhook/webhook.go index df3b8d49..669dea2f 100644 --- a/internal/pkg/core/notifier/providers/webhook/webhook.go +++ b/internal/pkg/core/notifier/providers/webhook/webhook.go @@ -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() { diff --git a/internal/pkg/core/notifier/providers/webhook/webhook_test.go b/internal/pkg/core/notifier/providers/webhook/webhook_test.go index 3655d76b..c416b3c9 100644 --- a/internal/pkg/core/notifier/providers/webhook/webhook_test.go +++ b/internal/pkg/core/notifier/providers/webhook/webhook_test.go @@ -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 {