@@ -91,125 +91,177 @@ type DNSProvider string
9191// DigitalOceanDNSProvider represents the Digital Ocean DNS provider.
9292const DigitalOceanDNSProvider = "digitalocean"
9393
94- type Tunnels struct {
95- // Domain is the default domain used to generate certificate for Tunnels.
96- Domain string
97- // Provider is the DNS provider used to generate wildcard certificates.
98- Provider DNSProvider
99- // Token is a DNS token used to generate wildcard certificates.
100- Token string
101- }
102-
10394type Config struct {
10495 // RootDir is the root directory for CertBot configurations.
10596 RootDir string
106- // Domain is the default domain used to generate certificate for ShellHub.
107- Domain string
10897 // Staging defines if the CertBot will use the staging server to generate certificates.
10998 Staging bool
11099 // RenewedCallback is a callback called after certificate renew.
111100 RenewedCallback func ()
101+ }
112102
113- Tunnels * Tunnels
103+ type Certificate interface {
104+ String () string
105+ Generate (staging bool ) error
114106}
115107
116- // CertBot handles the generation and renewal of SSL certificates.
117- type CertBot struct {
118- Config * Config
108+ type DefaultCertificate struct {
109+ RootDir string
110+ Domain string
119111
120112 ex Executor
121- tk Ticker
122113 fs afero.Fs
123114}
124115
125- func newCertBot ( config * Config ) * CertBot {
126- return & CertBot {
127- Config : config ,
116+ func NewDefaultCertificate ( domain string ) Certificate {
117+ return & DefaultCertificate {
118+ Domain : domain ,
128119
129- ex : new (executor ),
130- tk : new (ticker ),
120+ ex : NewExecutor (),
131121 fs : afero .NewOsFs (),
132122 }
133123}
134124
135- // ensureCertificates checks if the SSL certificate exists and generates it if not.
136- func (cb * CertBot ) ensureCertificates () {
137- certPath := fmt .Sprintf ("%s/live/%s/fullchain.pem" , cb .Config .RootDir , cb .Config .Domain )
138- if _ , err := cb .fs .Stat (certPath ); os .IsNotExist (err ) {
139- cb .generateCertificate ()
125+ func (d * DefaultCertificate ) startACMEServer () * http.Server {
126+ mux := http .NewServeMux ()
127+ mux .Handle (
128+ "/.well-known/acme-challenge/" ,
129+ http .StripPrefix (
130+ "/.well-known/acme-challenge/" ,
131+ http .FileServer (
132+ http .Dir (filepath .Join (d .RootDir , ".well-known/acme-challenge" )),
133+ ),
134+ ),
135+ )
136+
137+ server := & http.Server {
138+ Handler : mux ,
140139 }
141140
142- if cb .Config .Tunnels != nil {
143- // NOTE: We are recreating the INI file every time to ensure it has the latest token from the environment.
144- cb .generateProviderCredentialsFile ()
141+ listener , err := net .Listen ("tcp" , ":80" )
142+ if err != nil {
143+ log .WithError (err ).Fatal ("failed to start ACME server listener" )
144+ }
145145
146- certPath := fmt .Sprintf ("%s/live/*.%s/fullchain.pem" , cb .Config .RootDir , cb .Config .Tunnels .Domain )
147- if _ , err := cb .fs .Stat (certPath ); os .IsNotExist (err ) {
148- if err := cb .generateCertificateFromDNS (); err != nil {
149- log .WithError (err ).Fatal ("failed to generate the certificate from DNS" )
150- }
146+ go func () {
147+ if err := server .Serve (listener ); err != nil && ! errors .Is (err , http .ErrServerClosed ) {
148+ log .WithError (err ).Fatal ("acme server error" )
151149 }
150+ }()
151+
152+ return server
153+ }
154+
155+ // stopACMEServer stops the local ACME server.
156+ func (d * DefaultCertificate ) stopACMEServer (server * http.Server ) {
157+ if err := server .Close (); err != nil {
158+ log .WithError (err ).Fatal ("could not stop ACME server" )
152159 }
153160}
154161
155- // generateCertificate generates a new SSL certificate using Certbot.
156- func (cb * CertBot ) generateCertificate () {
162+ func (d * DefaultCertificate ) Generate (staging bool ) error {
157163 log .Info ("generating SSL certificate" )
158164
159- challengeDir := fmt .Sprintf ("%s/.well-known/acme-challenge" , cb .Config .RootDir )
160- if err := cb .fs .MkdirAll (challengeDir , 0o755 ); err != nil {
161- log .WithError (err ).Fatal ("failed to create acme challenge on filesystem" )
165+ challengeDir := fmt .Sprintf ("%s/.well-known/acme-challenge" , os .TempDir ())
166+ if err := d .fs .MkdirAll (challengeDir , 0o755 ); err != nil {
167+ log .WithError (err ).Error ("failed to create acme challenge on filesystem" )
168+
169+ return err
162170 }
163171
164- acmeServer := cb .startACMEServer ()
172+ acmeServer := d .startACMEServer ()
165173
166- cmd := cb .ex .Command (
167- "certbot" ,
174+ args := []string {
168175 "certonly" ,
169176 "--non-interactive" ,
170177 "--agree-tos" ,
171178 "--register-unsafely-without-email" ,
172179 "--webroot" ,
173- "--webroot-path" , cb . Config .RootDir ,
180+ "--webroot-path" , d .RootDir ,
174181 "--preferred-challenges" , "http" ,
175182 "-n" ,
176183 "-d" ,
177- cb .Config .Domain ,
178- )
179- if cb .Config .Staging {
184+ d .Domain ,
185+ }
186+
187+ if staging {
180188 log .Info ("running generate with staging" )
181189
182- cmd . Args = append (cmd . Args , "--staging" )
190+ args = append (args , "--staging" )
183191 }
192+
193+ // Build the CertBot command
194+ cmd := d .ex .Command (
195+ "certbot" ,
196+ args ... ,
197+ )
198+
184199 cmd .Stdout , cmd .Stderr = os .Stdout , os .Stderr
185200
186- if err := cmd .Run (); err != nil {
187- log .Fatal ("Failed to generate SSL certificate" )
201+ if err := d .ex .Run (cmd ); err != nil {
202+ log .Error ("Failed to generate SSL certificate" )
203+
204+ return err
188205 }
189206
190- cb .stopACMEServer (acmeServer )
207+ d .stopACMEServer (acmeServer )
191208
192209 log .Info ("generate run" )
210+
211+ return nil
212+ }
213+
214+ func (d * DefaultCertificate ) String () string {
215+ return d .Domain
216+ }
217+
218+ type TunnelsCertificate struct {
219+ // Domain is the default domain used to generate certificate for Tunnels.
220+ Domain string
221+ // Provider is the DNS provider used to generate wildcard certificates.
222+ Provider DNSProvider
223+ // Token is a DNS token used to generate wildcard certificates.
224+ Token string
225+
226+ ex Executor
227+ fs afero.Fs
228+ }
229+
230+ func NewTunnelsCertificate (domain string , provider DNSProvider , token string ) Certificate {
231+ return & TunnelsCertificate {
232+ Domain : domain ,
233+
234+ Provider : provider ,
235+ Token : token ,
236+
237+ ex : NewExecutor (),
238+ fs : afero .NewOsFs (),
239+ }
193240}
194241
195- func (cb * CertBot ) generateProviderCredentialsFile () (afero.File , error ) {
196- token := fmt .Sprintf ("dns_%s_token = %s" , cb .Config .Tunnels .Provider , cb .Config .Tunnels .Token )
197- file , err := cb .fs .Create (fmt .Sprintf ("/etc/shellhub-gateway/%s.ini" , string (cb .Config .Tunnels .Provider )))
242+ func (d * TunnelsCertificate ) generateProviderCredentialsFile () (afero.File , error ) {
243+ token := fmt .Sprintf ("dns_%s_token = %s" , d .Provider , d .Token )
244+
245+ file , err := d .fs .Create (fmt .Sprintf ("/etc/shellhub-gateway/%s.ini" , string (d .Provider )))
198246 if err != nil {
199247 log .WithError (err ).Error ("failed to create shellhub-gateway file with dns provider token" )
200248
201249 return nil , err
202250 }
203251
204- file .Write ([]byte (token ))
252+ if _ , err := file .Write ([]byte (token )); err != nil {
253+ log .WithError (err ).Error ("failed to write the token into credentials file" )
254+
255+ return nil , err
256+ }
205257
206258 return file , nil
207259}
208260
209- func (cb * CertBot ) generateCertificateFromDNS ( ) error {
261+ func (d * TunnelsCertificate ) Generate ( staging bool ) error {
210262 log .Info ("generating SSL certificate with DNS" )
211263
212- file , err := cb .generateProviderCredentialsFile ()
264+ file , err := d .generateProviderCredentialsFile ()
213265 if err != nil {
214266 log .WithError (err ).Error ("failed to generate INI file" )
215267
@@ -222,28 +274,28 @@ func (cb *CertBot) generateCertificateFromDNS() error {
222274 "--agree-tos" ,
223275 "--register-unsafely-without-email" ,
224276 "--cert-name" ,
225- fmt .Sprintf ("*.%s" , cb . Config . Tunnels .Domain ),
226- fmt .Sprintf ("--dns-%s" , cb . Config . Tunnels .Provider ),
227- fmt .Sprintf ("--dns-%s-credentials" , cb . Config . Tunnels .Provider ),
277+ fmt .Sprintf ("*.%s" , d .Domain ),
278+ fmt .Sprintf ("--dns-%s" , d .Provider ),
279+ fmt .Sprintf ("--dns-%s-credentials" , d .Provider ),
228280 file .Name (),
229281 "-d" ,
230- fmt .Sprintf ("*.%s" , cb . Config . Tunnels .Domain ),
282+ fmt .Sprintf ("*.%s" , d .Domain ),
231283 }
232284
233- if cb . Config . Staging {
285+ if staging {
234286 log .Info ("running generate with staging on dns" )
235287
236288 args = append (args , "--staging" )
237289 }
238290
239- cmd := cb .ex .Command ( //nolint:gosec
291+ cmd := d .ex .Command ( //nolint:gosec
240292 "certbot" ,
241293 args ... ,
242294 )
243295
244296 cmd .Stdout , cmd .Stderr = os .Stdout , os .Stderr
245297
246- if err := cb .ex .Run (cmd ); err != nil {
298+ if err := d .ex .Run (cmd ); err != nil {
247299 log .WithError (err ).Error ("failed to generate SSL certificate" )
248300
249301 return err
@@ -254,41 +306,38 @@ func (cb *CertBot) generateCertificateFromDNS() error {
254306 return nil
255307}
256308
257- // startACMEServer starts a local HTTP server for the ACME challenge.
258- func (cb * CertBot ) startACMEServer () * http.Server {
259- mux := http .NewServeMux ()
260- mux .Handle (
261- "/.well-known/acme-challenge/" ,
262- http .StripPrefix (
263- "/.well-known/acme-challenge/" ,
264- http .FileServer (
265- http .Dir (filepath .Join (cb .Config .RootDir , ".well-known/acme-challenge" )),
266- ),
267- ),
268- )
309+ func (d * TunnelsCertificate ) String () string {
310+ return d .Domain
311+ }
269312
270- server := & http. Server {
271- Handler : mux ,
272- }
313+ // CertBot handles the generation and renewal of SSL certificates.
314+ type CertBot struct {
315+ Config * Config
273316
274- listener , err := net .Listen ("tcp" , ":80" )
275- if err != nil {
276- log .WithError (err ).Fatal ("failed to start ACME server listener" )
277- }
317+ Certificates []Certificate
278318
279- go func () {
280- if err := server .Serve (listener ); err != nil && ! errors .Is (err , http .ErrServerClosed ) {
281- log .WithError (err ).Fatal ("acme server error" )
282- }
283- }()
319+ ex Executor
320+ tk Ticker
321+ fs afero.Fs
322+ }
284323
285- return server
324+ func newCertBot (config * Config ) * CertBot {
325+ return & CertBot {
326+ Config : config ,
327+
328+ ex : new (executor ),
329+ tk : new (ticker ),
330+ fs : afero .NewOsFs (),
331+ }
286332}
287333
288- // stopACMEServer stops the local ACME server.
289- func (cb * CertBot ) stopACMEServer (server * http.Server ) {
290- if err := server .Close (); err != nil {
291- log .WithError (err ).Fatal ("could not stop ACME server" )
334+ // ensureCertificates checks if the SSL certificate exists and generates if it doesn't.
335+ func (cb * CertBot ) ensureCertificates () {
336+ for _ , certificate := range cb .Certificates {
337+ certPath := fmt .Sprintf ("%s/live/%s/fullchain.pem" , cb .Config .RootDir , certificate )
338+ if _ , err := cb .fs .Stat (certPath ); os .IsNotExist (err ) {
339+ certificate .Generate (cb .Config .Staging )
340+ }
292341 }
293342}
294343
0 commit comments