about summary refs log tree commit diff stats
path: root/cmd/server/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/server/server.go')
-rw-r--r--cmd/server/server.go188
1 files changed, 188 insertions, 0 deletions
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))))
+}