| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 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" |
| 14 | |
| 15 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 16 | gitrelay "github.com/FelineStateMachine/tinyrelay/tinygit" |
| 17 | ) |
| 18 | |
| 19 | type repeatedGitByte struct{} |
| 20 | |
| 21 | func (repeatedGitByte) Read(p []byte) (int, error) { |
| 22 | for i := range p { |
| 23 | p[i] = 'x' |
| 24 | } |
| 25 | return len(p), nil |
| 26 | } |
| 27 | |
| 28 | func TestGitAuthorizationSpoolsPayloadAndCleansUp(t *testing.T) { |
| 29 | t.Setenv("TMPDIR" , t.TempDir()) |
| 30 | const size = 32 << 20 |
| 31 | 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 | } |
| 59 | |
| 60 | func 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 | } |
| 72 | |
| 73 | type unreadGitBody struct{ read bool } |
| 74 | |
| 75 | func (b *unreadGitBody) Read([]byte) (int, error) { |
| 76 | b.read = true |
| 77 | return 0, errors.New("unauthenticated body should not be read" ) |
| 78 | } |
| 79 | func (*unreadGitBody) Close() error { return nil } |
| 80 | |
| 81 | func TestGitAuthorizationRejectsUnsignedUploadBeforeReadingBody(t *testing.T) { |
| 82 | _, tenant := testTenant(t) |
| 83 | p := tenant.Policy() |
| 84 | p.Features.Grasp = true |
| 85 | if err := tenant.applyPolicy(context.Background(), p); err != nil { |
| 86 | t.Fatal(err) |
| 87 | } |
| 88 | body := &unreadGitBody{} |
| 89 | req := httptest.NewRequest(http.MethodPost, "http://relay.test/private.git/git-receive-pack" , nil) |
| 90 | req.Body = body |
| 91 | if err := tenant.authorizeGit(context.Background(), req, gitrelay.Repository{Owner: p.Owner, Private: true}); err == nil { |
| 92 | t.Fatal("unsigned private upload accepted" ) |
| 93 | } |
| 94 | if body.read { |
| 95 | t.Fatal("unsigned upload body was consumed" ) |
| 96 | } |
| 97 | } |