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
}