Files
Mako/cmd/mako/main.go
T
VeillaxandClaude Sonnet 5.5 9f2fe4a416
CI / test (push) Successful in 2m2s
Release / release (push) Successful in 2m15s
Encrypt pages at rest with a key per person
Pages are sealed with AES-256-GCM. Each person's key is derived (HKDF) from a master key in the settings file plus their Authentik issuer and subject. Plain files from earlier versions are read and re-saved encrypted. Mako refuses to open a notebook it cannot decrypt rather than overwrite it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 00:25:37 -06:00

181 lines
5.4 KiB
Go

// Command mako is a private notebook: pages you can link, nest and search, kept as plain
// markdown files. You sign in with Authentik; there is no other way in.
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"git.veillax.com/Veillax/mako/internal/config"
"git.veillax.com/Veillax/mako/internal/oidc"
"git.veillax.com/Veillax/mako/internal/web"
)
var version = "dev"
const usage = `mako is a private notebook that you sign in to with Authentik.
mako serve [-config mako.json] [-listen addr] [-data dir]
run the notebook
mako check [-config mako.json] test the Authentik settings without starting
mako version
Settings come from the settings file and can be overridden by environment variables:
MAKO_PUBLIC_URL where you open Mako, such as https://notes.example.com
MAKO_OIDC_ISSUER the Authentik provider's OpenID configuration issuer
MAKO_OIDC_CLIENT_ID the provider's Client ID
MAKO_OIDC_CLIENT_SECRET the provider's Client Secret
MAKO_ALLOWED optional: only these emails or usernames may sign in (comma separated)
MAKO_LISTEN, MAKO_DATA, MAKO_CONFIG
`
// exitConfig is the exit status for settings that need fixing by hand, so a service manager does
// not keep restarting something that cannot start.
const exitConfig = 2
func main() {
log.SetFlags(log.LstdFlags)
if len(os.Args) < 2 {
fmt.Fprint(os.Stderr, usage)
os.Exit(2)
}
var err error
switch os.Args[1] {
case "serve":
err = serve(os.Args[2:])
case "check":
err = check(os.Args[2:])
case "version", "-v", "--version":
fmt.Println("mako", version)
case "help", "-h", "--help":
fmt.Print(usage)
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n\n%s", os.Args[1], usage)
os.Exit(2)
}
if err != nil {
fmt.Fprintln(os.Stderr, "mako:", err)
var ce configError
if errors.As(err, &ce) {
os.Exit(exitConfig)
}
os.Exit(1)
}
}
type configError struct{ error }
func configFlag(fs *flag.FlagSet) *string {
def := os.Getenv("MAKO_CONFIG")
if def == "" {
def = "mako.json"
}
return fs.String("config", def, "path of the settings file")
}
func serve(args []string) error {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
path := configFlag(fs)
listen := fs.String("listen", "", "override the listen address")
data := fs.String("data", "", "override the folder the notes are kept in")
fs.Parse(args)
if *listen != "" {
os.Setenv("MAKO_LISTEN", *listen)
}
if *data != "" {
os.Setenv("MAKO_DATA", *data)
}
h, err := config.Load(*path)
if err != nil {
return configError{err}
}
if h.NeedsSave() { // writing it now is what makes the generated secret and key stick
if err := h.Save(); err != nil {
return fmt.Errorf("could not write %s: %w", *path, err)
}
}
cfg := h.Get()
web, err := web.New(h, version)
if err != nil {
return configError{err}
}
srv := &http.Server{
Addr: cfg.Listen,
Handler: web.Handler(),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 2 * time.Minute,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
errc := make(chan error, 1)
go func() { errc <- srv.ListenAndServe() }()
log.Printf("mako %s: notes are kept in %s; open %s", version, cfg.DataDir, cfg.PublicURL)
log.Printf("sign-in goes through %s; the redirect URI to allow there is %s", cfg.OIDC.Issuer, cfg.RedirectURL())
if len(cfg.Allowed) == 0 {
log.Print("anyone Authentik lets into this application gets a notebook of their own; set \"allowed\" to limit it")
}
if !cfg.Secure() && !strings.Contains(cfg.PublicURL, "localhost") && !strings.Contains(cfg.PublicURL, "127.0.0.1") {
log.Print("warning: public_url is not https, and session cookies will not be marked Secure")
}
go func() { // say early if Authentik cannot be reached, instead of at the first sign-in
c, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if _, err := web.OIDC().Discover(c); err != nil {
log.Printf("warning: %v (sign-in will work once Authentik is reachable)", err)
}
}()
select {
case err := <-errc:
return err
case <-ctx.Done():
}
sctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return srv.Shutdown(sctx)
}
func check(args []string) error {
fs := flag.NewFlagSet("check", flag.ExitOnError)
path := configFlag(fs)
fs.Parse(args)
h, err := config.Load(*path)
if err != nil {
return configError{err}
}
cfg := h.Get()
if err := cfg.Check(); err != nil {
return configError{err}
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
d, err := oidc.New(cfg.OIDC.Issuer, cfg.OIDC.ClientID, cfg.OIDC.ClientSecret, cfg.RedirectURL()).Discover(ctx)
if err != nil {
return err
}
fmt.Println("Authentik answered at", cfg.OIDC.Issuer)
fmt.Println(" sign-in page:", d.AuthorizationEndpoint)
fmt.Println(" token address:", d.TokenEndpoint)
fmt.Println("Make sure the provider in Authentik allows this redirect URI exactly:")
fmt.Println(" " + cfg.RedirectURL())
if len(cfg.Allowed) == 0 {
fmt.Println("Everyone Authentik lets into the application gets their own notebook.")
} else {
fmt.Println("Only these may sign in:", strings.Join(cfg.Allowed, ", "))
}
return nil
}