From cf84d97e47b2ff4f9022dc164f61b04f4083438d Mon Sep 17 00:00:00 2001 From: Peter Sanchez Date: Sat, 2 Jul 2022 13:12:32 -0600 Subject: [PATCH] Add ability to send email to admins on errors. --- config/config.go | 11 ++++ go.mod | 2 +- server/server.go | 131 +++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 121 insertions(+), 23 deletions(-) diff --git a/config/config.go b/config/config.go index 98e0f0f..8089510 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,7 @@ package config import ( "fmt" "strconv" + "strings" "github.com/vaughan0/go-ini" ) @@ -23,6 +24,7 @@ type Config struct { StorageSvc string AdminEmail string + EmailAdminErrors bool DefaultFromEmail string DefaultLang string @@ -53,6 +55,7 @@ func LoadConfig(fname string) (*Config, error) { EmailSvc: "console", StorageSvc: "fs", AdminEmail: "admin@localhost", + EmailAdminErrors: true, DefaultFromEmail: "gobwebs@localhost", DefaultLang: "en", Scheme: "http", @@ -110,6 +113,14 @@ func LoadConfig(fname string) (*Config, error) { if val, ok := file.Get("gobwebs", "default-from-email"); ok { conf.DefaultFromEmail = val } + if val, ok := file.Get("gobwebs", "email-admin-errors"); ok { + val = strings.ToLower(val) + if val == "true" || val == "false" { + conf.EmailAdminErrors = val == "true" + } else { + return nil, fmt.Errorf("gobwebs:email-admin-errors invalid value") + } + } if val, ok := file.Get("gobwebs", "default-language"); ok { conf.DefaultLang = val } diff --git a/go.mod b/go.mod index 8ad5e69..407f875 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/minio/minio-go/v7 v7.0.22 github.com/spazzymoto/echo-scs-session v1.0.0 github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec + golang.org/x/text v0.3.7 petersanchez.com/carrier v0.1.0 ) @@ -55,7 +56,6 @@ require ( golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect - golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/protobuf v1.23.0 // indirect diff --git a/server/server.go b/server/server.go index 9da7200..7abd71a 100644 --- a/server/server.go +++ b/server/server.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "os/signal" + "runtime" "syscall" "time" @@ -23,6 +24,7 @@ import ( "hg.code.netlandish.com/~netlandish/gobwebs/internal/localizer" "hg.code.netlandish.com/~netlandish/gobwebs/storage" "hg.code.netlandish.com/~netlandish/gobwebs/validate" + "petersanchez.com/carrier" // Postgres _ "github.com/lib/pq" @@ -82,16 +84,105 @@ func (c *Context) Render(code int, name string, data interface{}) (err error) { return c.HTMLBlob(code, buf.Bytes()) } +// Error is needed because otherwise it will send an instance of +// *echo.context, and we want the user info, etc. from the gobwebs +// Context struct +func (c *Context) Error(err error) { + c.Server.e.HTTPErrorHandler(err, c) +} + // HTTPErrorHandler custom error handler for entire app func (s *Server) HTTPErrorHandler(err error, c echo.Context) { - // Send to Sentry or some other logger - // Print Debug info - if s.Config.Debug { - s.e.Logger.Printf("%v - %v - %v\n", c.Path(), c.QueryParams(), err.Error()) + // TODO: Send to Sentry or some other logger + + he, ok := err.(*echo.HTTPError) + if ok { + if he.Internal != nil { + if herr, ok := he.Internal.(*echo.HTTPError); ok { + he = herr + } + } + } else { + he = &echo.HTTPError{ + Code: http.StatusInternalServerError, + Message: http.StatusText(http.StatusInternalServerError), + } + } + + gctx := c.(*Context) + errmap := gobwebs.Map{ + "user": gctx.User, + } + + if he.Code > 499 { + stack := make([]byte, 32768) // 32 KiB + i := runtime.Stack(stack, false) + stack = stack[:i] + + // Print Debug info + if s.Config.Debug { + msg := fmt.Sprintf("[ERROR] %s\n%v\n%s\n", c.Path(), c.QueryParams(), stack) + s.e.Logger.Printf(msg) + fmt.Fprintf(os.Stderr, msg) + } + + if !s.Config.Debug && s.Config.EmailAdminErrors && s.Email != nil { + // Send email to admin email address + var userStr string + if gctx.User.IsAuthenticated() { + userStr = fmt.Sprintf("ID: %d, Email: %s", + int(gctx.User.GetID()), + gctx.User.GetEmail(), + ) + } else { + userStr = "Anonymous User" + } + emsg := fmt.Sprintf(`Error occured processing the following request: + +%v + +At the following path: + +%s + +With these variables: + +%v + +By the user: %s + +The following stack trace was produced: + +%s`, err, c.Path(), c.QueryParams(), userStr, stack) + + msg := carrier.NewMessage(). + SetTo(s.Config.AdminEmail). + SetFrom(s.Config.DefaultFromEmail) + msg.SetSubject(fmt.Sprintf("[gobwebs]: Internal Server Error: %s", c.Path())) + msg.SetBody(emsg) + if rerr := s.Email.SendMail(msg); rerr != nil { + s.e.Logger.Printf("Unable to send admin error email: %v", rerr) + } + } + } + + tmpl := "500.html" + if he.Code == 404 { + tmpl = "404.html" + } + + // Try to render custom template. On error use default handler + if rerr := gctx.Render(he.Code, tmpl, errmap); rerr != nil { + // Call the default handler to return the HTTP response + s.e.DefaultHTTPErrorHandler(err, c) } +} - // Call the default handler to return the HTTP response - s.e.DefaultHTTPErrorHandler(err, c) +// LogErrorFunc will handle logging the stack trace for an error +func (s *Server) LogErrorFunc(c echo.Context, err error, stack []byte) error { + msg := fmt.Sprintf("[PANIC RECOVER] %s\n%v\n%s\n", c.Path(), c.QueryParams(), stack) + s.e.Logger.Printf(msg) + return err } // GetSessionManager returns the configured session @@ -194,10 +285,19 @@ func (s *Server) WithDefaultMiddleware() *Server { s.e.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ RedirectCode: http.StatusMovedPermanently, })) + s.e.Use(session.LoadAndSave(s.Session)) // Must be first + // Set custom context + s.e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + ctx := &Context{Context: c, Server: s} + return next(ctx) + } + }) s.e.Use( - session.LoadAndSave(s.Session), // Must be first - //middleware.RecoverWithConfig(middleware.RecoverConfig{LogErrorFunc: s.LogErrorFunc}), - middleware.Recover(), + middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 32768, // 32KiB + LogErrorFunc: s.LogErrorFunc, + }), middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: fmt.Sprintf( "${time_rfc3339} method=${method}, uri=${uri}, status=${status} remote_ip=${remote_ip} app=\"%s\"\n", @@ -210,13 +310,6 @@ func (s *Server) WithDefaultMiddleware() *Server { TokenLookup: "form:csrf", }), ) - // Set custom context - s.e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - ctx := &Context{Context: c, Server: s} - return next(ctx) - } - }) return s } @@ -226,12 +319,6 @@ func (s *Server) WithMiddleware(middlewares ...echo.MiddlewareFunc) *Server { return s } -// LogErrorFunc ... -func (s *Server) LogErrorFunc(c echo.Context, err error, stack []byte) error { - fmt.Println(stack) - return nil -} - // WithQueues add dowork task queues for this server to manage func (s *Server) WithQueues(queues ...*work.Queue) *Server { ctx := context.Background() -- 2.45.2