clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / internal / daemon / git_payload_test.go
git_payload_test.go 3.8K
1package daemon23import (4 "context"5 "crypto/sha256"6 "encoding/hex"7 "errors"8 "io"9 "net/http"10 "net/http/httptest"11 "os"12 "strings"13 "testing"1415 "github.com/FelineStateMachine/tinyrelay/internal/event"16 gitrelay "github.com/FelineStateMachine/tinyrelay/tinygit"17)1819type repeatedGitByte struct{}2021func (repeatedGitByte) Read(p []byte) (int, error) {22 for i := range p {23 p[i] = 'x'24 }25 return len(p), nil26}2728func TestGitAuthorizationSpoolsPayloadAndCleansUp(t *testing.T) {29 t.Setenv("TMPDIR", t.TempDir())30 const size = 32 << 2031 hash := sha256.New()32 if _, err := io.Copy(hash, io.LimitReader(repeatedGitByte{}, size)); err != nil {33 t.Fatal(err)34 }35 proof := event.Event{Tags: [][]string{{"payload", hex.EncodeToString(hash.Sum(nil))}}}36 req := httptest.NewRequest(http.MethodPost, "http://relay.test/private.git/git-receive-pack", io.LimitReader(repeatedGitByte{}, size))37 if err := spoolGitPayload(req, proof); err != nil {38 t.Fatal(err)39 }40 body, ok := req.Body.(*gitPayloadBody)41 if !ok {42 t.Fatal("large authenticated pack was not backed by a file")43 }44 defer body.Close()45 actual := sha256.New()46 if n, err := io.Copy(actual, body); err != nil || n != size || !strings.EqualFold(hex.EncodeToString(actual.Sum(nil)), event.Tag(proof, "payload")) {47 t.Fatalf("verified payload changed: bytes=%d err=%v", n, err)48 }49 if err := body.Close(); err != nil {50 t.Fatal(err)51 }52 if _, err := os.Stat(body.Name()); !errors.Is(err, os.ErrNotExist) {53 t.Fatalf("authorization spool remains: %v", err)54 }55 if err := body.Close(); err != nil {56 t.Fatalf("repeated close: %v", err)57 }58}5960func TestGitAuthorizationRejectsMismatchedPayloadWithoutLeavingSpool(t *testing.T) {61 dir := t.TempDir()62 t.Setenv("TMPDIR", dir)63 req := httptest.NewRequest(http.MethodPost, "http://relay.test/private.git/git-receive-pack", strings.NewReader("tampered pack"))64 if err := spoolGitPayload(req, event.Event{Tags: [][]string{{"payload", strings.Repeat("0", 64)}}}); err == nil {65 t.Fatal("mismatched payload was accepted")66 }67 entries, err := os.ReadDir(dir)68 if err != nil || len(entries) != 0 {69 t.Fatalf("failed authorization left temporary files: %v %v", entries, err)70 }71}7273func TestGitAuthorizationValidatesEmptyPayload(t *testing.T) {74 for _, bodyKind := range []string{"nil", "no body", "empty stream"} {75 for _, payload := range []string{"", emptyPayloadHash(), strings.Repeat("a", 64)} {76 t.Run(bodyKind+"/"+payload, func(t *testing.T) {77 req := httptest.NewRequest(http.MethodPost, "http://relay.test/repo.git/git-receive-pack", nil)78 switch bodyKind {79 case "nil":80 req.Body = nil81 case "empty stream":82 req.Body = io.NopCloser(strings.NewReader(""))83 }84 proof := event.Event{Tags: [][]string{}}85 if payload != "" {86 proof.Tags = append(proof.Tags, []string{"payload", payload})87 }88 err := spoolGitPayload(req, proof)89 wantErr := payload != "" && payload != emptyPayloadHash()90 if (err != nil) != wantErr {91 t.Fatalf("validate empty payload: %v, want error=%v", err, wantErr)92 }93 })94 }95 }96}9798type unreadGitBody struct{ read bool }99100func (b *unreadGitBody) Read([]byte) (int, error) {101 b.read = true102 return 0, errors.New("unauthenticated body should not be read")103}104func (*unreadGitBody) Close() error { return nil }105106func TestGitAuthorizationRejectsUnsignedUploadBeforeReadingBody(t *testing.T) {107 _, tenant := testTenant(t)108 p := tenant.Policy()109 p.Features.Grasp = true110 if err := tenant.applyPolicy(context.Background(), p); err != nil {111 t.Fatal(err)112 }113 body := &unreadGitBody{}114 req := httptest.NewRequest(http.MethodPost, "http://relay.test/private.git/git-receive-pack", nil)115 req.Body = body116 if err := tenant.authorizeGit(context.Background(), req, gitrelay.Repository{Owner: p.Owner, Private: true}); err == nil {117 t.Fatal("unsigned private upload accepted")118 }119 if body.read {120 t.Fatal("unsigned upload body was consumed")121 }122}