initial commit

This commit is contained in:
2025-02-22 23:00:56 +08:00
commit 61ffeeb3b8
20 changed files with 1676 additions and 0 deletions

95
middleware/httplog/log.go Normal file
View File

@ -0,0 +1,95 @@
package httplog
import (
"bufio"
"context"
"errors"
"log/slog"
"net"
"net/http"
"time"
"git.jeffthecoder.xyz/public/lazyhandler/magic"
)
type ctxKey int
const (
loggerKey ctxKey = iota
)
var (
ErrNotHijackable = errors.New("not a hijacker")
)
type Log struct {
LogStart bool
LogStartLevel slog.Level
LogFinish bool
LogFinishLevel slog.Level
}
type responseRecorder struct {
http.ResponseWriter
StatusCode int
}
func (recorder *responseRecorder) WriteHeader(statusCode int) {
recorder.StatusCode = statusCode
recorder.ResponseWriter.WriteHeader(statusCode)
}
func (recorder *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hijacker, ok := recorder.ResponseWriter.(http.Hijacker); ok {
recorder.ResponseWriter = nil
return hijacker.Hijack()
}
return nil, nil, ErrNotHijackable
}
var (
_ http.Hijacker = &responseRecorder{}
)
func (log Log) WrapHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
args := []any{
"remote_addr", r.RemoteAddr,
"host", r.Host,
"path", r.URL.Path,
}
startTime := time.Now()
if log.LogStart {
slog.With(append(args, "time", startTime)...).Log(r.Context(), log.LogStartLevel, "request")
}
recorder := &responseRecorder{ResponseWriter: w, StatusCode: 200}
next.ServeHTTP(recorder, r.WithContext(context.WithValue(r.Context(), loggerKey, slog.With(args...))))
if log.LogFinish && recorder.ResponseWriter != nil {
finishTime := time.Now()
slog.With(append(args, "time", finishTime, "duration", finishTime.Sub(startTime), "status", recorder.StatusCode)...).Log(r.Context(), log.LogFinishLevel, "response")
}
})
}
func Logger(r *http.Request) *slog.Logger {
if logger, ok := r.Context().Value(loggerKey).(*slog.Logger); ok {
return logger.With("time", time.Now())
}
return slog.With(
"remote_addr", r.RemoteAddr,
"host", r.Host,
"path", r.URL.Path,
"time", time.Now(),
)
}
func RegisterExtractor() {
magic.RegisterExtractorGeneric[*slog.Logger](func(r *http.Request) (any, error) {
return Logger(r), nil
})
}

13
middleware/middleware.go Normal file
View File

@ -0,0 +1,13 @@
package middleware
import "net/http"
type Middleware interface {
WrapHandler(next http.Handler) http.Handler
}
type WrapFunc func(next http.Handler) http.Handler
func (wrap WrapFunc) WrapHandler(next http.Handler) http.Handler {
return wrap(next)
}

View File

@ -0,0 +1,44 @@
package recover
import (
"encoding/json"
"net/http"
"git.jeffthecoder.xyz/public/lazyhandler/middleware"
"git.jeffthecoder.xyz/public/lazyhandler/middleware/httplog"
)
type PanicResponseFunc func(http.ResponseWriter, *http.Request, any)
func recoverer(w http.ResponseWriter, r *http.Request, fn PanicResponseFunc) {
if err := recover(); err != nil {
httplog.Logger(r).With("panic", err).Error("request panicked")
if fn != nil {
fn(w, r, err)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
}
}
func Recover(responseFunc PanicResponseFunc) middleware.Middleware {
return middleware.WrapFunc(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer recoverer(w, r, responseFunc)
next.ServeHTTP(w, r)
})
})
}
func DebugPanicHandler(w http.ResponseWriter, r *http.Request, err any) {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]any{
"error": err,
})
}
func Debug() middleware.Middleware {
return Recover(DebugPanicHandler)
}

View File

@ -0,0 +1,55 @@
package session
import (
"context"
"errors"
"net/http"
"git.jeffthecoder.xyz/public/lazyhandler/magic"
"git.jeffthecoder.xyz/public/lazyhandler/middleware"
"github.com/go-session/session/v3"
)
type ctxKey int
const (
sessionKey ctxKey = iota
)
var (
ErrSessionStoreNotInitialized = errors.New("no session store in request context")
)
func Session(sessionStore session.ManagerStore) middleware.Middleware {
manager := session.NewManager(
session.SetStore(sessionStore),
)
return middleware.WrapFunc(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
store, err := manager.Start(r.Context(), w, r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
defer store.Save()
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), sessionKey, store)))
})
})
}
func GetSession(r *http.Request) (session.Store, error) {
if store, ok := r.Context().Value(sessionKey).(session.Store); ok {
return store, nil
}
return nil, ErrSessionStoreNotInitialized
}
func RegisterExtractor() {
magic.RegisterExtractorGeneric[session.Store](func(r *http.Request) (any, error) {
store, err := GetSession(r)
return store, err
})
}

18
middleware/slash/slash.go Normal file
View File

@ -0,0 +1,18 @@
package slash
import (
"net/http"
"git.jeffthecoder.xyz/public/lazyhandler/middleware"
)
func StripSlash() middleware.Middleware {
return middleware.WrapFunc(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path[len(r.URL.Path)-1] == '/' && len(r.URL.Path) > 1 {
r.URL.Path = r.URL.Path[:len(r.URL.Path)-1]
}
next.ServeHTTP(w, r)
})
})
}

16
middleware/use.go Normal file
View File

@ -0,0 +1,16 @@
package middleware
import (
"net/http"
"slices"
)
func Use(handler http.Handler, middlewares ...Middleware) http.Handler {
slices.Reverse(middlewares)
for _, middleware := range middlewares {
handler = middleware.WrapHandler(handler)
}
return handler
}