package caddy_tailscale import ( "encoding/json" "fmt" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "net/http" "strings" ) func init() { httpcaddyfile.RegisterDirective("tailscale_auth", parseCaddyfile) httpcaddyfile.RegisterDirectiveOrder("tailscale_auth", httpcaddyfile.After, "basic_auth") } // parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // tailscale_auth [set_headers] [{ // [remap ] // [allowed_logins [...]] // }] // // See also for further examples: // - https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/caddyauth/caddyfile.go // - https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { h.Next() // consume directive name tsAuth := new(TailscaleAuth) var setHeaders bool if h.Next() { if h.Val() == "set_headers" { setHeaders = true } else { return nil, h.Errf("unknown argument %#v", h.ValRaw()) } } // All header names are in lowercase to easily do "case insensitive" comparisons as Caddy will normalise them later // on headersToSet := http.Header{ "tailscale-user-id": []string{"{http.auth.user.id}"}, "tailscale-user-name": []string{"{http.auth.user.display_name}"}, "tailscale-user-login": []string{"{http.auth.user.login_name}"}, } for nesting := h.Nesting(); h.NextBlock(nesting); { switch h.Val() { case "remap": var from, to string if !h.Args(&from, &to) { return nil, h.Errf("remap takes two arguments") } from = strings.ToLower(from) to = strings.ToLower(to) v, foundFrom := headersToSet[from] if !foundFrom { return nil, h.Errf("unknown from header %#v", from) } headersToSet[to] = v delete(headersToSet, from) case "allowed_logins": for h.NextArg() { tsAuth.AllowedUsers = append(tsAuth.AllowedUsers, h.Val()) } if len(tsAuth.AllowedUsers) == 0 { return nil, h.Err("allowed_logins must have 1 or more arguments") } default: return nil, h.Errf("unknown argument %#v", h.Val()) } } handlers := []json.RawMessage{ caddyconfig.JSONModuleObject( caddyauth.Authentication{ ProvidersRaw: caddy.ModuleMap{ "tailscale": caddyconfig.JSON(tsAuth, nil), }, }, "handler", "authentication", nil, ), } if setHeaders { handlers = append(handlers, caddyconfig.JSONModuleObject( &headers.Handler{ Request: &headers.HeaderOps{Set: headersToSet}, }, "handler", "headers", nil, )) } if h.NextArg() { return nil, h.Err("too many arguments") } fmt.Println(string(handlers[0])) return []httpcaddyfile.ConfigValue{ { Class: "route", Value: caddyhttp.Route{ HandlersRaw: handlers, }, }, }, nil }