diff options
author | Alan Pearce | 2025-01-30 22:16:09 +0100 |
---|---|---|
committer | Alan Pearce | 2025-01-30 22:16:09 +0100 |
commit | 99f8047ef20a64f948ac2b703c81eb49bed091c0 (patch) | |
tree | a0365a7b2e477467a91bef247db09624028e1807 /internal/server | |
parent | 4566db657dab6af43f8fce814cd0e42cbcc788bf (diff) | |
download | website-99f8047ef20a64f948ac2b703c81eb49bed091c0.tar.lz website-99f8047ef20a64f948ac2b703c81eb49bed091c0.tar.zst website-99f8047ef20a64f948ac2b703c81eb49bed091c0.zip |
re-organise everything sqlite
Diffstat (limited to 'internal/server')
-rw-r--r-- | internal/server/app.go | 10 | ||||
-rw-r--r-- | internal/server/dev.go | 109 | ||||
-rw-r--r-- | internal/server/logging.go | 3 | ||||
-rw-r--r-- | internal/server/server.go | 217 | ||||
-rw-r--r-- | internal/server/tcp.go | 4 | ||||
-rw-r--r-- | internal/server/tls.go | 52 |
6 files changed, 86 insertions, 309 deletions
diff --git a/internal/server/app.go b/internal/server/app.go new file mode 100644 index 0000000..d1a7dbc --- /dev/null +++ b/internal/server/app.go @@ -0,0 +1,10 @@ +package server + +import ( + "net/http" +) + +type App struct { + Domain string + Handler http.Handler +} diff --git a/internal/server/dev.go b/internal/server/dev.go deleted file mode 100644 index 6fcc93e..0000000 --- a/internal/server/dev.go +++ /dev/null @@ -1,109 +0,0 @@ -package server - -import ( - "fmt" - "io/fs" - "os" - "path" - "path/filepath" - "slices" - "time" - - "go.alanpearce.eu/x/log" - - "github.com/fsnotify/fsnotify" - "gitlab.com/tozd/go/errors" -) - -type FileWatcher struct { - *fsnotify.Watcher -} - -var ( - l *log.Logger - ignores = []string{ - "*.templ", - "*.go", - } - checkSettleInterval = 200 * time.Millisecond -) - -func matches(name string) func(string) bool { - return func(pattern string) bool { - matched, err := path.Match(pattern, name) - if err != nil { - l.Warn("error checking watcher ignores", "error", err) - } - - return matched - } -} - -func ignored(pathname string) bool { - return slices.ContainsFunc(ignores, matches(path.Base(pathname))) -} - -func NewFileWatcher(log *log.Logger) (*FileWatcher, error) { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return nil, errors.WithMessage(err, "could not create watcher") - } - l = log - - return &FileWatcher{watcher}, nil -} - -func (watcher FileWatcher) AddRecursive(from string) error { - l.Debug("walking directory tree", "root", from) - err := filepath.WalkDir(from, func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return errors.WithMessagef(err, "could not walk directory %s", path) - } - if entry.IsDir() { - l.Debug("adding directory to watcher", "path", path) - if err = watcher.Add(path); err != nil { - return errors.WithMessagef(err, "could not add directory %s to watcher", path) - } - } - - return nil - }) - - return errors.WithMessage(err, "error walking directory tree") -} - -func (watcher FileWatcher) Start(callback func(string)) { - var timer *time.Timer - for { - select { - case event := <-watcher.Events: - if !ignored(event.Name) { - l.Debug("watcher event", "name", event.Name, "op", event.Op.String()) - if event.Has(fsnotify.Create) || event.Has(fsnotify.Rename) { - f, err := os.Stat(event.Name) - if err != nil { - l.Error( - fmt.Sprintf("error handling %s event: %v", event.Op.String(), err), - ) - } else if f.IsDir() { - err = watcher.Add(event.Name) - if err != nil { - l.Error(fmt.Sprintf("error adding new folder to watcher: %v", err)) - } - } - } - if event.Has(fsnotify.Rename) || event.Has(fsnotify.Write) || - event.Has(fsnotify.Create) || event.Has(fsnotify.Chmod) { - if timer == nil { - timer = time.AfterFunc(checkSettleInterval, func() { - callback(event.Name) - }) - } - timer.Reset(checkSettleInterval) - } - } - case err := <-watcher.Errors: - l.Error("error in watcher", "error", err) - } - } -} diff --git a/internal/server/logging.go b/internal/server/logging.go index f744931..800e97a 100644 --- a/internal/server/logging.go +++ b/internal/server/logging.go @@ -27,9 +27,6 @@ func wrapHandlerWithLogging(wrappedHandler http.Handler, log *log.Logger) http.H return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lw := NewLoggingResponseWriter(w) wrappedHandler.ServeHTTP(lw, r) - if r.URL.Path == "/health" { - return - } log.Info( "http request", "method", r.Method, diff --git a/internal/server/server.go b/internal/server/server.go index 0f5e22f..b3161fb 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,22 +3,12 @@ package server import ( "context" "fmt" - "net" "net/http" - "net/url" - "regexp" - "slices" - "strconv" - "strings" "time" - "go.alanpearce.eu/website/internal/builder" cfg "go.alanpearce.eu/website/internal/config" - "go.alanpearce.eu/website/internal/storage/sqlite" - "go.alanpearce.eu/website/internal/website" "go.alanpearce.eu/x/log" - "github.com/osdevisnot/sorvor/pkg/livereload" "gitlab.com/tozd/go/errors" ) @@ -26,55 +16,31 @@ var ( CommitSHA = "local" ShortSHA = "local" serverHeader = fmt.Sprintf("website (%s)", ShortSHA) + + ReadHeaderTimeout = 10 * time.Second + ReadTimeout = 1 * time.Minute + WriteTimeout = 2 * time.Minute + IdleTimeout = 10 * time.Minute ) -type Config struct { - DBPath string `conf:"default:site.db"` - Redirect bool `conf:"default:true"` - ListenAddress string `conf:"default:localhost"` - Port int `conf:"default:8080,short:p"` - TLSPort int `conf:"default:8443"` - TLS bool `conf:"default:false"` - - Development bool `conf:"default:false,flag:dev"` - ACMECA string `conf:"env:ACME_CA"` - ACMECACert string `conf:"env:ACME_CA_CERT"` - Domains string -} +type Options struct { + Development bool + ListenAddress string + Port int + TLSPort int + TLS bool -type Server struct { - *http.Server - runtimeConfig *Config - config *cfg.Config - log *log.Logger -} + ACMEIssuer string + ACMEIssuerCert string -func applyDevModeOverrides(config *cfg.Config, runtimeConfig *Config) { - config.CSP.ScriptSrc = slices.Insert(config.CSP.ScriptSrc, 0, "'unsafe-inline'") - config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'") - if runtimeConfig.Domains != "" { - config.Domains = strings.Split(runtimeConfig.Domains, ",") - } else { - config.Domains = []string{runtimeConfig.ListenAddress} - } - scheme := "http" - port := runtimeConfig.Port - if runtimeConfig.TLS { - scheme = "https" - port = runtimeConfig.TLSPort - } - config.BaseURL = cfg.URL{ - URL: &url.URL{ - Scheme: scheme, - Host: net.JoinHostPort(config.Domains[0], strconv.Itoa(port)), - }, - } + Config *cfg.Config } -func updateCSPHashes(config *cfg.Config, r *builder.Result) { - for i, h := range r.Hashes { - config.CSP.StyleSrc[i] = fmt.Sprintf("'%s'", h) - } +type Server struct { + mux *http.ServeMux + options *Options + log *log.Logger + server *http.Server } func serverHeaderHandler(wrappedHandler http.Handler) http.Handler { @@ -84,109 +50,37 @@ func serverHeaderHandler(wrappedHandler http.Handler) http.Handler { }) } -func rebuild(builderConfig *builder.IOConfig, config *cfg.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 -} - -func New(runtimeConfig *Config, log *log.Logger) (*Server, error) { - builderConfig := &builder.IOConfig{ - Development: runtimeConfig.Development, - } - - config, err := cfg.GetConfig(builderConfig.Source, log.Named("config")) - if err != nil { - return nil, errors.WithMessage(err, "error parsing configuration file") - } - if runtimeConfig.Development { - applyDevModeOverrides(config, runtimeConfig) - } - - top := http.NewServeMux() - - err = rebuild(builderConfig, config, log) - if err != nil { - return nil, err - } - +func New(options *Options, log *log.Logger) (*Server, error) { fixupMIMETypes(log) - if runtimeConfig.Development { - liveReload := livereload.New() - top.Handle("/_/reload", liveReload) - liveReload.Start() - fw, err := NewFileWatcher(log.Named("watcher")) - if err != nil { - return nil, errors.WithMessage(err, "could not create file watcher") - } - for _, dir := range []string{"content", "static", "templates", "internal/builder"} { - err := fw.AddRecursive(dir) - if err != nil { - return nil, errors.WithMessagef( - err, - "could not add directory %s to file watcher", - dir, - ) - } - } - go fw.Start(func(filename string) { - log.Info("rebuilding site", "changed_file", filename) - err := rebuild(builderConfig, config, log) - if err != nil { - log.Error("error rebuilding site", "error", err) - } - }) - } + return &Server{ + mux: http.NewServeMux(), + log: log, + options: options, + }, nil +} - loggingMux := http.NewServeMux() +func (s *Server) HostApp(app *App) { + s.mux.Handle(app.Domain+"/", app.Handler) +} - log.Debug("creating reader") - reader, err := sqlite.NewReader(runtimeConfig.DBPath, log.Named("sqlite")) - if err != nil { - return nil, errors.WithMessage(err, "could not create sqlite reader") - } +func (s *Server) HostFallbackApp(app *App) { + s.mux.Handle("/", app.Handler) +} - mux, err := website.NewMux(config, reader, log.Named("website")) - if err != nil { - return nil, errors.WithMessage(err, "could not create website mux") +func (s *Server) serve(tls bool) error { + if tls { + return s.serveTLS() } - if runtimeConfig.Redirect { - re := regexp.MustCompile( - "^(.*)\\." + strings.ReplaceAll(config.WildcardDomain, ".", `\.`) + "$", - ) - replace := "${1}." + config.Domains[0] - loggingMux.Handle(config.BaseURL.Hostname()+"/", mux) - loggingMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - switch { - case slices.Contains(config.Domains, r.Host): - path, _ := reader.CanonicalisePath(r.URL.Path) - http.Redirect( - w, - r, - config.BaseURL.JoinPath(path).String(), - http.StatusMovedPermanently, - ) - case re.MatchString(r.Host): - url := config.BaseURL.JoinPath() - url.Host = re.ReplaceAllString(r.Host, replace) - http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect) - case true: - http.NotFound(w, r) - } - }) - } else { - loggingMux.Handle("/", mux) - } + return s.serveTCP() +} +func (s *Server) Start() error { + top := http.NewServeMux() top.Handle("/", serverHeaderHandler( - wrapHandlerWithLogging(loggingMux, log), + wrapHandlerWithLogging(s.mux, s.log), ), ) @@ -194,30 +88,15 @@ func New(runtimeConfig *Config, log *log.Logger) (*Server, error) { w.WriteHeader(http.StatusNoContent) }) - return &Server{ - Server: &http.Server{ - ReadHeaderTimeout: 10 * time.Second, - ReadTimeout: 1 * time.Minute, - WriteTimeout: 2 * time.Minute, - IdleTimeout: 10 * time.Minute, - Handler: top, - }, - log: log, - config: config, - runtimeConfig: runtimeConfig, - }, nil -} - -func (s *Server) serve(tls bool) error { - if tls { - return s.serveTLS() + s.server = &http.Server{ + ReadHeaderTimeout: ReadHeaderTimeout, + ReadTimeout: ReadTimeout, + WriteTimeout: WriteTimeout, + IdleTimeout: IdleTimeout, + Handler: s.mux, } - return s.serveTCP() -} - -func (s *Server) Start() error { - if err := s.serve(s.runtimeConfig.TLS); err != http.ErrServerClosed { + if err := s.serve(s.options.TLS); err != http.ErrServerClosed { return errors.WithMessage(err, "error creating/closing server") } @@ -233,7 +112,7 @@ func (s *Server) Stop() chan struct{} { s.log.Debug("shutting down server") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - err := s.Server.Shutdown(ctx) + err := s.server.Shutdown(ctx) s.log.Debug("server shut down") if err != nil { // Error from closing listeners, or context timeout: diff --git a/internal/server/tcp.go b/internal/server/tcp.go index 1d57c9c..14eb9e6 100644 --- a/internal/server/tcp.go +++ b/internal/server/tcp.go @@ -9,12 +9,12 @@ import ( func (s *Server) serveTCP() error { l, err := listenfd.GetListener(0, - net.JoinHostPort(s.runtimeConfig.ListenAddress, strconv.Itoa(s.runtimeConfig.Port)), + net.JoinHostPort(s.options.ListenAddress, strconv.Itoa(s.options.Port)), s.log.Named("tcp.listenfd"), ) if err != nil { return err } - return s.Serve(l) + return s.server.Serve(l) } diff --git a/internal/server/tls.go b/internal/server/tls.go index 40fddac..5e2819f 100644 --- a/internal/server/tls.go +++ b/internal/server/tls.go @@ -24,31 +24,31 @@ type redisConfig struct { Password string `conf:"required"` EncryptionKey string `conf:"required"` KeyPrefix string `conf:"default:certmagic"` - TLSEnabled bool `conf:"default:false,env:TLS_ENABLED"` - TLSInsecure bool `conf:"default:false,env:TLS_INSECURE"` + TLSEnabled bool `conf:"default:false"` + TLSInsecure bool `conf:"default:false"` } func (s *Server) serveTLS() (err error) { log := s.log.Named("tls") - wildcardDomain := "*." + s.config.WildcardDomain - certificateDomains := slices.Clone(s.config.Domains) + wildcardDomain := "*." + s.options.Config.WildcardDomain + certificateDomains := slices.Clone(s.options.Config.Domains) - certmagic.HTTPPort = s.runtimeConfig.Port - certmagic.HTTPSPort = s.runtimeConfig.TLSPort + certmagic.HTTPPort = s.options.Port + certmagic.HTTPSPort = s.options.TLSPort certmagic.Default.Logger = log.GetLogger().Named("certmagic") cfg := certmagic.NewDefault() acme := &certmagic.DefaultACME acme.Logger = certmagic.Default.Logger acme.Agreed = true - acme.Email = s.config.Email - acme.ListenHost = strings.Trim(s.runtimeConfig.ListenAddress, "[]") + acme.Email = s.options.Config.Email + acme.ListenHost = strings.Trim(s.options.ListenAddress, "[]") - if s.runtimeConfig.Development { - ca := s.runtimeConfig.ACMECA + if s.options.Development { + ca := s.options.ACMEIssuer if ca == "" { - return errors.New("can't enable tls in development without an ACME_CA") + return errors.New("can't enable tls in development without an ACME_ISSUER") } cp, err := x509.SystemCertPool() @@ -57,14 +57,14 @@ func (s *Server) serveTLS() (err error) { cp = x509.NewCertPool() } - if cacert := s.runtimeConfig.ACMECACert; cacert != "" { + if cacert := s.options.ACMEIssuerCert; cacert != "" { cp.AppendCertsFromPEM([]byte(cacert)) } // caddy's ACME server (step-ca) doesn't specify an OCSP server cfg.OCSP.DisableStapling = true - acme.CA = s.runtimeConfig.ACMECA + acme.CA = s.options.ACMEIssuer acme.TrustedRoots = cp acme.DisableTLSALPNChallenge = true } else { @@ -87,7 +87,7 @@ func (s *Server) serveTLS() (err error) { }, } - certificateDomains = append(slices.Clone(s.config.Domains), wildcardDomain) + certificateDomains = append(slices.Clone(s.options.Config.Domains), wildcardDomain) rs := certmagic_redis.New() rs.Address = []string{rc.Address} @@ -107,7 +107,7 @@ func (s *Server) serveTLS() (err error) { ln, err := listenfd.GetListener( 1, - net.JoinHostPort(s.runtimeConfig.ListenAddress, strconv.Itoa(s.runtimeConfig.Port)), + net.JoinHostPort(s.options.ListenAddress, strconv.Itoa(s.options.Port)), log.Named("listenfd"), ) if err != nil { @@ -123,7 +123,7 @@ func (s *Server) serveTLS() (err error) { } url := r.URL url.Scheme = "https" - port := s.config.BaseURL.Port() + port := s.options.Config.BaseURL.Port() if port == "" { url.Host = r.Host } else { @@ -132,9 +132,9 @@ func (s *Server) serveTLS() (err error) { log.Warn("error splitting host and port", "error", err) host = r.Host } - url.Host = net.JoinHostPort(host, s.config.BaseURL.Port()) + url.Host = net.JoinHostPort(host, s.options.Config.BaseURL.Port()) } - if slices.Contains(s.config.Domains, r.Host) { + if slices.Contains(s.options.Config.Domains, r.Host) { http.Redirect(w, r, url.String(), http.StatusMovedPermanently) } else { http.NotFound(w, r) @@ -146,18 +146,18 @@ func (s *Server) serveTLS() (err error) { log.Error("error in http handler", "error", err) } }(ln, &http.Server{ - ReadHeaderTimeout: s.ReadHeaderTimeout, - ReadTimeout: s.ReadTimeout, - WriteTimeout: s.WriteTimeout, - IdleTimeout: s.IdleTimeout, + ReadHeaderTimeout: ReadHeaderTimeout, + ReadTimeout: ReadTimeout, + WriteTimeout: WriteTimeout, + IdleTimeout: IdleTimeout, }) log.Debug( "starting certmagic", "http_port", - s.runtimeConfig.Port, + s.options.Port, "https_port", - s.runtimeConfig.TLSPort, + s.options.TLSPort, ) cfg.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(cfg, *acme)} err = cfg.ManageAsync(context.TODO(), certificateDomains) @@ -169,7 +169,7 @@ func (s *Server) serveTLS() (err error) { sln, err := listenfd.GetListenerTLS( 0, - net.JoinHostPort(s.runtimeConfig.ListenAddress, strconv.Itoa(s.runtimeConfig.TLSPort)), + net.JoinHostPort(s.options.ListenAddress, strconv.Itoa(s.options.TLSPort)), tlsConfig, log.Named("listenfd"), ) @@ -177,5 +177,5 @@ func (s *Server) serveTLS() (err error) { return errors.WithMessage(err, "could not bind tls socket") } - return s.Serve(sln) + return s.server.Serve(sln) } |