chore: git cache cleanup

This commit is contained in:
GitHub Actions
2026-03-04 18:34:49 +00:00
parent c32cce2a88
commit 27c252600a
2001 changed files with 683185 additions and 0 deletions

View File

@@ -0,0 +1,311 @@
// 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)
}