Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 1h ago 4b8c9e1d
history blame raw 9.8 KiB
1package lfs
2
3// These tests speak the wire protocol the way git-lfs does, so a change in
4// framing or in the shape of a status response fails here rather than in a
5// client's cache directory. The real client is exercised end to end in the
6// server's integration tests when git-lfs is installed.
7
8import (
9 "bytes"
10 "crypto/sha256"
11 "encoding/hex"
12 "io"
13 "net"
14 "strconv"
15 "strings"
16 "testing"
17)
18
19type wire struct {
20 t *testing.T
21 pr *pktReader
22 pw *pktWriter
23}
24
25func newWire(t *testing.T, r io.Reader, w io.Writer) *wire {
26 return &wire{t: t, pr: newPktReader(r), pw: newPktWriter(w)}
27}
28
29// capabilities reads the advertisement the server sends unprompted.
30func (c *wire) capabilities() []string {
31 var out []string
32 for {
33 data, kind, err := c.pr.next()
34 if err != nil {
35 c.t.Fatalf("read capabilities: %v", err)
36 }
37 if kind == pktFlush {
38 return out
39 }
40 out = append(out, strings.TrimRight(string(data), "\n"))
41 }
42}
43
44func (c *wire) send(cmd string, args ...string) {
45 c.t.Helper()
46 if err := c.pw.pkt(cmd); err != nil {
47 c.t.Fatal(err)
48 }
49 for _, a := range args {
50 if err := c.pw.pkt(a); err != nil {
51 c.t.Fatal(err)
52 }
53 }
54 if err := c.pw.flush(); err != nil {
55 c.t.Fatal(err)
56 }
57}
58
59func (c *wire) sendBatch(args, lines []string) {
60 c.t.Helper()
61 if err := c.pw.pkt("batch"); err != nil {
62 c.t.Fatal(err)
63 }
64 for _, a := range args {
65 c.pw.pkt(a)
66 }
67 c.pw.delim()
68 for _, l := range lines {
69 c.pw.pkt(l)
70 }
71 c.pw.flush()
72}
73
74// status reads a status line, its arguments, and the lines after the delimiter.
75func (c *wire) status() (int, []string, []string) {
76 c.t.Helper()
77 data, _, err := c.pr.next()
78 if err != nil {
79 c.t.Fatalf("read status: %v", err)
80 }
81 line := strings.TrimRight(string(data), "\n")
82 code, err := strconv.Atoi(strings.TrimPrefix(line, "status "))
83 if err != nil {
84 c.t.Fatalf("status line %q: %v", line, err)
85 }
86 var args, lines []string
87 inLines := false
88 for {
89 data, kind, err := c.pr.next()
90 if err != nil {
91 c.t.Fatalf("read status body: %v", err)
92 }
93 switch kind {
94 case pktFlush:
95 return code, args, lines
96 case pktDelim:
97 inLines = true
98 default:
99 s := strings.TrimRight(string(data), "\n")
100 if inLines {
101 lines = append(lines, s)
102 } else {
103 args = append(args, s)
104 }
105 }
106 }
107}
108
109// statusData reads a status frame whose body is binary.
110func (c *wire) statusData() (int, []string, []byte) {
111 c.t.Helper()
112 data, _, err := c.pr.next()
113 if err != nil {
114 c.t.Fatal(err)
115 }
116 code, _ := strconv.Atoi(strings.TrimPrefix(strings.TrimRight(string(data), "\n"), "status "))
117 var args []string
118 for {
119 data, kind, err := c.pr.next()
120 if err != nil {
121 c.t.Fatal(err)
122 }
123 if kind == pktDelim {
124 break
125 }
126 args = append(args, strings.TrimRight(string(data), "\n"))
127 }
128 var body bytes.Buffer
129 for {
130 data, kind, err := c.pr.next()
131 if err != nil {
132 c.t.Fatal(err)
133 }
134 if kind == pktFlush {
135 return code, args, body.Bytes()
136 }
137 body.Write(data)
138 }
139}
140
141// session starts a transfer over a socket pair and returns a client-side wire
142// already past the version negotiation.
143func session(t *testing.T, tr *Transfer, op string) (*wire, func()) {
144 t.Helper()
145 clientConn, serverConn := net.Pipe()
146 go func() {
147 if err := tr.Serve(op, serverConn, serverConn); err != nil {
148 serverConn.Close()
149 return
150 }
151 serverConn.Close()
152 }()
153 c := newWire(t, clientConn, clientConn)
154 caps := c.capabilities()
155 if !contains(caps, "version=1") {
156 t.Fatalf("capabilities %v do not offer version=1", caps)
157 }
158 c.send("version 1")
159 if code, _, _ := c.status(); code != 200 {
160 t.Fatalf("version negotiation answered %d", code)
161 }
162 return c, func() { clientConn.Close() }
163}
164
165func contains(list []string, want string) bool {
166 for _, s := range list {
167 if s == want {
168 return true
169 }
170 }
171 return false
172}
173
174func TestTransferUploadThenDownload(t *testing.T) {
175 t.Parallel()
176 store := New(t.TempDir(), 1<<20)
177 data := []byte("a release artifact that is not source code")
178 sum := sha256.Sum256(data)
179 oid := hex.EncodeToString(sum[:])
180 size := strconv.Itoa(len(data))
181
182 up := &Transfer{Store: store, Repo: "demo", CanRead: true, CanWrite: true}
183 c, done := session(t, up, "upload")
184 c.sendBatch([]string{"transfer=ssh", "hash-algo=sha256", "refname=refs/heads/main"},
185 []string{oid + " " + size})
186 code, args, lines := c.status()
187 if code != 200 {
188 t.Fatalf("upload batch answered %d (%v)", code, lines)
189 }
190 if !contains(args, "hash-algo=sha256") {
191 t.Fatalf("batch arguments %v do not name sha256", args)
192 }
193 if len(lines) != 1 || lines[0] != oid+" "+size+" upload" {
194 t.Fatalf("batch said %v; want an upload action for %s", lines, oid)
195 }
196
197 // put-object: command, size argument, delimiter, binary payload, flush.
198 c.pw.pkt("put-object " + oid)
199 c.pw.pkt("size=" + size)
200 c.pw.delim()
201 c.pw.Write(data)
202 c.pw.flush()
203 if code, _, lines := c.status(); code != 200 {
204 t.Fatalf("put-object answered %d (%v)", code, lines)
205 }
206
207 c.send("verify-object "+oid, "size="+size)
208 if code, _, lines := c.status(); code != 200 {
209 t.Fatalf("verify-object answered %d (%v)", code, lines)
210 }
211 c.send("quit")
212 done()
213
214 if !store.Has("demo", oid) {
215 t.Fatal("the uploaded object is not in the store")
216 }
217
218 // A second session, download: the object is already there, so the batch
219 // offers download and get-object returns the bytes.
220 down := &Transfer{Store: store, Repo: "demo", CanRead: true, CanWrite: true}
221 c2, done2 := session(t, down, "download")
222 c2.sendBatch([]string{"transfer=ssh", "hash-algo=sha256"}, []string{oid + " " + size})
223 code, _, lines = c2.status()
224 if code != 200 || len(lines) != 1 || lines[0] != oid+" "+size+" download" {
225 t.Fatalf("download batch answered %d %v", code, lines)
226 }
227 c2.send("get-object "+oid, "size="+size)
228 code, args, body := c2.statusData()
229 if code != 200 {
230 t.Fatalf("get-object answered %d", code)
231 }
232 if !contains(args, "size="+size) {
233 t.Fatalf("get-object arguments %v do not carry the size", args)
234 }
235 if !bytes.Equal(body, data) {
236 t.Fatalf("get-object returned %q", body)
237 }
238 c2.send("quit")
239 done2()
240}
241
242func TestTransferRefusesUploadsWithoutWriteAccess(t *testing.T) {
243 t.Parallel()
244 store := New(t.TempDir(), 1<<20)
245 data := []byte("nope")
246 oid := oidOf(data)
247
248 tr := &Transfer{Store: store, Repo: "demo", CanRead: true, CanWrite: false}
249 c, done := session(t, tr, "upload")
250 defer done()
251 c.sendBatch([]string{"transfer=ssh"}, []string{oid + " " + strconv.Itoa(len(data))})
252 code, _, lines := c.status()
253 if code != 403 {
254 t.Fatalf("batch answered %d; want 403", code)
255 }
256 if len(lines) == 0 || !strings.Contains(strings.Join(lines, " "), "maintain access") {
257 t.Fatalf("refusal does not explain itself: %v", lines)
258 }
259}
260
261func TestTransferRejectsContentThatDoesNotMatchItsID(t *testing.T) {
262 t.Parallel()
263 store := New(t.TempDir(), 1<<20)
264 claimed := oidOf([]byte("what the client says it sent"))
265 actual := []byte("what the client actually sent")
266
267 tr := &Transfer{Store: store, Repo: "demo", CanRead: true, CanWrite: true}
268 c, done := session(t, tr, "upload")
269 defer done()
270 c.pw.pkt("put-object " + claimed)
271 c.pw.pkt("size=" + strconv.Itoa(len(actual)))
272 c.pw.delim()
273 c.pw.Write(actual)
274 c.pw.flush()
275 code, _, lines := c.status()
276 if code != 422 {
277 t.Fatalf("put-object answered %d (%v); want 422", code, lines)
278 }
279 if store.Has("demo", claimed) {
280 t.Fatal("mismatched content was stored")
281 }
282
283 // The session is still usable: the client may send its next object.
284 c.sendBatch([]string{"transfer=ssh"}, []string{claimed + " 5"})
285 if code, _, _ := c.status(); code != 200 {
286 t.Fatalf("batch after a refused object answered %d", code)
287 }
288}
289
290func TestTransferMissingObjectIs404(t *testing.T) {
291 t.Parallel()
292 store := New(t.TempDir(), 1<<20)
293 tr := &Transfer{Store: store, Repo: "demo", CanRead: true, CanWrite: true}
294 c, done := session(t, tr, "download")
295 defer done()
296 c.send("get-object "+strings.Repeat("a", 64), "size=10")
297 code, _, _ := c.statusData()
298 if code != 404 {
299 t.Fatalf("get-object of an absent object answered %d; want 404", code)
300 }
301}
302
303func TestTransferBatchReportsNoopForObjectsItAlreadyHas(t *testing.T) {
304 t.Parallel()
305 store := New(t.TempDir(), 1<<20)
306 data := []byte("already uploaded")
307 oid := oidOf(data)
308 if _, _, err := store.Put("demo", oid, int64(len(data)), bytes.NewReader(data)); err != nil {
309 t.Fatal(err)
310 }
311 tr := &Transfer{Store: store, Repo: "demo", CanRead: true, CanWrite: true}
312 c, done := session(t, tr, "upload")
313 defer done()
314 c.sendBatch([]string{"transfer=ssh"}, []string{oid + " " + strconv.Itoa(len(data))})
315 code, _, lines := c.status()
316 if code != 200 || len(lines) != 1 || !strings.HasSuffix(lines[0], " noop") {
317 t.Fatalf("batch answered %d %v; want a noop for an object already stored", code, lines)
318 }
319}
320
321// A repository's objects, logs and artifacts are bounded together, and the bound is
322// enforced where the bytes arrive: an upload that would cross it is refused with
323// 413 before the object is written.
324func TestTransferRefusesAnUploadOverTheRepoQuota(t *testing.T) {
325 t.Parallel()
326 store := New(t.TempDir(), 1<<20)
327 first := []byte("the first object")
328 sum := sha256.Sum256(first)
329 firstOID := hex.EncodeToString(sum[:])
330 if _, _, err := store.Put("demo", firstOID, int64(len(first)), bytes.NewReader(first)); err != nil {
331 t.Fatal(err)
332 }
333
334 second := []byte("an object that would cross the cap")
335 sum = sha256.Sum256(second)
336 secondOID := hex.EncodeToString(sum[:])
337 up := &Transfer{
338 Store: store, Repo: "demo", CanWrite: true,
339 RepoMaxBytes: int64(len(first) + len(second) - 1),
340 }
341 c, done := session(t, up, "upload")
342 c.sendBatch(nil, []string{secondOID + " " + strconv.Itoa(len(second))})
343 if code, _, lines := c.status(); code != 200 {
344 t.Fatalf("batch answered %d %v", code, lines)
345 }
346 c.pw.pkt("put-object " + secondOID)
347 c.pw.pkt("size=" + strconv.Itoa(len(second)))
348 c.pw.delim()
349 c.pw.Write(second)
350 c.pw.flush()
351 code, _, lines := c.status()
352 if code != 413 {
353 t.Fatalf("put-object answered %d (%v), want 413", code, lines)
354 }
355 if store.Has("demo", secondOID) {
356 t.Fatal("an object over the repository quota was stored")
357 }
358 c.send("quit")
359 done()
360}
361