From a81f35d5a28e442f025d22e4dee3d0b5392c63e5 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Mon, 21 Feb 2022 18:56:09 +0100 Subject: [PATCH] Nice api landing & 404 pages --- app/cli.go | 2 +- app/servers.go | 59 ++++------------------------------ assets/embed.go | 61 ++++++++++++++++++++++++++++++++++- assets/src/api-404.html | 63 ++++++++++++++++++++++++++++++++++++ assets/src/api-landing.html | 64 +++++++++++++++++++++++++++++++++++++ pkg/api/server/handlers.go | 59 +++++++++++++++++++++++++++++++++- pkg/api/server/server.go | 24 ++++++-------- pkg/api/server/waitfor.go | 4 +-- 8 files changed, 265 insertions(+), 71 deletions(-) create mode 100644 assets/src/api-404.html create mode 100644 assets/src/api-landing.html diff --git a/app/cli.go b/app/cli.go index 5d9b0e770..b88946e38 100644 --- a/app/cli.go +++ b/app/cli.go @@ -74,7 +74,7 @@ func (app *CortezaApp) InitCLI() { wg := &sync.WaitGroup{} { // @todo refactor wait-for out of HTTP API server. - app.HttpServer = server.New(app.Log, app.Opt.Environment, app.Opt.HTTPServer, app.Opt.WaitFor) + app.HttpServer = server.New(app.Log, app.Opt) wg.Add(1) go func() { diff --git a/app/servers.go b/app/servers.go index 12cd2808f..2d3a83c37 100644 --- a/app/servers.go +++ b/app/servers.go @@ -1,10 +1,7 @@ package app import ( - "fmt" - "io/fs" "net/http" - "os" "path" "regexp" "strings" @@ -35,31 +32,16 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) { var ( url = options.CleanBase(ho.BaseUrl, "assets") aPath = ho.AssetsPath - files fs.FS - err error + files = assets.Files(app.Log, aPath) ) - if len(aPath) > 0 { - if files, err = loadAssetsFromPath(aPath); err != nil { - // log warning but fallback to embedded assets - app.Log.Warn( - fmt.Sprintf("failed to use custom assets path (HTTP_SERVER_ASSETS_PATH=%s)", aPath), - zap.Error(err), - ) - } - } - - if files == nil { - aPath = "embedded" - files, err = fs.Sub(assets.Embedded, "src") - if err != nil { - // if this is off, we might as well panic - panic(err) - } - } - r.Handle(url+"/*", http.StripPrefix(url+"/", http.FileServer(http.FS(files)))) - app.Log.Info("web assets mounted", zap.String("url", url), zap.String("path", aPath)) + + if aPath != "" { + app.Log.Info("custom web assets mounted", zap.String("url", url), zap.String("path", aPath)) + } else { + app.Log.Info("embedded web assets mounted", zap.String("url", url)) + } }() func() { @@ -171,30 +153,3 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) { r.Handle("/.well-known/openid-configuration", app.AuthService.WellKnownOpenIDConfiguration()) }() } - -func loadAssetsFromPath(path string) (assets fs.FS, err error) { - // at least favicon file should exist in the custom asset path - // otherwise we default to embedded files - const check = "favicon32x32.png" - - var ( - fi os.FileInfo - ) - - if fi, err = os.Stat(path); err != nil { - return - - } - - if !fi.IsDir() { - return nil, fmt.Errorf("expecting directory") - - } - - assets = os.DirFS(path) - if _, err = assets.Open(check); err != nil { - return nil, err - } - - return -} diff --git a/assets/embed.go b/assets/embed.go index 49a2a281e..646db83fc 100644 --- a/assets/embed.go +++ b/assets/embed.go @@ -2,9 +2,68 @@ package assets import ( "embed" + "fmt" + "io/fs" + "os" + + "go.uber.org/zap" ) var ( //go:embed src/* - Embedded embed.FS + ff embed.FS ) + +// Files return files from assets path or falls back to embedded files +// +// @todo try to find a way to merge this with auth/assets +func Files(log *zap.Logger, aPath string) (files fs.FS) { + var err error + if len(aPath) > 0 { + if files, err = fromPath(aPath); err != nil { + // log warning but fallback to embedded assets + log.Warn( + fmt.Sprintf("failed to use custom assets path (HTTP_SERVER_ASSETS_PATH=%s)", aPath), + zap.Error(err), + ) + } + } + + if files == nil { + aPath = "embedded" + files, err = fs.Sub(ff, "src") + if err != nil { + // something is seriously wrong, we might as well panic + panic(err) + } + } + + return +} + +func fromPath(path string) (assets fs.FS, err error) { + // at least favicon file should exist in the custom asset path + // otherwise we default to embedded files + const check = "favicon32x32.png" + + var ( + fi os.FileInfo + ) + + if fi, err = os.Stat(path); err != nil { + return + + } + + if !fi.IsDir() { + return nil, fmt.Errorf("expecting directory") + + } + + assets = os.DirFS(path) + if _, err = assets.Open(check); err != nil { + return nil, err + } + + return +} diff --git a/assets/src/api-404.html b/assets/src/api-404.html new file mode 100644 index 000000000..24404785e --- /dev/null +++ b/assets/src/api-404.html @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + Page not found | Corteza + + + +
+
+

404

+

+ It looks like the page you're looking for does not exist. +

+ + +
+ +
+ + + + diff --git a/assets/src/api-landing.html b/assets/src/api-landing.html new file mode 100644 index 000000000..24134a75b --- /dev/null +++ b/assets/src/api-landing.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + Corteza API + + + +
+
+

HEY

+

+ How are you? Thank you for stopping by, +
here is what you can do: +

+ + +
+ +
+ + + + diff --git a/pkg/api/server/handlers.go b/pkg/api/server/handlers.go index 3364d982c..6cf82f359 100644 --- a/pkg/api/server/handlers.go +++ b/pkg/api/server/handlers.go @@ -1,12 +1,15 @@ package server import ( + "bytes" "fmt" + "io" "net/http" "path" "strconv" "strings" + "github.com/cortezaproject/corteza-server/assets" "github.com/cortezaproject/corteza-server/pkg/api" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/errors" @@ -71,10 +74,14 @@ func shutdownRoutes() (r chi.Router) { } // routes used when in active mode -func activeRoutes(log *zap.Logger, mountable []func(r chi.Router), envOpt options.EnvironmentOpt, httpOpt options.HttpServerOpt) (r chi.Router) { +func activeRoutes(log *zap.Logger, mountable []func(r chi.Router), opts *options.Options) (r chi.Router) { r = chi.NewRouter() r.Use(handleCORS) + httpOpt := opts.HTTPServer + authOpt := opts.Auth + envOpt := opts.Environment + r.Route("/"+strings.TrimPrefix(httpOpt.BaseUrl, "/"), func(r chi.Router) { // Reports error to Sentry if enabled if httpOpt.EnablePanicReporting { @@ -120,6 +127,11 @@ func activeRoutes(log *zap.Logger, mountable []func(r chi.Router), envOpt option } mountServiceHandlers(r, log, httpOpt, active) + + r.HandleFunc(httpOpt.ApiBaseUrl, handleStaticPages(log, httpOpt, authOpt, "api-landing.html")) + r.HandleFunc(httpOpt.ApiBaseUrl+"/", handleStaticPages(log, httpOpt, authOpt, "api-landing.html")) + r.NotFound(handleStaticPages(log, httpOpt, authOpt, "api-404.html")) + return } @@ -252,3 +264,48 @@ func mountDebugLogViewer(r chi.Router, log *zap.Logger) { _, _ = logger.WriteLogBuffer(w, after, limit) }) } + +func handleStaticPages(log *zap.Logger, hOpt options.HttpServerOpt, aOpt options.AuthOpt, file string) http.HandlerFunc { + // "good-enough" for now, plan to move to templates when + // merging with auth + const linkTpl = `%s` + var ( + links = make([]string, 0) + buf []byte + + placeholder = []byte("") + ) + + links = append(links, fmt.Sprintf(linkTpl, aOpt.BaseURL, "Login")) + + if hOpt.ApiEnabled { + links = append(links, fmt.Sprintf(linkTpl, "https://docs.cortezaproject.org/", "Documentation")) + } + + if hOpt.WebConsoleEnabled { + links = append(links, fmt.Sprintf(linkTpl, "/console", "Console")) + } + + page, err := assets.Files(log, hOpt.AssetsPath).Open(file) + if err != nil { + log.Warn("could not open static page", zap.String("file", file), zap.Error(err)) + } + + buf, err = io.ReadAll(page) + if err != nil { + log.Warn("could not prepare static page", zap.String("file", file), zap.Error(err)) + } + + buf = bytes.ReplaceAll(buf, placeholder, []byte(strings.Join(links, ""))) + + return func(w http.ResponseWriter, r *http.Request) { + if page == nil { + // fallback to default 404 handler + http.NotFound(w, r) + return + } + + w.WriteHeader(http.StatusNotFound) + w.Write(buf) + } +} diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 0dd2f832c..89fb22c43 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -12,11 +12,9 @@ import ( type ( server struct { - log *zap.Logger - httpOpt options.HttpServerOpt - waitForOpt options.WaitForOpt - environmentOpt options.EnvironmentOpt - endpoints []func(r chi.Router) + log *zap.Logger + opts *options.Options + endpoints []func(r chi.Router) demux *demux } @@ -38,17 +36,15 @@ const ( // - /healthcheck // - /healthcheck -func New(log *zap.Logger, envOpt options.EnvironmentOpt, httpOpt options.HttpServerOpt, waitForOpt options.WaitForOpt) *server { +func New(log *zap.Logger, opts *options.Options) *server { s := &server{ endpoints: make([]func(r chi.Router), 0), log: log.Named("http"), - environmentOpt: envOpt, - httpOpt: httpOpt, - waitForOpt: waitForOpt, + opts: opts, } - s.demux = Demux(waiting, waitingRoutes(s.log.Named("waiting"), s.httpOpt)) + s.demux = Demux(waiting, waitingRoutes(s.log.Named("waiting"), s.opts.HTTPServer)) s.demux.Router(shutdown, shutdownRoutes()) return s @@ -56,7 +52,7 @@ func New(log *zap.Logger, envOpt options.EnvironmentOpt, httpOpt options.HttpSer // Activate reconfigures server to use active routes func (s *server) Activate(mm ...func(chi.Router)) { - s.demux.Router(active, activeRoutes(s.log, mm, s.environmentOpt, s.httpOpt)) + s.demux.Router(active, activeRoutes(s.log, mm, s.opts)) s.log.Debug("entering active state") s.demux.State(active) @@ -72,11 +68,11 @@ func (s server) Serve(ctx context.Context) { s.log.Info( "starting HTTP server", - zap.String("path-prefix", s.httpOpt.BaseUrl), - zap.String("address", s.httpOpt.Addr), + zap.String("path-prefix", s.opts.HTTPServer.BaseUrl), + zap.String("address", s.opts.HTTPServer.Addr), ) - listener, err := net.Listen("tcp", s.httpOpt.Addr) + listener, err := net.Listen("tcp", s.opts.HTTPServer.Addr) if err != nil { s.log.Error("cannot start server", zap.Error(err)) return diff --git a/pkg/api/server/waitfor.go b/pkg/api/server/waitfor.go index 025ef7ae4..df24de0e2 100644 --- a/pkg/api/server/waitfor.go +++ b/pkg/api/server/waitfor.go @@ -18,7 +18,7 @@ import ( // WaitFor sets up a simple status page, delays execution and probes services func (s server) WaitFor(ctx context.Context) { var ( - opt = s.waitForOpt + opt = s.opts.WaitFor services = opt.GetServices() ) @@ -37,7 +37,7 @@ func (s server) WaitFor(ctx context.Context) { ) // Setup a simple HTTP server that will inform the impatient users - listener, err := net.Listen("tcp", s.httpOpt.Addr) + listener, err := net.Listen("tcp", s.opts.HTTPServer.Addr) if err != nil { s.log.Error("cannot start server", zap.Error(err)) os.Exit(1)