add log levels, method chaining, remove unused code

This commit is contained in:
grumbulon 2023-05-28 01:19:19 -04:00
parent f360c45224
commit 64c43839de
9 changed files with 213 additions and 199 deletions

View file

@ -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)
})

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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{}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}