Skip to content

Commit 6b6c5dc

Browse files
henrybarretogustavosbarreto
authored andcommitted
feat(gateway): isolate certificate generation from certbot by abstracting it
1 parent 8ac1ff0 commit 6b6c5dc

File tree

3 files changed

+188
-141
lines changed

3 files changed

+188
-141
lines changed

gateway/certbot.go

Lines changed: 141 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -91,125 +91,177 @@ type DNSProvider string
9191
// DigitalOceanDNSProvider represents the Digital Ocean DNS provider.
9292
const 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-
10394
type 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

Comments
 (0)