package website import ( "os" "path/filepath" "slices" "sync" "gitlab.com/tozd/go/errors" "go.alanpearce.eu/homestead/internal/config" "go.alanpearce.eu/homestead/internal/events" "go.alanpearce.eu/homestead/internal/fetcher" "go.alanpearce.eu/homestead/internal/file" ihttp "go.alanpearce.eu/homestead/internal/http" "go.alanpearce.eu/homestead/internal/server" "go.alanpearce.eu/homestead/internal/stats" "go.alanpearce.eu/homestead/internal/stats/goatcounter" "go.alanpearce.eu/homestead/internal/stats/nullcounter" "go.alanpearce.eu/homestead/internal/storage" "go.alanpearce.eu/homestead/internal/storage/sqlite" "go.alanpearce.eu/x/log" "github.com/benpate/digit" "github.com/osdevisnot/sorvor/pkg/livereload" ) type Options struct { DataRoot string `conf:"noprint"` Root string `conf:"default:./website"` Redirect bool `conf:"default:true"` Development bool `conf:"default:false,flag:dev"` FetchURL config.URL `conf:"default:https://ci.alanpearce.eu/archive/website/"` BaseURL config.URL GoatcounterToken string Redis *events.RedisOptions LiveReload *livereload.LiveReload `conf:"-"` } type Website struct { config *config.Config counter stats.Counter 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() {}, }, } err := prepareRootDirectory(opts.DataRoot, opts.Root) if err != nil { return nil, errors.WithMessage(err, "could not prepare root directory") } var listener events.Listener if opts.Redis.Enabled { log.Debug("using redis listener") listener, err = events.NewRedisListener(opts.Redis, log.Named("redis")) } else { log.Debug("using file watcher") listener, err = events.NewFileWatcher(log.Named("events"), "website") } if err != nil { return nil, errors.WithMessage(err, "could not create update listener") } var cfg *config.Config fetcher := fetcher.New(log.Named("fetcher"), &fetcher.Options{ FetchURL: opts.FetchURL, RedisEnabled: opts.Redis.Enabled, Root: opts.Root, Listener: listener, }) roots, err := fetcher.Subscribe() if err != nil { return nil, errors.WithMessage(err, "could not set up fetcher") } firstUpdate := make(chan bool) go func() { updated := sync.OnceFunc(func() { firstUpdate <- true close(firstUpdate) }) for root := range roots { log.Debug("getting config from source", "source", root) cfg, err = config.GetConfig(root, log) if err != nil { log.Panic("could not load configuration", "error", err) } website.config = cfg if opts.Development { cfg.CSP.ScriptSrc = slices.Insert(cfg.CSP.ScriptSrc, 0, "'unsafe-inline'") cfg.CSP.ConnectSrc = slices.Insert(cfg.CSP.ConnectSrc, 0, "'self'") } if opts.GoatcounterToken == "" { if !opts.Development { log.Warn("in production without a goatcounter token") } website.counter = nullcounter.New(&nullcounter.Options{ Logger: log.Named("counter"), }) } else { website.counter = goatcounter.New(&goatcounter.Options{ Logger: log.Named("counter"), URL: &cfg.GoatCounter, Token: opts.GoatcounterToken, }) } if opts.BaseURL.URL != nil && opts.BaseURL.Hostname() != "" { cfg.BaseURL = opts.BaseURL } website.Domain = cfg.BaseURL.Hostname() siteDB := filepath.Join(root, "site.db") db, err := sqlite.OpenDB(siteDB) if err != nil { log.Panic("could not open database", "error", err) } website.reader, err = sqlite.NewReader(db, log.Named("reader")) if err != nil { log.Panic("could not create database reader", "error", err) } updated() } }() <-firstUpdate website.acctResource = "acct:" + cfg.Email website.me = digit.NewResource(website.acctResource). Link("http://openid.net/specs/connect/1.0/issuer", "", cfg.OIDCHost.String()) mux := ihttp.NewServeMux() mux.HandleError(website.ErrorHandler) 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.App.Handler = mux return website, nil } func prepareRootDirectory(dataRoot, root string) error { if !file.Exists(root) { err := os.MkdirAll(root, 0755) if err != nil { return errors.WithMessage(err, "could not create root directory") } } else if dataRoot != "" && file.Exists(filepath.Join(dataRoot, "public")) { // must be pre-sqlite if this exists return file.CleanDir(dataRoot) } return nil }