all repos — homestead @ de8e02d2a10ce0a01817729e47f31e7b37103af2

Code for my website

split options of website/server

Alan Pearce
commit

de8e02d2a10ce0a01817729e47f31e7b37103af2

parent

5434a11ad1bf6693705a9ba38ec9464f2d001da9

M cmd/server/main.gocmd/server/main.go
@@ -3,15 +3,9 @@
import ( "context" "fmt" - "net" - "net/url" "os" "os/signal" - "slices" - "strconv" - "strings" - "go.alanpearce.eu/website/internal/config" "go.alanpearce.eu/website/internal/server" "go.alanpearce.eu/website/internal/website" "go.alanpearce.eu/x/log"
@@ -21,17 +15,8 @@ "gitlab.com/tozd/go/errors"
) type Options struct { - Development bool `conf:"default:false,flag:dev"` - Source string `conf:"default:../website"` - Destination string `conf:"default:public"` - Redirect bool `conf:"default:true"` - Port int `conf:"default:8080,short:p"` - TLS bool `conf:"default:false"` - TLSPort int `conf:"default:8443"` - ListenAddress string `conf:"default:localhost"` - Domains string `conf:"default:localhost"` - ACMEIssuer string - ACMEIssuerCert string + Website *website.Options + Server *server.Options } func main() {
@@ -44,60 +29,16 @@ os.Exit(1)
} panic("parsing runtime configuration" + err.Error()) } - log := log.Configure(!options.Development) + log := log.Configure(!options.Website.Development) - cfg, err := config.GetConfig(options.Source, log) - if err != nil { - log.Fatal("could not read config", "error", err) - } - - // Domains? - webOpts := &website.Options{ - Source: options.Source, - Destination: options.Destination, - Redirect: options.Redirect, - Development: options.Development, - Config: cfg, - } - - if options.Development { - tmpdir, err := os.MkdirTemp("", "website") - if err != nil { - log.Fatal("could not create temporary directory", "error", err) - } - log.Info("using temporary directory", "dir", tmpdir) - defer os.RemoveAll(tmpdir) - webOpts.Destination = tmpdir - - cfg.CSP.ScriptSrc = slices.Insert(cfg.CSP.ScriptSrc, 0, "'unsafe-inline'") - cfg.CSP.ConnectSrc = slices.Insert(cfg.CSP.ConnectSrc, 0, "'self'") - if options.Domains != "" { - cfg.Domains = strings.Split(options.Domains, ",") - } else { - cfg.Domains = []string{options.ListenAddress} - } - cfg.BaseURL = mkBaseURL(options, cfg) - } - - serverOpts := &server.Options{ - Development: options.Development, - ListenAddress: options.ListenAddress, - Port: options.Port, - TLS: options.TLS, - TLSPort: options.TLSPort, - ACMEIssuer: options.ACMEIssuer, - ACMEIssuerCert: options.ACMEIssuerCert, - Config: cfg, - } - - sv, err := server.New(serverOpts, log.Named("server")) + sv, err := server.New(options.Server, log.Named("server")) if err != nil { log.Error("could not create server", "error", err) return } - website, err := website.New(webOpts, log.Named("website")) + website, err := website.New(options.Website, log.Named("website")) if err != nil { log.Error("could not initialise website", "error", err)
@@ -105,7 +46,7 @@ return
} sv.HostApp(website.App) - if options.Redirect { + if options.Website.Redirect { sv.HostFallbackApp(website.MakeRedirectorApp()) }
@@ -122,21 +63,7 @@ }()
<-ctx.Done() log.Debug("calling stop") + website.App.Shutdown() <-sv.Stop() log.Debug("done") } - -func mkBaseURL(options *Options, cfg *config.Config) config.URL { - scheme := "http" - port := options.Port - if options.TLS { - scheme = "https" - port = options.TLSPort - } - return config.URL{ - URL: &url.URL{ - Scheme: scheme, - Host: net.JoinHostPort(cfg.Domains[0], strconv.Itoa(port)), - }, - } -}
M internal/config/config.gointernal/config/config.go
@@ -46,7 +46,11 @@ WildcardDomain string `toml:"wildcard_domain"`
OIDCHost URL `toml:"oidc_host"` Taxonomies []Taxonomy CSP *CSP `toml:"content-security-policy"` - Extra struct { + VCS struct { + Branch string + RemoteURL URL `toml:"remote_url"` + } + Extra struct { Headers map[string]string } Menus map[string][]MenuItem
M internal/server/app.gointernal/server/app.go
@@ -5,6 +5,7 @@ "net/http"
) type App struct { - Domain string - Handler http.Handler + Domain string + Handler http.Handler + Shutdown func() }
M internal/server/server.gointernal/server/server.go
@@ -6,7 +6,6 @@ "fmt"
"net/http" "time" - cfg "go.alanpearce.eu/website/internal/config" "go.alanpearce.eu/x/log" "gitlab.com/tozd/go/errors"
@@ -24,16 +23,16 @@ IdleTimeout = 10 * time.Minute
) type Options struct { - Development bool - ListenAddress string - Port int - TLSPort int - TLS bool + ListenAddress string `conf:"default:localhost"` + Port int `conf:"default:8080,short:p"` + TLS bool `conf:"default:false"` + TLSPort int `conf:"default:8443"` ACMEIssuer string ACMEIssuerCert string - Config *cfg.Config + Domains []string `conf:"-"` + WildcardDomains []string `conf:"-"` } type Server struct {
@@ -61,6 +60,7 @@ }, nil
} func (s *Server) HostApp(app *App) { + s.options.Domains = append(s.options.Domains, app.Domain) s.mux.Handle(app.Domain+"/", app.Handler) }
M internal/server/tls.gointernal/server/tls.go
@@ -31,8 +31,8 @@
func (s *Server) serveTLS() (err error) { log := s.log.Named("tls") - wildcardDomain := "*." + s.options.Config.WildcardDomain - certificateDomains := slices.Clone(s.options.Config.Domains) + wildcardDomains := slices.Clone(s.options.WildcardDomains) + certificateDomains := slices.Clone(s.options.Domains) certmagic.HTTPPort = s.options.Port certmagic.HTTPSPort = s.options.TLSPort
@@ -42,15 +42,9 @@
acme := &certmagic.DefaultACME acme.Logger = certmagic.Default.Logger acme.Agreed = true - acme.Email = s.options.Config.Email acme.ListenHost = strings.Trim(s.options.ListenAddress, "[]") - if s.options.Development { - ca := s.options.ACMEIssuer - if ca == "" { - return errors.New("can't enable tls in development without an ACME_ISSUER") - } - + if s.options.ACMEIssuer != "" { cp, err := x509.SystemCertPool() if err != nil { log.Warn("could not get system certificate pool", "error", err)
@@ -87,7 +81,9 @@ Logger: cfg.Logger,
}, } - certificateDomains = append(slices.Clone(s.options.Config.Domains), wildcardDomain) + if len(wildcardDomains) > 0 { + certificateDomains = append(certificateDomains, wildcardDomains...) + } rs := certmagic_redis.New() rs.Address = []string{rc.Address}
@@ -121,20 +117,10 @@ if certmagic.LooksLikeHTTPChallenge(r) &&
acme.HandleHTTPChallenge(w, r) { return } - url := r.URL - url.Scheme = "https" - port := s.options.Config.BaseURL.Port() - if port == "" { + if slices.Contains(s.options.Domains, r.Host) { + url := r.URL + url.Scheme = "https" url.Host = r.Host - } else { - host, _, err := net.SplitHostPort(r.Host) - if err != nil { - log.Warn("error splitting host and port", "error", err) - host = r.Host - } - url.Host = net.JoinHostPort(host, s.options.Config.BaseURL.Port()) - } - if slices.Contains(s.options.Config.Domains, r.Host) { http.Redirect(w, r, url.String(), http.StatusMovedPermanently) } else { http.NotFound(w, r)
M internal/vcs/repository.gointernal/vcs/repository.go
@@ -1,6 +1,7 @@
package vcs import ( + "io/fs" "os" "go.alanpearce.eu/website/internal/config"
@@ -11,10 +12,10 @@ "github.com/go-git/go-git/v5/plumbing"
"gitlab.com/tozd/go/errors" ) -type Config struct { +type Options struct { LocalPath string RemoteURL config.URL - Branch string `conf:"default:main"` + Branch string } type Repository struct {
@@ -22,35 +23,24 @@ repo *git.Repository
log *log.Logger } -func CloneOrUpdate(cfg *Config, log *log.Logger) (*Repository, error) { - gr, err := git.PlainClone(cfg.LocalPath, false, &git.CloneOptions{ - URL: cfg.RemoteURL.String(), - Progress: os.Stdout, - }) - if err != nil { - if !errors.Is(err, git.ErrRepositoryAlreadyExists) { - return nil, err - } - gr, err = git.PlainOpen(cfg.LocalPath) - if err != nil { - return nil, err - } - repo := &Repository{ - repo: gr, - log: log, - } - _, err := repo.Update() - if err != nil { - return nil, err - } - - return repo, nil +func CloneOrOpen(cfg *Options, log *log.Logger) (repo *Repository, err error) { + stat, err := os.Stat(cfg.LocalPath) + if err != nil && errors.Is(err, fs.ErrNotExist) { + return nil, err + } + repo = &Repository{ + log: log, + } + if stat == nil { + repo.repo, err = git.PlainClone(cfg.LocalPath, false, &git.CloneOptions{ + URL: cfg.RemoteURL.String(), + Progress: os.Stdout, + }) + } else { + repo.repo, err = git.PlainOpen(cfg.LocalPath) } - return &Repository{ - repo: gr, - log: log, - }, nil + return } func (r *Repository) Update() (bool, error) {
@@ -90,6 +80,7 @@ var hash plumbing.Hash
for _, ref := range refs { if ref.Name() == plumbing.Main { hash = ref.Hash() + break } }
M internal/website/mux.gointernal/website/mux.go
@@ -4,6 +4,7 @@ import (
"encoding/json" "fmt" "net/http" + "os" "regexp" "slices" "strings"
@@ -15,6 +16,7 @@ ihttp "go.alanpearce.eu/website/internal/http"
"go.alanpearce.eu/website/internal/server" "go.alanpearce.eu/website/internal/storage" "go.alanpearce.eu/website/internal/storage/files" + "go.alanpearce.eu/website/internal/vcs" "go.alanpearce.eu/website/internal/watcher" "go.alanpearce.eu/website/templates" "go.alanpearce.eu/x/log"
@@ -25,11 +27,11 @@ "github.com/osdevisnot/sorvor/pkg/livereload"
) type Options struct { - Source string - Destination string - Redirect bool - Development bool - Config *config.Config + Source string `conf:"default:../website"` + Destination string `conf:"default:public"` + Redirect bool `conf:"default:true"` + Development bool `conf:"default:false,flag:dev"` + BaseURL config.URL `conf:"default:localhost"` } type Website struct {
@@ -79,9 +81,12 @@ func New(
opts *Options, log *log.Logger, ) (*Website, error) { + mux := http.NewServeMux() website := &Website{ - config: opts.Config, - log: log, + log: log, + App: &server.App{ + Handler: mux, + }, } builderOptions := &builder.Options{ Source: opts.Source,
@@ -89,12 +94,46 @@ Development: opts.Development,
Destination: opts.Destination, } - mux := &http.ServeMux{} templates.Setup() - cfg := opts.Config + log.Debug("getting config from source", "source", opts.Source) + cfg, err := config.GetConfig(opts.Source, log) + if err != nil { + return nil, errors.WithMessage(err, "could not load configuration") + } - err := rebuild(builderOptions, cfg, log) + if opts.Development { + tmpdir, err := os.MkdirTemp("", "website") + if err != nil { + log.Fatal("could not create temporary directory", "error", err) + } + log.Info("using temporary directory", "dir", tmpdir) + website.App.Shutdown = func() { + os.RemoveAll(tmpdir) + } + builderOptions.Destination = tmpdir + + cfg.CSP.ScriptSrc = slices.Insert(cfg.CSP.ScriptSrc, 0, "'unsafe-inline'") + cfg.CSP.ConnectSrc = slices.Insert(cfg.CSP.ConnectSrc, 0, "'self'") + + cfg.BaseURL = opts.BaseURL + } + + website.Domain = cfg.BaseURL.Hostname() + + repo, err := vcs.CloneOrOpen(&vcs.Options{ + LocalPath: opts.Source, + RemoteURL: cfg.VCS.RemoteURL, + Branch: cfg.VCS.Branch, + }, log.Named("vcs")) + if err != nil { + return nil, errors.WithMessage(err, "could not update repository") + } + + _ = repo + // config may have changed, reload + + err = rebuild(builderOptions, cfg, log) if err != nil { return nil, errors.WithMessage(err, "could not build site") }
@@ -199,10 +238,7 @@ u := cfg.OIDCHost.JoinPath(oidcPath)
http.Redirect(w, r, u.String(), 302) }) - website.App = &server.App{ - Domain: cfg.Domains[0], - Handler: mux, - } + website.config = cfg return website, nil }
M justfilejustfile
@@ -3,9 +3,9 @@ #!nix develop ``.#ci`` --command just --justfile
docker_registry := "registry.fly.io/alanpearce-eu" docker-tag := env_var_or_default("DOCKER_TAG", `date -u +%Y%m%d%H%M%S` + "-" + `git rev-parse --short HEAD`) -listen_address := env_var_or_default("LISTEN_ADDRESS", "::1") -tls_port := env_var_or_default("TLS_PORT", "8443") -port := env_var_or_default("PORT", "8080") +listen_address := env_var_or_default("SERVER_LISTEN_ADDRESS", "::1") +tls_port := env_var_or_default("SERVER_TLS_PORT", "8443") +port := env_var_or_default("SERVER_PORT", "8080") default: @just --list --justfile {{ justfile() }} --unsorted