Basepath cleanup & improvements
This commit is contained in:
@@ -68,7 +68,7 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
|
||||
return
|
||||
}
|
||||
|
||||
r.Route("/"+ho.WebappBaseUrl, webapp.MakeWebappServer(app.Log, ho, app.Opt.Auth))
|
||||
r.Route(options.CleanBase(ho.WebappBaseUrl), webapp.MakeWebappServer(app.Log, ho, app.Opt.Auth))
|
||||
|
||||
app.Log.Info(
|
||||
"client web applications enabled",
|
||||
@@ -87,7 +87,7 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) {
|
||||
return
|
||||
}
|
||||
|
||||
r.Route("/"+strings.TrimPrefix(ho.ApiBaseUrl, "/"), func(r chi.Router) {
|
||||
r.Route(options.CleanBase(ho.ApiBaseUrl), func(r chi.Router) {
|
||||
var fullpathAPI = "/" + strings.TrimPrefix(options.CleanBase(ho.BaseUrl, ho.ApiBaseUrl), "/")
|
||||
|
||||
app.Log.Info(
|
||||
|
||||
@@ -46,7 +46,7 @@ type (
|
||||
var BasePath string = "/"
|
||||
|
||||
func GetLinks() Links {
|
||||
var b = BasePath + "/"
|
||||
var b = strings.TrimSuffix(BasePath, "/") + "/"
|
||||
|
||||
return Links{
|
||||
Profile: b + "auth",
|
||||
|
||||
@@ -2,6 +2,11 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/api"
|
||||
"github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/healthcheck"
|
||||
@@ -10,10 +15,6 @@ import (
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"go.uber.org/zap"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -99,19 +100,13 @@ func (s server) Serve(ctx context.Context) {
|
||||
|
||||
})
|
||||
|
||||
if s.httpOpt.BaseUrl != "" {
|
||||
if s.httpOpt.BaseUrl != "/" {
|
||||
router.Handle("/", http.RedirectHandler(s.httpOpt.BaseUrl, http.StatusTemporaryRedirect))
|
||||
|
||||
}
|
||||
|
||||
s.bindMiscRoutes(router)
|
||||
|
||||
if s.httpOpt.EnableDebugRoute {
|
||||
router.Get("/__routes", debugRoutes(router))
|
||||
router.Get(s.httpOpt.BaseUrl+"/__routes", debugRoutes(router))
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
srv := http.Server{
|
||||
Handler: router,
|
||||
@@ -135,6 +130,9 @@ func (s server) Serve(ctx context.Context) {
|
||||
|
||||
func (s server) bindMiscRoutes(r chi.Router) {
|
||||
if s.httpOpt.EnableDebugRoute {
|
||||
s.log.Debug("route debugger enabled: /__routes")
|
||||
r.Get("/__routes", debugRoutes(r))
|
||||
|
||||
s.log.Debug("profiler enabled: /__profiler")
|
||||
r.Mount("/__profiler", middleware.Profiler())
|
||||
|
||||
|
||||
2
pkg/options/HTTPServer.gen.go
generated
2
pkg/options/HTTPServer.gen.go
generated
@@ -57,7 +57,7 @@ func HTTPServer() (o *HTTPServerOpt) {
|
||||
ApiBaseUrl: "/",
|
||||
WebappEnabled: false,
|
||||
WebappBaseUrl: "/",
|
||||
WebappBaseDir: "webapp/public",
|
||||
WebappBaseDir: "./webapp/public",
|
||||
WebappList: "admin,compose,workflow",
|
||||
SslTerminated: isSecure(),
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ props:
|
||||
default: "/"
|
||||
description: |-
|
||||
When webapps are enabled (HTTP_WEBAPP_ENABLED) this is moved to '/api' if not explicitly set otherwise.
|
||||
API base URL is prefixed with baseUrl
|
||||
API base URL is internaly prefixed with baseUrl
|
||||
|
||||
|
||||
- name: webappEnabled
|
||||
@@ -99,11 +99,11 @@ props:
|
||||
env: HTTP_WEBAPP_BASE_URL
|
||||
default: "/"
|
||||
description: |-
|
||||
Webapp base URL is prefixed with baseUrl
|
||||
Webapp base URL is internaly prefixed with baseUrl
|
||||
|
||||
- name: webappBaseDir
|
||||
env: HTTP_WEBAPP_BASE_DIR
|
||||
default: "webapp/public"
|
||||
default: "./webapp/public"
|
||||
|
||||
- name: webappList
|
||||
env: HTTP_WEBAPP_LIST
|
||||
|
||||
6
pkg/options/auth.gen.go
generated
6
pkg/options/auth.gen.go
generated
@@ -43,11 +43,11 @@ func Auth() (o *AuthOpt) {
|
||||
o = &AuthOpt{
|
||||
Secret: getSecretFromEnv("jwt secret"),
|
||||
Expiry: time.Hour * 24 * 30,
|
||||
ExternalRedirectURL: fullURL() + "/auth/external/{provider}/callback",
|
||||
ExternalRedirectURL: fullURL("/auth/external/{provider}/callback"),
|
||||
ExternalCookieSecret: getSecretFromEnv("external cookie secret"),
|
||||
BaseURL: fullURL() + "/auth",
|
||||
BaseURL: fullURL("/auth"),
|
||||
SessionCookieName: "session",
|
||||
SessionCookiePath: pathPrefix() + "/auth",
|
||||
SessionCookiePath: pathPrefix("/auth"),
|
||||
SessionCookieDomain: guessHostname(),
|
||||
SessionCookieSecure: isSecure(),
|
||||
SessionLifetime: 24 * time.Hour,
|
||||
|
||||
@@ -29,7 +29,7 @@ props:
|
||||
description: Experation time for the auth JWT tokens.
|
||||
|
||||
- name: externalRedirectURL
|
||||
default: fullURL() + "/auth/external/{provider}/callback"
|
||||
default: fullURL("/auth/external/{provider}/callback")
|
||||
description: |-
|
||||
Redirect URL to be sent with OAuth2 authentication request to provider
|
||||
|
||||
@@ -47,7 +47,7 @@ props:
|
||||
====
|
||||
|
||||
- name: baseURL
|
||||
default: fullURL() + "/auth"
|
||||
default: fullURL("/auth")
|
||||
description: |-
|
||||
Frontend base URL. Must be an absolute URL, with the domain.
|
||||
This is used for some redirects and links in auth emails.
|
||||
@@ -58,7 +58,7 @@ props:
|
||||
Session cookie name
|
||||
|
||||
- name: sessionCookiePath
|
||||
default: pathPrefix() + "/auth"
|
||||
default: pathPrefix("/auth")
|
||||
description: |-
|
||||
Session cookie path
|
||||
|
||||
|
||||
@@ -82,12 +82,12 @@ func guessHostname() string {
|
||||
}
|
||||
|
||||
// returns path prefix
|
||||
func pathPrefix() string {
|
||||
return CleanBase(EnvString("HTTP_BASE_URL", ""))
|
||||
func pathPrefix(pp ...string) string {
|
||||
return CleanBase(append([]string{EnvString("HTTP_BASE_URL", "")}, pp...)...)
|
||||
}
|
||||
|
||||
// will return base URL with domain and prefix path
|
||||
func fullURL() string {
|
||||
func fullURL(pp ...string) string {
|
||||
var (
|
||||
full string
|
||||
host = guessHostname()
|
||||
@@ -99,8 +99,8 @@ func fullURL() string {
|
||||
full = "https://" + host
|
||||
}
|
||||
|
||||
if pp := pathPrefix(); pp != "" {
|
||||
return full + pp
|
||||
if pfix := pathPrefix(pp...); pfix != "" {
|
||||
return full + pfix
|
||||
}
|
||||
|
||||
return full
|
||||
@@ -118,12 +118,12 @@ func isSecure() bool {
|
||||
|
||||
// Path joins all parts with / and prefixes result (if not empty) with /
|
||||
func CleanBase(pp ...string) string {
|
||||
if p := strings.Trim(path.Join(pp...), "/"); p != "" {
|
||||
// prefix base with slash
|
||||
return "/" + p
|
||||
}
|
||||
//if p := strings.Trim(path.Join(pp...), "/"); p != "" {
|
||||
// // prefix base with slash
|
||||
// return "/" + p
|
||||
//}
|
||||
|
||||
return ""
|
||||
return "/" + strings.Trim(path.Join(pp...), "/")
|
||||
}
|
||||
|
||||
func EnvString(key string, def string) string {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza-server/pkg/options"
|
||||
@@ -17,10 +17,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
htmlIndex struct {
|
||||
body []byte
|
||||
mod time.Time
|
||||
}
|
||||
baseHrefMatcher = regexp.MustCompile(`<base\s+href="?.+?"?\s*\/?>`)
|
||||
)
|
||||
|
||||
func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HTTPServerOpt, authOpt options.AuthOpt) func(r chi.Router) {
|
||||
@@ -36,12 +33,7 @@ func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HTTPServerOpt, authOpt
|
||||
|
||||
// Preload index files for all apps
|
||||
for _, app := range append(apps, "") {
|
||||
pathPrefix := path.Join(httpSrvOpt.BaseUrl, httpSrvOpt.WebappBaseUrl, app)
|
||||
if !strings.HasSuffix(pathPrefix, "/") {
|
||||
pathPrefix += "/"
|
||||
}
|
||||
|
||||
appIndexHTMLs[app], err = modifyIndexHTML(path.Join(httpSrvOpt.WebappBaseDir, app), pathPrefix)
|
||||
appIndexHTMLs[app], err = modifyIndexHTML(app, httpSrvOpt.WebappBaseDir, httpSrvOpt.BaseUrl)
|
||||
if err != nil {
|
||||
log.Error("could not preload application index HTML", zap.Error(err))
|
||||
}
|
||||
@@ -49,31 +41,32 @@ func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HTTPServerOpt, authOpt
|
||||
|
||||
// Serves static files directly from FS
|
||||
return func(r chi.Router) {
|
||||
fileserver := http.StripPrefix(
|
||||
path.Join(httpSrvOpt.BaseUrl, httpSrvOpt.WebappBaseUrl),
|
||||
fs := http.StripPrefix(
|
||||
options.CleanBase(httpSrvOpt.BaseUrl, httpSrvOpt.WebappBaseUrl),
|
||||
http.FileServer(http.Dir(httpSrvOpt.WebappBaseDir)),
|
||||
)
|
||||
|
||||
for _, app := range apps {
|
||||
webBaseUrl = "/" + path.Join(httpSrvOpt.WebappBaseUrl, app)
|
||||
webBaseUrl = options.CleanBase(httpSrvOpt.WebappBaseUrl, app)
|
||||
serveConfig(r, webBaseUrl, apiBaseUrl, authOpt.BaseURL, httpSrvOpt.BaseUrl)
|
||||
|
||||
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[app], fileserver))
|
||||
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[app], fs))
|
||||
}
|
||||
|
||||
webBaseUrl = "/" + path.Join(httpSrvOpt.WebappBaseUrl)
|
||||
webBaseUrl = options.CleanBase(httpSrvOpt.WebappBaseUrl)
|
||||
serveConfig(r, webBaseUrl, apiBaseUrl, authOpt.BaseURL, httpSrvOpt.BaseUrl)
|
||||
|
||||
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[""], fileserver))
|
||||
r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[""], fs))
|
||||
}
|
||||
}
|
||||
|
||||
// Serves index.html in case the requested file isn't found (or some other os.Stat error)
|
||||
func serveIndex(opt options.HTTPServerOpt, indexHTML []byte, serve http.Handler) http.HandlerFunc {
|
||||
//indexPage := path.Join(opt.WebappBaseDir, indexPath, "index.html")
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
requestedFile := path.Join(opt.WebappBaseDir, strings.TrimPrefix(r.URL.Path, opt.BaseUrl))
|
||||
|
||||
requestedFile := path.Join(
|
||||
// could be relative path
|
||||
opt.WebappBaseDir,
|
||||
strings.TrimPrefix(r.URL.Path, opt.BaseUrl),
|
||||
)
|
||||
|
||||
f, err := os.Stat(requestedFile)
|
||||
// When file does not exist or is a directory, serve app's index
|
||||
@@ -102,7 +95,7 @@ func serveIndex(opt options.HTTPServerOpt, indexHTML []byte, serve http.Handler)
|
||||
}
|
||||
|
||||
func serveConfig(r chi.Router, appUrl, apiBaseUrl, authBaseUrl, webappBaseUrl string) {
|
||||
r.Get(strings.TrimSuffix(appUrl, "/")+"/config.js", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Get(options.CleanBase(appUrl, "config.js"), func(w http.ResponseWriter, r *http.Request) {
|
||||
const line = "window.%s = '%s';\n"
|
||||
_, _ = fmt.Fprintf(w, line, "CortezaAPI", apiBaseUrl)
|
||||
_, _ = fmt.Fprintf(w, line, "CortezaAuth", authBaseUrl)
|
||||
@@ -112,28 +105,30 @@ func serveConfig(r chi.Router, appUrl, apiBaseUrl, authBaseUrl, webappBaseUrl st
|
||||
|
||||
// Reads and modifies index HTML for the webapp
|
||||
//
|
||||
// It replaces <base> tag with the exact location of the web app
|
||||
func modifyIndexHTML(dir, baseHref string) (buf []byte, err error) {
|
||||
var (
|
||||
warning = []byte("\n\n<!--\n\nError!\n\nFailed could not locate or modify <base> tag, your webapp might misbehave\n\n-->\n")
|
||||
placeholder = []byte("<base href=/ >")
|
||||
replacement = []byte(fmt.Sprintf(`<base href="%s" />`, baseHref))
|
||||
fh *os.File
|
||||
)
|
||||
|
||||
fh, err = os.Open(path.Join(dir, "index.html"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf, err = io.ReadAll(fh)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if bytes.Contains(buf, placeholder) {
|
||||
return bytes.Replace(buf, placeholder, replacement, 1), nil
|
||||
// It replaces value for href in <base> tag with the virtual location of the web app
|
||||
// This is controlled with HTTP_WEBAPP_BASE_URL
|
||||
func modifyIndexHTML(app, dir, baseHref string) ([]byte, error) {
|
||||
if fh, err := os.Open(path.Join(dir, app, "index.html")); err != nil {
|
||||
return nil, err
|
||||
} else if buf, err := io.ReadAll(fh); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return append(buf, warning...), nil
|
||||
return replaceBaseHrefPlaceholder(buf, app, baseHref), nil
|
||||
}
|
||||
}
|
||||
|
||||
func replaceBaseHrefPlaceholder(buf []byte, app, baseHref string) []byte {
|
||||
var (
|
||||
base = strings.TrimSuffix(options.CleanBase(baseHref, app), "/") + "/"
|
||||
|
||||
warning = []byte(`\n\n<!--\n\nError!\n\nFailed could not locate or modify <base> tag, your webapp might misbehave\n\n-->\n`)
|
||||
replacement = []byte(fmt.Sprintf(`<base href="%s" />`, base))
|
||||
fixed = baseHrefMatcher.ReplaceAll(buf, replacement)
|
||||
)
|
||||
|
||||
if bytes.Equal(buf, fixed) {
|
||||
return append(buf, warning...)
|
||||
}
|
||||
|
||||
return fixed
|
||||
}
|
||||
|
||||
41
pkg/webapp/serve_test.go
Normal file
41
pkg/webapp/serve_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package webapp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_replaceBaseHrefPlaceholder(t *testing.T) {
|
||||
var (
|
||||
tcc = []struct {
|
||||
name string
|
||||
app string
|
||||
base string
|
||||
body string
|
||||
}{
|
||||
{"unquoted condensed", "test-app", "", `... <base href=/> ... `},
|
||||
{"unquoted spacy", "test-app", "", `... <base href=/ > ... `},
|
||||
{"quoted spacy", "test-app", "", `... <base href="/" ... > `},
|
||||
{"preset", "test-app", "", `... <base href="/foo" > ... `},
|
||||
{"unquoted condensed", "test-app", "base", `... <base href=/> ... `},
|
||||
{"unquoted spacy", "test-app", "base", `... <base href=/ > ... `},
|
||||
{"quoted spacy", "test-app", "base", `... <base href="/" ... > `},
|
||||
{"preset", "test-app", "base", `... <base href="/foo" > ... `},
|
||||
}
|
||||
)
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
fixed = string(replaceBaseHrefPlaceholder([]byte(tc.body), tc.app, tc.base))
|
||||
)
|
||||
|
||||
req.NotContains(fixed, "Error!")
|
||||
req.Contains(fixed, "<base href=")
|
||||
req.Contains(fixed, tc.app)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user