tinyrelayfile
tinyrelay/internal/daemon/blob_multipart_test.go/

internal/daemon/blob_multipart_test.go raw
1package daemon
2
3import (
4 "crypto/sha256"
5 "encoding/base64"
6 "encoding/hex"
7 "encoding/json"
8 "net/http"
9 "net/http/httptest"
10 "strconv"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/FelineStateMachine/tinyrelay/internal/blob"
16 "github.com/FelineStateMachine/tinyrelay/internal/event"
17)
18
19func multipartAuthorization(t *testing.T, hash string) string {
20 t.Helper()
21 token := event.Event{Kind: 24242, CreatedAt: time.Now().Unix(), Tags: [][]string{
22 {"t", "upload"}, {"x", hash}, {"expiration", strconv.FormatInt(time.Now().Unix()+60, 10)},
23 }, Content: "Authorize multipart upload"}
24 if err := event.Sign(&token, strings.Repeat("0", 63)+"1"); err != nil {
25 t.Fatal(err)
26 }
27 raw, err := json.Marshal(token)
28 if err != nil {
29 t.Fatal(err)
30 }
31 return "Nostr " + base64.RawURLEncoding.EncodeToString(raw)
32}
33
34func TestMultipartAuthorizationBindsFinalHashBeforeReading(t *testing.T) {
35 _, tenant := testTenant(t)
36 finalHash := strings.Repeat("a", 64)
37 r := httptest.NewRequest(http.MethodPatch, "http://relay.test/"+finalHash+".txt", strings.NewReader("chunk"))
38 r.Header.Set("Authorization", multipartAuthorization(t, strings.Repeat("b", 64)))
39 if _, err := tenant.authorizeBlob(r, blob.ActionUpload); err == nil {
40 t.Fatal("multipart authorization accepted another final hash")
41 }
42 r.Header.Set("Authorization", multipartAuthorization(t, finalHash))
43 if _, err := tenant.authorizeBlob(r, blob.ActionUpload); err != nil {
44 t.Fatal(err)
45 }
46 chunkHash := sha256.Sum256([]byte("chunk"))
47 if err := tenant.validateBlobUpload(r, hex.EncodeToString(chunkHash[:])); err != nil {
48 t.Fatalf("Blossom proof incorrectly required a chunk hash: %v", err)
49 }
50}
51
52func TestMultipartNIP98BindsEachChunkBody(t *testing.T) {
53 _, tenant := testTenant(t)
54 r := httptest.NewRequest(http.MethodPatch, "http://relay.test/"+strings.Repeat("c", 64), strings.NewReader("chunk"))
55 signRequest(t, r, "chunk")
56 if _, err := tenant.authorizeBlob(r, blob.ActionUpload); err != nil {
57 t.Fatal(err)
58 }
59 chunkHash := sha256.Sum256([]byte("chunk"))
60 if err := tenant.validateBlobUpload(r, hex.EncodeToString(chunkHash[:])); err != nil {
61 t.Fatal(err)
62 }
63 if err := tenant.validateBlobUpload(r, strings.Repeat("c", 64)); err == nil {
64 t.Fatal("NIP-98 proof accepted final hash in place of chunk payload")
65 }
66}
67
68func TestMultipartPreflightAdvertisesMountedFileDoor(t *testing.T) {
69 _, tenant := testTenant(t)
70 r := httptest.NewRequest(http.MethodOptions, "http://relay.test/"+strings.Repeat("a", 64), nil)
71 w := httptest.NewRecorder()
72 tenant.ServeHTTP(w, r)
73 if w.Code != http.StatusNoContent || !strings.Contains(w.Header().Get("Allow"), "PATCH") {
74 t.Fatalf("multipart preflight: %d, Allow=%q", w.Code, w.Header().Get("Allow"))
75 }
76 for _, header := range []string{"upload-type", "upload-length", "upload-offset"} {
77 if !strings.Contains(strings.ToLower(w.Header().Get("Access-Control-Allow-Headers")), header) {
78 t.Fatalf("multipart CORS does not allow %s", header)
79 }
80 }
81 if !strings.Contains(w.Header().Get("Access-Control-Expose-Headers"), "Allow") {
82 t.Fatal("browser cannot read PATCH support")
83 }
84 for _, capability := range tenant.Capabilities(nil) {
85 if capability.ID == "BUD-14" && capability.Status == "enabled" {
86 return
87 }
88 }
89 t.Fatal("BUD-14 capability is missing")
90}
91
92func TestMultipartHTTPVerifiesEachNIP98ChunkBeforeAcceptingIt(t *testing.T) {
93 _, tenant := testTenant(t)
94 const body = "first and second"
95 digest := sha256.Sum256([]byte(body))
96 hash := hex.EncodeToString(digest[:])
97 patch := func(offset int, chunk, signed string) *httptest.ResponseRecorder {
98 r := httptest.NewRequest(http.MethodPatch, "http://relay.test/"+hash, strings.NewReader(chunk))
99 r.Header.Set("Content-Type", "application/octet-stream")
100 r.Header.Set("Upload-Type", "text/plain")
101 r.Header.Set("Upload-Length", strconv.Itoa(len(body)))
102 r.Header.Set("Upload-Offset", strconv.Itoa(offset))
103 signRequest(t, r, signed)
104 w := httptest.NewRecorder()
105 tenant.ServeHTTP(w, r)
106 return w
107 }
108 if w := patch(0, body[:6], "altered"); w.Code != http.StatusUnauthorized {
109 t.Fatalf("invalid chunk proof = %d: %s", w.Code, w.Body.String())
110 }
111 if w := patch(6, body[6:], body[6:]); w.Code != http.StatusNoContent {
112 t.Fatalf("out-of-order chunk = %d: %s", w.Code, w.Body.String())
113 }
114 if w := patch(0, body[:6], body[:6]); w.Code != http.StatusCreated {
115 t.Fatalf("complete upload = %d: %s", w.Code, w.Body.String())
116 }
117 if w := patch(0, body[:6], "still altered"); w.Code != http.StatusUnauthorized {
118 t.Fatalf("existing blob bypassed payload validation = %d: %s", w.Code, w.Body.String())
119 }
120 get := httptest.NewRecorder()
121 tenant.ServeHTTP(get, httptest.NewRequest(http.MethodGet, "http://relay.test/"+hash, nil))
122 if get.Code != http.StatusOK || get.Body.String() != body {
123 t.Fatalf("retrieved upload = %d: %s", get.Code, get.Body.String())
124 }
125}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs