package httpadapter import ( "context" "errors" "os" "path/filepath" "testing" "gitea.maximumdirect.net/eric/scriptorium" ) func TestRestrictedArtifactReaderReadsContainedFiles(t *testing.T) { root := t.TempDir() outside := t.TempDir() inputPath := filepath.Join(root, "input.html") if err := os.WriteFile(inputPath, []byte("allowed"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "input.unknown"), []byte("unknown type"), 0o644); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(outside, "secret.txt"), []byte("denied"), 0o644); err != nil { t.Fatal(err) } reader, err := NewRestrictedArtifactReader(root, 0) if err != nil { t.Fatalf("construct restricted reader: %v", err) } for _, ref := range []scriptorium.ArtifactRef{ {Type: scriptorium.ArtifactRefFile, URI: "nested/../input.html"}, {Type: scriptorium.ArtifactRefFile, URI: inputPath}, } { artifact, err := reader.Read(context.Background(), ref) if err != nil { t.Fatalf("read contained path %q: %v", ref.URI, err) } if artifact.Name != "input.html" || artifact.URI != inputPath || artifact.Size != int64(len("allowed")) || string(artifact.Body) != "allowed" { t.Fatalf("unexpected artifact metadata: %#v", artifact) } if artifact.ContentType != "text/html; charset=utf-8" { t.Fatalf("unexpected artifact content type: %q", artifact.ContentType) } if artifact.Hash != artifactHash([]byte("allowed")) { t.Fatalf("unexpected artifact hash: %q", artifact.Hash) } } artifact, err := reader.Read(context.Background(), scriptorium.File("input.unknown")) if err != nil { t.Fatalf("read unknown-extension path: %v", err) } if artifact.ContentType != fallbackArtifactContentType { t.Fatalf("unexpected fallback content type: %q", artifact.ContentType) } for _, ref := range []scriptorium.ArtifactRef{ {Type: scriptorium.ArtifactRefFile, URI: filepath.Join("..", filepath.Base(outside), "secret.txt")}, {Type: scriptorium.ArtifactRefFile, URI: filepath.Join(outside, "secret.txt")}, } { _, err := reader.Read(context.Background(), ref) if !errors.Is(err, ErrFileOutsideRoot) { t.Fatalf("expected ErrFileOutsideRoot for %q, got %v", ref.URI, err) } } } func TestRestrictedArtifactReaderFollowsSymlinkAfterLexicalCheck(t *testing.T) { root := t.TempDir() outside := t.TempDir() target := filepath.Join(outside, "linked.txt") if err := os.WriteFile(target, []byte("linked outside root"), 0o644); err != nil { t.Fatal(err) } if err := os.Symlink(target, filepath.Join(root, "linked.txt")); err != nil { t.Skipf("symlink creation unavailable: %v", err) } reader, err := NewRestrictedArtifactReader(root, 0) if err != nil { t.Fatalf("construct restricted reader: %v", err) } artifact, err := reader.Read(context.Background(), scriptorium.File("linked.txt")) if err != nil { t.Fatalf("read symlink inside root: %v", err) } if string(artifact.Body) != "linked outside root" { t.Fatalf("unexpected symlink artifact body: %q", artifact.Body) } } func TestRestrictedArtifactReaderWithoutRootDeniesFiles(t *testing.T) { reader, err := NewRestrictedArtifactReader("", 0) if err != nil { t.Fatalf("construct rootless reader: %v", err) } artifact, err := reader.Read(context.Background(), scriptorium.Inline("inline")) if err != nil { t.Fatalf("read inline artifact: %v", err) } if artifact.ContentType != fallbackArtifactContentType || string(artifact.Body) != "inline" || artifact.Hash != artifactHash([]byte("inline")) { t.Fatalf("unexpected inline artifact: %#v", artifact) } _, err = reader.Read(context.Background(), scriptorium.File("input.txt")) if !errors.Is(err, ErrFileNotAllowed) { t.Fatalf("expected ErrFileNotAllowed, got %v", err) } } func TestRestrictedArtifactReaderEnforcesLimits(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "exact.txt"), []byte("12345"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "large.txt"), []byte("123456"), 0o644); err != nil { t.Fatal(err) } reader, err := NewRestrictedArtifactReader(root, 5) if err != nil { t.Fatalf("construct limited reader: %v", err) } artifact, err := reader.Read(context.Background(), scriptorium.File("exact.txt")) if err != nil || string(artifact.Body) != "12345" { t.Fatalf("expected exact-limit artifact, got %#v and %v", artifact, err) } _, err = reader.Read(context.Background(), scriptorium.File("large.txt")) if !errors.Is(err, ErrFileTooLarge) { t.Fatalf("expected ErrFileTooLarge, got %v", err) } unlimited, err := NewRestrictedArtifactReader(root, 0) if err != nil { t.Fatalf("construct unlimited reader: %v", err) } artifact, err = unlimited.Read(context.Background(), scriptorium.File("large.txt")) if err != nil || string(artifact.Body) != "123456" { t.Fatalf("expected unlimited artifact, got %#v and %v", artifact, err) } if _, err := NewRestrictedArtifactReader(root, -1); err == nil { t.Fatal("expected negative limit to fail") } } func TestRestrictedArtifactReaderRejectsCanceledAndMalformedReferences(t *testing.T) { reader, err := NewRestrictedArtifactReader(t.TempDir(), 0) if err != nil { t.Fatalf("construct reader: %v", err) } canceledCtx, cancel := context.WithCancel(context.Background()) cancel() for _, ref := range []scriptorium.ArtifactRef{ scriptorium.Inline("input"), scriptorium.File("input.txt"), } { _, err := reader.Read(canceledCtx, ref) if !errors.Is(err, context.Canceled) { t.Fatalf("expected cancellation for %#v, got %v", ref, err) } } for _, ref := range []scriptorium.ArtifactRef{ {Type: scriptorium.ArtifactRefType("unsupported")}, {Type: scriptorium.ArtifactRefInline}, {Type: scriptorium.ArtifactRefFile}, } { if _, err := reader.Read(context.Background(), ref); err == nil { t.Fatalf("expected malformed reference %#v to fail", ref) } } }