Files
1Panel/core/utils/alert_webhook/secret.go

137 lines
4.3 KiB
Go

package alert_webhook
import (
"encoding/json"
"fmt"
"io"
"net/url"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
"github.com/google/uuid"
)
const (
agentKeyPrefix = "agent:v1:"
secretSchemaV1 = 1
maxSecretLength = 256 * 1024
maxSecretHeaders = 64
maxSecretValueSize = 16 * 1024
)
type secretConfig struct {
SchemaVersion int `json:"schemaVersion"`
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
}
func ExportSecretPlain(config model.AlertConfig) (string, error) {
if config.Type != constant.Custom {
return "", fmt.Errorf("alert config %d is not a custom webhook", config.ID)
}
if !strings.HasPrefix(config.SecretConfig, agentKeyPrefix) {
return "", fmt.Errorf("custom webhook secret is not encrypted with the agent key")
}
payload := strings.TrimPrefix(config.SecretConfig, agentKeyPrefix)
if payload == "" {
return "", fmt.Errorf("custom webhook secret ciphertext is empty")
}
key, err := loadEncryptKey()
if err != nil {
return "", err
}
plainText, err := encrypt.StringDecryptWithKey(payload, key)
if err != nil {
return "", fmt.Errorf("decrypt custom webhook secret: %w", err)
}
secret, err := decodeAndValidateSecret(plainText)
if err != nil {
return "", err
}
canonical, err := json.Marshal(secret)
if err != nil {
return "", fmt.Errorf("encode custom webhook secret: %w", err)
}
return string(canonical), nil
}
// Read the source Agent key, never the Core configuration or Core database key.
func loadEncryptKey() (string, error) {
if global.AgentDB == nil {
return "", fmt.Errorf("source Agent database is unavailable")
}
var setting model.Setting
if err := global.AgentDB.Where("key = ?", "EncryptKey").First(&setting).Error; err != nil {
return "", fmt.Errorf("read source Agent encrypt key: %w", err)
}
key := strings.TrimSpace(setting.Value)
if key == "" {
return "", fmt.Errorf("source Agent encrypt key is empty")
}
return key, nil
}
func decodeAndValidateSecret(plainText string) (secretConfig, error) {
if len(plainText) == 0 || len(plainText) > maxSecretLength {
return secretConfig{}, fmt.Errorf("custom webhook secret has an invalid size")
}
var secret secretConfig
decoder := json.NewDecoder(strings.NewReader(plainText))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&secret); err != nil {
return secretConfig{}, fmt.Errorf("decode custom webhook secret: %w", err)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
return secretConfig{}, fmt.Errorf("decode custom webhook secret: multiple JSON values are not allowed")
}
if secret.SchemaVersion != secretSchemaV1 {
return secretConfig{}, fmt.Errorf("unsupported custom webhook secret schemaVersion: %d", secret.SchemaVersion)
}
if err := validateSecretURL(secret.URL); err != nil {
return secretConfig{}, err
}
if len(secret.Headers) > maxSecretHeaders {
return secretConfig{}, fmt.Errorf("custom webhook secret headers exceed the limit")
}
if secret.Headers == nil {
secret.Headers = map[string]string{}
}
for uid, value := range secret.Headers {
if _, err := uuid.Parse(strings.TrimSpace(uid)); err != nil {
return secretConfig{}, fmt.Errorf("custom webhook secret header uid must be a UUID")
}
if value == "" || len(value) > maxSecretValueSize || strings.ContainsAny(value, "\r\n") {
return secretConfig{}, fmt.Errorf("custom webhook secret header is invalid")
}
}
return secret, nil
}
func validateSecretURL(rawURL string) error {
if rawURL == "" {
return nil
}
parsed, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("invalid custom webhook URL")
}
hostname := strings.TrimSuffix(strings.TrimSpace(parsed.Hostname()), ".")
if (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || hostname == "" || strings.Contains(hostname, "%") || parsed.User != nil || parsed.Fragment != "" {
return fmt.Errorf("invalid custom webhook URL")
}
if strings.HasSuffix(parsed.Host, ":") {
return fmt.Errorf("invalid custom webhook URL")
}
if port := parsed.Port(); port != "" {
value, err := strconv.Atoi(port)
if err != nil || value < 1 || value > 65535 {
return fmt.Errorf("invalid custom webhook URL")
}
}
return nil
}