From 73603079e29bc89c54296a9e12b5a779cd84c023 Mon Sep 17 00:00:00 2001
From: Alan Pearce
Date: Thu, 2 May 2024 23:18:19 +0200
Subject: feat: serve a very basic html template
---
.gitignore | 5 ++
config.toml | 23 ++++++
frontend/templates/index.tmpl | 11 +++
go.mod | 16 ++++
go.sum | 46 +++++++++++
gomod2nix.toml | 30 ++++++++
internal/config/config.go | 51 ++++++++++++
internal/config/csp.go | 45 +++++++++++
internal/server/headers.go | 17 ++++
internal/server/logging.go | 55 +++++++++++++
internal/server/server.go | 175 ++++++++++++++++++++++++++++++++++++++++++
justfile | 3 +
serve/main.go | 66 ++++++++++++++++
shell.nix | 1 +
14 files changed, 544 insertions(+)
create mode 100644 config.toml
create mode 100644 frontend/templates/index.tmpl
create mode 100644 go.sum
create mode 100644 internal/config/config.go
create mode 100644 internal/config/csp.go
create mode 100644 internal/server/headers.go
create mode 100644 internal/server/logging.go
create mode 100644 internal/server/server.go
diff --git a/.gitignore b/.gitignore
index 2368a38..ebdb921 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
!/README.md
!/LICENSE
!/justfile
+!/config.toml
!/nix
!*.nix
@@ -25,5 +26,9 @@
!/gomod2nix.toml
!*.go
+!*.tmpl
+!*.js
+!/frontend
+
# ...even if they are in subdirectories
!*/
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..cdbe7d1
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,23 @@
+base_url = "https://searchix.alanpearce.eu"
+
+[content-security-policy]
+default-src = [
+ "'none'",
+]
+image-src = [
+ "'self'",
+ "http://gc.zgo.at",
+]
+script-src = [
+ "'self'",
+ "http://gc.zgo.at",
+]
+style-src = [
+ "'unsafe-inline'",
+]
+require-trusted-types-for = [
+ "'script'",
+]
+
+[headers]
+x-content-type-options = "nosniff"
diff --git a/frontend/templates/index.tmpl b/frontend/templates/index.tmpl
new file mode 100644
index 0000000..fd9cc8f
--- /dev/null
+++ b/frontend/templates/index.tmpl
@@ -0,0 +1,11 @@
+
+
+
+
+ Searchix
+
+
+ Searchix
+ {{ .LiveReload }}
+
+
diff --git a/go.mod b/go.mod
index dcd2a6c..ac4a28b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,19 @@
module searchix
go 1.22.2
+
+require (
+ github.com/ardanlabs/conf/v3 v3.1.7
+ github.com/crewjam/csp v0.0.2
+ github.com/getsentry/sentry-go v0.27.0
+ github.com/osdevisnot/sorvor v0.4.4
+ github.com/pelletier/go-toml/v2 v2.2.1
+ github.com/pkg/errors v0.9.1
+ github.com/shengyanli1982/law v0.1.15
+)
+
+require (
+ github.com/google/go-cmp v0.6.0 // indirect
+ golang.org/x/sys v0.19.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..0869ec4
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,46 @@
+github.com/ardanlabs/conf/v3 v3.1.7 h1:p232cF68TafoA5U9ZlbxUIhGJtGNdKHBXF80Fdqb5t0=
+github.com/ardanlabs/conf/v3 v3.1.7/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU=
+github.com/crewjam/csp v0.0.2 h1:fIq6o0Z6bkABlvLT3kB0XgPnVX9iNXSAGMILs6AqHVw=
+github.com/crewjam/csp v0.0.2/go.mod h1:0tirp4wHwMLZZtV+HXRqGFkUO7uD2ux+1ECvK+7/xFI=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/evanw/esbuild v0.14.11/go.mod h1:GG+zjdi59yh3ehDn4ZWfPcATxjPDUH53iU4ZJbp7dkY=
+github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
+github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
+github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
+github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/osdevisnot/sorvor v0.4.4 h1:hcMWsWOKpUtDUE3F7dra1Jf12ftLHfgDcxlyPeVlz0Y=
+github.com/osdevisnot/sorvor v0.4.4/go.mod h1:D/j+vvJEmjIXndJf37uwFWD0Hjcq9DiGojyt4yMo7H0=
+github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
+github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
+github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/shengyanli1982/law v0.1.15 h1:puSn0Saa+0ptjszspycfWHPSu0D3kBcU3oEeW83MRIc=
+github.com/shengyanli1982/law v0.1.15/go.mod h1:20k9YnOTwilUB4X5Z4S7TIX5Ek1Ok4xfx8V8ZxIWlyM=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/gomod2nix.toml b/gomod2nix.toml
index 43cd4cf..436a69c 100644
--- a/gomod2nix.toml
+++ b/gomod2nix.toml
@@ -1,3 +1,33 @@
schema = 3
[mod]
+ [mod."github.com/ardanlabs/conf/v3"]
+ version = "v3.1.7"
+ hash = "sha256-7H53l0JN5Q6hkAgBivVQ8lFd03oNmP1IG8ihzLKm2CQ="
+ [mod."github.com/crewjam/csp"]
+ version = "v0.0.2"
+ hash = "sha256-4vlGmDdQjPiXmueCV51fJH/hRcG8eqhCi9TENCXjzfA="
+ [mod."github.com/getsentry/sentry-go"]
+ version = "v0.27.0"
+ hash = "sha256-PTkTzVNogqFA/5rc6INLY6RxK5uR1AoJFOO+pOPdE7Q="
+ [mod."github.com/google/go-cmp"]
+ version = "v0.6.0"
+ hash = "sha256-qgra5jze4iPGP0JSTVeY5qV5AvEnEu39LYAuUCIkMtg="
+ [mod."github.com/osdevisnot/sorvor"]
+ version = "v0.4.4"
+ hash = "sha256-BhyO7bvwxIdEV+c6Eo1uqahhcgsHiS8nJpg2aT8t+8s="
+ [mod."github.com/pelletier/go-toml/v2"]
+ version = "v2.2.1"
+ hash = "sha256-gmQ4CTz/MI97D3pYqU7mpxqo8gBTDccQ1Cp0lAMmJUc="
+ [mod."github.com/pkg/errors"]
+ version = "v0.9.1"
+ hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw="
+ [mod."github.com/shengyanli1982/law"]
+ version = "v0.1.15"
+ hash = "sha256-Z5G3PtR7V0d04MN+kBge33Pv6VDjJryx+N7JGJkzfLQ="
+ [mod."golang.org/x/sys"]
+ version = "v0.19.0"
+ hash = "sha256-cmuL31TYLJmDm/fDnI2Sn0wB88cpdOHV1+urorsJWx4="
+ [mod."golang.org/x/text"]
+ version = "v0.14.0"
+ hash = "sha256-yh3B0tom1RfzQBf1RNmfdNWF1PtiqxV41jW1GVS6JAg="
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..e9300ce
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,51 @@
+package config
+
+import (
+ "log/slog"
+ "net/url"
+ "os"
+
+ "github.com/pelletier/go-toml/v2"
+ "github.com/pkg/errors"
+)
+
+type URL struct {
+ *url.URL
+}
+
+func (u *URL) UnmarshalText(text []byte) (err error) {
+ u.URL, err = url.Parse(string(text))
+
+ if err != nil {
+ return errors.WithMessage(err, "could not parse URL")
+ }
+
+ return nil
+}
+
+type Config struct {
+ BaseURL URL `toml:"base_url"`
+ CSP *CSP `toml:"content-security-policy"`
+ Headers map[string]string
+}
+
+func GetConfig() (*Config, error) {
+ config := Config{}
+ slog.Debug("reading config.toml")
+ file, err := os.Open("config.toml")
+ if err != nil {
+ return nil, errors.WithMessage(err, "error reading configuration")
+ }
+ dec := toml.NewDecoder(file)
+ err = dec.Decode(&config)
+ if err != nil {
+ var tomlError toml.DecodeError
+ if errors.As(err, &tomlError) {
+ return nil, errors.WithMessage(err, tomlError.Error())
+ }
+
+ return nil, errors.Wrap(err, "config error")
+ }
+
+ return &config, nil
+}
diff --git a/internal/config/csp.go b/internal/config/csp.go
new file mode 100644
index 0000000..970663c
--- /dev/null
+++ b/internal/config/csp.go
@@ -0,0 +1,45 @@
+package config
+
+// Code generated DO NOT EDIT.
+
+import (
+ "github.com/crewjam/csp"
+)
+
+type CSP struct {
+ BaseURI []string `csp:"base-uri" toml:"base-uri"`
+ BlockAllMixedContent bool `csp:"block-all-mixed-content" toml:"block-all-mixed-content"`
+ ChildSrc []string `csp:"child-src" toml:"child-src"`
+ ConnectSrc []string `csp:"connect-src" toml:"connect-src"`
+ DefaultSrc []string `csp:"default-src" toml:"default-src"`
+ FontSrc []string `csp:"font-src" toml:"font-src"`
+ FormAction []string `csp:"form-action" toml:"form-action"`
+ FrameAncestors []string `csp:"frame-ancestors" toml:"frame-ancestors"`
+ FrameSrc []string `csp:"frame-src" toml:"frame-src"`
+ ImgSrc []string `csp:"img-src" toml:"img-src"`
+ ManifestSrc []string `csp:"manifest-src" toml:"manifest-src"`
+ MediaSrc []string `csp:"media-src" toml:"media-src"`
+ NavigateTo []string `csp:"navigate-to" toml:"navigate-to"`
+ ObjectSrc []string `csp:"object-src" toml:"object-src"`
+ PluginTypes []string `csp:"plugin-types" toml:"plugin-types"`
+ PrefetchSrc []string `csp:"prefetch-src" toml:"prefetch-src"`
+ Referrer csp.ReferrerPolicy `csp:"referrer" toml:"referrer"`
+ ReportTo string `csp:"report-to" toml:"report-to"`
+ ReportURI string `csp:"report-uri" toml:"report-uri"`
+ RequireSRIFor []csp.RequireSRIFor `csp:"require-sri-for" toml:"require-sri-for"`
+ RequireTrustedTypesFor []csp.RequireTrustedTypesFor `csp:"require-trusted-types-for" toml:"require-trusted-types-for"`
+ Sandbox csp.Sandbox `csp:"sandbox" toml:"sandbox"`
+ ScriptSrc []string `csp:"script-src" toml:"script-src"`
+ ScriptSrcAttr []string `csp:"script-src-attr" toml:"script-src-attr"`
+ ScriptSrcElem []string `csp:"script-src-elem" toml:"script-src-elem"`
+ StyleSrc []string `csp:"style-src" toml:"style-src"`
+ StyleSrcAttr []string `csp:"style-src-attr" toml:"style-src-attr"`
+ StyleSrcElem []string `csp:"style-src-elem" toml:"style-src-elem"`
+ TrustedTypes []string `csp:"trusted-types" toml:"trusted-types"`
+ UpgradeInsecureRequests bool `csp:"upgrade-insecure-requests" toml:"upgrade-insecure-requests"`
+ WorkerSrc []string `csp:"worker-src" toml:"worker-src"`
+}
+
+func (c *CSP) String() string {
+ return csp.Header(*c).String()
+}
diff --git a/internal/server/headers.go b/internal/server/headers.go
new file mode 100644
index 0000000..0efc384
--- /dev/null
+++ b/internal/server/headers.go
@@ -0,0 +1,17 @@
+package server
+
+import (
+ "net/http"
+ cfg "searchix/internal/config"
+)
+
+func AddHeadersMiddleware(next http.Handler, config *cfg.Config) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ for h, v := range config.Headers {
+ w.Header().Add(h, v)
+ }
+ w.Header().Add("Content-Security-Policy", config.CSP.String())
+
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/internal/server/logging.go b/internal/server/logging.go
new file mode 100644
index 0000000..6a16f42
--- /dev/null
+++ b/internal/server/logging.go
@@ -0,0 +1,55 @@
+package server
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+)
+
+type LoggingResponseWriter struct {
+ http.ResponseWriter
+ statusCode int
+}
+
+func (lrw *LoggingResponseWriter) WriteHeader(code int) {
+ lrw.statusCode = code
+ // avoids warning: superfluous response.WriteHeader call
+ if lrw.statusCode != http.StatusOK {
+ lrw.ResponseWriter.WriteHeader(code)
+ }
+}
+
+func NewLoggingResponseWriter(w http.ResponseWriter) *LoggingResponseWriter {
+ return &LoggingResponseWriter{w, http.StatusOK}
+}
+
+type wrappedHandlerOptions struct {
+ defaultHostname string
+ logger io.Writer
+}
+
+func wrapHandlerWithLogging(wrappedHandler http.Handler, opts wrappedHandlerOptions) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ scheme := r.Header.Get("X-Forwarded-Proto")
+ if scheme == "" {
+ scheme = "http"
+ }
+ host := r.Header.Get("Host")
+ if host == "" {
+ host = opts.defaultHostname
+ }
+ lw := NewLoggingResponseWriter(w)
+ wrappedHandler.ServeHTTP(lw, r)
+ statusCode := lw.statusCode
+ fmt.Fprintf(
+ opts.logger,
+ "%s %s %d %s %s %s\n",
+ scheme,
+ r.Method,
+ statusCode,
+ host,
+ r.URL.Path,
+ lw.Header().Get("Location"),
+ )
+ })
+}
diff --git a/internal/server/server.go b/internal/server/server.go
new file mode 100644
index 0000000..fc56d47
--- /dev/null
+++ b/internal/server/server.go
@@ -0,0 +1,175 @@
+package server
+
+import (
+ "context"
+ "html/template"
+ "io"
+ "log"
+ "log/slog"
+ "net"
+ "net/http"
+ "os"
+ "path"
+ "slices"
+ "time"
+
+ cfg "searchix/internal/config"
+
+ "github.com/getsentry/sentry-go"
+ sentryhttp "github.com/getsentry/sentry-go/http"
+ "github.com/osdevisnot/sorvor/pkg/livereload"
+ "github.com/pkg/errors"
+ "github.com/shengyanli1982/law"
+)
+
+var config *cfg.Config
+
+var (
+ CommitSHA string
+ ShortSHA string
+)
+
+type Config struct {
+ Production bool `conf:"default:false"`
+ InDevServer bool `conf:"default:false"`
+ LiveReload bool `conf:"default:false,flag:live"`
+ Root string `conf:"default:website"`
+ ListenAddress string `conf:"default:localhost"`
+ Port string `conf:"default:3000,short:p"`
+ BaseURL cfg.URL `conf:"default:http://localhost:3000,short:b"`
+}
+
+type HTTPError struct {
+ Error error
+ Message string
+ Code int
+}
+
+type Server struct {
+ *http.Server
+}
+
+const jsSnippet = template.HTML(livereload.JsSnippet) // #nosec G203
+
+type TemplateData struct {
+ LiveReload template.HTML
+ Data map[string]interface{}
+}
+
+func applyDevModeOverrides(config *cfg.Config) {
+ config.CSP.ScriptSrc = slices.Insert(config.CSP.ScriptSrc, 0, "'unsafe-inline'")
+ config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'")
+}
+
+func New(runtimeConfig *Config) (*Server, error) {
+ var err error
+ config, err = cfg.GetConfig()
+ if err != nil {
+ return nil, errors.WithMessage(err, "error parsing configuration file")
+ }
+
+ env := "development"
+ if runtimeConfig.Production {
+ env = "production"
+ }
+ err = sentry.Init(sentry.ClientOptions{
+ EnableTracing: true,
+ TracesSampleRate: 1.0,
+ Dsn: os.Getenv("SENTRY_DSN"),
+ Release: CommitSHA,
+ Environment: env,
+ })
+ if err != nil {
+ return nil, errors.WithMessage(err, "could not set up sentry")
+ }
+ defer sentry.Flush(2 * time.Second)
+ sentryHandler := sentryhttp.New(sentryhttp.Options{
+ Repanic: true,
+ })
+
+ tpl := template.Must(template.ParseGlob(path.Join("frontend", "templates", "*.tmpl")))
+
+ top := http.NewServeMux()
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
+ tdata := TemplateData{
+ LiveReload: jsSnippet,
+ Data: make(map[string]interface{}),
+ }
+ err := tpl.Execute(w, tdata)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ })
+
+ mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("frontend/static"))))
+
+ if runtimeConfig.LiveReload {
+ applyDevModeOverrides(config)
+ liveReload := livereload.New()
+ liveReload.Start()
+ mux.Handle("/livereload", liveReload)
+ // liveReload.Reload()
+ }
+
+ var logWriter io.Writer
+ if runtimeConfig.Production {
+ logWriter = law.NewWriteAsyncer(os.Stdout, nil)
+ } else {
+ logWriter = os.Stdout
+ }
+ top.Handle("/",
+ AddHeadersMiddleware(
+ sentryHandler.Handle(
+ wrapHandlerWithLogging(mux, wrappedHandlerOptions{
+ defaultHostname: runtimeConfig.BaseURL.Hostname(),
+ logger: logWriter,
+ }),
+ ),
+ config,
+ ),
+ )
+ // no logging, no sentry
+ top.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ listenAddress := net.JoinHostPort(runtimeConfig.ListenAddress, runtimeConfig.Port)
+
+ return &Server{
+ &http.Server{
+ Addr: listenAddress,
+ Handler: top,
+ ReadHeaderTimeout: 20 * time.Second,
+ },
+ }, nil
+}
+
+func (s *Server) Start() error {
+ if err := s.ListenAndServe(); err != http.ErrServerClosed {
+ return errors.WithMessage(err, "could not start server")
+ }
+
+ return nil
+}
+
+func (s *Server) Stop() chan struct{} {
+ slog.Debug("stop called")
+
+ idleConnsClosed := make(chan struct{})
+
+ go func() {
+ slog.Debug("shutting down server")
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ err := s.Server.Shutdown(ctx)
+ slog.Debug("server shut down")
+ if err != nil {
+ // Error from closing listeners, or context timeout:
+ log.Printf("HTTP server Shutdown: %v", err)
+ }
+ close(idleConnsClosed)
+ }()
+
+ return idleConnsClosed
+}
diff --git a/justfile b/justfile
index 8054509..d78325f 100644
--- a/justfile
+++ b/justfile
@@ -14,3 +14,6 @@ fix:
precommit:
nix-build -A pre-commit-check
+
+dev:
+ wgo run ./serve/
diff --git a/serve/main.go b/serve/main.go
index 7905807..6b55977 100644
--- a/serve/main.go
+++ b/serve/main.go
@@ -1,5 +1,71 @@
package main
+import (
+ "fmt"
+ "log"
+ "log/slog"
+ "os"
+ "os/signal"
+ "sync"
+
+ "searchix/internal/server"
+
+ "github.com/ardanlabs/conf/v3"
+ "github.com/pkg/errors"
+)
+
+var (
+ CommitSHA string
+ ShortSHA string
+)
+
func main() {
+ if os.Getenv("DEBUG") != "" {
+ slog.SetLogLoggerLevel(slog.LevelDebug)
+ }
+ log.SetFlags(log.LstdFlags | log.Lmsgprefix)
+ log.SetPrefix("searchix: ")
+
+ runtimeConfig := server.Config{}
+ help, err := conf.Parse("", &runtimeConfig)
+ if err != nil {
+ if errors.Is(err, conf.ErrHelpWanted) {
+ fmt.Println(help)
+ os.Exit(1)
+ }
+ log.Panicf("parsing runtime configuration: %v", err)
+ }
+
+ c := make(chan os.Signal, 2)
+ signal.Notify(c, os.Interrupt)
+ sv, err := server.New(&runtimeConfig)
+ if err != nil {
+ log.Fatalf("error setting up server: %v", err)
+ }
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ sig := <-c
+ log.Printf("signal captured: %v", sig)
+ <-sv.Stop()
+ slog.Debug("server stopped")
+ }()
+
+ sErr := make(chan error)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ sErr <- sv.Start()
+ }()
+ if !runtimeConfig.InDevServer {
+ log.Printf("server listening on %s", sv.Addr)
+ }
+ err = <-sErr
+ if err != nil {
+ // Error starting or closing listener:
+ log.Fatalf("error: %v", err)
+ }
+ wg.Wait()
}
diff --git a/shell.nix b/shell.nix
index ae73c6b..2c635ad 100644
--- a/shell.nix
+++ b/shell.nix
@@ -20,6 +20,7 @@ pkgs.mkShell {
packages = with pkgs; [
goEnv
+ wgo
gomod2nix
niv
nixpkgs-lint
--
cgit 1.4.1