Skip to content
Snippets Groups Projects
main.go 5.06 KiB
Newer Older
gbe's avatar
gbe committed
package main

import (
gbe's avatar
gbe committed
	"embed"
	"flag"
gbe's avatar
gbe committed
	"net/http"
	"os/signal"
	"time"
gbe's avatar
gbe committed

gbe's avatar
gbe committed
	kitlog "github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/jmoiron/sqlx"
	"modernc.org/ql"
	_ "modernc.org/ql/driver"

	"git.c3pb.de/gbe/invinoveritas/auth"
gbe's avatar
gbe committed
	"git.c3pb.de/gbe/invinoveritas/log"
gbe's avatar
gbe committed
	"git.c3pb.de/gbe/invinoveritas/session"
gbe's avatar
gbe committed
)

//go:embed templates/*.tpl
var templateFS embed.FS

//go:embed static/*
var staticFS embed.FS

gbe's avatar
gbe committed
// Build info. Will be set by Gitlab pipeline.
var (
	commitHash string
	buildTime  string
)

gbe's avatar
gbe committed
func httpError(w http.ResponseWriter, r *http.Request, msg string, err error, status int) {
gbe's avatar
gbe committed
	if err != nil {
		msg += ": " + err.Error()
	}

gbe's avatar
gbe committed
	level.Error(log.Get(r)).
		Log("status", status,
			"error", err,
			"msg", msg)
gbe's avatar
gbe committed

	http.Error(w, msg, status)
type sessionProvider interface {
	ListSessions(context.Context) ([]session.Info, error)
	DeleteSession(ctx context.Context, token string) error
}

type userProvider interface {
	ListUsers(context.Context) ([]string, error)
	CreateUser(ctx context.Context, name string) (string, error)
	DeleteUser(ctx context.Context, name string) error

	UpdatePassword(ctx context.Context, name string, oldPW, newPW string) error
}

gbe's avatar
gbe committed
type Handler struct {
gbe's avatar
gbe committed
	sqlDB *sqlx.DB
	sp    sessionProvider
	up    userProvider
func addCacheHeaders(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Cache-Control", "public, max-age=86400, immutable")
		next.ServeHTTP(w, r)
	})
}

gbe's avatar
gbe committed
func openDB(path string, logger kitlog.Logger) (*sqlx.DB, error) {
	var retried bool

	path += ".ql"

retry:
	db, err := sqlx.Open("ql2", path)
	if err != nil {
		return nil, err
	}

gbe's avatar
gbe committed
	level.Info(logger).Log("msg", "running test-select on db")
	_, err = db.Exec(`SELECT count(*) FROM __Table`)
	if err != nil && !retried {
gbe's avatar
gbe committed
		level.Error(logger).
			Log("error", err,
				"msg", "got error")

		retried = true

		// WAL may be corrupted. Manually remove it and re-try open. See
		// https://gitlab.com/cznic/ql/-/issues/227 for more info.
		err := os.Remove(ql.WalName(path))
		if err != nil {
			return nil, err
		}

		goto retry
	}

	if err != nil {
		return nil, err
	}

	return db, nil
}

// wrapMiddleware wraps common middleware around hdlr:
// - log.Request to make a logger available in the request context
// - addCacheHeader for caching
// - auth.Require
gbe's avatar
gbe committed
func wrapMiddleware(hdlr http.Handler, sessions session.Provider, logger kitlog.Logger) http.Handler {
gbe's avatar
gbe committed
	authFailed := log.Request(sessions.Handler(templateFS), logger)
	return log.Request(auth.Require(hdlr, authFailed, sessions), logger)
gbe's avatar
gbe committed
func main() {
	dbPath := flag.String("db", "vino", "Path to database file")
	listenAddr := flag.String("listen", "127.0.0.1:7878", "Listening address")
	debug := flag.Bool("debug", false, "Enable debug logging")

	flag.Parse()

gbe's avatar
gbe committed
	logger := kitlog.NewLogfmtLogger(kitlog.NewSyncWriter(os.Stdout))
	logger = kitlog.With(logger, "ts", kitlog.DefaultTimestampUTC)

	filter := level.AllowInfo()
	if *debug {
gbe's avatar
gbe committed
		filter = level.AllowDebug()
gbe's avatar
gbe committed
	logger = level.NewFilter(logger, filter)

gbe's avatar
gbe committed
	level.Info(logger).
		Log("commit", commitHash, "build", buildTime)
gbe's avatar
gbe committed
	db, err := openDB(*dbPath, logger)
gbe's avatar
gbe committed
		logger.Log("error", err, "msg", "can't open DB")
		os.Exit(1)
gbe's avatar
gbe committed
	defer db.Close()

	ctx, done := context.WithCancel(context.Background())
	defer done()

	err = initDB(ctx, db)
gbe's avatar
gbe committed
		level.Error(logger).
			Log("wal_name", ql.WalName(*dbPath+".ql"),
				"error", err, "msg", "can't initalize DB")
gbe's avatar
gbe committed
		os.Exit(1)
	http.HandleFunc("/favicon.ico", http.NotFound)

gbe's avatar
gbe committed
	http.Handle("/static/", log.Request(addCacheHeaders(http.FileServer(http.FS(staticFS))), logger))
gbe's avatar
gbe committed

	sessions := session.Provider{
gbe's avatar
gbe committed
		DB: db,
gbe's avatar
gbe committed
	}
gbe's avatar
gbe committed

gbe's avatar
gbe committed
	handler := Handler{
gbe's avatar
gbe committed
		sqlDB: db,
		sp:    sessions,
		up:    sessions,
gbe's avatar
gbe committed
	}
gbe's avatar
gbe committed
	http.Handle("/details/img/", addCacheHeaders(wrapMiddleware(handler.img(), sessions, logger)))
gbe's avatar
gbe committed
	http.Handle("/details/", wrapMiddleware(handler.details(), sessions, logger))
gbe's avatar
gbe committed

	http.Handle("/user/sessions", wrapMiddleware(handler.user(pageSessions), sessions, logger))
	http.Handle("/user/all-users", wrapMiddleware(handler.user(pageAllUsers), sessions, logger))
	http.Handle("/user/you", wrapMiddleware(handler.user(pageYou), sessions, logger))

gbe's avatar
gbe committed
	http.Handle("/", wrapMiddleware(handler.index(), sessions, logger))
gbe's avatar
gbe committed

	srv := http.Server{
		Addr: *listenAddr,
	}

gbe's avatar
gbe committed
	level.Info(logger).
		Log("addr", "http://"+*listenAddr, "msg", "starting http server")
gbe's avatar
gbe committed

	errs := make(chan error, 1)

	go func() {
		errs <- http.ListenAndServe(*listenAddr, nil)
	}()

	// Wait for OS signal or error, shut down server if signal received
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt)

	select {
	case sig := <-sigChan:
		level.Info(logger).
			Log("signal", sig, "msg", "received signal, terminating")

		shutdownCtx, done := context.WithTimeout(context.Background(), 15*time.Second)
		defer done()

		err = srv.Shutdown(shutdownCtx)
		if err != nil {
			level.Error(logger).
				Log("error", err, "msg", "clean shut down failed")
		}
	case err = <-errs:
gbe's avatar
gbe committed
		level.Error(logger).
			Log("error", err, "msg", "HTTP server failed to start")
gbe's avatar
gbe committed
	}
}