all repos — homestead @ f9feb2339cf736d5df28d4e37307c90595d5ab98

Code for my website

split website/mux

Alan Pearce
commit

f9feb2339cf736d5df28d4e37307c90595d5ab98

parent

88ef9f538edea96b82a71e91b1a2953ccdddc476

3 files changed, 220 insertions(+), 192 deletions(-)

jump to
M internal/http/mux.gointernal/http/mux.go
@@ -6,7 +6,10 @@
"go.alanpearce.eu/x/log" ) -type WebHandler func(http.ResponseWriter, *http.Request) *Error +type HandleFunc func(http.ResponseWriter, *http.Request) *Error +type Handler interface { + ServeHTTP(http.ResponseWriter, *http.Request) *Error +} type ErrorHandler func(*Error, http.ResponseWriter, *http.Request) type ServeMux struct {
@@ -24,7 +27,11 @@ },
} } -func (sm *ServeMux) HandleFunc(pattern string, handler WebHandler) { +func (sm *ServeMux) Handle(pattern string, handler Handler) { + sm.HandleFunc(pattern, handler.ServeHTTP) +} + +func (sm *ServeMux) HandleFunc(pattern string, handler HandleFunc) { sm.ServeMux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { defer func() { if fail := recover(); fail != nil {
M internal/website/mux.gointernal/website/mux.go
@@ -2,26 +2,15 @@ package website
import ( "encoding/json" - "fmt" "net/http" - "os" "regexp" "slices" "strings" - "gitlab.com/tozd/go/errors" - "go.alanpearce.eu/website/internal/builder" "go.alanpearce.eu/website/internal/config" 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" - "github.com/benpate/digit" "github.com/kevinpollet/nego" "github.com/osdevisnot/sorvor/pkg/livereload" )
@@ -40,181 +29,56 @@
LiveReload *livereload.LiveReload `conf:"-"` } -type Website struct { - config *config.Config - log *log.Logger - reader storage.Reader - *server.App -} - -func New( - opts *Options, - log *log.Logger, -) (*Website, error) { - website := &Website{ - log: log, - App: &server.App{ - Shutdown: func() {}, - }, - } - builderOptions := &builder.Options{ - Source: opts.Source, - Development: opts.Development, - Destination: opts.Destination, - } - - repo, exists, err := vcs.CloneOrOpen(&vcs.Options{ - LocalPath: opts.Source, - RemoteURL: opts.VCS.RemoteURL, - Branch: opts.VCS.Branch, - }, log.Named("vcs")) - if err != nil { - return nil, errors.WithMessage(err, "could not open repository") - } - builderOptions.Repo = repo - - if exists && !opts.Development { - _, err := repo.Update() - if err != nil { - return nil, errors.WithMessage(err, "could not update repository") +func (website *Website) webfinger(w http.ResponseWriter, r *http.Request) *ihttp.Error { + if r.URL.Query().Get("resource") == website.acctResource { + w.Header().Add("Content-Type", "application/jrd+json") + w.Header().Add("Access-Control-Allow-Origin", "*") + if err := json.NewEncoder(w).Encode(website.me); err != nil { + return &ihttp.Error{ + Code: http.StatusInternalServerError, + Cause: err, + } } } - 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") - } + return nil +} - mux := ihttp.NewServeMux() - mux.HandleError(func(err *ihttp.Error, w http.ResponseWriter, r *http.Request) { - if strings.Contains(r.Header.Get("Accept"), "text/html") { - w.WriteHeader(err.Code) - err := templates.Error(cfg, r.URL.Path, err).Render(r.Context(), w) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - http.Error(w, err.Message, err.Code) - } - }) - 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'") +func (website *Website) ServeHTTP(w http.ResponseWriter, r *http.Request) *ihttp.Error { + urlPath, shouldRedirect := website.reader.CanonicalisePath(r.URL.Path) + if shouldRedirect { + http.Redirect(w, r, urlPath, 302) - cfg.BaseURL = opts.BaseURL + return nil } - - website.Domain = cfg.BaseURL.Hostname() - - err = rebuild(builderOptions, cfg, log) + file, err := website.reader.GetFile(urlPath) if err != nil { - return nil, errors.WithMessage(err, "could not build site") - } - - if opts.Development { - fw, err := watcher.New(log.Named("watcher")) - if err != nil { - return nil, errors.WithMessage(err, "could not create file watcher") + return &ihttp.Error{ + Message: "Error reading file", + Code: http.StatusInternalServerError, } - err = fw.AddRecursive(opts.Source) - if err != nil { - return nil, errors.WithMessage( - err, - "could not add directory to file watcher", - ) + } + if file == nil { + return &ihttp.Error{ + Message: "File not found", + Code: http.StatusNotFound, } - - go fw.Start(func(filename string) { - log.Info("rebuilding site", "changed_file", filename) - err := rebuild(builderOptions, cfg, log) - if err != nil { - log.Error("error rebuilding site", "error", err) - } - opts.LiveReload.Reload() - }) } - - website.reader, err = files.NewReader(builderOptions.Destination, log.Named("reader")) - if err != nil { - return nil, errors.WithMessage(err, "error creating sqlite reader") + w.Header().Add("ETag", file.Etag) + w.Header().Add("Vary", "Accept-Encoding") + w.Header().Add("Content-Security-Policy", website.config.CSP.String()) + for k, v := range website.config.Extra.Headers { + w.Header().Add(k, v) } - - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) *ihttp.Error { - urlPath, shouldRedirect := website.reader.CanonicalisePath(r.URL.Path) - if shouldRedirect { - http.Redirect(w, r, urlPath, 302) - - return nil - } - file, err := website.reader.GetFile(urlPath) - if err != nil { - return &ihttp.Error{ - Message: "Error reading file", - Code: http.StatusInternalServerError, - } - } - if file == nil { - return &ihttp.Error{ - Message: "File not found", - Code: http.StatusNotFound, - } - } - w.Header().Add("ETag", file.Etag) - w.Header().Add("Vary", "Accept-Encoding") - w.Header().Add("Content-Security-Policy", cfg.CSP.String()) - for k, v := range cfg.Extra.Headers { - w.Header().Add(k, v) - } - enc := nego.NegotiateContentEncoding(r, file.AvailableEncodings()...) - switch enc { - case "br", "gzip", "zstd": - w.Header().Add("Content-Encoding", enc) - } - w.Header().Add("Content-Type", file.ContentType) - http.ServeContent(w, r, file.Path, file.LastModified, file.Encodings[enc]) + enc := nego.NegotiateContentEncoding(r, file.AvailableEncodings()...) + switch enc { + case "br", "gzip", "zstd": + w.Header().Add("Content-Encoding", enc) + } + w.Header().Add("Content-Type", file.ContentType) + http.ServeContent(w, r, file.Path, file.LastModified, file.Encodings[enc]) - return nil - }) - - var acctResource = "acct:" + cfg.Email - me := digit.NewResource(acctResource). - Link("http://openid.net/specs/connect/1.0/issuer", "", cfg.OIDCHost.String()) - - mux.HandleFunc( - "/.well-known/webfinger", - func(w http.ResponseWriter, r *http.Request) *ihttp.Error { - if r.URL.Query().Get("resource") == acctResource { - w.Header().Add("Content-Type", "application/jrd+json") - w.Header().Add("Access-Control-Allow-Origin", "*") - if err := json.NewEncoder(w).Encode(me); err != nil { - return &ihttp.Error{ - Code: http.StatusInternalServerError, - Cause: err, - } - } - } - - return nil - }, - ) - const oidcPath = "/.well-known/openid-configuration" - mux.Handle(oidcPath, ihttp.RedirectHandler(cfg.OIDCHost.JoinPath(oidcPath), 302)) - - website.config = cfg - website.App.Handler = mux - - return website, nil + return nil } func (website *Website) MakeRedirectorApp() *server.App {
@@ -249,19 +113,3 @@ Domains: website.config.Domains,
Handler: mux, } } - -func updateCSPHashes(config *config.Config, r *builder.Result) { - for i, h := range r.Hashes { - config.CSP.StyleSrc[i] = fmt.Sprintf("'%s'", h) - } -} - -func rebuild(builderConfig *builder.Options, config *config.Config, log *log.Logger) error { - r, err := builder.BuildSite(builderConfig, config, log.Named("builder")) - if err != nil { - return errors.WithMessage(err, "could not build site") - } - updateCSPHashes(config, r) - - return nil -}
A internal/website/website.go
@@ -0,0 +1,173 @@
+package website + +import ( + "fmt" + "net/http" + "os" + "slices" + "strings" + + "gitlab.com/tozd/go/errors" + "go.alanpearce.eu/website/internal/builder" + "go.alanpearce.eu/website/internal/config" + 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" + + "github.com/benpate/digit" +) + +type Website struct { + config *config.Config + log *log.Logger + reader storage.Reader + me digit.Resource + acctResource string + *server.App +} + +func New( + opts *Options, + log *log.Logger, +) (*Website, error) { + website := &Website{ + log: log, + App: &server.App{ + Shutdown: func() {}, + }, + } + builderOptions := &builder.Options{ + Source: opts.Source, + Development: opts.Development, + Destination: opts.Destination, + } + + repo, exists, err := vcs.CloneOrOpen(&vcs.Options{ + LocalPath: opts.Source, + RemoteURL: opts.VCS.RemoteURL, + Branch: opts.VCS.Branch, + }, log.Named("vcs")) + if err != nil { + return nil, errors.WithMessage(err, "could not open repository") + } + builderOptions.Repo = repo + + if exists && !opts.Development { + _, err := repo.Update() + if err != nil { + return nil, errors.WithMessage(err, "could not update repository") + } + } + + 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") + } + + mux := ihttp.NewServeMux() + mux.HandleError(func(err *ihttp.Error, w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.Header.Get("Accept"), "text/html") { + w.WriteHeader(err.Code) + err := templates.Error(cfg, r.URL.Path, err).Render(r.Context(), w) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.Error(w, err.Message, err.Code) + } + }) + 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() + + err = rebuild(builderOptions, cfg, log) + if err != nil { + return nil, errors.WithMessage(err, "could not build site") + } + + if opts.Development { + fw, err := watcher.New(log.Named("watcher")) + if err != nil { + return nil, errors.WithMessage(err, "could not create file watcher") + } + err = fw.AddRecursive(opts.Source) + if err != nil { + return nil, errors.WithMessage( + err, + "could not add directory to file watcher", + ) + } + err = fw.AddRecursive("templates") + if err != nil { + return nil, errors.WithMessage( + err, + "could not add templates directory to file watcher", + ) + } + + go fw.Start(func(filename string) { + log.Info("rebuilding site", "changed_file", filename) + err := rebuild(builderOptions, cfg, log) + if err != nil { + log.Error("error rebuilding site", "error", err) + } + opts.LiveReload.Reload() + }) + } + + website.reader, err = files.NewReader(builderOptions.Destination, log.Named("reader")) + if err != nil { + return nil, errors.WithMessage(err, "error creating sqlite reader") + } + + website.acctResource = "acct:" + cfg.Email + website.me = digit.NewResource(website.acctResource). + Link("http://openid.net/specs/connect/1.0/issuer", "", cfg.OIDCHost.String()) + + mux.Handle("/", website) + mux.HandleFunc("/.well-known/webfinger", website.webfinger) + const oidcPath = "/.well-known/openid-configuration" + mux.ServeMux.Handle(oidcPath, ihttp.RedirectHandler(cfg.OIDCHost.JoinPath(oidcPath), 302)) + + website.config = cfg + website.App.Handler = mux + + return website, nil +} + +func updateCSPHashes(config *config.Config, r *builder.Result) { + for i, h := range r.Hashes { + config.CSP.StyleSrc[i] = fmt.Sprintf("'%s'", h) + } +} + +func rebuild(builderConfig *builder.Options, config *config.Config, log *log.Logger) error { + r, err := builder.BuildSite(builderConfig, config, log.Named("builder")) + if err != nil { + return errors.WithMessage(err, "could not build site") + } + updateCSPHashes(config, r) + + return nil +}