Pure-stdlib Go implementation of the whole control plane: same nexusgate.json schema (unknown fields preserved verbatim, explicit null/false tri-states modeled), same HTTP API and error strings, embedded frontend (NG_PUBLIC_DIR override), self-implemented scrypt auth, X25519 Reality keys, panel-hosted keyStore and heartbeat reconciliation included. Version stays 0.6.8. Verified: Go tests 33/33 (scrypt + Reality vectors generated from the Node implementation, subscription goldens byte-identical), Node suite 52/52 untouched, differential E2E against the Node panel 50/50 including two-way DB swap. See panel-go/PARITY.md and panel-go/INSTALL-GO.md.
131 lines
3.1 KiB
Go
131 lines
3.1 KiB
Go
package main
|
|
|
|
// Static file serving: the Go port of lib/http.js serveStatic. Assets are
|
|
// embedded from embed_public/ (populated by build.sh from ../public);
|
|
// when NG_PUBLIC_DIR is set, files are read from that directory instead.
|
|
|
|
import (
|
|
"embed"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
//go:embed embed_public
|
|
var embeddedPublic embed.FS
|
|
|
|
const embedRoot = "embed_public"
|
|
|
|
var staticMIME = map[string]string{
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "text/javascript; charset=utf-8",
|
|
".css": "text/css; charset=utf-8",
|
|
".svg": "image/svg+xml",
|
|
".json": "application/json; charset=utf-8",
|
|
}
|
|
|
|
// ServeStatic serves pathname from the public root; returns false when no
|
|
// regular file matches (mirroring serveStatic's false), and an error for
|
|
// unexpected I/O failures (mirroring its throw).
|
|
func ServeStatic(w http.ResponseWriter, pathname string) (bool, error) {
|
|
if dir := os.Getenv("NG_PUBLIC_DIR"); dir != "" {
|
|
return serveStaticDisk(w, dir, pathname)
|
|
}
|
|
relative := "index.html"
|
|
if pathname != "/" {
|
|
relative = strings.TrimLeft(pathname, "/")
|
|
}
|
|
// Reject any path that would escape the root (JS path.resolve check).
|
|
if escapesRoot(relative) {
|
|
return false, nil
|
|
}
|
|
name := embedRoot + "/" + relative
|
|
info, err := fs.Stat(embeddedPublic, name)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return false, nil
|
|
}
|
|
body, err := fs.ReadFile(embeddedPublic, name)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
writeStatic(w, relative, body)
|
|
return true, nil
|
|
}
|
|
|
|
// escapesRoot reports whether a slash-separated relative path climbs above
|
|
// its root via ".." segments.
|
|
func escapesRoot(relative string) bool {
|
|
depth := 0
|
|
for _, seg := range strings.Split(relative, "/") {
|
|
switch seg {
|
|
case "..":
|
|
if depth == 0 {
|
|
return true
|
|
}
|
|
depth--
|
|
case "", ".":
|
|
default:
|
|
depth++
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func serveStaticDisk(w http.ResponseWriter, publicDir, pathname string) (bool, error) {
|
|
relative := "index.html"
|
|
if pathname != "/" {
|
|
relative = strings.TrimLeft(pathname, "/")
|
|
}
|
|
root := filepath.Clean(publicDir)
|
|
target := filepath.Join(root, filepath.FromSlash(relative))
|
|
if !strings.HasPrefix(target, root+string(filepath.Separator)) && target != filepath.Join(root, "index.html") {
|
|
return false, nil
|
|
}
|
|
info, err := os.Stat(target)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return false, nil
|
|
}
|
|
body, err := os.ReadFile(target)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
writeStatic(w, relative, body)
|
|
return true, nil
|
|
}
|
|
|
|
func writeStatic(w http.ResponseWriter, name string, body []byte) {
|
|
contentType := staticMIME[path.Ext(name)]
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
h := w.Header()
|
|
h.Set("Content-Type", contentType)
|
|
h.Set("Content-Length", strconv.Itoa(len(body)))
|
|
h.Set("Cache-Control", "no-store")
|
|
h.Set("X-Content-Type-Options", "nosniff")
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write(body)
|
|
}
|