Start implementation of error emails

This commit is contained in:
Melon 2024-02-21 00:21:22 +00:00
parent 7e65015b89
commit 438e44ebfa
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
8 changed files with 85 additions and 8 deletions

View File

@ -1,12 +1,16 @@
package main
import "github.com/1f349/orchid/renewal"
import (
"github.com/1f349/orchid/renewal"
"github.com/1f349/simplemail"
)
type startUpConfig struct {
Listen string `yaml:"listen"`
Acme acmeConfig `yaml:"acme"`
LE renewal.LetsEncryptConfig `yaml:"letsEncrypt"`
Domains []string `yaml:"domains"`
Mail mailConfig `json:"mail"`
}
type acmeConfig struct {
@ -14,3 +18,8 @@ type acmeConfig struct {
CleanUpUrl string `yaml:"cleanUpUrl"`
RefreshUrl string `yaml:"refreshUrl"`
}
type mailConfig struct {
simplemail.Mail
To simplemail.FromAddress `json:"to"`
}

View File

@ -2,6 +2,7 @@ package main
import (
"context"
"embed"
"flag"
"github.com/1f349/mjwt"
"github.com/1f349/orchid"
@ -9,6 +10,7 @@ import (
"github.com/1f349/orchid/logger"
"github.com/1f349/orchid/renewal"
"github.com/1f349/orchid/servers"
"github.com/1f349/simplemail"
"github.com/1f349/violet/utils"
"github.com/google/subcommands"
_ "github.com/mattn/go-sqlite3"
@ -62,6 +64,9 @@ func (s *serveCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
return subcommands.ExitSuccess
}
//go:embed mail-templates/*.go.{html,txt}
var mailTemplates embed.FS
func normalLoad(conf startUpConfig, wd string) {
// load the MJWT RSA public key from a pem encoded file
mJwtVerify, err := mjwt.NewKeyStoreFromPath(filepath.Join(wd, "keys"))
@ -69,6 +74,11 @@ func normalLoad(conf startUpConfig, wd string) {
logger.Logger.Fatal("Failed to load MJWT verifier public key from file", "path", filepath.Join(wd, "keys"), "err", err)
}
mail, err := simplemail.New(&conf.Mail.Mail, "mail", mailTemplates)
if err != nil {
logger.Logger.Fatal("Failed to load email sender", "err", err)
}
// open sqlite database
db, err := orchid.InitDB(filepath.Join(wd, "orchid.db.sqlite"))
if err != nil {
@ -83,7 +93,7 @@ func normalLoad(conf startUpConfig, wd string) {
if err != nil {
logger.Logger.Fatal("HTTP Acme Error", "err", err)
}
renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir)
renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir, mail, conf.Mail.To)
if err != nil {
logger.Logger.Fatal("Service Error", "err", err)
}
@ -91,7 +101,7 @@ func normalLoad(conf startUpConfig, wd string) {
logger.Logger.Info("Starting API server", "listen", srv.Addr)
go utils.RunBackgroundHttp(logger.Logger, srv)
exit_reload.ExitReload("Violet", func() {}, func() {
exit_reload.ExitReload("Orchid", func() {}, func() {
// stop renewal service and api server
renewalService.Shutdown()
srv.Close()

6
go.mod
View File

@ -4,6 +4,7 @@ go 1.22
require (
github.com/1f349/mjwt v0.4.1
github.com/1f349/simplemail v0.0.1
github.com/1f349/violet v0.0.14
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/log v0.4.0
@ -22,6 +23,7 @@ require (
)
require (
github.com/1f349/overlapfs v0.0.1 // indirect
github.com/1f349/rsa-helper v0.0.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/becheran/wildmatch-go v1.0.0 // indirect
@ -29,6 +31,10 @@ require (
github.com/charmbracelet/lipgloss v0.12.1 // indirect
github.com/charmbracelet/x/ansi v0.1.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emersion/go-message v0.18.0 // indirect
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
github.com/emersion/go-smtp v0.20.2 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect

21
go.sum
View File

@ -1,7 +1,11 @@
github.com/1f349/mjwt v0.4.1 h1:ooCroMMw2kcL5c9L3sLbdtxI0H4/QC8RfTxiloKr+4Y=
github.com/1f349/mjwt v0.4.1/go.mod h1:qwnzokkqc7Z9YmKA1m9beI3OZL1GvGYHOQU2rOwoV1M=
github.com/1f349/overlapfs v0.0.1 h1:LAxBolrXFAgU0yqZtXg/C/aaPq3eoQSPpBc49BHuTp0=
github.com/1f349/overlapfs v0.0.1/go.mod h1:I6aItQycr7nrzplmfNXp/QF9tTmKRSgY3fXmu/7Ky2o=
github.com/1f349/rsa-helper v0.0.2 h1:N/fLQqg5wrjIzG6G4zdwa5Xcv9/jIPutCls9YekZr9U=
github.com/1f349/rsa-helper v0.0.2/go.mod h1:VUQ++1tYYhYrXeOmVFkQ82BegR24HQEJHl5lHbjg7yg=
github.com/1f349/simplemail v0.0.1 h1:/euBoIpXVall46loDDTjkpEFHEWo213peBBE77n8kRY=
github.com/1f349/simplemail v0.0.1/go.mod h1:7QNDldEOFto/4rsbk5KZGYBOK3Wem2/Bv2uQ5YoczP4=
github.com/1f349/violet v0.0.14 h1:MpBZ4n1dJjdiIwYMTfh0PBIFll3kjqowxR6DLasafqE=
github.com/1f349/violet v0.0.14/go.mod h1:iAREhm+wxnGXkmuvmBhOuhUx2T7/5w7stLYNgQGbqC8=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
@ -25,6 +29,15 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-message v0.18.0 h1:7LxAXHRpSeoO/Wom3ZApVZYG7c3d17yCScYce8WiXA8=
github.com/emersion/go-message v0.18.0/go.mod h1:Zi69ACvzaoV/MBnrxfVBPV3xWEuCmC2nEN39oJF4B8A=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.20.2 h1:peX42Qnh5Q0q3vrAnRy43R/JwTnnv75AebxbkTL7Ia4=
github.com/emersion/go-smtp v0.20.2/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/go-acme/lego/v4 v4.17.3 h1:5our7Qdyik0abag40abdmQuytq97iweaNHFMT4pYDnQ=
github.com/go-acme/lego/v4 v4.17.3/go.mod h1:Ol6l04hnmavqVHKYS/ByhXXqE64x8yVYhomha82uAUk=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
@ -114,15 +127,18 @@ golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn5
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -132,22 +148,27 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

View File

View File

@ -13,6 +13,7 @@ import (
"fmt"
"github.com/1f349/orchid/database"
"github.com/1f349/orchid/pebble"
"github.com/1f349/simplemail"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
@ -64,10 +65,12 @@ type Service struct {
keyDir string
insecure bool
client *lego.Client
mail *simplemail.SimpleMail
mailTo simplemail.FromAddress
}
// NewService creates a new certificate renewal service.
func NewService(wg *sync.WaitGroup, db *database.Queries, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string) (*Service, error) {
func NewService(wg *sync.WaitGroup, db *database.Queries, httpAcme challenge.Provider, leConfig LetsEncryptConfig, certDir, keyDir string, mail *simplemail.SimpleMail, mailTo simplemail.FromAddress) (*Service, error) {
s := &Service{
db: db,
httpAcme: httpAcme,
@ -80,6 +83,8 @@ func NewService(wg *sync.WaitGroup, db *database.Queries, httpAcme challenge.Pro
certDir: certDir,
keyDir: keyDir,
insecure: leConfig.insecure,
mail: mail,
mailTo: mailTo,
}
// make certDir and keyDir
@ -124,6 +129,26 @@ func (s *Service) Shutdown() {
close(s.certDone)
}
func (s *Service) sendErrorEmail(templateName, subject string, data map[string]any, err error) error {
// sending mail is disabled
if s.mail == nil {
return nil
}
// create empty data map
if data == nil {
data = make(map[string]any)
}
data["error"] = err
// send email and listen for errors
mailErr := s.mail.Send(templateName, subject, s.mailTo.Address, data)
if mailErr != nil {
return fmt.Errorf("failed to send error email for: %w because: %w", err, mailErr)
}
return err
}
// resolveLEPrivKey resolves the private key for the LetsEncrypt account.
// If the string is a path to a file then the contents of the file is read.
func (s *Service) resolveLEPrivKey(a string) error {
@ -255,7 +280,7 @@ func (s *Service) renewalCheck() error {
// check for running out certificates in the database
localData, err := s.findNextCertificateToRenew()
if err != nil {
return fmt.Errorf("failed to find a certificate to renew: %w", err)
return s.sendErrorEmail("failed-to-find", "Failed to find a certificate to renew", nil, fmt.Errorf("failed to find a certificate to renew: %w", err))
}
// no certificates to update
@ -268,7 +293,12 @@ func (s *Service) renewalCheck() error {
err = s.renewCert(localData)
if err != nil {
Logger.Debug("Failed to renew certificate", "err", err)
return err
return s.sendErrorEmail("failed-to-renew", "Failed to renew a certificate", map[string]any{
"id": localData.id,
"dns-name": localData.dns.name,
"domains": localData.domains,
"not-after": localData.notAfter,
}, fmt.Errorf("failed to renew a certificate: %w", err))
}
// renew succeeded

View File

@ -13,6 +13,7 @@ import (
"github.com/1f349/orchid/logger"
"github.com/1f349/orchid/pebble"
"github.com/1f349/orchid/test"
"github.com/1f349/simplemail"
"github.com/charmbracelet/log"
"github.com/go-acme/lego/v4/lego"
"github.com/google/uuid"
@ -125,7 +126,7 @@ func setupPebbleTest(t *testing.T, serverTls *certgen.CertGen) (*Service, *sql.D
Directory: "https://localhost:14000/dir",
Certificate: "insecure",
insecure: true,
}, certDir, keyDir)
}, certDir, keyDir, nil, simplemail.FromAddress{})
fmt.Println(err)
assert.NoError(t, err)
@ -170,7 +171,7 @@ func TestPebbleRenewal(t *testing.T) {
_, err := db2.Exec("DELETE FROM certificate_domains")
assert.NoError(t, err)
_, err = db2.Exec(`INSERT INTO certificates (owner, dns, auto_renew, active, renewing, renew_failed, not_after, updated_at) VALUES (1, 1, 1, 1, 0, 0, "2000-01-01 00:00:00+00:00", "2000-01-01 00:00:00+00:00")`)
_, err = db2.Exec(`INSERT INTO certificates (owner, dns, auto_renew, active, renewing, renew_failed, not_after, updated_at) VALUES (1, 1, 1, 1, 0, 0, '2000-01-01 00:00:00+00:00', '2000-01-01 00:00:00+00:00')`)
assert.NoError(t, err)
for _, j := range i.domains {
_, err = db2.Exec(`INSERT INTO certificate_domains (cert_id, domain) VALUES (1, ?)`, j)