exec.go raw
1 // Package exec implements a DNS provider which runs a program for adding/removing the DNS record.
2 package exec
3
4 import (
5 "bufio"
6 "context"
7 "errors"
8 "fmt"
9 "os"
10 "os/exec"
11 "time"
12
13 "github.com/go-acme/lego/v4/challenge"
14 "github.com/go-acme/lego/v4/challenge/dns01"
15 "github.com/go-acme/lego/v4/log"
16 "github.com/go-acme/lego/v4/platform/config/env"
17 )
18
19 // Environment variables names.
20 const (
21 envNamespace = "EXEC_"
22
23 EnvPath = envNamespace + "PATH"
24 EnvMode = envNamespace + "MODE"
25
26 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
27 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
28 EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL"
29 )
30
31 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
32
33 // Config Provider configuration.
34 type Config struct {
35 Program string
36 Mode string
37 PropagationTimeout time.Duration
38 PollingInterval time.Duration
39 SequenceInterval time.Duration
40 }
41
42 // NewDefaultConfig returns a default configuration for the DNSProvider.
43 func NewDefaultConfig() *Config {
44 return &Config{
45 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
46 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
47 SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
48 }
49 }
50
51 // DNSProvider implements the challenge.Provider interface.
52 type DNSProvider struct {
53 config *Config
54 }
55
56 // NewDNSProvider returns a new DNS provider which runs the program in the
57 // environment variable EXEC_PATH for adding and removing the DNS record.
58 func NewDNSProvider() (*DNSProvider, error) {
59 values, err := env.Get(EnvPath)
60 if err != nil {
61 return nil, fmt.Errorf("exec: %w", err)
62 }
63
64 config := NewDefaultConfig()
65 config.Program = values[EnvPath]
66 config.Mode = os.Getenv(EnvMode)
67
68 return NewDNSProviderConfig(config)
69 }
70
71 // NewDNSProviderConfig returns a new DNS provider which runs the given configuration
72 // for adding and removing the DNS record.
73 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
74 if config == nil {
75 return nil, errors.New("exec: the configuration is nil")
76 }
77
78 return &DNSProvider{config: config}, nil
79 }
80
81 // Present creates a TXT record to fulfill the dns-01 challenge.
82 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
83 err := d.run(context.Background(), "present", domain, token, keyAuth)
84 if err != nil {
85 return fmt.Errorf("exec: %w", err)
86 }
87
88 return nil
89 }
90
91 // CleanUp removes the TXT record matching the specified parameters.
92 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
93 err := d.run(context.Background(), "cleanup", domain, token, keyAuth)
94 if err != nil {
95 return fmt.Errorf("exec: %w", err)
96 }
97
98 return nil
99 }
100
101 // Timeout returns the timeout and interval to use when checking for DNS propagation.
102 // Adjusting here to cope with spikes in propagation times.
103 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
104 return d.config.PropagationTimeout, d.config.PollingInterval
105 }
106
107 // Sequential All DNS challenges for this provider will be resolved sequentially.
108 // Returns the interval between each iteration.
109 func (d *DNSProvider) Sequential() time.Duration {
110 return d.config.SequenceInterval
111 }
112
113 func (d *DNSProvider) run(ctx context.Context, command, domain, token, keyAuth string) error {
114 var args []string
115 if d.config.Mode == "RAW" {
116 args = []string{command, "--", domain, token, keyAuth}
117 } else {
118 info := dns01.GetChallengeInfo(domain, keyAuth)
119 args = []string{command, info.EffectiveFQDN, info.Value}
120 }
121
122 cmd := exec.CommandContext(ctx, d.config.Program, args...)
123
124 stdout, err := cmd.StdoutPipe()
125 if err != nil {
126 return fmt.Errorf("create pipe: %w", err)
127 }
128
129 cmd.Stderr = cmd.Stdout
130
131 err = cmd.Start()
132 if err != nil {
133 return fmt.Errorf("start command: %w", err)
134 }
135
136 scanner := bufio.NewScanner(stdout)
137 for scanner.Scan() {
138 log.Println(scanner.Text())
139 }
140
141 err = cmd.Wait()
142 if err != nil {
143 return fmt.Errorf("wait command: %w", err)
144 }
145
146 return nil
147 }
148