package main import ( "compress/gzip" "crypto/rand" "database/sql" "encoding/json" "fmt" "html/template" "io" "log" "log/slog" "math/big" "net" "net/http" "strings" "time" ) type ( server struct { listen net.Listener db *sql.DB h *http.ServeMux log *slog.Logger tmpl *template.Template auth func(c *Context) error // authentication autho func(c *Context) error // authorization } handleFunc func(c *Context) Context struct { // internal maintenance stuff w http.ResponseWriter r *http.Request tmpl *template.Template db *sql.DB log *slog.Logger User string `json:"-"` Filter *filter `json:"-"` CanEdit bool `json:"-"` // has user permission to edit stuff? Title string `json:"title,omitempty"` CurrentPath string `json:"-"` Error string `json:"error,omitempty"` Mappings map[int]map[int]MapEntry `json:"mappings,omitempty"` Commands map[string]int `json:"commands,omitempty"` Checks []check `json:"checks,omitempty"` CheckDetails *checkDetails `json:"check_details,omitempty"` Groups []group `json:"groups,omitempty"` Unhandled bool `json:"-"` // set this flag when unhandled was called Content map[string]any `json:"-"` // used for the configuration dashboard } ) func newServer(l net.Listener, db *sql.DB, log *slog.Logger, tmpl *template.Template, auth func(c *Context) error, autho func(c *Context) error) *server { s := &server{ listen: l, db: db, tmpl: tmpl, h: http.NewServeMux(), auth: auth, autho: autho, log: log, } return s } func (s *server) ListenAndServe() error { server := http.Server{Handler: s.h} return server.Serve(s.listen) } func (s *server) Handle(path string, fun handleFunc) { s.h.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { txid, err := newTxId() if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(`internal error occured`)) s.log.Error("could not create txid", "error", err) return } c := &Context{ w: w, r: r, tmpl: s.tmpl, db: s.db, log: s.log.With("path", path, "txid", txid), } if err := s.auth(c); err != nil { return } if err := s.autho(c); err != nil { return } fun(c) return }) } func (s *server) HandleStatic(path string, h func(w http.ResponseWriter, r *http.Request)) { s.h.HandleFunc(path, h) } // Render calls the template with the given name to // render the appropiate content. // In case of an error, a error message is automatically pushed // to the client. func (c *Context) Render(t string) error { var w io.Writer = c.w if strings.Contains(c.r.Header.Get("Accept-Encoding"), "gzip") { gz, err := gzip.NewWriterLevel(w, 5) if err != nil { log.Printf("could not create gzip writer: %s", err) return fmt.Errorf("could not create gzip writer: %s", err) } defer gz.Close() w = gz c.w.Header().Set("Content-Encoding", "gzip") } if c.r.Header.Get("Accept") == "application/json" { c.w.Header().Set("Content-Type", "application/json") enc := json.NewEncoder(w) enc.SetIndent("", "") // disable indentation to save traffic if err := enc.Encode(c); err != nil { c.w.WriteHeader(http.StatusInternalServerError) c.w.Write([]byte("could not write json output")) log.Printf("could not write json output: %s", err) return err } return nil } if err := c.tmpl.ExecuteTemplate(w, t, c); err != nil { c.w.WriteHeader(http.StatusInternalServerError) c.w.Write([]byte("problem with a template")) log.Printf("could not execute template: %s", err) return err } return nil } // Get a cookie value. func (c *Context) GetCookieVal(name string) string { cook, err := c.r.Cookie(name) if err == http.ErrNoCookie { return "" } return cook.Value } // Set a new key value cookie with a deadline. func (c *Context) SetCookie(name, val string, expire time.Time) { cook := http.Cookie{ Name: name, Value: val, Expires: expire, Secure: true, SameSite: http.SameSiteStrictMode, HttpOnly: true, Path: "/", } http.SetCookie(c.w, &cook) return } // Alphabet are the available characters used to generate the txids. The more // characters available the more bits can be represented with shorter IDs. const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_" // alphalen is the number of available characters in the alphabet. var alphalen = big.NewInt(int64(len(alphabet))) // txlen is the length of the generated transaction IDs const txlen = 15 // generate a new transaction ID // // This function generates a unique ID using the above parameters. If the IDs // generated are not unique enough to track a transaction, the txlen should // be raised. func newTxId() (string, error) { s := make([]byte, txlen) var err error var num *big.Int for i := 0; i < txlen; i++ { num, err = rand.Int(rand.Reader, alphalen) if err != nil { return "", err } s[i] = alphabet[num.Int64()] } return string(s), nil }