chore: clean .gitignore cache
This commit is contained in:
@@ -1,311 +0,0 @@
|
||||
// Package custom provides custom DNS provider plugins for non-built-in integrations.
|
||||
package custom
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
|
||||
)
|
||||
|
||||
// Script provider constants.
|
||||
const (
|
||||
ScriptDefaultTimeoutSeconds = 60
|
||||
ScriptDefaultPropagationTimeout = 120 * time.Second
|
||||
ScriptDefaultPollingInterval = 5 * time.Second
|
||||
ScriptMinTimeoutSeconds = 5
|
||||
ScriptMaxTimeoutSeconds = 300
|
||||
ScriptAllowedDirectory = "/scripts/"
|
||||
)
|
||||
|
||||
// scriptArgPattern validates script arguments to prevent injection attacks.
|
||||
// Only allows alphanumeric characters, dots, underscores, equals, and hyphens.
|
||||
var scriptArgPattern = regexp.MustCompile(`^[a-zA-Z0-9._=-]+$`)
|
||||
|
||||
// envVarLinePattern validates environment variable format (KEY=VALUE).
|
||||
var envVarLinePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*=.*$`)
|
||||
|
||||
// ScriptProvider implements the ProviderPlugin interface for shell script DNS challenges.
|
||||
// This provider executes local scripts to create/delete DNS TXT records.
|
||||
//
|
||||
// SECURITY WARNING: This is a HIGH-RISK feature. Scripts are executed on the server
|
||||
// with the same privileges as the Charon process. Only administrators should configure
|
||||
// this provider, and scripts must be carefully reviewed before deployment.
|
||||
type ScriptProvider struct {
|
||||
propagationTimeout time.Duration
|
||||
pollingInterval time.Duration
|
||||
}
|
||||
|
||||
// NewScriptProvider creates a new ScriptProvider with default settings.
|
||||
func NewScriptProvider() *ScriptProvider {
|
||||
return &ScriptProvider{
|
||||
propagationTimeout: ScriptDefaultPropagationTimeout,
|
||||
pollingInterval: ScriptDefaultPollingInterval,
|
||||
}
|
||||
}
|
||||
|
||||
// Type returns the unique provider type identifier.
|
||||
func (p *ScriptProvider) Type() string {
|
||||
return "script"
|
||||
}
|
||||
|
||||
// Metadata returns descriptive information about the provider.
|
||||
func (p *ScriptProvider) Metadata() dnsprovider.ProviderMetadata {
|
||||
return dnsprovider.ProviderMetadata{
|
||||
Type: "script",
|
||||
Name: "Script (Shell)",
|
||||
Description: "⚠️ ADVANCED: Execute shell scripts for DNS challenges. Scripts must be located in /scripts/. HIGH-RISK feature - scripts run with server privileges. Only for administrators with custom DNS infrastructure.",
|
||||
DocumentationURL: "https://charon.dev/docs/features/script-dns",
|
||||
IsBuiltIn: false,
|
||||
Version: "1.0.0",
|
||||
InterfaceVersion: dnsprovider.InterfaceVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// Init is called after the plugin is registered.
|
||||
func (p *ScriptProvider) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup is called before the plugin is unregistered.
|
||||
func (p *ScriptProvider) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequiredCredentialFields returns credential fields that must be provided.
|
||||
func (p *ScriptProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "script_path",
|
||||
Label: "Script Path",
|
||||
Type: "text",
|
||||
Placeholder: "/scripts/dns-challenge.sh",
|
||||
Hint: "Path to the DNS challenge script. Must be located in /scripts/ directory. Script receives: action (create/delete), domain, token, and key_auth as arguments.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// OptionalCredentialFields returns credential fields that may be provided.
|
||||
func (p *ScriptProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
|
||||
return []dnsprovider.CredentialFieldSpec{
|
||||
{
|
||||
Name: "timeout_seconds",
|
||||
Label: "Script Timeout (seconds)",
|
||||
Type: "text",
|
||||
Placeholder: strconv.Itoa(ScriptDefaultTimeoutSeconds),
|
||||
Hint: fmt.Sprintf("Maximum execution time for the script (%d-%d seconds, default: %d)", ScriptMinTimeoutSeconds, ScriptMaxTimeoutSeconds, ScriptDefaultTimeoutSeconds),
|
||||
},
|
||||
{
|
||||
Name: "env_vars",
|
||||
Label: "Environment Variables",
|
||||
Type: "textarea",
|
||||
Placeholder: "DNS_API_KEY=your-key\nDNS_API_URL=https://api.example.com",
|
||||
Hint: "Optional environment variables passed to the script. One KEY=VALUE pair per line. Keys must start with a letter or underscore.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateCredentials checks if the provided credentials are valid.
|
||||
func (p *ScriptProvider) ValidateCredentials(creds map[string]string) error {
|
||||
// Validate required script path
|
||||
scriptPath := strings.TrimSpace(creds["script_path"])
|
||||
if scriptPath == "" {
|
||||
return fmt.Errorf("script_path is required")
|
||||
}
|
||||
|
||||
// Validate script path for security
|
||||
if err := validateScriptPath(scriptPath); err != nil {
|
||||
return fmt.Errorf("script_path validation failed: %w", 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 < ScriptMinTimeoutSeconds || timeout > ScriptMaxTimeoutSeconds {
|
||||
return fmt.Errorf("timeout_seconds must be between %d and %d", ScriptMinTimeoutSeconds, ScriptMaxTimeoutSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate environment variables if provided
|
||||
if envVars := strings.TrimSpace(creds["env_vars"]); envVars != "" {
|
||||
if err := validateEnvVars(envVars); err != nil {
|
||||
return fmt.Errorf("env_vars validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateScriptPath validates a script path for security.
|
||||
// SECURITY: This function is critical for preventing path traversal attacks.
|
||||
func validateScriptPath(scriptPath string) error {
|
||||
// Clean the path first to normalize it
|
||||
// SECURITY: filepath.Clean resolves ".." sequences, so "/scripts/../etc/passwd"
|
||||
// becomes "/etc/passwd" - the directory check below will then reject it.
|
||||
cleaned := filepath.Clean(scriptPath)
|
||||
|
||||
// SECURITY: Must start with the allowed directory
|
||||
// This check catches path traversal because filepath.Clean already resolved ".."
|
||||
if !strings.HasPrefix(cleaned, ScriptAllowedDirectory) {
|
||||
return fmt.Errorf("script must be in %s directory, got: %s", ScriptAllowedDirectory, cleaned)
|
||||
}
|
||||
|
||||
// SECURITY: Validate the path doesn't contain null bytes (common injection vector)
|
||||
if strings.ContainsRune(scriptPath, '\x00') {
|
||||
return fmt.Errorf("path contains invalid characters")
|
||||
}
|
||||
|
||||
// SECURITY: Validate the filename portion doesn't start with a hyphen
|
||||
// (to prevent argument injection in shell commands)
|
||||
base := filepath.Base(cleaned)
|
||||
if strings.HasPrefix(base, "-") {
|
||||
return fmt.Errorf("script filename cannot start with hyphen")
|
||||
}
|
||||
|
||||
// SECURITY: Validate the script name matches safe pattern
|
||||
if !scriptArgPattern.MatchString(base) {
|
||||
return fmt.Errorf("script filename contains invalid characters: only alphanumeric, dots, underscores, equals, and hyphens allowed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateEnvVars validates environment variable format.
|
||||
func validateEnvVars(envVars string) error {
|
||||
lines := strings.Split(envVars, "\n")
|
||||
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue // Skip empty lines
|
||||
}
|
||||
|
||||
// Validate format: KEY=VALUE
|
||||
if !strings.Contains(line, "=") {
|
||||
return fmt.Errorf("line %d: invalid format, expected KEY=VALUE", i+1)
|
||||
}
|
||||
|
||||
// Validate the line matches the pattern
|
||||
if !envVarLinePattern.MatchString(line) {
|
||||
return fmt.Errorf("line %d: invalid environment variable format, key must start with letter or underscore", i+1)
|
||||
}
|
||||
|
||||
// Extract and validate key
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
key := parts[0]
|
||||
|
||||
// SECURITY: Prevent overriding critical environment variables
|
||||
criticalVars := []string{"PATH", "LD_PRELOAD", "LD_LIBRARY_PATH", "HOME", "USER", "SHELL"}
|
||||
for _, critical := range criticalVars {
|
||||
if strings.EqualFold(key, critical) {
|
||||
return fmt.Errorf("line %d: cannot override critical environment variable %q", i+1, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseEnvVars parses environment variable string into a map.
|
||||
func parseEnvVars(envVars string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
if envVars == "" {
|
||||
return result
|
||||
}
|
||||
|
||||
lines := strings.Split(envVars, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
result[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// TestCredentials attempts to verify credentials work.
|
||||
// For script provider, we validate the format but cannot test without executing the script.
|
||||
func (p *ScriptProvider) TestCredentials(creds map[string]string) error {
|
||||
return p.ValidateCredentials(creds)
|
||||
}
|
||||
|
||||
// SupportsMultiCredential indicates if the provider can handle zone-specific credentials.
|
||||
func (p *ScriptProvider) SupportsMultiCredential() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// BuildCaddyConfig constructs the Caddy DNS challenge configuration.
|
||||
// For script, this returns a config that Charon's internal script handler will use.
|
||||
func (p *ScriptProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
|
||||
scriptPath := strings.TrimSpace(creds["script_path"])
|
||||
|
||||
config := map[string]any{
|
||||
"name": "script",
|
||||
"script_path": filepath.Clean(scriptPath),
|
||||
}
|
||||
|
||||
// Add timeout with default
|
||||
timeoutSeconds := ScriptDefaultTimeoutSeconds
|
||||
if timeoutStr := strings.TrimSpace(creds["timeout_seconds"]); timeoutStr != "" {
|
||||
if t, err := strconv.Atoi(timeoutStr); err == nil && t >= ScriptMinTimeoutSeconds && t <= ScriptMaxTimeoutSeconds {
|
||||
timeoutSeconds = t
|
||||
}
|
||||
}
|
||||
config["timeout_seconds"] = timeoutSeconds
|
||||
|
||||
// Add environment variables if provided
|
||||
if envVars := strings.TrimSpace(creds["env_vars"]); envVars != "" {
|
||||
config["env_vars"] = parseEnvVars(envVars)
|
||||
} else {
|
||||
config["env_vars"] = map[string]string{}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// BuildCaddyConfigForZone constructs config for a specific zone.
|
||||
func (p *ScriptProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
|
||||
return p.BuildCaddyConfig(creds)
|
||||
}
|
||||
|
||||
// PropagationTimeout returns the recommended DNS propagation wait time.
|
||||
func (p *ScriptProvider) PropagationTimeout() time.Duration {
|
||||
return p.propagationTimeout
|
||||
}
|
||||
|
||||
// PollingInterval returns the recommended polling interval for DNS verification.
|
||||
func (p *ScriptProvider) PollingInterval() time.Duration {
|
||||
return p.pollingInterval
|
||||
}
|
||||
|
||||
// GetTimeoutSeconds returns the configured timeout in seconds or the default.
|
||||
func (p *ScriptProvider) GetTimeoutSeconds(creds map[string]string) int {
|
||||
if timeoutStr := strings.TrimSpace(creds["timeout_seconds"]); timeoutStr != "" {
|
||||
if timeout, err := strconv.Atoi(timeoutStr); err == nil {
|
||||
if timeout >= ScriptMinTimeoutSeconds && timeout <= ScriptMaxTimeoutSeconds {
|
||||
return timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
return ScriptDefaultTimeoutSeconds
|
||||
}
|
||||
|
||||
// GetEnvVars returns the parsed environment variables from credentials.
|
||||
func (p *ScriptProvider) GetEnvVars(creds map[string]string) map[string]string {
|
||||
envVars := strings.TrimSpace(creds["env_vars"])
|
||||
return parseEnvVars(envVars)
|
||||
}
|
||||
Reference in New Issue
Block a user