- Created a comprehensive documentation file for DNS provider types, including RFC 2136, Webhook, and Script providers, detailing their use cases, configurations, and security notes. - Updated the DNSProviderForm component to handle new field types including select and textarea for better user input management. - Enhanced the DNS provider schemas to include new fields for script execution, webhook authentication, and RFC 2136 configurations, improving flexibility and usability.
339 lines
12 KiB
Go
339 lines
12 KiB
Go
// Package custom provides custom DNS provider plugins for non-built-in integrations.
|
|
package custom
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
|
)
|
|
|
|
// Webhook provider constants.
|
|
const (
|
|
WebhookDefaultTimeoutSeconds = 30
|
|
WebhookDefaultRetryCount = 3
|
|
WebhookDefaultPropagationTimeout = 120 * time.Second
|
|
WebhookDefaultPollingInterval = 5 * time.Second
|
|
WebhookMinTimeoutSeconds = 5
|
|
WebhookMaxTimeoutSeconds = 300
|
|
WebhookMinRetryCount = 0
|
|
WebhookMaxRetryCount = 10
|
|
)
|
|
|
|
// WebhookProvider implements the ProviderPlugin interface for generic HTTP webhook DNS challenges.
|
|
// This provider calls external HTTP endpoints to create/delete DNS TXT records,
|
|
// enabling integration with custom or proprietary DNS systems.
|
|
type WebhookProvider struct {
|
|
propagationTimeout time.Duration
|
|
pollingInterval time.Duration
|
|
}
|
|
|
|
// NewWebhookProvider creates a new WebhookProvider with default settings.
|
|
func NewWebhookProvider() *WebhookProvider {
|
|
return &WebhookProvider{
|
|
propagationTimeout: WebhookDefaultPropagationTimeout,
|
|
pollingInterval: WebhookDefaultPollingInterval,
|
|
}
|
|
}
|
|
|
|
// Type returns the unique provider type identifier.
|
|
func (p *WebhookProvider) Type() string {
|
|
return "webhook"
|
|
}
|
|
|
|
// Metadata returns descriptive information about the provider.
|
|
func (p *WebhookProvider) Metadata() dnsprovider.ProviderMetadata {
|
|
return dnsprovider.ProviderMetadata{
|
|
Type: "webhook",
|
|
Name: "Webhook (HTTP)",
|
|
Description: "Generic HTTP webhook for DNS challenges. Calls external endpoints to create and delete TXT records. Useful for custom DNS APIs or proprietary systems.",
|
|
DocumentationURL: "https://charon.dev/docs/features/webhook-dns",
|
|
IsBuiltIn: false,
|
|
Version: "1.0.0",
|
|
InterfaceVersion: dnsprovider.InterfaceVersion,
|
|
}
|
|
}
|
|
|
|
// Init is called after the plugin is registered.
|
|
func (p *WebhookProvider) Init() error {
|
|
return nil
|
|
}
|
|
|
|
// Cleanup is called before the plugin is unregistered.
|
|
func (p *WebhookProvider) Cleanup() error {
|
|
return nil
|
|
}
|
|
|
|
// RequiredCredentialFields returns credential fields that must be provided.
|
|
func (p *WebhookProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
|
return []dnsprovider.CredentialFieldSpec{
|
|
{
|
|
Name: "create_url",
|
|
Label: "Create URL",
|
|
Type: "text",
|
|
Placeholder: "https://dns-api.example.com/txt/create",
|
|
Hint: "POST endpoint for creating DNS TXT records. Must be HTTPS (HTTP allowed for localhost in development).",
|
|
},
|
|
{
|
|
Name: "delete_url",
|
|
Label: "Delete URL",
|
|
Type: "text",
|
|
Placeholder: "https://dns-api.example.com/txt/delete",
|
|
Hint: "POST/DELETE endpoint for removing DNS TXT records. Must be HTTPS (HTTP allowed for localhost in development).",
|
|
},
|
|
}
|
|
}
|
|
|
|
// OptionalCredentialFields returns credential fields that may be provided.
|
|
func (p *WebhookProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
|
return []dnsprovider.CredentialFieldSpec{
|
|
{
|
|
Name: "auth_header",
|
|
Label: "Authorization Header Name",
|
|
Type: "text",
|
|
Placeholder: "Authorization",
|
|
Hint: "Custom header name for authentication (e.g., Authorization, X-API-Key)",
|
|
},
|
|
{
|
|
Name: "auth_value",
|
|
Label: "Authorization Header Value",
|
|
Type: "password",
|
|
Placeholder: "",
|
|
Hint: "Value for the authorization header (e.g., Bearer token, API key)",
|
|
},
|
|
{
|
|
Name: "timeout_seconds",
|
|
Label: "Request Timeout (seconds)",
|
|
Type: "text",
|
|
Placeholder: strconv.Itoa(WebhookDefaultTimeoutSeconds),
|
|
Hint: fmt.Sprintf("HTTP request timeout (%d-%d seconds, default: %d)", WebhookMinTimeoutSeconds, WebhookMaxTimeoutSeconds, WebhookDefaultTimeoutSeconds),
|
|
},
|
|
{
|
|
Name: "retry_count",
|
|
Label: "Retry Count",
|
|
Type: "text",
|
|
Placeholder: strconv.Itoa(WebhookDefaultRetryCount),
|
|
Hint: fmt.Sprintf("Number of retries on failure (%d-%d, default: %d)", WebhookMinRetryCount, WebhookMaxRetryCount, WebhookDefaultRetryCount),
|
|
},
|
|
{
|
|
Name: "insecure_skip_verify",
|
|
Label: "Skip TLS Verification",
|
|
Type: "select",
|
|
Placeholder: "",
|
|
Hint: "⚠️ DEVELOPMENT ONLY: Skip TLS certificate verification. Never enable in production!",
|
|
Options: []dnsprovider.SelectOption{
|
|
{Value: "false", Label: "No (Recommended)"},
|
|
{Value: "true", Label: "Yes (Insecure - Dev Only)"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ValidateCredentials checks if the provided credentials are valid.
|
|
func (p *WebhookProvider) ValidateCredentials(creds map[string]string) error {
|
|
// Validate required fields
|
|
createURL := strings.TrimSpace(creds["create_url"])
|
|
if createURL == "" {
|
|
return fmt.Errorf("create_url is required")
|
|
}
|
|
|
|
deleteURL := strings.TrimSpace(creds["delete_url"])
|
|
if deleteURL == "" {
|
|
return fmt.Errorf("delete_url is required")
|
|
}
|
|
|
|
// Validate create URL format and security
|
|
if err := p.validateWebhookURL(createURL, "create_url"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate delete URL format and security
|
|
if err := p.validateWebhookURL(deleteURL, "delete_url"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate timeout if provided
|
|
if timeoutStr := strings.TrimSpace(creds["timeout_seconds"]); timeoutStr != "" {
|
|
timeout, err := strconv.Atoi(timeoutStr)
|
|
if err != nil {
|
|
return fmt.Errorf("timeout_seconds must be a number: %w", err)
|
|
}
|
|
if timeout < WebhookMinTimeoutSeconds || timeout > WebhookMaxTimeoutSeconds {
|
|
return fmt.Errorf("timeout_seconds must be between %d and %d", WebhookMinTimeoutSeconds, WebhookMaxTimeoutSeconds)
|
|
}
|
|
}
|
|
|
|
// Validate retry count if provided
|
|
if retryStr := strings.TrimSpace(creds["retry_count"]); retryStr != "" {
|
|
retry, err := strconv.Atoi(retryStr)
|
|
if err != nil {
|
|
return fmt.Errorf("retry_count must be a number: %w", err)
|
|
}
|
|
if retry < WebhookMinRetryCount || retry > WebhookMaxRetryCount {
|
|
return fmt.Errorf("retry_count must be between %d and %d", WebhookMinRetryCount, WebhookMaxRetryCount)
|
|
}
|
|
}
|
|
|
|
// Validate insecure_skip_verify if provided
|
|
if insecureStr := strings.TrimSpace(creds["insecure_skip_verify"]); insecureStr != "" {
|
|
insecureStr = strings.ToLower(insecureStr)
|
|
if insecureStr != "true" && insecureStr != "false" {
|
|
return fmt.Errorf("insecure_skip_verify must be 'true' or 'false'")
|
|
}
|
|
}
|
|
|
|
// Validate auth header/value consistency
|
|
authHeader := strings.TrimSpace(creds["auth_header"])
|
|
authValue := strings.TrimSpace(creds["auth_value"])
|
|
if (authHeader != "" && authValue == "") || (authHeader == "" && authValue != "") {
|
|
return fmt.Errorf("both auth_header and auth_value must be provided together, or neither")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateWebhookURL validates a webhook URL for format and SSRF protection.
|
|
// Note: During validation, we only check format and basic security constraints.
|
|
// Full SSRF validation with DNS resolution happens at runtime when the webhook is called.
|
|
func (p *WebhookProvider) validateWebhookURL(rawURL, fieldName string) error {
|
|
// Parse URL first for basic validation
|
|
parsed, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return fmt.Errorf("%s has invalid URL format: %w", fieldName, err)
|
|
}
|
|
|
|
// Validate scheme
|
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
|
return fmt.Errorf("%s must use http or https scheme", fieldName)
|
|
}
|
|
|
|
// Validate hostname exists
|
|
host := parsed.Hostname()
|
|
if host == "" {
|
|
return fmt.Errorf("%s is missing hostname", fieldName)
|
|
}
|
|
|
|
// Check if this is a localhost URL (allowed for development)
|
|
isLocalhost := host == "localhost" || host == "127.0.0.1" || host == "::1"
|
|
|
|
// Require HTTPS for non-localhost URLs
|
|
if !isLocalhost && parsed.Scheme != "https" {
|
|
return fmt.Errorf("%s must use HTTPS for non-localhost URLs (security requirement)", fieldName)
|
|
}
|
|
|
|
// For external URLs (non-localhost), we skip DNS-based SSRF validation during
|
|
// credential validation as the target might not be reachable from the validation
|
|
// environment. Runtime SSRF protection will be enforced when actually calling the webhook.
|
|
// This matches the pattern used by RFC2136Provider which also validates format only.
|
|
|
|
return nil
|
|
}
|
|
|
|
// TestCredentials attempts to verify credentials work.
|
|
// For webhook, we validate the format but cannot test without making actual HTTP calls.
|
|
func (p *WebhookProvider) TestCredentials(creds map[string]string) error {
|
|
return p.ValidateCredentials(creds)
|
|
}
|
|
|
|
// SupportsMultiCredential indicates if the provider can handle zone-specific credentials.
|
|
func (p *WebhookProvider) SupportsMultiCredential() bool {
|
|
return false
|
|
}
|
|
|
|
// BuildCaddyConfig constructs the Caddy DNS challenge configuration.
|
|
// For webhook, this returns a config that Charon's internal webhook handler will use.
|
|
func (p *WebhookProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
|
config := map[string]any{
|
|
"name": "webhook",
|
|
"create_url": strings.TrimSpace(creds["create_url"]),
|
|
"delete_url": strings.TrimSpace(creds["delete_url"]),
|
|
}
|
|
|
|
// Add auth header if provided
|
|
if authHeader := strings.TrimSpace(creds["auth_header"]); authHeader != "" {
|
|
config["auth_header"] = authHeader
|
|
}
|
|
|
|
// Add auth value if provided
|
|
if authValue := strings.TrimSpace(creds["auth_value"]); authValue != "" {
|
|
config["auth_value"] = authValue
|
|
}
|
|
|
|
// Add timeout with default
|
|
timeoutSeconds := WebhookDefaultTimeoutSeconds
|
|
if timeoutStr := strings.TrimSpace(creds["timeout_seconds"]); timeoutStr != "" {
|
|
if t, err := strconv.Atoi(timeoutStr); err == nil && t >= WebhookMinTimeoutSeconds && t <= WebhookMaxTimeoutSeconds {
|
|
timeoutSeconds = t
|
|
}
|
|
}
|
|
config["timeout_seconds"] = timeoutSeconds
|
|
|
|
// Add retry count with default
|
|
retryCount := WebhookDefaultRetryCount
|
|
if retryStr := strings.TrimSpace(creds["retry_count"]); retryStr != "" {
|
|
if r, err := strconv.Atoi(retryStr); err == nil && r >= WebhookMinRetryCount && r <= WebhookMaxRetryCount {
|
|
retryCount = r
|
|
}
|
|
}
|
|
config["retry_count"] = retryCount
|
|
|
|
// Add insecure skip verify with default (false)
|
|
insecureSkipVerify := false
|
|
if insecureStr := strings.TrimSpace(creds["insecure_skip_verify"]); insecureStr != "" {
|
|
insecureSkipVerify = strings.ToLower(insecureStr) == "true"
|
|
}
|
|
config["insecure_skip_verify"] = insecureSkipVerify
|
|
|
|
return config
|
|
}
|
|
|
|
// BuildCaddyConfigForZone constructs config for a specific zone.
|
|
func (p *WebhookProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
|
return p.BuildCaddyConfig(creds)
|
|
}
|
|
|
|
// PropagationTimeout returns the recommended DNS propagation wait time.
|
|
func (p *WebhookProvider) PropagationTimeout() time.Duration {
|
|
return p.propagationTimeout
|
|
}
|
|
|
|
// PollingInterval returns the recommended polling interval for DNS verification.
|
|
func (p *WebhookProvider) PollingInterval() time.Duration {
|
|
return p.pollingInterval
|
|
}
|
|
|
|
// GetTimeoutSeconds returns the configured timeout in seconds or the default.
|
|
func (p *WebhookProvider) GetTimeoutSeconds(creds map[string]string) int {
|
|
if timeoutStr := strings.TrimSpace(creds["timeout_seconds"]); timeoutStr != "" {
|
|
if timeout, err := strconv.Atoi(timeoutStr); err == nil {
|
|
if timeout >= WebhookMinTimeoutSeconds && timeout <= WebhookMaxTimeoutSeconds {
|
|
return timeout
|
|
}
|
|
}
|
|
}
|
|
return WebhookDefaultTimeoutSeconds
|
|
}
|
|
|
|
// GetRetryCount returns the configured retry count or the default.
|
|
func (p *WebhookProvider) GetRetryCount(creds map[string]string) int {
|
|
if retryStr := strings.TrimSpace(creds["retry_count"]); retryStr != "" {
|
|
if retry, err := strconv.Atoi(retryStr); err == nil {
|
|
if retry >= WebhookMinRetryCount && retry <= WebhookMaxRetryCount {
|
|
return retry
|
|
}
|
|
}
|
|
}
|
|
return WebhookDefaultRetryCount
|
|
}
|
|
|
|
// IsInsecureSkipVerify returns whether TLS verification should be skipped.
|
|
func (p *WebhookProvider) IsInsecureSkipVerify(creds map[string]string) bool {
|
|
if insecureStr := strings.TrimSpace(creds["insecure_skip_verify"]); insecureStr != "" {
|
|
return strings.ToLower(insecureStr) == "true"
|
|
}
|
|
return false
|
|
}
|