split website/mux
1 file changed, 38 insertions(+), 190 deletions(-)
changed files
M internal/website/mux.go → internal/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 -}