1package domain
2
3import (
4 "bufio"
5 "bytes"
6 "regexp"
7 "strings"
8)
9
10// envDomainRegex validates domain names used for env secrets.
11// Allows: alphanumeric, dots, hyphens, colons (for ports), and forward slashes (for paths).
12var envDomainRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._:/-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$`)
13var envKeyRegex = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
14var containerNameRegex = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`)
15
16// SanitizeDomainForEnvFile validates and sanitizes a domain name for env file storage.
17// Returns a safe domain string suitable for filenames.
18// This function is idempotent: calling it on already-sanitized input returns the same value.
19func SanitizeDomainForEnvFile(domainName string) (string, error) {
20 if domainName == "" {
21 return "", ErrPathTraversal
22 }
23
24 if strings.Contains(domainName, "..") {
25 return "", ErrPathTraversal
26 }
27
28 if !envDomainRegex.MatchString(domainName) {
29 return "", ErrPathTraversal
30 }
31
32 safeDomain := strings.ReplaceAll(domainName, ".", "_")
33 safeDomain = strings.ReplaceAll(safeDomain, ":", "_")
34 safeDomain = strings.ReplaceAll(safeDomain, "/", "_")
35 return safeDomain, nil
36}
37
38// ParseEnvData parses env data into a key-value map.
39func ParseEnvData(data []byte) (map[string]string, error) {
40 secrets := make(map[string]string)
41 scanner := bufio.NewScanner(bytes.NewReader(data))
42 // Allow large env values (certs/keys) without hitting the default ~64KB limit.
43 scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
44 for scanner.Scan() {
45 line := strings.TrimSpace(scanner.Text())
46 if line == "" || strings.HasPrefix(line, "#") {
47 continue
48 }
49
50 parts := strings.SplitN(line, "=", 2)
51 if len(parts) != 2 {
52 continue
53 }
54
55 key := strings.TrimSpace(parts[0])
56 rawValue := parts[1]
57
58 trimmedValue := strings.TrimSpace(rawValue)
59 value := trimmedValue
60
61 if len(trimmedValue) >= 2 {
62 if (strings.HasPrefix(trimmedValue, "\"") && strings.HasSuffix(trimmedValue, "\"")) ||
63 (strings.HasPrefix(trimmedValue, "'") && strings.HasSuffix(trimmedValue, "'")) {
64 value = trimmedValue[1 : len(trimmedValue)-1]
65 }
66 }
67
68 if key != "" {
69 secrets[key] = value
70 }
71 }
72
73 if err := scanner.Err(); err != nil {
74 return nil, err
75 }
76
77 return secrets, nil
78}
79
80// ValidateEnvKey validates an env key for storage.
81func ValidateEnvKey(key string) error {
82 if key == "" {
83 return ErrInvalidEnvKey
84 }
85 if strings.Contains(key, "..") || strings.ContainsAny(key, "/\\") {
86 return ErrPathTraversal
87 }
88 if !envKeyRegex.MatchString(key) {
89 return ErrInvalidEnvKey
90 }
91 return nil
92}
93
94// ValidateContainerName validates a container name for attachment storage.
95// Container names can contain alphanumeric characters, hyphens, and underscores,
96// but must start with a letter.
97func ValidateContainerName(name string) error {
98 if name == "" {
99 return ErrInvalidContainerName
100 }
101 if strings.Contains(name, "..") || strings.ContainsAny(name, "/\\") {
102 return ErrPathTraversal
103 }
104 if !containerNameRegex.MatchString(name) {
105 return ErrInvalidContainerName
106 }
107 return nil
108}