1package main23import (4 "context"5 "embed"6 "fmt"7 "io/fs"8 "log/slog"9 "net/http"10 "strings"11 "time"12)1314//go:embed static/css/*.css15var staticfs embed.FS1617type Mux struct {18 *http.ServeMux19 middlewares []Middleware20}2122type Middleware func(http.HandlerFunc) http.HandlerFunc2324func (m *Mux) Use(mws ...Middleware) *Mux {25 return &Mux{26 ServeMux: m.ServeMux,27 middlewares: append(m.middlewares, mws...)[:],28 }29}3031func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {32 var handler http.HandlerFunc = m.ServeMux.ServeHTTP33 for _, mw := range m.middlewares {34 handler = mw(handler)35 }36 handler.ServeHTTP(w, r)37}3839func NewMux(db NoteDB) (*Mux, error) {40 l := slog.Default()4142 mux := &Mux{43 ServeMux: http.NewServeMux(),44 }4546 mux = mux.Use(47 LogMiddleware(l),48 URLMiddleware(),49 DBMiddleware(db),50 )5152 staticfs, _ := fs.Sub(staticfs, "static")53 mux.Handle("/-/", http.StripPrefix("/-/", http.FileServer(http.FS(staticfs))))5455 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {56 switch r.Method {57 case http.MethodPost:58 NewOrUpdateNoteAPI(!isPlainTextAgent(r.UserAgent()))(w, r)59 case http.MethodGet:60 if r.URL.EscapedPath() == "/" {61 Index(!isPlainTextAgent(r.UserAgent()))(w, r)62 } else {63 var isrich bool64 slog.Info(r.URL.EscapedPath())65 if strings.HasPrefix(r.URL.EscapedPath(), "/!/") {66 isrich = false67 } else {68 isrich = !isPlainTextAgent(r.UserAgent())69 }70 GetNoteAPI(isrich)(w, r)71 }72 case http.MethodDelete:73 DeleteNoteAPI(!isPlainTextAgent(r.UserAgent()))(w, r)74 default:75 w.WriteHeader(http.StatusNotFound)76 fmt.Fprintf(w, "Not found\n")77 }78 })7980 return mux, nil81}8283const LOGGER_MWKEY = "logger"8485func LogMiddleware(l *slog.Logger) Middleware {86 return func(next http.HandlerFunc) http.HandlerFunc {87 return func(w http.ResponseWriter, r *http.Request) {88 l := l.With(89 "path", r.URL.EscapedPath(),90 "remote", r.RemoteAddr,91 )9293 start := time.Now()9495 l.Debug("new request",96 "method", r.Method,97 )9899 r = r.WithContext(context.WithValue(r.Context(), LOGGER_MWKEY, l))100 next(w, r)101102 l.Debug("request ended",103 "duration", time.Now().Sub(start),104 )105 }106 }107}108109func LogCtx(ctx context.Context) *slog.Logger {110 raw, ok := ctx.Value(LOGGER_MWKEY).(*slog.Logger)111 if !ok {112 return slog.Default()113 }114 return raw115}116117func URLMiddleware() Middleware {118 return func(next http.HandlerFunc) http.HandlerFunc {119 return func(w http.ResponseWriter, r *http.Request) {120 // scheme121 if r.TLS != nil {122 r.URL.Scheme = "https"123 } else if scheme := r.Header.Get("X-Forwarded-Proto"); scheme != "" {124 r.URL.Scheme = scheme125 } else if scheme := r.Header.Get("X-Forwarded-Protocol"); scheme != "" {126 r.URL.Scheme = scheme127 } else if ssl := r.Header.Get("X-Forwarded-Ssl"); ssl == "on" {128 r.URL.Scheme = "https"129 } else if scheme := r.Header.Get("X-Url-Scheme"); scheme != "" {130 r.URL.Scheme = scheme131 } else {132 r.URL.Scheme = "http"133 }134135 next(w, r)136 }137 }138}139140func isPlainTextAgent(userAgent string) bool {141 var plainTextAgents = []string{142 "curl",143 "httpie",144 "lwp-request",145 "wget",146 "python-httpx",147 "python-requests",148 "openbsd ftp",149 "powershell",150 "fetch",151 "aiohttp",152 "http_get",153 "xh",154 }155156 userAgentLower := strings.ToLower(userAgent)157 for _, signature := range plainTextAgents {158 if strings.Contains(userAgentLower, signature) {159 return true160 }161 }162 return false163}