all repos — homestead @ 4b11ea624dc32fdb0950cdb4665656218874a702

Code for my website

determine tailscale remote user and greet them

Alan Pearce
commit

4b11ea624dc32fdb0950cdb4665656218874a702

parent

c641b47bd01a0f1fbf3dc7749594ceec373e794c

M domain/content/publisher/app.godomain/content/publisher/app.go
@@ -6,6 +6,7 @@
"github.com/Southclaws/fault" "github.com/Southclaws/fault/fmsg" "go.hacdias.com/indielib/indieauth" + "tailscale.com/client/local" "alin.ovh/homestead/domain/web/server" "alin.ovh/homestead/domain/web/templates"
@@ -17,13 +18,15 @@
type Options struct { Development bool `conf:"-"` BaseURL config.URL - VCSRemoteURL config.URL `conf:"default:https://git.alin.ovh/website"` + VCSRemoteURL config.URL `conf:"default:https://git.alin.ovh/website"` + LocalClient *local.Client `conf:"-"` } type App struct { log *log.Logger indieauthServer *indieauth.Server siteSettings templates.SiteSettings + localClient *local.Client *server.App }
@@ -32,6 +35,7 @@ var err error
app := &App{ log: log, indieauthServer: indieauth.NewServer(true, &http.Client{}), + localClient: opts.LocalClient, siteSettings: templates.SiteSettings{ Title: "Barkeep", Language: "en-GB",
@@ -44,13 +48,16 @@ Shutdown: func() {},
}, } + if opts.BaseURL.Path == "" { + opts.BaseURL.Path = "/" + } err = indieauth.IsValidProfileURL(opts.BaseURL.String()) if err != nil { return nil, fault.Wrap(err, fmsg.With("invalid base URL")) } mux := ihttp.NewServeMux() - mux.HandleFunc("/", app.Index) + mux.HandleFunc("/", app.WithUserContext(app.Index)) mux.HandleFunc("/style.css", app.Style) app.Handler = mux
M domain/content/publisher/barkeep/.envrcdomain/content/publisher/barkeep/.envrc
@@ -1,7 +1,8 @@
ROOT=$(git rev-parse --show-toplevel) -source_env $ROOT +source_env "$ROOT" -export KO_DATA_PATH=$(expand_path $ROOT/kodata) +KO_DATA_PATH=$(expand_path "$ROOT/kodata") +export KO_DATA_PATH watch_file .env dotenv_if_exists
M domain/content/publisher/barkeep/justfiledomain/content/publisher/barkeep/justfile
@@ -3,7 +3,7 @@ port := env_var_or_default("SERVER_PORT", "8081")
root := `git rev-parse --show-toplevel` run: - systemfd -s http::{{ listen_address }}:{{ port }} -- wgo run --root {{ root }} . --dev + LOG_LEVEL=debug wgo run --root {{ root }} . --dev build: go build .
M domain/content/publisher/barkeep/main.godomain/content/publisher/barkeep/main.go
@@ -1,22 +1,24 @@
package main import ( + "context" "errors" "fmt" "os" + "path/filepath" "github.com/ardanlabs/conf/v3" + "tailscale.com/tsnet" "alin.ovh/homestead/domain/content/publisher" "alin.ovh/homestead/domain/web/server" - "alin.ovh/homestead/domain/web/tssrv" + "alin.ovh/homestead/shared/env" "alin.ovh/x/log" ) type Options struct { Development bool `conf:"flag:dev"` Publisher publisher.Options - Tailscale tssrv.Options Server server.Options }
@@ -35,38 +37,57 @@ log := log.Configure(!options.Development)
if options.Development { options.Publisher.Development = true - options.Tailscale.Development = true + } + + hostname := env.GetEnvFallback("TAILSCALE_HOSTNAME", "barkeep") + stateDir := env.GetEnvFallback("XDG_STATE_HOME", "~/.local/state") + + ts := &tsnet.Server{ + Dir: filepath.Join(stateDir, "barkeep"), + Hostname: hostname, + Ephemeral: options.Development, + } + + lc, err := ts.LocalClient() + if err != nil { + log.Fatal("error creating tailscale local client", "error", err) } + options.Publisher.LocalClient = lc + pub, err := publisher.New(&options.Publisher, log.Named("publisher")) if err != nil { log.Fatal("error running publisher", "error", err) } - if options.Development { - srv, err := server.New(&options.Server, log.Named("server")) - if err != nil { - log.Fatal("error creating server", "error", err) - } + listener, err := ts.ListenTLS("tcp", ":443") + if err != nil { + log.Fatal("error creating tailscale listener", "error", err) + } + + options.Server.Listener = listener + + srv, err := server.New(&options.Server, log.Named("server")) + if err != nil { + log.Fatal("error creating server", "error", err) + } - if err := srv.HostApp(pub.App); err != nil { - log.Fatal("error hosting app", "error", err) - } + if err := srv.HostApp(pub.App); err != nil { + log.Fatal("error hosting app", "error", err) + } - if err := srv.Start(); err != nil { - log.Fatal("error starting server", "error", err) + go func() { + _, err := ts.Up(context.Background()) + if err != nil { + log.Error("error starting tailscale", "error", err) } - return - } + log.Info("starting server", "base_url", options.Publisher.BaseURL) + }() - ts, err := tssrv.New(&options.Tailscale) - if err != nil { - log.Fatal("error creating tailscale server", "error", err) + if err := srv.Start(); err != nil { + log.Fatal("error starting server", "error", err) } - err = ts.Serve(pub.Handler) - if err != nil { - log.Fatal("error serving", "error", err) - } + defer ts.Close() }
M domain/content/publisher/mux.godomain/content/publisher/mux.go
@@ -1,16 +1,38 @@
package publisher import ( + "context" "net/http" - pubtpl "alin.ovh/homestead/domain/content/publisher/templates" - "alin.ovh/homestead/domain/web/templates" + "alin.ovh/homestead/domain/content/publisher/templates" + basetpl "alin.ovh/homestead/domain/web/templates" ihttp "alin.ovh/homestead/shared/http" + "tailscale.com/tailcfg" ) -func (app *App) Index(w http.ResponseWriter, _ *http.Request) ihttp.Error { - err := pubtpl.IndexPage(app.siteSettings, templates.PageSettings{ - Title: "Home", +type user struct{} + +func (app *App) WithUserContext(fn ihttp.HandleFunc) ihttp.HandleFunc { + return func(w http.ResponseWriter, r *http.Request) ihttp.Error { + ctx := r.Context() + who, err := app.localClient.WhoIs(ctx, r.RemoteAddr) + if err != nil { + return ihttp.InternalServerError("cannot determine user", err) + } + + return fn(w, r.WithContext( + context.WithValue(ctx, user{}, who.UserProfile.Clone()), + )) + } +} + +func (app *App) Index(w http.ResponseWriter, r *http.Request) ihttp.Error { + user := r.Context().Value(user{}).(*tailcfg.UserProfile) + err := templates.IndexPage(app.siteSettings, templates.PageSettings{ + PageSettings: basetpl.PageSettings{ + Title: "Home", + }, + User: user.LoginName, }).Render(w) if err != nil { return ihttp.InternalServerError("Failed to render index page", err)
@@ -21,7 +43,7 @@ }
func (app *App) Style(w http.ResponseWriter, r *http.Request) ihttp.Error { w.Header().Set("Content-Type", "text/css") - http.ServeFileFS(w, r, templates.Files, "style.css") + http.ServeFileFS(w, r, basetpl.Files, "style.css") return nil }
M domain/content/publisher/templates/index.godomain/content/publisher/templates/index.go
@@ -2,9 +2,19 @@ package templates
import ( g "alin.ovh/gomponents" + . "alin.ovh/gomponents/html" base "alin.ovh/homestead/domain/web/templates" ) -func IndexPage(site base.SiteSettings, page base.PageSettings) g.Node { - return Layout(site, page, g.Text("Index Page")) +type PageSettings struct { + User string + base.PageSettings +} + +func IndexPage(site base.SiteSettings, page PageSettings) g.Node { + return Layout(site, page.PageSettings, + P( + g.Textf("Hello, %s", page.User), + ), + ) }
D domain/web/tssrv/LICENSE
@@ -1,29 +0,0 @@
-BSD 3-Clause License - -Copyright (c) 2020 Tailscale & AUTHORS. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
D domain/web/tssrv/tailscale.go
@@ -1,194 +0,0 @@
-package tssrv - -import ( - "context" - "log" - "net/http" - "net/url" - "strings" - "time" - - "tailscale.com/client/local" - "tailscale.com/tailcfg" - "tailscale.com/tsnet" - "tailscale.com/util/dnsname" -) - -type Options struct { - Development bool `conf:"-"` - AllowUnknownUsers bool `conf:"flag:allow-unknown-users"` - Hostname string - Dir string - - ReadHeaderTimeout time.Duration `conf:"default:5s"` - WriteTimeout time.Duration `conf:"default:10s"` - IdleTimeout time.Duration `conf:"default:1m"` -} - -type TailscaleServer struct { - options *Options - srv *tsnet.Server - localClient *local.Client - Server *http.Server -} - -type User struct { - Login string - IsAdmin bool -} - -type capabilities struct { - Admin bool `json:"admin"` -} - -const peerCapName = "tailscale.com/cap/golink" - -func New(options *Options) (*TailscaleServer, error) { - srv := &tsnet.Server{ - Dir: options.Dir, - Hostname: options.Hostname, - RunWebClient: true, - } - if options.Dir == "" { - options.Dir = options.Hostname - } - - if err := srv.Start(); err != nil { - return nil, err - } - - localClient, err := srv.LocalClient() - if err != nil { - return nil, err - } - - for { - upCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - status, err := srv.Up(upCtx) - if err == nil && status != nil { - break - } - } - - return &TailscaleServer{ - options: options, - srv: srv, - localClient: localClient, - }, nil -} - -func (ts *TailscaleServer) Serve(httpHandler http.Handler) error { - statusCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - status, err := ts.localClient.Status(statusCtx) - if err != nil { - return err - } - enableTLS := status.Self.HasCap(tailcfg.CapabilityHTTPS) && len(ts.srv.CertDomains()) > 0 - fqdn := strings.TrimSuffix(status.Self.DNSName, ".") - - if enableTLS { - httpsHandler := HSTS(httpHandler) - httpHandler = redirectHandler(fqdn) - - httpsListener, err := ts.srv.ListenTLS("tcp", ":443") - if err != nil { - return err - } - log.Printf("Listening on :443") - go func() { - log.Printf("Serving https://%s/ ...", fqdn) - httpsServer := &http.Server{ - Handler: httpsHandler, - ReadHeaderTimeout: ts.options.ReadHeaderTimeout, - WriteTimeout: ts.options.WriteTimeout, - IdleTimeout: ts.options.IdleTimeout, - } - if err := httpsServer.Serve(httpsListener); err != nil { - log.Fatalf("error serving https: %v", err) - } - }() - } - - httpListener, err := ts.srv.Listen("tcp", ":80") - log.Printf("Listening on :80") - if err != nil { - return err - } - log.Printf("Serving http://%s/ ...", ts.options.Hostname) - httpServer := &http.Server{ - Handler: httpHandler, - ReadHeaderTimeout: ts.options.ReadHeaderTimeout, - WriteTimeout: ts.options.WriteTimeout, - IdleTimeout: ts.options.IdleTimeout, - } - if err := httpServer.Serve(httpListener); err != nil { - return err - } - - return nil -} - -// redirectHandler returns the http.Handler for serving all plaintext HTTP -// requests. It redirects all requests to the HTTPs version of the same URL. -func redirectHandler(hostname string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - u := &url.URL{ - Scheme: "https", - Host: hostname, - Path: r.URL.Path, - RawQuery: r.URL.RawQuery, - } - http.Redirect(w, r, u.String(), http.StatusFound) - }) -} - -// HSTS wraps the provided handler and sets Strict-Transport-Security header on -// responses. It inspects the Host header to ensure we do not specify HSTS -// response on non fully qualified domain name origins. -func HSTS(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - host, found := r.Header["Host"] - if found { - host := host[0] - fqdn, err := dnsname.ToFQDN(host) - if err == nil { - segCount := fqdn.NumLabels() - if segCount > 1 { - w.Header().Set("Strict-Transport-Security", "max-age=31536000") - } - } - } - h.ServeHTTP(w, r) - }) -} - -// CurrentUser returns the Tailscale user associated with the request. -// In most cases, this will be the user that owns the device that made the request. -// For tagged devices, the value "tagged-devices" is returned. -// If the user can't be determined (such as requests coming through a subnet router), -// an error is returned unless the -allow-unknown-users flag is set. -func (ts *TailscaleServer) CurrentUser(r *http.Request) (User, error) { - if ts.options.Development { - return User{Login: "foo@example.com"}, nil - } - whois, err := ts.localClient.WhoIs(r.Context(), r.RemoteAddr) - if err != nil { - if ts.options.AllowUnknownUsers { - // Don't report the error if we are allowing unknown Users. - return User{}, nil - } - - return User{}, err - } - login := whois.UserProfile.LoginName - caps, _ := tailcfg.UnmarshalCapJSON[capabilities](whois.CapMap, peerCapName) - for _, cap := range caps { - if cap.Admin { - return User{Login: login, IsAdmin: true}, nil - } - } - - return User{Login: login}, nil -}