diff --git a/common/spa_handler/handler.go b/common/spa_handler/handler.go new file mode 100644 index 000000000..11d310eba --- /dev/null +++ b/common/spa_handler/handler.go @@ -0,0 +1,82 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package spa_handler + +import ( + "net/http" + "os" + "path/filepath" + "strings" +) + +type SinglePageAppHandler struct { + HttpHandler http.Handler + BindingKey string +} + +func (spa *SinglePageAppHandler) Binding() string { + return spa.BindingKey +} + +func (spa *SinglePageAppHandler) Options() map[interface{}]interface{} { + return nil +} + +func (spa *SinglePageAppHandler) RootPath() string { + return "/" + spa.BindingKey +} + +func (spa *SinglePageAppHandler) IsHandler(r *http.Request) bool { + return strings.HasPrefix(r.URL.Path, spa.RootPath()) || strings.HasPrefix(r.URL.Path, "/assets") +} + +func (spa *SinglePageAppHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + spa.HttpHandler.ServeHTTP(writer, request) +} + +// Thanks to https://github.com/roberthodgen/spa-server +// Serve from a public directory with specific index +type spaHandler struct { + content string // The directory from which to serve + contextRoot string // The context root to remove + indexFile string // The fallback/default file to serve +} + +// Falls back to a supplied index (indexFile) when either condition is true: +// (1) Request (file) path is not found +// (2) Request path is a directory +// Otherwise serves the requested file. +func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.URL.Path = strings.TrimPrefix(r.URL.Path, h.contextRoot) + p := filepath.Join(h.content, filepath.Clean(r.URL.Path)) + + if info, err := os.Stat(p); err != nil { + http.ServeFile(w, r, filepath.Join(h.content, h.indexFile)) + return + } else if info.IsDir() { + http.ServeFile(w, r, filepath.Join(h.content, h.indexFile)) + return + } + + http.ServeFile(w, r, p) +} + +// SpaHandler returns a request handler (http.Handler) that serves a single +// page application from a given public directory (location). +func SpaHandler(location string, contextRoot string, indexFile string) http.Handler { + return &spaHandler{location, contextRoot, indexFile} +} diff --git a/controller/controller.go b/controller/controller.go index 8b9058800..aefec258a 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -28,6 +28,7 @@ import ( "github.com/openziti/ziti/controller/event" "github.com/openziti/ziti/controller/events" "github.com/openziti/ziti/controller/handler_peer_ctrl" + "github.com/openziti/ziti/controller/zac" "math/big" "os" "sync/atomic" @@ -247,6 +248,10 @@ func (c *Controller) initWeb() { logrus.WithError(err).Fatalf("failed to create metrics api factory") } + if err := c.xweb.GetRegistry().Add(zac.NewZitiAdminConsoleFactory()); err != nil { + logrus.WithError(err).Fatalf("failed to create single page application factory") + } + } func (c *Controller) Run() error { diff --git a/controller/internal/routes/version_router.go b/controller/internal/routes/version_router.go index be613c91f..9b2663d3c 100644 --- a/controller/internal/routes/version_router.go +++ b/controller/internal/routes/version_router.go @@ -22,12 +22,12 @@ import ( clientInformational "github.com/openziti/edge-api/rest_client_api_server/operations/informational" managementInformational "github.com/openziti/edge-api/rest_management_api_server/operations/informational" "github.com/openziti/edge-api/rest_model" + "github.com/openziti/xweb/v2" + "github.com/openziti/ziti/common/build" "github.com/openziti/ziti/controller" "github.com/openziti/ziti/controller/env" "github.com/openziti/ziti/controller/internal/permissions" "github.com/openziti/ziti/controller/response" - "github.com/openziti/ziti/common/build" - "github.com/openziti/xweb/v2" "runtime" "sync" ) diff --git a/controller/zac/factory.go b/controller/zac/factory.go new file mode 100644 index 000000000..be2721433 --- /dev/null +++ b/controller/zac/factory.go @@ -0,0 +1,46 @@ +package zac + +import ( + "github.com/openziti/xweb/v2" + "github.com/openziti/ziti/common/spa_handler" + log "github.com/sirupsen/logrus" +) + +const ( + Binding = "zac" +) + +type ZitiAdminConsoleFactory struct { +} + +var _ xweb.ApiHandlerFactory = &ZitiAdminConsoleFactory{} + +func NewZitiAdminConsoleFactory() *ZitiAdminConsoleFactory { + return &ZitiAdminConsoleFactory{} +} + +func (factory ZitiAdminConsoleFactory) Validate(*xweb.InstanceConfig) error { + return nil +} + +func (factory ZitiAdminConsoleFactory) Binding() string { + return Binding +} + +func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) { + loc := options["location"] + if loc == nil || loc == "" { + log.Fatal("location must be supplied in " + Binding + " options") + } + indexFile := options["indexFile"] + if indexFile == nil || indexFile == "" { + indexFile = "index.html" + } + spa := &spa_handler.SinglePageAppHandler{ + HttpHandler: spa_handler.SpaHandler(loc.(string), "/"+Binding, indexFile.(string)), + BindingKey: Binding, + } + + log.Infof("initializing ZAC SPA Handler from %s", loc) + return spa, nil +} diff --git a/ziti/cmd/create/config_templates/controller.yml b/ziti/cmd/create/config_templates/controller.yml index cd421a677..d476983e9 100644 --- a/ziti/cmd/create/config_templates/controller.yml +++ b/ziti/cmd/create/config_templates/controller.yml @@ -215,3 +215,5 @@ web: options: { } - binding: fabric options: { } +# - binding: zac +# options: { "location": "./zac", "indexFile":"index.html" }