mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(webapp): the hub 404s a root file it does not have (BEA-148) (#184)
GET /llms.txt answered 200 with the sign-in page. So did /robots.txt, /sitemap.xml and every mistyped root file: the SPA fallback treated only api/, auth/ and s/ as genuine 404s, so a crawler probing any conventional path got a success status and a chunk of login HTML. In hub mode a first path segment is a project id (UUID or p-xxxxxxxx) or a reserved word, none of which contain a dot, so a single dotted segment with no embedded asset can only be a file that is not there. The check runs after the asset lookup, so a real root asset (share-mermaid.js today, favicon.ico whenever the build emits one) is unaffected with no allowlist to maintain. Gated on hub mode: the plain-folder viewer shares this handler and there /README.md IS the route for a file, so ungating this would make every top-level file in every `bdrive serve <dir>` unreachable. There is a test that fails if the gate is removed. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
20f59352ca
commit
eef37fa89b
@@ -689,3 +689,55 @@ func seccfgMedian(d []time.Duration) time.Duration {
|
||||
sort.Slice(c, func(i, j int) bool { return c[i] < c[j] })
|
||||
return c[len(c)/2]
|
||||
}
|
||||
|
||||
// TestFrontendRootDottedPathsAre404: a root-level path shaped like a file
|
||||
// (/llms.txt, /robots.txt) that matches no embedded asset must 404 rather than
|
||||
// answer the app shell — a soft 200 of login HTML told every crawler probing a
|
||||
// conventional path that the file exists. Deeper dots are real client routes.
|
||||
func TestFrontendRootDottedPathsAre404(t *testing.T) {
|
||||
h, _, _, p := permHub(t)
|
||||
|
||||
for _, target := range []string{"/llms.txt", "/robots.txt", "/sitemap.xml", "/nope.json"} {
|
||||
rec := seccfgRaw(t, h, target)
|
||||
if rec.Code != 404 {
|
||||
t.Errorf("GET %s: %d, want 404 (got %s) — the SPA fallback still masks a missing root file",
|
||||
target, rec.Code, rec.Header().Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
// A root-level dotted path that IS an embedded asset keeps being served:
|
||||
// the rule is "no embedded asset", not "has a dot", because the check runs
|
||||
// after the asset lookup. share-mermaid.js is the live case (the /s/ share
|
||||
// pages import it); a favicon.ico would be the next one, with no allowlist
|
||||
// to remember to update.
|
||||
rec := seccfgRaw(t, h, "/share-mermaid.js")
|
||||
if rec.Code != 200 {
|
||||
t.Errorf("GET /share-mermaid.js: %d, want 200 — the dotted-path 404 is deciding on the URL "+
|
||||
"instead of on whether the asset exists, and share pages just lost their mermaid renderer", rec.Code)
|
||||
}
|
||||
|
||||
// Everything that is a genuine client route still resolves to the shell.
|
||||
// /index.html is here because the asset block skips it deliberately and
|
||||
// falls through: without the explicit exclusion the rule above would 404 it.
|
||||
for _, target := range []string{"/", "/index.html", "/" + p.ID, "/" + p.ID + "/dashboard", "/" + p.ID + "/notes/index.md"} {
|
||||
rec := seccfgRaw(t, h, target)
|
||||
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "<div id=\"root\">") {
|
||||
t.Errorf("GET %s: %d — a client route stopped resolving to the app shell", target, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFrontendVolumeModeServesRootFiles: the dotted-root-path 404 above is
|
||||
// hub-only reasoning. The plain-folder viewer shares this handler, and there
|
||||
// /README.md IS the route for a file (router.ts parsePath, non-hub branch), so
|
||||
// the rule is gated on hub mode. THIS TEST FAILS IF THAT GATE IS REMOVED —
|
||||
// ungated, the fix 404s every top-level file in every `bdrive serve <dir>`.
|
||||
func TestFrontendVolumeModeServesRootFiles(t *testing.T) {
|
||||
h := dirServer(t, map[string]string{"README.md": "# Local"})
|
||||
|
||||
rec := seccfgRaw(t, h, "/README.md")
|
||||
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "<div id=\"root\">") {
|
||||
t.Errorf("GET /README.md in volume mode: %d — the hub-mode gate on the dotted-path 404 is gone; "+
|
||||
"every root-level file in a plain-folder viewer is now unreachable", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -906,8 +906,9 @@ func (s *Server) Handler() http.Handler {
|
||||
|
||||
// frontend serves the embedded single-page app. Real asset files (app.js,
|
||||
// style.css) are served directly; every other GET that isn't an API, auth,
|
||||
// or share route returns index.html, so client-side routes like
|
||||
// /<project-id>/<path> and /join/<token> survive a deep link or refresh.
|
||||
// or share route — or, on a hub, a root-level path shaped like a file —
|
||||
// returns index.html, so client-side routes like /<project-id>/<path> and
|
||||
// /join/<token> survive a deep link or refresh.
|
||||
func (s *Server) frontend(static fs.FS) http.HandlerFunc {
|
||||
files := http.FileServerFS(static)
|
||||
index, _ := fs.ReadFile(static, "index.html")
|
||||
@@ -970,6 +971,21 @@ func (s *Server) frontend(static fs.FS) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
}
|
||||
// No embedded asset matched, so a root-level dotted path is a request
|
||||
// for a file that does not exist, not a client route: in hub mode the
|
||||
// first segment is a project id (projectIDRe: UUID or p-xxxxxxxx) or a
|
||||
// reserved word (orgs/, billing/, join/), none of which contain a dot.
|
||||
// Answering the shell made /llms.txt, /robots.txt and every mistyped
|
||||
// root file look like they exist — a soft 200 of login HTML to any
|
||||
// crawler probing a conventional path. Deeper dots (/<id>/notes/a.md)
|
||||
// are real client routes and untouched. index.html is excluded because
|
||||
// the asset block above skips it deliberately and it must keep
|
||||
// answering the shell.
|
||||
if s.Root != nil && upath != "index.html" &&
|
||||
!strings.Contains(upath, "/") && strings.Contains(upath, ".") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write(index)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user