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 · 50m ago 4b8c9e1d
history blame raw 15.7 KiB
1package lfs
2
3// The pure SSH transfer protocol: what a git-lfs client speaks when it runs
4// `ssh host git-lfs-transfer <repo> upload|download`. Objects move inside the
5// SSH session that carries the push, so there is no token and no second
6// endpoint to authenticate: the key that pushed the pointer is the key that
7// uploaded the bytes it names.
8//
9// The wire format is git's pkt-line framing with a JSON-free command set; see
10// git-lfs docs/proposals/ssh_adapter.md. Moving objects needs version, batch,
11// put-object, get-object, verify-object and quit; locking adds lock, unlock and
12// list-lock, and is advertised as the `locking` capability — a client will not
13// offer it otherwise (see locks.go).
14
15import (
16 "bufio"
17 "errors"
18 "fmt"
19 "io"
20 "log/slog"
21 "strconv"
22 "strings"
23)
24
25const (
26 pktFlush = 0
27 pktDelim = 1
28 pktEnd = 2
29 pktErr = 3
30 // maxPkt is the largest packet payload: the 4-byte length is hex and must
31 // fit in 16 bits, so a whole packet is 65520 bytes including the header.
32 maxPkt = 65516
33)
34
35var errProtocol = errors.New("lfs: protocol error")
36
37// Transfer serves one LFS session. CanRead, CanWrite and IsAdmin come from the
38// repo's ACL, resolved by the SSH server from the account that owns the key.
39type Transfer struct {
40 Store *Store
41 Repo string
42 // RepoID is the repository's stable id, which is what a lock is keyed by: a
43 // rename must not release a lock.
44 RepoID string
45 // User is the account name the SSH key resolved to.
46 User string
47 CanRead bool
48 CanWrite bool
49 // IsAdmin lets an account release somebody else's lock. The lock store does not
50 // read ACLs; the session tells it, so the decision stays in one place.
51 IsAdmin bool
52 // Locks is the lock table. Nil means this server offers none, and the `locking`
53 // capability is not advertised.
54 Locks *LockStore
55 // RepoMaxBytes caps the objects, logs and artifacts one repository may hold.
56 // It is checked on each upload, against what the repository already holds, so
57 // the bytes are refused where they arrive rather than after a push names them.
58 // Zero disables the cap.
59 RepoMaxBytes int64
60 Log *slog.Logger
61}
62
63// Serve runs the session until the client quits or the connection ends. op is
64// the operation the client asked for ("upload" or "download").
65func (t *Transfer) Serve(op string, r io.Reader, w io.Writer) error {
66 if op != "upload" && op != "download" {
67 return fmt.Errorf("%w: unsupported operation %q", errProtocol, op)
68 }
69 pr := newPktReader(r)
70 pw := newPktWriter(w)
71
72 // Capabilities first: the client reads them before it says anything. `locking` is
73 // advertised only when there is a lock table, because a client that sees it will
74 // offer `git lfs lock` and expects it to work.
75 if err := pw.pkt("version=1"); err != nil {
76 return err
77 }
78 if t.Locks != nil {
79 if err := pw.pkt("locking"); err != nil {
80 return err
81 }
82 }
83 if err := pw.flush(); err != nil {
84 return err
85 }
86
87 for {
88 head, kind, err := pr.next()
89 if err != nil {
90 return err
91 }
92 if kind == pktFlush {
93 continue
94 }
95 line := strings.TrimSpace(string(head))
96 switch {
97 case line == "version 1":
98 if err := pr.skipArgs(); err != nil {
99 return err
100 }
101 if err := pw.statusLines(200, nil, nil); err != nil {
102 return err
103 }
104 case line == "quit":
105 if err := pr.skipArgs(); err != nil {
106 return err
107 }
108 return pw.status(200)
109 case line == "batch":
110 if err := t.batch(op, pr, pw); err != nil {
111 return err
112 }
113 case strings.HasPrefix(line, "put-object "):
114 if err := t.put(op, strings.TrimSpace(strings.TrimPrefix(line, "put-object ")), pr, pw); err != nil {
115 return err
116 }
117 case strings.HasPrefix(line, "get-object "):
118 if err := t.get(strings.TrimSpace(strings.TrimPrefix(line, "get-object ")), pr, pw); err != nil {
119 return err
120 }
121 case strings.HasPrefix(line, "verify-object "):
122 if err := t.verify(strings.TrimSpace(strings.TrimPrefix(line, "verify-object ")), pr, pw); err != nil {
123 return err
124 }
125 case line == "lock":
126 if err := t.lock(pr, pw); err != nil {
127 return err
128 }
129 case line == "list-lock":
130 if err := t.listLock(op, pr, pw); err != nil {
131 return err
132 }
133 case strings.HasPrefix(line, "unlock "):
134 if err := t.unlock(strings.TrimSpace(strings.TrimPrefix(line, "unlock ")), pr, pw); err != nil {
135 return err
136 }
137 default:
138 // Unknown commands must still consume their arguments, or the next
139 // command is read from the middle of this one.
140 if err := pr.skipArgs(); err != nil {
141 return err
142 }
143 if err := pw.statusLines(400, nil, []string{"unsupported command: " + line}); err != nil {
144 return err
145 }
146 }
147 }
148}
149
150// batch answers which objects the client still has to send (upload) or may
151// fetch (download). Existing objects are answered `noop`, which is how the
152// client learns it can skip them.
153func (t *Transfer) batch(op string, pr *pktReader, pw *pktWriter) error {
154 args, oidLines, err := pr.readBatch()
155 if err != nil {
156 return err
157 }
158 _ = args // transfer=, hash-algo=, refname=: nothing here changes the answer
159 if op == "upload" && !t.CanWrite {
160 return pw.statusLines(403, []string{t.denied()}, []string{t.denied()})
161 }
162 if op == "download" && !t.CanRead {
163 return pw.statusLines(403, []string{t.denied()}, []string{t.denied()})
164 }
165
166 var out []string
167 for _, l := range oidLines {
168 f := strings.Fields(l)
169 if len(f) < 2 || !ValidOID(f[0]) {
170 return pw.statusLines(400, nil, []string{"malformed object line: " + l})
171 }
172 oid := f[0]
173 size, _ := strconv.ParseInt(f[1], 10, 64)
174 switch op {
175 case "upload":
176 if t.Store.Has(t.Repo, oid) {
177 out = append(out, oid+" "+strconv.FormatInt(size, 10)+" noop")
178 } else {
179 out = append(out, oid+" "+strconv.FormatInt(size, 10)+" upload")
180 }
181 case "download":
182 // An object we do not have gets no action; the client then reports
183 // the missing object by oid, which is the only thing it can say.
184 if n, err := t.Store.Stat(t.Repo, oid); err == nil {
185 out = append(out, oid+" "+strconv.FormatInt(n, 10)+" download")
186 }
187 }
188 }
189 return pw.statusLines(200, []string{"hash-algo=sha256"}, out)
190}
191
192// put receives one object. The bytes are hashed while they stream to disk, so a
193// client that sends the wrong content gets an error instead of an object filed
194// under a key that lies about it.
195func (t *Transfer) put(op, oid string, pr *pktReader, pw *pktWriter) error {
196 if op != "upload" {
197 if err := pr.skipMessage(); err != nil {
198 return err
199 }
200 return pw.statusLines(403, nil, []string{"uploads are not accepted in a download session"})
201 }
202 args, data, stop, err := pr.readData()
203 if err != nil {
204 return err
205 }
206 defer stop()
207 if !t.CanWrite {
208 if _, err := io.Copy(io.Discard, data); err != nil {
209 return err
210 }
211 return pw.statusLines(403, []string{t.denied()}, []string{t.denied()})
212 }
213 var size int64
214 for _, a := range args {
215 if v, ok := strings.CutPrefix(a, "size="); ok {
216 size, _ = strconv.ParseInt(v, 10, 64)
217 }
218 }
219 if t.RepoMaxBytes > 0 && size > 0 {
220 used, err := t.repoBytes()
221 if err != nil {
222 if _, derr := io.Copy(io.Discard, data); derr != nil && err == nil {
223 err = derr
224 }
225 return pw.statusLines(500, nil, []string{"cannot measure the repository's object usage: " + err.Error()})
226 }
227 if used+size > t.RepoMaxBytes {
228 if _, derr := io.Copy(io.Discard, data); derr != nil && err == nil {
229 err = derr
230 }
231 return pw.statusLines(413, nil, []string{fmt.Sprintf(
232 "this repository's objects, logs and artifacts take %d bytes; a %d-byte object would exceed the limit of %d",
233 used, size, t.RepoMaxBytes)})
234 }
235 }
236 n, created, err := t.Store.Put(t.Repo, oid, size, data)
237 // Drain whatever the client still has queued so the session stays in sync.
238 if _, derr := io.Copy(io.Discard, data); derr != nil && err == nil {
239 err = derr
240 }
241 if err != nil {
242 msg := err.Error()
243 code := 422
244 if errors.Is(err, ErrTooLarge) {
245 code = 413
246 }
247 if t.Log != nil {
248 t.Log.Warn("lfs upload refused", "repo", t.Repo, "oid", oid, "err", err)
249 }
250 return pw.statusLines(code, nil, []string{msg})
251 }
252 if t.Log != nil {
253 t.Log.Info("lfs upload", "repo", t.Repo, "oid", oid, "bytes", n, "created", created, "user", t.User)
254 }
255 return pw.statusLines(200, nil, nil)
256}
257
258// repoBytes is what this repository's objects, logs and artifacts already take. It
259// is a walk of the repository's links, so it is done once per upload: an upload is
260// a large object by definition, and the count is what the cap is measured against.
261func (t *Transfer) repoBytes() (int64, error) {
262 var total int64
263 err := t.Store.RepoWalk(t.Repo, func(_ string, size int64) error {
264 total += size
265 return nil
266 })
267 return total, err
268}
269
270// get sends one object. `size=` is mandatory on a successful response; the
271// client uses it to size its temp file and rejects the object without it.
272func (t *Transfer) get(oid string, pr *pktReader, pw *pktWriter) error {
273 if err := pr.skipArgs(); err != nil {
274 return err
275 }
276 if !t.CanRead {
277 return pw.statusLines(403, []string{t.denied()}, []string{t.denied()})
278 }
279 f, size, err := t.Store.Open(t.Repo, oid)
280 if err != nil {
281 code := 404
282 if !errors.Is(err, ErrNotFound) {
283 code = 500
284 }
285 return pw.statusLines(code, nil, []string{err.Error()})
286 }
287 defer f.Close()
288 if err := pw.statusData(200, []string{"size=" + strconv.FormatInt(size, 10)}); err != nil {
289 return err
290 }
291 buf := make([]byte, 32<<10)
292 if _, err := io.CopyBuffer(pw, f, buf); err != nil {
293 return err
294 }
295 return pw.flush()
296}
297
298// verify confirms an upload landed and is the size the client said. A client
299// that gets a non-2xx here reports the upload as failed.
300func (t *Transfer) verify(oid string, pr *pktReader, pw *pktWriter) error {
301 args, err := pr.readUntilFlush()
302 if err != nil {
303 return err
304 }
305 if !t.CanWrite {
306 return pw.statusLines(403, nil, []string{t.denied()})
307 }
308 want := int64(-1)
309 for _, a := range args {
310 if v, ok := strings.CutPrefix(a, "size="); ok {
311 want, _ = strconv.ParseInt(v, 10, 64)
312 }
313 }
314 n, err := t.Store.Stat(t.Repo, oid)
315 if err != nil {
316 return pw.statusLines(404, nil, []string{err.Error()})
317 }
318 if want >= 0 && n != want {
319 return pw.statusLines(422, nil, []string{
320 fmt.Sprintf("object %s is %d bytes, client said %d", oid, n, want)})
321 }
322 return pw.statusLines(200, nil, nil)
323}
324
325func (t *Transfer) denied() string {
326 return "your account does not have maintain access to " + t.Repo
327}
328
329// --- pkt-line -------------------------------------------------------------
330
331type pktReader struct {
332 r *bufio.Reader
333}
334
335func newPktReader(r io.Reader) *pktReader { return &pktReader{r: bufio.NewReaderSize(r, 64<<10)} }
336
337// next returns one packet. kind is pktFlush, pktDelim, pktEnd or -1 for data.
338func (p *pktReader) next() ([]byte, int, error) {
339 var hdr [4]byte
340 if _, err := io.ReadFull(p.r, hdr[:]); err != nil {
341 return nil, -1, err
342 }
343 n, err := strconv.ParseUint(string(hdr[:]), 16, 16)
344 if err != nil {
345 return nil, -1, fmt.Errorf("%w: bad packet header %q", errProtocol, hdr)
346 }
347 switch n {
348 case pktFlush, pktDelim, pktEnd, pktErr:
349 return nil, int(n), nil
350 }
351 if n < 4 {
352 return nil, -1, fmt.Errorf("%w: short packet %d", errProtocol, n)
353 }
354 data := make([]byte, n-4)
355 if _, err := io.ReadFull(p.r, data); err != nil {
356 return nil, -1, err
357 }
358 return data, -1, nil
359}
360
361// readBatch reads the argument packets, the delimiter, and then the object
362// lines of a batch request.
363func (p *pktReader) readBatch() (args, lines []string, err error) {
364 args, err = p.readUntilDelim()
365 if err != nil {
366 return nil, nil, err
367 }
368 for {
369 data, kind, err := p.next()
370 if err != nil {
371 return nil, nil, err
372 }
373 if kind == pktFlush {
374 return args, lines, nil
375 }
376 if kind != -1 {
377 continue
378 }
379 lines = append(lines, strings.TrimRight(string(data), "\n"))
380 }
381}
382
383// readData reads the argument packets of a put-object, then hands back a reader
384// over the binary packets that follow. stop must be called: it consumes the
385// rest of the message so the stream does not desynchronise.
386func (p *pktReader) readData() (args []string, data io.Reader, stop func(), err error) {
387 args, err = p.readUntilDelim()
388 if err != nil {
389 return nil, nil, nil, err
390 }
391 dr := &dataStream{p: p}
392 return args, dr, dr.stop, nil
393}
394
395// skipArgs consumes packets until a flush, discarding them.
396func (p *pktReader) skipArgs() error {
397 _, err := p.readUntilFlush()
398 return err
399}
400
401// skipMessage consumes packets until a flush, including packets after a
402// delimiter.
403func (p *pktReader) skipMessage() error {
404 for {
405 _, kind, err := p.next()
406 if err != nil {
407 return err
408 }
409 if kind == pktFlush {
410 return nil
411 }
412 }
413}
414
415func (p *pktReader) readUntilDelim() ([]string, error) {
416 var out []string
417 for {
418 data, kind, err := p.next()
419 if err != nil {
420 return nil, err
421 }
422 switch kind {
423 case pktFlush:
424 return out, nil
425 case pktDelim:
426 return out, nil
427 case -1:
428 out = append(out, strings.TrimRight(string(data), "\n"))
429 }
430 }
431}
432
433func (p *pktReader) readUntilFlush() ([]string, error) {
434 var out []string
435 for {
436 data, kind, err := p.next()
437 if err != nil {
438 return nil, err
439 }
440 if kind == pktFlush {
441 return out, nil
442 }
443 if kind == -1 {
444 out = append(out, strings.TrimRight(string(data), "\n"))
445 }
446 }
447}
448
449// dataStream presents the binary part of a put-object as an io.Reader. Once a
450// flush is seen it reports EOF; further reads keep returning EOF, and stop is
451// idempotent so a caller can always defer it.
452type dataStream struct {
453 p *pktReader
454 buf []byte
455 done bool
456 stopped bool
457}
458
459func (d *dataStream) Read(b []byte) (int, error) {
460 for len(d.buf) == 0 {
461 if d.done {
462 return 0, io.EOF
463 }
464 data, kind, err := d.p.next()
465 if err != nil {
466 return 0, err
467 }
468 if kind == pktFlush {
469 d.done = true
470 return 0, io.EOF
471 }
472 if kind != -1 {
473 continue
474 }
475 d.buf = data
476 }
477 n := copy(b, d.buf)
478 d.buf = d.buf[n:]
479 return n, nil
480}
481
482func (d *dataStream) stop() {
483 d.stopped = true
484 io.Copy(io.Discard, d)
485}
486
487type pktWriter struct {
488 w *bufio.Writer
489}
490
491func newPktWriter(w io.Writer) *pktWriter { return &pktWriter{w: bufio.NewWriterSize(w, 64<<10)} }
492
493// pkt writes one text packet. git's framing includes the trailing newline
494// inside the length, and the client strips it.
495func (p *pktWriter) pkt(s string) error {
496 b := []byte(s)
497 if len(b) > maxPkt {
498 return fmt.Errorf("%w: packet of %d bytes", errProtocol, len(b))
499 }
500 if _, err := fmt.Fprintf(p.w, "%04x%s\n", len(b)+5, b); err != nil {
501 return err
502 }
503 return nil
504}
505
506func (p *pktWriter) delim() error { _, err := p.w.WriteString("0001"); return err }
507func (p *pktWriter) flush() error {
508 if _, err := p.w.WriteString("0000"); err != nil {
509 return err
510 }
511 return p.w.Flush()
512}
513
514func (p *pktWriter) status(code int) error {
515 if err := p.pkt(fmt.Sprintf("status %d", code)); err != nil {
516 return err
517 }
518 return p.flush()
519}
520
521func (p *pktWriter) statusLines(code int, args, lines []string) error {
522 if err := p.pkt(fmt.Sprintf("status %d", code)); err != nil {
523 return err
524 }
525 for _, a := range args {
526 if err := p.pkt(a); err != nil {
527 return err
528 }
529 }
530 if err := p.delim(); err != nil {
531 return err
532 }
533 for _, l := range lines {
534 if err := p.pkt(l); err != nil {
535 return err
536 }
537 }
538 return p.flush()
539}
540
541// statusData writes the status and arguments, then the delimiter. The caller
542// writes the payload through pktWriter's Write and ends with flush, so a large
543// object is never held in memory.
544func (p *pktWriter) statusData(code int, args []string) error {
545 if err := p.pkt(fmt.Sprintf("status %d", code)); err != nil {
546 return err
547 }
548 for _, a := range args {
549 if err := p.pkt(a); err != nil {
550 return err
551 }
552 }
553 return p.delim()
554}
555
556// Write frames b as binary packets of at most maxPkt bytes.
557func (p *pktWriter) Write(b []byte) (int, error) {
558 written := 0
559 for len(b) > 0 {
560 n := len(b)
561 if n > maxPkt {
562 n = maxPkt
563 }
564 if _, err := fmt.Fprintf(p.w, "%04x", n+4); err != nil {
565 return written, err
566 }
567 if _, err := p.w.Write(b[:n]); err != nil {
568 return written, err
569 }
570 b = b[n:]
571 written += n
572 }
573 return written, nil
574}
575