mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
Two additive capabilities on the zip surface, neither touching the existing Use(Handler...) c.Next model. Static(fs.FS, ...StaticOption) — leaf file handler for a wildcard route. Serves the "*" subpath from any fs.FS (embed.FS or os.DirFS); path.Clean + fs.ValidPath reject traversal fail-closed (404); a missing file calls c.Next() so a later route or SPA catch-all wins, never a 500. Sets Content-Type/Length/Last-Modified, honours HEAD and If-Modified-Since (304). WithIndex / WithStripPrefix options. fasthttp owns the streamed file's close. Middleware = func(Handler) Handler, Chain(mw...), and App.With(mw...) — chi's per-route wrapping idiom. With wraps the leaf at registration (not via c.Next), Chain composes left-to-right. Coexists with Use: Use = global/prefix stack (declaration order), With/Chain = per-route explicit wrapping. Tests: serves file (content/type/len) over embed.FS AND os.DirFS, HEAD, traversal blocked, missing->Next->catch-all, specificity beats the wildcard, index, strip-prefix, 304; Chain/With execution order + the cloud rateLimit+CSRF order and short-circuit proof. Suite green except the two pre-existing macOS-host failures (TestListen_DualTransport, TestMCP_FreeToolSurface). Co-authored-by: Hanzo Dev <[email protected]>
36 lines
1.3 KiB
Go
36 lines
1.3 KiB
Go
package zip
|
|
|
|
import "testing"
|
|
|
|
// staticClean is the fail-closed traversal guard, tested directly so the
|
|
// security property holds regardless of what URL normalization the transport
|
|
// does upstream: any ".." escape or non-fs.ValidPath input is rejected, while
|
|
// in-root navigation ("a/../b") collapses and is allowed.
|
|
func TestStaticClean_FailClosed(t *testing.T) {
|
|
cases := []struct {
|
|
sub, index string
|
|
wantName string
|
|
wantOK bool
|
|
}{
|
|
{"main.css", "", "main.css", true},
|
|
{"a/b/c.js", "", "a/b/c.js", true},
|
|
{"a/../b.js", "", "b.js", true}, // in-root nav collapses — allowed
|
|
{"./x.css", "", "x.css", true}, // leading . collapses
|
|
{"", "", "", true}, // root, no index → nothing to serve
|
|
{"", "index.html", "index.html", true}, // root → index
|
|
{"/", "", "", true}, // just a slash → root
|
|
{"..", "", "", false}, // bare escape
|
|
{"../secret", "", "", false}, // escape one level
|
|
{"../../etc/passwd", "", "", false},
|
|
{"a/../../secret", "", "", false}, // escapes after collapse
|
|
{"sub/../../secret", "", "", false},
|
|
}
|
|
for _, c := range cases {
|
|
name, ok := staticClean(c.sub, c.index)
|
|
if ok != c.wantOK || name != c.wantName {
|
|
t.Errorf("staticClean(%q, %q) = (%q, %v), want (%q, %v)",
|
|
c.sub, c.index, name, ok, c.wantName, c.wantOK)
|
|
}
|
|
}
|
|
}
|