From a86e275fdbde49b5530f09915a4f3ec3f6d6a308 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Sun, 21 Apr 2024 14:00:47 +0200 Subject: move server code to cmd/server --- cmd/server/server.go | 188 +++++++++++++++++++++++++++++++++++++++++++++++++++ nix/default.nix | 8 +-- nix/scripts.nix | 4 +- server.go | 188 --------------------------------------------------- 4 files changed, 194 insertions(+), 194 deletions(-) create mode 100644 cmd/server/server.go delete mode 100644 server.go diff --git a/cmd/server/server.go b/cmd/server/server.go new file mode 100644 index 0000000..935cc1e --- /dev/null +++ b/cmd/server/server.go @@ -0,0 +1,188 @@ +package main + +import ( + "errors" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "time" + + cfg "website/internal/config" + + "github.com/ansrivas/fiberprometheus/v2" + "github.com/ardanlabs/conf/v3" + "github.com/getsentry/sentry-go" + "github.com/gofiber/contrib/fibersentry" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cache" + "github.com/gofiber/fiber/v2/middleware/compress" + "github.com/gofiber/fiber/v2/middleware/etag" + "github.com/gofiber/fiber/v2/middleware/filesystem" + "github.com/gofiber/fiber/v2/middleware/healthcheck" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/gofiber/fiber/v2/middleware/skip" + + "github.com/shengyanli1982/law" +) + +type Config struct { + Production bool `conf:"default:false"` + ListenAddress string `conf:"default:localhost"` + Port uint16 `conf:"default:3000,short:p"` + BaseURL cfg.URL `conf:"default:http://localhost:3000,short:b"` + RedirectOtherHostnames bool `conf:"default:false"` +} + +// TODO purge CSS +// TODO HTTP2 https://github.com/dgrr/http2 + +type Host struct { + Fiber *fiber.App +} + +var Commit string + +func main() { + runtimeConfig := Config{} + if help, err := conf.Parse("", &runtimeConfig); err != nil { + if errors.Is(err, conf.ErrHelpWanted) { + fmt.Println(help) + os.Exit(1) + } + log.Panicf("parsing runtime configuration: %v", err) + } + config, err := cfg.GetConfig() + if err != nil { + log.Panicf("parsing configuration file: %v", err) + } + + err = sentry.Init(sentry.ClientOptions{ + Dsn: os.Getenv("SENTRY_DSN"), + Release: os.Getenv("FLY_MACHINE_VERSION"), + Environment: os.Getenv("ENV"), + }) + if err != nil { + log.Panic("could not set up sentry") + } + defer sentry.Flush(2 * time.Second) + + metricServer := fiber.New(fiber.Config{ + GETOnly: true, + StrictRouting: true, + DisableDefaultDate: true, + DisableHeaderNormalizing: true, + DisableStartupMessage: true, + Network: fiber.NetworkTCP, + }) + prometheus := fiberprometheus.New("homestead") + prometheus.RegisterAt(metricServer, "/metrics") + + hosts := map[string]*Host{} + + internal := fiber.New(fiber.Config{ + GETOnly: true, + StrictRouting: true, + }) + internal.Use(healthcheck.New(healthcheck.Config{})) + hosts["fly-internal"] = &Host{internal} + + website := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"172.16.0.0/16"}, + ProxyHeader: "Fly-Client-IP", + GETOnly: true, + ReadTimeout: 5 * time.Minute, + WriteTimeout: 5 * time.Minute, + StrictRouting: true, + UnescapePath: true, + }) + + website.Use(prometheus.Middleware) + website.Use(fibersentry.New(fibersentry.Config{})) + website.Use(func(c *fiber.Ctx) error { + for k, v := range config.Extra.Headers { + c.Set(k, v) + } + if c.Secure() { + c.Set("Strict-Transport-Security", "max-age=31536000; includeSubdomains; preload") + } + return c.Next() + }) + + website.Use(compress.New()) + website.Use(cache.New(cache.Config{ + CacheControl: true, + Expiration: 24 * time.Hour, + StoreResponseHeaders: true, + })) + // must be after compress to be encoding-independent + website.Use(etag.New(etag.Config{ + Weak: true, + })) + + website.Use(recover.New(recover.Config{})) + + files := http.Dir("public") + notFoundHandler := func(c *fiber.Ctx) error { + c.Status(fiber.StatusNotFound).Type("html", "utf-8") + content, err := files.Open("404.html") + if err != nil { + c.Type("txt") + return c.SendString("404 Not Found") + } + return c.SendStream(content) + } + website.Get("/404.html", notFoundHandler) + website.Use("/", filesystem.New(filesystem.Config{ + Root: files, + ContentTypeCharset: "utf-8", + MaxAge: int((24 * time.Hour).Seconds()), + })) + website.Use(notFoundHandler) + hosts[runtimeConfig.BaseURL.Host] = &Host{website} + + toplevel := fiber.New(fiber.Config{ + DisableStartupMessage: runtimeConfig.Production, + ServerHeader: fmt.Sprintf("website (%s)", Commit), + Network: fiber.NetworkTCP, + }) + toplevel.Get("/health", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + var logWriter io.Writer + if runtimeConfig.Production { + logWriter = law.NewWriteAsyncer(os.Stdout, nil) + } else { + logWriter = os.Stdout + } + toplevel.Use(skip.New(logger.New(logger.Config{ + Output: logWriter, + Format: "${protocol} ${method} ${status} ${host} ${url} ${respHeader:Location}\n", + }), func(c *fiber.Ctx) bool { + return c.Hostname() == "fly-internal" + })) + toplevel.Use(func(c *fiber.Ctx) error { + host := hosts[c.Hostname()] + if host == nil { + if runtimeConfig.RedirectOtherHostnames { + return c.Redirect(runtimeConfig.BaseURL.JoinPath(c.OriginalURL()).String()) + } else { + hosts[runtimeConfig.BaseURL.Host].Fiber.Handler()(c.Context()) + return nil + } + } else { + host.Fiber.Handler()(c.Context()) + return nil + } + }) + + go func() { + err := metricServer.Listen(net.JoinHostPort(runtimeConfig.ListenAddress, "9091")) + log.Printf("failed to start metrics server: %v", err) + }() + log.Fatal(toplevel.Listen(net.JoinHostPort(runtimeConfig.ListenAddress, fmt.Sprint(runtimeConfig.Port)))) +} diff --git a/nix/default.nix b/nix/default.nix index 4299676..32c546f 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -14,7 +14,7 @@ let public ]; config = { - Cmd = [ "${server}/bin/website" ]; + Cmd = [ "${server}/bin/server" ]; Env = [ "PRODUCTION=true" "LISTEN_ADDRESS=::" @@ -39,7 +39,7 @@ with pkgs; rec { fileset = unions [ ./../go.mod ./../go.sum - ./../cmd + ./../cmd/build ./../internal ]; }; @@ -61,7 +61,7 @@ with pkgs; rec { ${builder}/bin/build -s $src -d $out/public ''; server = buildGoApplication { - pname = "website"; + pname = "server"; inherit version; CGO_ENABLED = 0; src = with lib.fileset; toSource { @@ -69,7 +69,7 @@ with pkgs; rec { fileset = unions [ ./../go.mod ./../go.sum - ./../server.go + ./../cmd/server ./../internal ]; }; diff --git a/nix/scripts.nix b/nix/scripts.nix index b50ba16..e097648 100644 --- a/nix/scripts.nix +++ b/nix/scripts.nix @@ -16,10 +16,10 @@ let in with pkgs; [ (writeShellScriptBin "watch-builder" '' - ${watchFlake} "direnv exec . watchexec -i server.go -i public -r go run ./cmd/build $@" + ${watchFlake} "direnv exec . watchexec -i cmd/server -i public -r go run ./cmd/build $@" '') (writeShellScriptBin "watch-server" '' - ${watchFlake} "direnv exec . watchexec -r go run ./server.go $@" + ${watchFlake} "direnv exec . watchexec -r go run ./cmd/server $@" '') (writeShellScriptBin "check-licenses" '' ${go-licenses}/bin/go-licenses check --include_tests ./... --disallowed_types=restricted,forbidden diff --git a/server.go b/server.go deleted file mode 100644 index 935cc1e..0000000 --- a/server.go +++ /dev/null @@ -1,188 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io" - "log" - "net" - "net/http" - "os" - "time" - - cfg "website/internal/config" - - "github.com/ansrivas/fiberprometheus/v2" - "github.com/ardanlabs/conf/v3" - "github.com/getsentry/sentry-go" - "github.com/gofiber/contrib/fibersentry" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cache" - "github.com/gofiber/fiber/v2/middleware/compress" - "github.com/gofiber/fiber/v2/middleware/etag" - "github.com/gofiber/fiber/v2/middleware/filesystem" - "github.com/gofiber/fiber/v2/middleware/healthcheck" - "github.com/gofiber/fiber/v2/middleware/logger" - "github.com/gofiber/fiber/v2/middleware/recover" - "github.com/gofiber/fiber/v2/middleware/skip" - - "github.com/shengyanli1982/law" -) - -type Config struct { - Production bool `conf:"default:false"` - ListenAddress string `conf:"default:localhost"` - Port uint16 `conf:"default:3000,short:p"` - BaseURL cfg.URL `conf:"default:http://localhost:3000,short:b"` - RedirectOtherHostnames bool `conf:"default:false"` -} - -// TODO purge CSS -// TODO HTTP2 https://github.com/dgrr/http2 - -type Host struct { - Fiber *fiber.App -} - -var Commit string - -func main() { - runtimeConfig := Config{} - if help, err := conf.Parse("", &runtimeConfig); err != nil { - if errors.Is(err, conf.ErrHelpWanted) { - fmt.Println(help) - os.Exit(1) - } - log.Panicf("parsing runtime configuration: %v", err) - } - config, err := cfg.GetConfig() - if err != nil { - log.Panicf("parsing configuration file: %v", err) - } - - err = sentry.Init(sentry.ClientOptions{ - Dsn: os.Getenv("SENTRY_DSN"), - Release: os.Getenv("FLY_MACHINE_VERSION"), - Environment: os.Getenv("ENV"), - }) - if err != nil { - log.Panic("could not set up sentry") - } - defer sentry.Flush(2 * time.Second) - - metricServer := fiber.New(fiber.Config{ - GETOnly: true, - StrictRouting: true, - DisableDefaultDate: true, - DisableHeaderNormalizing: true, - DisableStartupMessage: true, - Network: fiber.NetworkTCP, - }) - prometheus := fiberprometheus.New("homestead") - prometheus.RegisterAt(metricServer, "/metrics") - - hosts := map[string]*Host{} - - internal := fiber.New(fiber.Config{ - GETOnly: true, - StrictRouting: true, - }) - internal.Use(healthcheck.New(healthcheck.Config{})) - hosts["fly-internal"] = &Host{internal} - - website := fiber.New(fiber.Config{ - EnableTrustedProxyCheck: true, - TrustedProxies: []string{"172.16.0.0/16"}, - ProxyHeader: "Fly-Client-IP", - GETOnly: true, - ReadTimeout: 5 * time.Minute, - WriteTimeout: 5 * time.Minute, - StrictRouting: true, - UnescapePath: true, - }) - - website.Use(prometheus.Middleware) - website.Use(fibersentry.New(fibersentry.Config{})) - website.Use(func(c *fiber.Ctx) error { - for k, v := range config.Extra.Headers { - c.Set(k, v) - } - if c.Secure() { - c.Set("Strict-Transport-Security", "max-age=31536000; includeSubdomains; preload") - } - return c.Next() - }) - - website.Use(compress.New()) - website.Use(cache.New(cache.Config{ - CacheControl: true, - Expiration: 24 * time.Hour, - StoreResponseHeaders: true, - })) - // must be after compress to be encoding-independent - website.Use(etag.New(etag.Config{ - Weak: true, - })) - - website.Use(recover.New(recover.Config{})) - - files := http.Dir("public") - notFoundHandler := func(c *fiber.Ctx) error { - c.Status(fiber.StatusNotFound).Type("html", "utf-8") - content, err := files.Open("404.html") - if err != nil { - c.Type("txt") - return c.SendString("404 Not Found") - } - return c.SendStream(content) - } - website.Get("/404.html", notFoundHandler) - website.Use("/", filesystem.New(filesystem.Config{ - Root: files, - ContentTypeCharset: "utf-8", - MaxAge: int((24 * time.Hour).Seconds()), - })) - website.Use(notFoundHandler) - hosts[runtimeConfig.BaseURL.Host] = &Host{website} - - toplevel := fiber.New(fiber.Config{ - DisableStartupMessage: runtimeConfig.Production, - ServerHeader: fmt.Sprintf("website (%s)", Commit), - Network: fiber.NetworkTCP, - }) - toplevel.Get("/health", func(c *fiber.Ctx) error { - return c.SendStatus(fiber.StatusOK) - }) - var logWriter io.Writer - if runtimeConfig.Production { - logWriter = law.NewWriteAsyncer(os.Stdout, nil) - } else { - logWriter = os.Stdout - } - toplevel.Use(skip.New(logger.New(logger.Config{ - Output: logWriter, - Format: "${protocol} ${method} ${status} ${host} ${url} ${respHeader:Location}\n", - }), func(c *fiber.Ctx) bool { - return c.Hostname() == "fly-internal" - })) - toplevel.Use(func(c *fiber.Ctx) error { - host := hosts[c.Hostname()] - if host == nil { - if runtimeConfig.RedirectOtherHostnames { - return c.Redirect(runtimeConfig.BaseURL.JoinPath(c.OriginalURL()).String()) - } else { - hosts[runtimeConfig.BaseURL.Host].Fiber.Handler()(c.Context()) - return nil - } - } else { - host.Fiber.Handler()(c.Context()) - return nil - } - }) - - go func() { - err := metricServer.Listen(net.JoinHostPort(runtimeConfig.ListenAddress, "9091")) - log.Printf("failed to start metrics server: %v", err) - }() - log.Fatal(toplevel.Listen(net.JoinHostPort(runtimeConfig.ListenAddress, fmt.Sprint(runtimeConfig.Port)))) -} -- cgit 1.4.1