From 64c43839de42317d1f35115525b4dc7afe70302d Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sun, 28 May 2023 01:19:19 -0400 Subject: [PATCH] add log levels, method chaining, remove unused code --- internal/api/api.go | 7 ++- internal/api/api_test.go | 11 +++-- internal/api/auth.go | 82 ++++++++++++++++++--------------- internal/api/fs.go | 18 ++++---- internal/api/helpers.go | 99 +++++++++++++++++++++------------------- internal/api/types.go | 37 +++++++-------- internal/api/users.go | 61 +++++++++++++------------ internal/api/zone.go | 82 +++++++++++++++++++-------------- internal/sys.go | 15 ------ 9 files changed, 213 insertions(+), 199 deletions(-) delete mode 100644 internal/sys.go diff --git a/internal/api/api.go b/internal/api/api.go index 86e3fe8..c8efa2d 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -33,7 +33,7 @@ func API() http.Handler { api.Use(jwtauth.Verifier(tokenAuth)) api.Use(jwtauth.Authenticator) - api.With(setDBMiddleware).Post("/upload", ReceiveFile) + api.With(setDBMiddleware).With(pommeLogger).Post("/upload", ReceiveFile) }) // Open routes @@ -51,9 +51,8 @@ func API() http.Handler { render.JSON(w, r, resp) }), )) - - api.With(setDBMiddleware).Post("/create", NewUser) - api.With(setDBMiddleware).Post("/login", Login) + api.With(setDBMiddleware).With(pommeLogger).Post("/create", NewUser) + api.With(setDBMiddleware).With(pommeLogger).Post("/login", Login) api.Post("/logout", Logout) }) diff --git a/internal/api/api_test.go b/internal/api/api_test.go index f8da10c..fc780f2 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -350,10 +350,11 @@ func (a *accountTest) TestProtectedRoutes(t *testing.T) { func (a *accountTest) CleanUp(db *gorm.DB) { if err := os.Remove("pomme-test.sqlite"); err != nil { - l := newResponder(Response[any]{ - Message: "unable to clean up test DB", - Err: err.Error(), - }) - l.writeLogEntry() + l := newResponder() + l.Response = Response{ + Message: "unable to clean test database", + Err: err, + } + l.newLogEntry().errorLogger(l.Response) } } diff --git a/internal/api/auth.go b/internal/api/auth.go index 0a4015b..9d05343 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -1,6 +1,8 @@ package api import ( + "errors" + "fmt" "net/http" "time" @@ -27,27 +29,31 @@ import ( // @Router /api/login [post] func Login(w http.ResponseWriter, r *http.Request) { var result internal.User + logger, ok := r.Context().Value(keyLoggerContextID).(*Responder) + if !ok { + return + } if _, err := r.Cookie("jwt"); err == nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "already logged in", Status: http.StatusOK, - }) - logger.apiError(w, r) - logger.writeLogEntry() + } + logger.newLogEntry().apiError(logger.Response, w, r) + logger.newLogEntry().infoLogger(logger.Response) return } err := r.ParseForm() if err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "unable to parse request", Status: http.StatusInternalServerError, - Err: err.Error(), - }) - logger.apiError(w, r) - logger.writeLogEntry() + Err: err, + } + logger.newLogEntry().apiError(logger.Response, w, r) + logger.newLogEntry().infoLogger(logger.Response) return } @@ -57,34 +63,36 @@ func Login(w http.ResponseWriter, r *http.Request) { password := r.Form.Get("password") if username == "" { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "no username provided", Status: http.StatusInternalServerError, - }) - logger.apiError(w, r) + } + logger.newLogEntry().infoLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } if password == "" { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "no password provided", Status: http.StatusInternalServerError, - }) - logger.apiError(w, r) + } + + logger.newLogEntry().apiError(logger.Response, w, r) return } db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB) if !ok { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "no password provided", Status: http.StatusInternalServerError, - Err: "DB connection failed", - }) - logger.apiError(w, r) - logger.writeLogEntry() + Err: errors.New("db connection failed"), + } + logger.newLogEntry().apiError(logger.Response, w, r) + logger.newLogEntry().errorLogger(logger.Response) return } @@ -92,13 +100,13 @@ func Login(w http.ResponseWriter, r *http.Request) { db.Where("username = ?", username).First(&result) if result.Username == "" { - logger := newResponder(Response[any]{ - Message: "login failed", + logger.Response = Response{ + Message: fmt.Sprintf("login failed: %s", username), Status: http.StatusUnauthorized, Realm: "authentication", - }) - logger.apiError(w, r) - logger.writeLogEntry() + } + logger.newLogEntry().apiError(logger.Response, w, r) + logger.newLogEntry().infoLogger(logger.Response) return } @@ -106,26 +114,26 @@ func Login(w http.ResponseWriter, r *http.Request) { err = bcrypt.CompareHashAndPassword([]byte(result.HashedPassword), []byte(password)) if err != nil { - logger := newResponder(Response[any]{ - Message: "login failed", + logger.Response = Response{ + Message: fmt.Sprintf("login failed: %s", username), Status: http.StatusUnauthorized, Realm: "authentication", - }) - logger.apiError(w, r) - logger.writeLogEntry() + } + logger.newLogEntry().apiError(logger.Response, w, r) + logger.newLogEntry().infoLogger(logger.Response) return } token, err := makeToken(username) if err != nil { - logger := newResponder(Response[any]{ - Message: "internal server error", - Status: http.StatusInternalServerError, - Err: err.Error(), - }) - logger.apiError(w, r) - logger.writeLogEntry() + logger.Response = Response{ + Message: fmt.Sprintf("login failed: %s", username), + Status: http.StatusUnauthorized, + Realm: "authentication", + } + logger.newLogEntry().apiError(logger.Response, w, r) + logger.newLogEntry().errorLogger(logger.Response) return } diff --git a/internal/api/fs.go b/internal/api/fs.go index 94e5189..fafad11 100644 --- a/internal/api/fs.go +++ b/internal/api/fs.go @@ -21,24 +21,26 @@ func makeLocal(zone *ZoneRequest) error { return errEmptyFile } + logger := newResponder() + c, err := internal.ReadConfig() if err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "no config file defined", - Err: err.Error(), - }) - logger.writeLogEntry() + Err: err, + } + logger.newLogEntry().panicLogger(logger.Response) return fmt.Errorf("unable to parse directory: %w", err) } path := fmt.Sprintf("%s/%s/", c.ZoneDir, zone.FileName) if err = os.MkdirAll(path, 0o750); err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "unable to make directory for zone files", - Err: err.Error(), - }) - logger.writeLogEntry() + Err: err, + } + logger.newLogEntry().errorLogger(logger.Response) return fmt.Errorf("unable to make zone directory: %w", err) } diff --git a/internal/api/helpers.go b/internal/api/helpers.go index 750a2f4..a39e7c4 100644 --- a/internal/api/helpers.go +++ b/internal/api/helpers.go @@ -23,27 +23,25 @@ func setDBMiddleware(next http.Handler) http.Handler { ok bool ) + logger := newResponder() + c, err := internal.ReadConfig() if err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "No config file defined", - Err: err.Error(), - }) - logger.writeLogEntry() + Err: err, + } + logger.newLogEntry().panicLogger(logger.Response) } pommeDB, err, ok = db.InitDb(c.DB, mode) if err != nil && !ok { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "Error initializing DB", - Err: err.Error(), - }) - logger.writeLogEntry() - err = internal.SysKill() - if err != nil { - panic("this should not happen.") + Err: err, } + logger.newLogEntry().panicLogger(logger.Response) } timeoutContext, cancelContext := context.WithTimeout(context.Background(), time.Second) @@ -53,55 +51,60 @@ func setDBMiddleware(next http.Handler) http.Handler { }) } -// apiError sends unsuccessful API requests back to the user with the appropriate information. -func (e *Response[T]) apiError(w http.ResponseWriter, r *http.Request) { - v := map[string]any{ - "message": e.Message, - "status": e.Status, - "realm": e.Realm, - "error": e.Err, - } +func pommeLogger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger := newResponder() + ctx := context.WithValue(r.Context(), keyLoggerContextID, logger) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +// newLogEntry takes a Response struct and starts a new response chain +func (e *Responder) newLogEntry() *Responder { + return &Responder{} +} + +// apiError sends unsuccessful API requests back to the user with the appropriate information. +func (e *Responder) apiError(logEntry Response, w http.ResponseWriter, r *http.Request) { + //log.Fatalln(e.Response, logEntry) w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Type", "application/json; charset=utf-8") - switch v["realm"] { - default: - w.Header().Add("WWW-Authenticate", fmt.Sprintf(`realm="%v"`, e.Realm)) - delete(v, "realm") - - fallthrough - case nil: - w.Header().Add("API Error", v["message"].(string)) + if logEntry.Realm != "" { + w.Header().Add("WWW-Authenticate", fmt.Sprintf(`realm="%v"`, logEntry.Realm)) } - w.WriteHeader(v["status"].(int)) + w.Header().Add("API Error", logEntry.Message) - delete(v, "error") - delete(v, "status") - delete(v, "realm") + w.WriteHeader(logEntry.Status) - render.JSON(w, r, v) + if logEntry.Err != nil { + logEntry.Err = nil + } // nullify errors before returning to user + + render.JSON(w, r, logEntry) } -// writeLogEntry takes a response struct and writes info and error level logs -// todo: make it write to file maybe -func (e *Response[T]) writeLogEntry() { - v := map[string]any{ - "message": e.Message, - "error": e.Err, - } - +func (e *Responder) panicLogger(logEntry Response) { logger := httplog.NewLogger("Pomme", httplog.Options{ JSON: true, }) - switch v["error"] { - default: - logger.Error().Msg(v["error"].(string)) - - fallthrough - case nil: - logger.Info().Msg(v["message"].(string)) - } + logger.Panic().Msg(logEntry.Message) +} + +func (e *Responder) infoLogger(logEntry Response) { + logger := httplog.NewLogger("Pomme", httplog.Options{ + JSON: true, + }) + + logger.Info().Msg(logEntry.Message) +} + +func (e *Responder) errorLogger(logEntry Response) { + logger := httplog.NewLogger("Pomme", httplog.Options{ + JSON: true, + }) + + logger.Err(logEntry.Err).Msg(logEntry.Message) } diff --git a/internal/api/types.go b/internal/api/types.go index 34b4282..259375c 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -8,6 +8,7 @@ import ( const ( keyPrincipalContextID key = iota + keyLoggerContextID ) type key int @@ -48,30 +49,30 @@ type ndr interface { var _ ndr = (*ZoneRequest)(nil) -// HTTPResponder interface exists for type constraints on generic HTTP logging functions. -type HTTPResponder interface { - any -} - // HTTPLogger interface handles logging and sending responses to the user. -type HTTPLogger[T HTTPResponder] interface { - apiError(http.ResponseWriter, *http.Request) - writeLogEntry() +type HTTPLogger[T Response] interface { + newLogEntry() *Responder + apiError(T, http.ResponseWriter, *http.Request) + panicLogger(T) + infoLogger(T) + errorLogger(T) } // Response is a generic response struct containing the necessary keys for a log and response action. -type Response[T HTTPResponder] struct { - Message, Status, Realm, Err T +type Response struct { + Message string + Status int + Realm string + Err error } -var _ HTTPLogger[HTTPResponder] = (*Response[HTTPResponder])(nil) +type Responder struct { + Response Response +} + +var _ HTTPLogger[Response] = (*Responder)(nil) // newResponder instantiates a new HTTPLogger object. -func newResponder[T HTTPResponder](m Response[T]) HTTPLogger[T] { - return &Response[T]{ - Message: m.Message, - Status: m.Status, - Realm: m.Realm, - Err: m.Err, - } +func newResponder() *Responder { + return &Responder{} } diff --git a/internal/api/users.go b/internal/api/users.go index fb12155..9d89387 100644 --- a/internal/api/users.go +++ b/internal/api/users.go @@ -16,27 +16,31 @@ import ( // NewUser takes a POST request and user form and creates a user in the database. func NewUser(w http.ResponseWriter, r *http.Request) { var result internal.User + logger, ok := r.Context().Value(keyLoggerContextID).(*Responder) + if !ok { + return + } db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB) if !ok { - logger := newResponder(Response[any]{ - Message: "internal server error", - Status: http.StatusInternalServerError, - }) - logger.apiError(w, r) + logger.Response = Response{ + Message: "unable to init DB", + } + logger.newLogEntry().apiError(logger.Response, w, r) return } err := r.ParseForm() if err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "unable to parse request", Status: http.StatusInternalServerError, - Err: err.Error(), - }) - logger.apiError(w, r) - logger.writeLogEntry() + Err: err, + } + + logger.newLogEntry().infoLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } @@ -50,12 +54,13 @@ func NewUser(w http.ResponseWriter, r *http.Request) { password := r.Form.Get("password") if password == "" { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "no password provided", Status: http.StatusInternalServerError, - }) - logger.apiError(w, r) - logger.writeLogEntry() + } + + logger.newLogEntry().infoLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } @@ -63,24 +68,24 @@ func NewUser(w http.ResponseWriter, r *http.Request) { db.Where("username = ?", username).First(&result) if result.Username != "" { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "internal server error", Status: http.StatusInternalServerError, - }) - logger.apiError(w, r) - logger.writeLogEntry() + } + logger.newLogEntry().infoLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "login failed", Status: http.StatusUnauthorized, Realm: "authentication", - }) - logger.apiError(w, r) + } + logger.newLogEntry().infoLogger(logger.Response) return } @@ -89,12 +94,12 @@ func NewUser(w http.ResponseWriter, r *http.Request) { token, err := makeToken(username) if err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "internal server error", Status: http.StatusInternalServerError, - Err: err.Error(), - }) - logger.apiError(w, r) + Err: err, + } + logger.newLogEntry().errorLogger(logger.Response) return } @@ -114,11 +119,9 @@ func NewUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) - resp := internal.Response{ + render.JSON(w, r, internal.Response{ Message: "Successfully created account and logged in", - } - - render.JSON(w, r, resp) + }) http.Redirect(w, r, "/", http.StatusSeeOther) } diff --git a/internal/api/zone.go b/internal/api/zone.go index 68906e7..d9fb118 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -1,6 +1,7 @@ package api import ( + "errors" "fmt" "io" "log" @@ -39,19 +40,25 @@ import ( func ReceiveFile(w http.ResponseWriter, r *http.Request) { var result internal.User + logger, ok := r.Context().Value(keyLoggerContextID).(*Responder) + if !ok { + return + } + _, claims, _ := jwtauth.FromContext(r.Context()) r.Body = http.MaxBytesReader(w, r.Body, 1*1024*1024) // approx 1 mb max upload file, header, err := r.FormFile("file") if err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "File upload failed", Status: http.StatusInternalServerError, - Err: err.Error(), - }) - logger.apiError(w, r) - logger.writeLogEntry() + Err: err, + } + + logger.newLogEntry().errorLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } @@ -60,25 +67,26 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { b, err := io.ReadAll(file) if err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "internal server error", Status: http.StatusInternalServerError, - Err: err.Error(), - }) - logger.apiError(w, r) - logger.writeLogEntry() + Err: err, + } + logger.newLogEntry().errorLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } - ok := validateContentType(file) + ok = validateContentType(file) if !ok { - logger := newResponder(Response[any]{ + + logger.Response = Response{ Message: "file type must be text/plain", Status: http.StatusUnsupportedMediaType, - }) - logger.apiError(w, r) - logger.writeLogEntry() + } + logger.newLogEntry().infoLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } @@ -86,26 +94,27 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { zoneFile := newDNSRequest(header.Filename, claims["username"].(string), b) if err := zoneFile.parse(); err != nil { - logger := newResponder(Response[any]{ + logger.Response = Response{ Message: "Unable to parse zonefile", Status: http.StatusInternalServerError, - Err: err.Error(), - }) - logger.apiError(w, r) - logger.writeLogEntry() + Err: err, + } + logger.newLogEntry().errorLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB) if !ok { - logger := newResponder(Response[any]{ - Message: "internal server error", + + logger.Response = Response{ + Message: "Unable to parse zonefile", Status: http.StatusInternalServerError, - Err: "unable to connect to DB", - }) - logger.apiError(w, r) - logger.writeLogEntry() + Err: errors.New("unable to connect to DB"), + } + logger.newLogEntry().errorLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } @@ -114,11 +123,13 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { db.Where("username = ?", claims["username"].(string)).First(&result) if result.Username == "" { - logger := newResponder(Response[any]{ - Message: "user does not exist", + logger.Response = Response{ + Message: "Internal server error", Status: http.StatusInternalServerError, - }) - logger.apiError(w, r) + Err: errors.New("user does not exist"), + } + logger.newLogEntry().errorLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } @@ -132,13 +143,14 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { }) if err := zoneFile.save(); err != nil { - logger := newResponder(Response[any]{ + + logger.Response = Response{ Message: "Unable to save zonefile", Status: http.StatusInternalServerError, - Err: err.Error(), - }) - logger.apiError(w, r) - logger.writeLogEntry() + Err: err, + } + logger.newLogEntry().errorLogger(logger.Response) + logger.newLogEntry().apiError(logger.Response, w, r) return } diff --git a/internal/sys.go b/internal/sys.go deleted file mode 100644 index 943b7d0..0000000 --- a/internal/sys.go +++ /dev/null @@ -1,15 +0,0 @@ -package internal - -import "golang.org/x/sys/unix" - -func SysKill() (err error) { - err = killPomme() - - return -} - -func killPomme() (err error) { - err = unix.Kill(unix.Getpid(), unix.SIGINT) - - return -}