aboutsummaryrefslogblamecommitdiff
path: root/cmd/monfront/server.go
blob: 06afe1108a716db8a58f57c7c4beec3ce795998e (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11



                       
                     





                       

                  










                                     
                                   












                                                               
                                 













                                                                                                           

                                                                                         


         
                                                                                                                                                          






                                           
                            










                                                                           






                                                                          




                                     
                                                                     













































































                                                                                            




























                                                                                   
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
}