package website import ( "fmt" "net/http" "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/sqlite" "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, } 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 { opts.DBPath = ":memory:" 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 } builderOptions.DB, err = sqlite.OpenDB(opts.DBPath) if err != nil { return nil, errors.WithMessage(err, "could not open database") } 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) builderOptions.DB.Close() builderOptions.DB, err = sqlite.OpenDB(opts.DBPath) if err != nil { log.Error("error opening database", "error", err) } website.reader, err = sqlite.NewReader(builderOptions.DB, log.Named("reader")) if err != nil { log.Error("error creating sqlite reader", "error", err) } err := rebuild(builderOptions, cfg, log) if err != nil { log.Error("error rebuilding site", "error", err) } opts.LiveReload.Reload() }) } website.reader, err = sqlite.NewReader(builderOptions.DB, 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 }