package api import ( "bytes" "mime" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strconv" "syscall" "testing" "git.misaka.ren/m1saka/dsh-web/internal/store" ) func TestWorkspaceImage(t *testing.T) { a, sid := runtimeTestAPI(t, func(http.ResponseWriter, *http.Request) { t.Error("unexpected Docker call") }) for _, i := range []store.Instance{{ID: "one", UID: "owner"}, {ID: "foreign", UID: "other"}} { if err := a.s.AddInstance(i); err != nil { t.Fatal(err) } } dir := filepath.Join(a.root, "workspaces/owner/instances/one/home") if err := os.MkdirAll(dir, 0750); err != nil { t.Fatal(err) } put := func(name string, b []byte) { t.Helper() if err := os.WriteFile(filepath.Join(dir, name), b, 0600); err != nil { t.Fatal(err) } } request := func(method, query, session string, want int) *httptest.ResponseRecorder { t.Helper() w := httptest.NewRecorder() a.workspace(w, runtimeRequest(method, "/api/ws/image?"+query, "", session)) if w.Code != want { t.Fatalf("%s %s: got %d want %d: %s", method, query, w.Code, want, w.Body.String()) } if len(a.workspaceReaders) != 0 { t.Fatal("workspace lease leaked") } return w } get := func(name string, want int) *httptest.ResponseRecorder { t.Helper() return request("GET", "instance=one&path="+url.QueryEscape(name), sid, want) } request("GET", "instance=one&path=file", "", 401) request("GET", "instance=foreign&path=file", sid, 404) request("GET", "instance=missing&path=file", sid, 404) request("GET", "path=file", sid, 410) for _, method := range []string{"HEAD", "POST", "PUT", "DELETE", "PATCH"} { request(method, "instance=one&path=file", sid, 405) } for _, p := range []string{"", ".", "..", "../file", "/etc/passwd", "a/../file", "./file", "a//file", "file/", "a/./file", "a\x00b"} { get(p, 400) } get("missing", 404) for _, tc := range []struct{ name, content, mime string }{ {"png.txt", "\x89PNG\r\n\x1a\n", "image/png"}, {"jpeg", "\xff\xd8\xff\xe0", "image/jpeg"}, {"gif87", "GIF87a", "image/gif"}, {"gif89", "GIF89a", "image/gif"}, {"webp", "RIFF\x00\x00\x00\x00WEBPVP8 ", "image/webp"}, {"active.svg", ``, "image/svg+xml"}, {"prefix.SVG", ``, "image/svg+xml"}, {" space ", "GIF89a exact", "image/gif"}, {`a\b`, "GIF89a backslash", "image/gif"}, } { t.Run(tc.name, func(t *testing.T) { put(tc.name, []byte(tc.content)) w := get(tc.name, 200) if w.Body.String() != tc.content { t.Fatal("raw image bytes changed") } disposition, params, err := mime.ParseMediaType(w.Header().Get("Content-Disposition")) if err != nil || disposition != "attachment" || params["filename"] != tc.name { t.Fatalf("invalid download filename: %s", w.Header().Get("Content-Disposition")) } for key, want := range map[string]string{"Content-Type": tc.mime, "Content-Length": strconv.Itoa(len(tc.content)), "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Content-Security-Policy": "sandbox; default-src 'none'; style-src 'unsafe-inline'"} { if got := w.Header().Get(key); got != want { t.Errorf("%s = %q, want %q", key, got, want) } } }) } for _, content := range []string{"", "plain text", "", ``, ``, ``, ``, `junk`, `]>&x;`, `&unknown;`} { put("bad.svg", []byte(content)) get("bad.svg", 415) } put("wrong.png", []byte(``)) get("wrong.png", 415) put("fake.png", []byte("not an image")) get("fake.png", 415) if err := os.Mkdir(filepath.Join(dir, "directory"), 0700); err != nil { t.Fatal(err) } get("directory", 415) if err := os.Symlink("png.txt", filepath.Join(dir, "link")); err != nil { t.Fatal(err) } get("link", 404) if err := os.Symlink(dir, filepath.Join(dir, "parent")); err != nil { t.Fatal(err) } get("parent/png.txt", 404) if err := os.Link(filepath.Join(dir, "png.txt"), filepath.Join(dir, "hard")); err != nil { t.Fatal(err) } get("hard", 404) get("png.txt", 404) if err := syscall.Mkfifo(filepath.Join(dir, "fifo"), 0600); err != nil { t.Fatal(err) } get("fifo", 404) large := bytes.Repeat([]byte{0}, maxImageBytes) copy(large, "GIF89a") put("limit", large) get("limit", 200) put("oversize", append(large, 0)) get("oversize", 413) if _, err := a.s.DB.Exec("UPDATE instances SET status='error' WHERE id='one'"); err != nil { t.Fatal(err) } get("gif89", 200) }