181 lines
5.4 KiB
Go
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.Exists() { // writing it now is what makes the generated secret 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
|
|
}
|