Use unique static path for auth assets to avoid collision with routes

This commit is contained in:
Viktor Liu
2026-02-09 01:10:50 +08:00
parent 2f390e1794
commit 3b43c00d12
6 changed files with 70 additions and 36 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,12 +2,12 @@
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<link rel="icon" type="image/x-icon" href="/__netbird__/assets/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NetBird Service</title>
<meta name="robots" content="noindex, nofollow" />
<script type="module" crossorigin src="/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/assets/style.css">
<script type="module" crossorigin src="/__netbird__/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/__netbird__/assets/style.css">
</head>
<body>
<!-- Go template variables injected here -->

View File

@@ -5,6 +5,7 @@ import path from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
base: '/__netbird__/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),

View File

@@ -11,6 +11,10 @@ import (
"strings"
)
// PathPrefix is the unique URL prefix for serving the proxy's own web assets.
// Using a distinctive prefix prevents collisions with backend application routes.
const PathPrefix = "/__netbird__"
//go:embed dist/*
var files embed.FS
@@ -35,6 +39,38 @@ func init() {
tmpl, initErr = template.New("index").Parse(string(indexHTML))
}
// AssetHandler returns middleware that intercepts requests for the proxy's
// own web assets (under PathPrefix) and serves them from the embedded
// filesystem, preventing them from being forwarded to backend services.
func AssetHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, PathPrefix+"/") {
serveAsset(w, r)
return
}
next.ServeHTTP(w, r)
})
}
// serveAsset serves a static file from the embedded filesystem.
func serveAsset(w http.ResponseWriter, r *http.Request) {
if initErr != nil {
http.Error(w, initErr.Error(), http.StatusInternalServerError)
return
}
// Strip the prefix to get the embedded FS path (e.g. "assets/index.js").
filePath := strings.TrimPrefix(r.URL.Path, PathPrefix+"/")
content, err := fs.ReadFile(webFS, filePath)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
setContentType(w, filePath)
w.Write(content) //nolint:errcheck
}
// ServeHTTP serves the web UI. For static assets it serves them directly,
// for other paths it renders the page with the provided data.
// Optional statusCode can be passed to set a custom HTTP status code (default 200).
@@ -54,42 +90,19 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, data any, statusCode ...i
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write(content)
w.Write(content) //nolint:errcheck
return
}
// Serve static assets directly
if strings.HasPrefix(path, "/assets/") {
filePath := strings.TrimPrefix(path, "/")
content, err := fs.ReadFile(webFS, filePath)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
switch filepath.Ext(filePath) {
case ".js":
w.Header().Set("Content-Type", "application/javascript")
case ".css":
w.Header().Set("Content-Type", "text/css")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".ttf":
w.Header().Set("Content-Type", "font/ttf")
case ".woff":
w.Header().Set("Content-Type", "font/woff")
case ".woff2":
w.Header().Set("Content-Type", "font/woff2")
case ".ico":
w.Header().Set("Content-Type", "image/x-icon")
}
w.Write(content)
// Serve static assets directly (handles requests that reach here
// via auth middleware calling ServeHTTP for unauthenticated requests).
if strings.HasPrefix(path, PathPrefix+"/") {
serveAsset(w, r)
return
}
// Render the page with data
dataJSON, _ := json.Marshal(data)
dataJSON, _ := json.Marshal(data) //nolint:errcheck
var buf bytes.Buffer
if err := tmpl.Execute(&buf, struct {
@@ -105,7 +118,26 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, data any, statusCode ...i
if len(statusCode) > 0 {
w.WriteHeader(statusCode[0])
}
w.Write(buf.Bytes())
w.Write(buf.Bytes()) //nolint:errcheck
}
func setContentType(w http.ResponseWriter, filePath string) {
switch filepath.Ext(filePath) {
case ".js":
w.Header().Set("Content-Type", "application/javascript")
case ".css":
w.Header().Set("Content-Type", "text/css")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".ttf":
w.Header().Set("Content-Type", "font/ttf")
case ".woff":
w.Header().Set("Content-Type", "font/woff")
case ".woff2":
w.Header().Set("Content-Type", "font/woff2")
case ".ico":
w.Header().Set("Content-Type", "image/x-icon")
}
}
// ErrorStatus represents the connection status for each component in the error page.