feat: 建立 Sense Windows 交付包 (#70)
This commit is contained in:
@@ -92,7 +92,7 @@ func run() error {
|
||||
for _, db := range sdk.Runtime.GetDb() {
|
||||
runtimeDBFound = true
|
||||
if err := media.StartRuntime(runtimeCtx, db); err != nil {
|
||||
log.Errorf("MediaMTX runtime unavailable: %v", err)
|
||||
return fmt.Errorf("MediaMTX runtime unavailable: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -196,5 +196,6 @@ func initRouter() {
|
||||
Use(api.SetRequestLogger)
|
||||
|
||||
common.InitMiddleware(r)
|
||||
configureWebUI(r)
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// configureWebUI adds an optional SPA fallback to the existing GoAdmin Gin
|
||||
// engine. API and framework routes keep their normal handlers; the fallback is
|
||||
// enabled only for Windows delivery packages that set SENSE_WEB_ROOT.
|
||||
func configureWebUI(r *gin.Engine) {
|
||||
root := strings.TrimSpace(os.Getenv("SENSE_WEB_ROOT"))
|
||||
if root == "" {
|
||||
return
|
||||
}
|
||||
absRoot, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
index := filepath.Join(absRoot, "index.html")
|
||||
if info, statErr := os.Stat(index); statErr != nil || info.IsDir() {
|
||||
return
|
||||
}
|
||||
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if isBackendPath(c.Request.URL.Path) {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
requested := filepath.Clean(filepath.FromSlash(strings.TrimPrefix(c.Request.URL.Path, "/")))
|
||||
if requested == "." {
|
||||
requested = ""
|
||||
}
|
||||
candidate := filepath.Join(absRoot, requested)
|
||||
if withinRoot(absRoot, candidate) {
|
||||
if info, statErr := os.Stat(candidate); statErr == nil && !info.IsDir() {
|
||||
c.File(candidate)
|
||||
return
|
||||
}
|
||||
}
|
||||
if filepath.Ext(requested) != "" {
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.File(index)
|
||||
})
|
||||
}
|
||||
|
||||
func withinRoot(root, candidate string) bool {
|
||||
rel, err := filepath.Rel(root, candidate)
|
||||
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
||||
}
|
||||
|
||||
func isBackendPath(path string) bool {
|
||||
for _, prefix := range []string{"/api/", "/swagger/", "/static/", "/form-generator/"} {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestConfigureWebUIServesAssetsAndSPAFallback(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("sense-index"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(root, "js"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "js", "app.js"), []byte("sense-app"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("SENSE_WEB_ROOT", root)
|
||||
r := gin.New()
|
||||
configureWebUI(r)
|
||||
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
code int
|
||||
body string
|
||||
}{
|
||||
{path: "/", code: http.StatusOK, body: "sense-index"},
|
||||
{path: "/device/list", code: http.StatusOK, body: "sense-index"},
|
||||
{path: "/js/app.js", code: http.StatusOK, body: "sense-app"},
|
||||
{path: "/js/missing.js", code: http.StatusNotFound},
|
||||
{path: "/api/v1/missing", code: http.StatusNotFound},
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
|
||||
res := httptest.NewRecorder()
|
||||
r.ServeHTTP(res, req)
|
||||
if res.Code != tc.code || (tc.body != "" && res.Body.String() != tc.body) {
|
||||
t.Fatalf("%s: got %d %q", tc.path, res.Code, res.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithinRootRejectsTraversal(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if withinRoot(root, filepath.Join(root, "..", "secret.txt")) {
|
||||
t.Fatal("path traversal must be rejected")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user