186 lines
6.0 KiB
Go
186 lines
6.0 KiB
Go
package httpadapter
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"mime"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit"
|
|
)
|
|
|
|
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)
|
|
}
|
|
expectedContentType := mime.TypeByExtension(filepath.Ext(inputPath))
|
|
if expectedContentType == "" {
|
|
t.Fatal("expected built-in HTML content type")
|
|
}
|
|
|
|
reader, err := NewRestrictedArtifactReader(root, 0)
|
|
if err != nil {
|
|
t.Fatalf("construct restricted reader: %v", err)
|
|
}
|
|
|
|
for _, ref := range []promptkit.ArtifactRef{
|
|
{Type: promptkit.ArtifactRefFile, URI: "nested/../input.html"},
|
|
{Type: promptkit.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 != expectedContentType {
|
|
t.Fatalf("unexpected artifact content type: got %q, want %q", artifact.ContentType, expectedContentType)
|
|
}
|
|
if artifact.Hash != artifactHash([]byte("allowed")) {
|
|
t.Fatalf("unexpected artifact hash: %q", artifact.Hash)
|
|
}
|
|
}
|
|
|
|
artifact, err := reader.Read(context.Background(), promptkit.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 []promptkit.ArtifactRef{
|
|
{Type: promptkit.ArtifactRefFile, URI: filepath.Join("..", filepath.Base(outside), "secret.txt")},
|
|
{Type: promptkit.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(), promptkit.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(), promptkit.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(), promptkit.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(), promptkit.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(), promptkit.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(), promptkit.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 []promptkit.ArtifactRef{
|
|
promptkit.Inline("input"),
|
|
promptkit.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 []promptkit.ArtifactRef{
|
|
{Type: promptkit.ArtifactRefType("unsupported")},
|
|
{Type: promptkit.ArtifactRefInline},
|
|
{Type: promptkit.ArtifactRefFile},
|
|
} {
|
|
if _, err := reader.Read(context.Background(), ref); err == nil {
|
|
t.Fatalf("expected malformed reference %#v to fail", ref)
|
|
}
|
|
}
|
|
}
|