fix(engineering-dashboard): drop startup token for personal local use

Rely on loopback bind plus Host/Origin checks; open http://127.0.0.1:8787/
without a query token after SSH tunnel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-17 09:46:26 +00:00
co-authored by Cursor
parent 37b1aaa3ae
commit aba38ef7a9
5 changed files with 27 additions and 86 deletions
+1 -1
View File
@@ -91,7 +91,7 @@ func runServe(args []string) error {
if err != nil {
return err
}
fmt.Printf("engineering-dashboard listening on http://%s/?token=%s\n", addr, srv.Token)
fmt.Printf("engineering-dashboard listening on http://%s/\n", addr)
fmt.Printf("repo=%s\n", root)
return engineeringdashboard.ListenAndServe(addr, srv.Handler())
}
+2 -20
View File
@@ -1,13 +1,4 @@
(function () {
function resolveToken() {
const fromURL = new URL(window.location.href).searchParams.get("token");
if (fromURL) return fromURL;
const meta = document.querySelector('meta[name="engineering-dashboard-token"]');
const fromMeta = meta && meta.getAttribute("content");
if (fromMeta && fromMeta !== "__TOKEN__") return fromMeta;
return "";
}
const token = resolveToken();
const state = {
snap: null,
view: "overview",
@@ -33,7 +24,7 @@
function syncURL() {
const u = new URL(window.location.href);
if (token) u.searchParams.set("token", token);
u.searchParams.delete("token");
u.searchParams.set("view", state.view);
if (state.q) u.searchParams.set("q", state.q); else u.searchParams.delete("q");
if (state.selectedDoc) u.searchParams.set("doc", state.selectedDoc); else u.searchParams.delete("doc");
@@ -52,13 +43,8 @@
}
async function fetchSnapshot(force) {
if (!token) {
el.status.textContent = "缺少访问令牌";
renderHardError("缺少 token:请使用服务启动时打印的完整 URL(含 ?token=)打开页面");
return false;
}
el.status.textContent = state.snap ? "刷新中…" : "首次加载中…";
const url = "/api/v1/snapshot?token=" + encodeURIComponent(token) + (force ? "&refresh=1" : "");
const url = "/api/v1/snapshot" + (force ? "?refresh=1" : "");
const headers = {};
if (state.snap && state.snap.revision_id) headers["If-None-Match"] = '"' + state.snap.revision_id + '"';
const res = await fetch(url, { headers, credentials: "same-origin" });
@@ -294,10 +280,6 @@
async function loop() {
loadURL();
if (!token) {
renderHardError("缺少 token:请使用服务启动时打印的完整 URL(含 ?token=)打开页面");
return;
}
try {
const changed = await fetchSnapshot(true);
if (changed || state.snap) render();
@@ -3,9 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="engineering-dashboard-token" content="__TOKEN__">
<title>个人工程中控台</title>
<link rel="stylesheet" href="/assets/app.css?token=__TOKEN__">
<link rel="stylesheet" href="/assets/app.css">
</head>
<body>
<a class="skip" href="#main">跳到主内容</a>
@@ -38,6 +37,6 @@
<div id="content"></div>
</main>
</div>
<script src="/assets/app.js?token=__TOKEN__"></script>
<script src="/assets/app.js"></script>
</body>
</html>
+4 -31
View File
@@ -1,8 +1,6 @@
package engineeringdashboard
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net"
@@ -13,27 +11,15 @@ import (
)
// Server is the local read-only HTTP surface.
// Access control is loopback bind + Host/Origin checks (personal local tool; no startup token).
type Server struct {
Collector *Collector
Token string
mu sync.Mutex
}
// NewServer creates a server with a random startup token.
// NewServer creates a local read-only server.
func NewServer(c *Collector) (*Server, error) {
tok, err := randomToken(16)
if err != nil {
return nil, err
}
return &Server{Collector: c, Token: tok}, nil
}
func randomToken(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
return &Server{Collector: c}, nil
}
// Handler returns the HTTP handler.
@@ -76,17 +62,6 @@ func (s *Server) secure(next http.Handler) http.Handler {
http.Error(w, "cross-site fetch denied", http.StatusForbidden)
return
}
// token required for API and page (query or header)
if strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/" || strings.HasPrefix(r.URL.Path, "/assets") {
tok := r.URL.Query().Get("token")
if tok == "" {
tok = r.Header.Get("X-Dashboard-Token")
}
if tok != s.Token {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
}
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; img-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "no-referrer")
@@ -117,7 +92,6 @@ func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {
return
}
} else {
// soft refresh if TTL expired
if time.Since(snap.GeneratedAt) > s.Collector.TTL {
go func() { _, _ = s.Collector.Refresh(ctx) }()
}
@@ -176,9 +150,8 @@ func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500)
return
}
html := strings.ReplaceAll(string(b), "__TOKEN__", s.Token)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(html))
_, _ = w.Write(b)
case "/assets/app.js":
b, err := readAsset("assets/app.js")
if err != nil {
+17 -30
View File
@@ -22,7 +22,7 @@ func TestServer_RejectsNonGETAndBadHost(t *testing.T) {
}
h := srv.Handler()
req := httptest.NewRequest(http.MethodPost, "/api/v1/snapshot?token="+srv.Token, nil)
req := httptest.NewRequest(http.MethodPost, "/api/v1/snapshot", nil)
req.Host = "127.0.0.1:8787"
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
@@ -30,7 +30,7 @@ func TestServer_RejectsNonGETAndBadHost(t *testing.T) {
t.Fatalf("POST: %d", rr.Code)
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot?token="+srv.Token, nil)
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot", nil)
req.Host = "evil.example"
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
@@ -39,7 +39,7 @@ func TestServer_RejectsNonGETAndBadHost(t *testing.T) {
}
}
func TestServer_TokenAndSnapshotIdentity(t *testing.T) {
func TestServer_SnapshotWithoutToken(t *testing.T) {
c := NewCollector(t.TempDir(), &FakeRunner{Map: map[string]FakeResult{}})
snap := Snapshot{
SchemaVersion: SchemaVersion, GenerationID: "gen_9", RevisionID: "rev_abc",
@@ -59,16 +59,8 @@ func TestServer_TokenAndSnapshotIdentity(t *testing.T) {
req.Host = "127.0.0.1:8787"
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusUnauthorized {
t.Fatalf("no token: %d", rr.Code)
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot?token="+srv.Token, nil)
req.Host = "127.0.0.1:8787"
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != 200 {
t.Fatalf("snapshot: %d %s", rr.Code, rr.Body.String())
t.Fatalf("snapshot without token: %d %s", rr.Code, rr.Body.String())
}
body := rr.Body.String()
if !strings.Contains(body, `"generation_id": "gen_9"`) || !strings.Contains(body, `"revision_id": "rev_abc"`) {
@@ -78,7 +70,7 @@ func TestServer_TokenAndSnapshotIdentity(t *testing.T) {
t.Fatalf("etag %q", rr.Header().Get("ETag"))
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot?token="+srv.Token, nil)
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot", nil)
req.Host = "127.0.0.1:8787"
req.Header.Set("If-None-Match", `"rev_abc"`)
rr = httptest.NewRecorder()
@@ -96,7 +88,7 @@ func TestServer_DocPathTraversalDenied(t *testing.T) {
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/api/v1/doc/not-a-real.md?token="+srv.Token, nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/doc/not-a-real.md", nil)
req.Host = "127.0.0.1:8787"
req.URL.Path = "/api/v1/doc/../../.env"
rr := httptest.NewRecorder()
@@ -119,7 +111,7 @@ func TestServer_CrossOriginDenied(t *testing.T) {
c.current = &Snapshot{RevisionID: "r", GenerationID: "g", GeneratedAt: time.Now().UTC()}
c.mu.Unlock()
srv, _ := NewServer(c)
req := httptest.NewRequest(http.MethodGet, "/api/v1/snapshot?token="+srv.Token, nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/snapshot", nil)
req.Host = "127.0.0.1:8787"
req.Header.Set("Origin", "https://evil.example")
rr := httptest.NewRecorder()
@@ -129,7 +121,7 @@ func TestServer_CrossOriginDenied(t *testing.T) {
}
}
func TestServer_IndexUsesMetaTokenNoInlineScript(t *testing.T) {
func TestServer_IndexHasNoTokenMachinery(t *testing.T) {
c := NewCollector(t.TempDir(), &FakeRunner{})
c.mu.Lock()
c.current = &Snapshot{RevisionID: "r", GenerationID: "g", GeneratedAt: time.Now().UTC()}
@@ -138,7 +130,7 @@ func TestServer_IndexUsesMetaTokenNoInlineScript(t *testing.T) {
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/?token="+srv.Token, nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Host = "127.0.0.1:8787"
rr := httptest.NewRecorder()
srv.Handler().ServeHTTP(rr, req)
@@ -146,25 +138,20 @@ func TestServer_IndexUsesMetaTokenNoInlineScript(t *testing.T) {
t.Fatalf("index: %d %s", rr.Code, rr.Body.String())
}
body := rr.Body.String()
if strings.Contains(body, "window.__DASHBOARD_TOKEN__") {
t.Fatal("index must not use inline token script (CSP script-src 'self')")
for _, banned := range []string{"__DASHBOARD_TOKEN__", "engineering-dashboard-token", "?token="} {
if strings.Contains(body, banned) {
t.Fatalf("index still contains %q", banned)
}
meta := `name="engineering-dashboard-token" content="` + srv.Token + `"`
if !strings.Contains(body, meta) {
t.Fatalf("missing token meta: %s", body[:min(400, len(body))])
}
if !strings.Contains(rr.Header().Get("Content-Security-Policy"), "script-src 'self'") {
t.Fatalf("unexpected CSP: %s", rr.Header().Get("Content-Security-Policy"))
}
// app.js must not reference the removed global
js, err := readAsset("assets/app.js")
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(js), "__DASHBOARD_TOKEN__") {
t.Fatal("app.js still references __DASHBOARD_TOKEN__")
jsBody := string(js)
if strings.Contains(jsBody, "token=") || strings.Contains(jsBody, "__DASHBOARD_TOKEN__") {
t.Fatal("app.js still references token auth")
}
if !strings.Contains(string(js), "engineering-dashboard-token") {
t.Fatal("app.js should read meta engineering-dashboard-token")
if !strings.Contains(rr.Header().Get("Content-Security-Policy"), "script-src 'self'") {
t.Fatalf("unexpected CSP: %s", rr.Header().Get("Content-Security-Policy"))
}
}