chore: move '/internal/pkg' to '/pkg'

This commit is contained in:
Fu Diwei
2025-06-17 15:54:21 +08:00
parent 30840bbba5
commit 205275b52d
611 changed files with 693 additions and 693 deletions

View File

@@ -0,0 +1,205 @@
package webhook
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/pkg/core"
xcert "github.com/usual2970/certimate/pkg/utils/cert"
)
type SSLDeployerProviderConfig struct {
// Webhook 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 SSLDeployerProvider struct {
config *SSLDeployerProviderConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
if config == nil {
return nil, errors.New("the configuration of the ssl deployer provider 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 &SSLDeployerProvider{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to parse x509: %w", err)
}
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %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]string{
"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 unmarshal webhook data: %w", err)
}
replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName)
replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM)
replaceJsonValueRecursively(webhookData, "${SERVER_CERTIFICATE}", serverCertPEM)
replaceJsonValueRecursively(webhookData, "${INTERMEDIA_CERTIFICATE}", intermediaCertPEM)
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().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.Send()
if err != nil {
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())
}
d.logger.Debug("webhook responded", slog.Any("response", resp.String()))
return &core.SSLDeployResult{}, 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
}