package api import ( "io/fs" "net/http" "os" "github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config/ui" "github.com/TwiN/gatus/v5/config/web" static "github.com/TwiN/gatus/v5/web" "github.com/TwiN/health" "github.com/TwiN/logr" fiber "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/adaptor" "github.com/gofiber/fiber/v2/middleware/compress" "github.com/gofiber/fiber/v2/middleware/cors" fiberfs "github.com/gofiber/fiber/v2/middleware/filesystem" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/fiber/v2/middleware/redirect" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp") type API struct { router *fiber.App} func New(cfg *config.Config) *API { api := &API{} if cfg.Web == nil { logr.Warnf("[api.New] nil web config passed as parameter. This should only happen in tests. Using default web configuration") cfg.Web = web.GetDefaultConfig() } if cfg.UI == nil { logr.Warnf("[api.New] nil ui config passed as parameter. This should only happen in tests. Using default ui configuration") cfg.UI = ui.GetDefaultConfig() } api.router = api.createRouter(cfg) return api} func (a *API) Router() *fiber.App { return a.router} func (a *API) createRouter(cfg *config.Config) *fiber.App { app := fiber.New(fiber.Config{ ErrorHandler: func(c *fiber.Ctx, err error) error { logr.Errorf("[api.ErrorHandler] %s", err.Error()) return fiber.DefaultErrorHandler(c, err) }, ReadBufferSize: cfg.Web.ReadBufferSize, Network: fiber.NetworkTCP, Immutable: true, // If not enabled, will cause issues due to fiber's zero allocation. See #1268 and https://docs.gofiber.io/#zero-allocation }) if os.Getenv("ENVIRONMENT") == "dev" { app.Use(cors.New(cors.Config{ AllowOrigins: "http://localhost:8081", AllowCredentials: true, })) } // Middlewares app.Use(recover.New()) app.Use(compress.New()) // Define metrics handler, if necessary if cfg.Metrics { metricsHandler := promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{ DisableCompression: true, })) app.Get("/metrics", adaptor.HTTPHandler(metricsHandler)) } // Define main router apiRouter := app.Group("/api") //////////////////////// // UNPROTECTED ROUTES // //////////////////////// unprotectedAPIRouter := apiRouter.Group("/") unprotectedAPIRouter.Get("/v1/config", ConfigHandler{securityConfig: cfg.Security, config: cfg}.GetConfig) unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.svg", HealthBadge) unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.shields", HealthBadgeShields) unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration", UptimeRaw) unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration/badge.svg", UptimeBadge) unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration", ResponseTimeRaw) unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/badge.svg", ResponseTimeBadge(cfg)) unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/chart.svg", ResponseTimeChart) unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/history", ResponseTimeHistory) // This endpoint requires authz with bearer token, so technically it is protected unprotectedAPIRouter.Post("/v1/endpoints/:key/external", CreateExternalEndpointResult(cfg)) // SPA app.Get("/", SinglePageApplication(cfg.UI)) app.Get("/endpoints/:key", SinglePageApplication(cfg.UI)) app.Get("/suites/:key", SinglePageApplication(cfg.UI)) // Health endpoint healthHandler := health.Handler().WithJSON(true) app.Get("/health", func(c *fiber.Ctx) error { statusCode, body := healthHandler.GetResponseStatusCodeAndBody() return c.Status(statusCode).Send(body) }) // Custom CSS app.Get("/css/custom.css", CustomCSSHandler{customCSS: cfg.UI.CustomCSS}.GetCustomCSS) // Everything else falls back on static content app.Use(redirect.New(redirect.Config{ Rules: map[string]string{ "/index.html": "/", }, StatusCode: 301, })) staticFileSystem, err := fs.Sub(static.FileSystem, static.RootPath) if err != nil { panic(err) } app.Use("/", fiberfs.New(fiberfs.Config{ Root: http.FS(staticFileSystem), Index: "index.html", Browse: true, })) ////////////////////// // PROTECTED ROUTES // ////////////////////// // ORDER IS IMPORTANT: all routes applied AFTER the security middleware will require authn protectedAPIRouter := apiRouter.Group("/") if cfg.Security != nil { if err := cfg.Security.RegisterHandlers(app); err != nil { panic(err) } if err := cfg.Security.ApplySecurityMiddleware(protectedAPIRouter); err != nil { panic(err) } } protectedAPIRouter.Get("/v1/endpoints/statuses", EndpointStatuses(cfg)) protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus(cfg)) protectedAPIRouter.Get("/v1/suites/statuses", SuiteStatuses(cfg)) protectedAPIRouter.Get("/v1/suites/:key/statuses", SuiteStatus(cfg)) return app}
package api
import (
"io/fs"
"net/http"
"os"
"github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/config/ui"
"github.com/TwiN/gatus/v5/config/web"
static "github.com/TwiN/gatus/v5/web"
"github.com/TwiN/health"
"github.com/TwiN/logr"
fiber "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/adaptor"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
fiberfs "github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/redirect"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
type API struct {
router *fiber.App
}
func New(cfg *config.Config) *API {
api := &API{}
if cfg.Web == nil {
logr.Warnf("[api.New] nil web config passed as parameter. This should only happen in tests. Using default web configuration")
cfg.Web = web.GetDefaultConfig()
}
if cfg.UI == nil {
logr.Warnf("[api.New] nil ui config passed as parameter. This should only happen in tests. Using default ui configuration")
cfg.UI = ui.GetDefaultConfig()
}
api.router = api.createRouter(cfg)
return api
}
func (a *API) Router() *fiber.App {
return a.router
}
func (a *API) createRouter(cfg *config.Config) *fiber.App {
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
logr.Errorf("[api.ErrorHandler] %s", err.Error())
return fiber.DefaultErrorHandler(c, err)
},
ReadBufferSize: cfg.Web.ReadBufferSize,
Network: fiber.NetworkTCP,
Immutable: true, // If not enabled, will cause issues due to fiber's zero allocation. See #1268 and https://docs.gofiber.io/#zero-allocation
})
if os.Getenv("ENVIRONMENT") == "dev" {
app.Use(cors.New(cors.Config{
AllowOrigins: "http://localhost:8081",
AllowCredentials: true,
}))
}
// Middlewares
app.Use(recover.New())
app.Use(compress.New())
// Define metrics handler, if necessary
if cfg.Metrics {
metricsHandler := promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
DisableCompression: true,
}))
app.Get("/metrics", adaptor.HTTPHandler(metricsHandler))
}
// Define main router
apiRouter := app.Group("/api")
////////////////////////
// UNPROTECTED ROUTES //
////////////////////////
unprotectedAPIRouter := apiRouter.Group("/")
unprotectedAPIRouter.Get("/v1/config", ConfigHandler{securityConfig: cfg.Security, config: cfg}.GetConfig)
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.svg", HealthBadge)
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.shields", HealthBadgeShields)
unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration", UptimeRaw)
unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration/badge.svg", UptimeBadge)
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration", ResponseTimeRaw)
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/badge.svg", ResponseTimeBadge(cfg))
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/chart.svg", ResponseTimeChart)
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/history", ResponseTimeHistory)
// This endpoint requires authz with bearer token, so technically it is protected
unprotectedAPIRouter.Post("/v1/endpoints/:key/external", CreateExternalEndpointResult(cfg))
// SPA
app.Get("/", SinglePageApplication(cfg.UI))
app.Get("/endpoints/:key", SinglePageApplication(cfg.UI))
app.Get("/suites/:key", SinglePageApplication(cfg.UI))
// Health endpoint
healthHandler := health.Handler().WithJSON(true)
app.Get("/health", func(c *fiber.Ctx) error {
statusCode, body := healthHandler.GetResponseStatusCodeAndBody()
return c.Status(statusCode).Send(body)
})
// Custom CSS
app.Get("/css/custom.css", CustomCSSHandler{customCSS: cfg.UI.CustomCSS}.GetCustomCSS)
// Everything else falls back on static content
app.Use(redirect.New(redirect.Config{
Rules: map[string]string{
"/index.html": "/",
},
StatusCode: 301,
}))
staticFileSystem, err := fs.Sub(static.FileSystem, static.RootPath)
if err != nil {
panic(err)
}
app.Use("/", fiberfs.New(fiberfs.Config{
Root: http.FS(staticFileSystem),
Index: "index.html",
Browse: true,
}))
//////////////////////
// PROTECTED ROUTES //
//////////////////////
// ORDER IS IMPORTANT: all routes applied AFTER the security middleware will require authn
protectedAPIRouter := apiRouter.Group("/")
if cfg.Security != nil {
if err := cfg.Security.RegisterHandlers(app); err != nil {
panic(err)
}
if err := cfg.Security.ApplySecurityMiddleware(protectedAPIRouter); err != nil {
panic(err)
}
}
protectedAPIRouter.Get("/v1/endpoints/statuses", EndpointStatuses(cfg))
protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus(cfg))
protectedAPIRouter.Get("/v1/suites/statuses", SuiteStatuses(cfg))
protectedAPIRouter.Get("/v1/suites/:key/statuses", SuiteStatus(cfg))
return app
}