package ssh import ( "io" "strconv" "strings" "sync" "github.com/git-lfs/git-lfs/v3/errors" "github.com/git-lfs/git-lfs/v3/subprocess" "github.com/git-lfs/git-lfs/v3/tr" ) type PktlineConnection struct { mu sync.Mutex cmd *subprocess.Cmd pl Pktline } func (conn *PktlineConnection) Lock() { conn.mu.Lock() } func (conn *PktlineConnection) Unlock() { conn.mu.Unlock() } func (conn *PktlineConnection) Start() error { conn.Lock() defer conn.Unlock() return conn.negotiateVersion() } func (conn *PktlineConnection) End() error { conn.Lock() defer conn.Unlock() err := conn.SendMessage("quit", nil) if err != nil { return err } _, err = conn.ReadStatus() conn.cmd.Wait() return err } func (conn *PktlineConnection) negotiateVersion() error { pkts, err := conn.pl.ReadPacketList() if err != nil { return errors.NewProtocolError(tr.Tr.Get("Unable to negotiate version with remote side (unable to read capabilities)"), err) } ok := false for _, line := range pkts { if line == "version=1" { ok = true } } if !ok { return errors.NewProtocolError(tr.Tr.Get("Unable to negotiate version with remote side (missing version=1)"), nil) } err = conn.SendMessage("version 1", nil) if err != nil { return errors.NewProtocolError(tr.Tr.Get("Unable to negotiate version with remote side (unable to send version)"), err) } status, args, _, err := conn.ReadStatusWithLines() if err != nil { return errors.NewProtocolError(tr.Tr.Get("Unable to negotiate version with remote side (unable to read status)"), err) } if status != 200 { text := tr.Tr.Get("no error provided") if len(args) > 0 { text = tr.Tr.Get("server said: %q", args[0]) } return errors.NewProtocolError(tr.Tr.Get("Unable to negotiate version with remote side (unexpected status %d; %s)", status, text), nil) } return nil } func (conn *PktlineConnection) SendMessage(command string, args []string) error { err := conn.pl.WritePacketText(command) if err != nil { return err } for _, arg := range args { err = conn.pl.WritePacketText(arg) if err != nil { return err } } return conn.pl.WriteFlush() } func (conn *PktlineConnection) SendMessageWithLines(command string, args []string, lines []string) error { err := conn.pl.WritePacketText(command) if err != nil { return err } for _, arg := range args { err = conn.pl.WritePacketText(arg) if err != nil { return err } } err = conn.pl.WriteDelim() if err != nil { return err } for _, line := range lines { err = conn.pl.WritePacketText(line) if err != nil { return err } } return conn.pl.WriteFlush() } func (conn *PktlineConnection) SendMessageWithData(command string, args []string, data io.Reader) error { err := conn.pl.WritePacketText(command) if err != nil { return err } for _, arg := range args { err = conn.pl.WritePacketText(arg) if err != nil { return err } } err = conn.pl.WriteDelim() if err != nil { return err } buf := make([]byte, 32768) for { n, err := data.Read(buf) if n > 0 { err := conn.pl.WritePacket(buf[0:n]) if err != nil { return err } } if err != nil { break } } return conn.pl.WriteFlush() } func (conn *PktlineConnection) ReadStatus() (int, error) { status := 0 seenStatus := false for { s, pktLen, err := conn.pl.ReadPacketTextWithLength() if err != nil { return 0, errors.NewProtocolError(tr.Tr.Get("error reading packet"), err) } switch { case pktLen == 0: if !seenStatus { return 0, errors.NewProtocolError(tr.Tr.Get("no status seen"), nil) } return status, nil case !seenStatus: ok := false if strings.HasPrefix(s, "status ") { status, err = strconv.Atoi(s[7:]) ok = err == nil } if !ok { return 0, errors.NewProtocolError(tr.Tr.Get("expected status line, got %q", s), err) } seenStatus = true default: return 0, errors.NewProtocolError(tr.Tr.Get("unexpected data, got %q", s), err) } } } // ReadStatusWithData reads a status, arguments, and any binary data. Note that // the reader must be fully exhausted before invoking any other read methods. func (conn *PktlineConnection) ReadStatusWithData() (int, []string, io.Reader, error) { args := make([]string, 0, 100) status := 0 seenStatus := false for { s, pktLen, err := conn.pl.ReadPacketTextWithLength() if err != nil { return 0, nil, nil, errors.NewProtocolError(tr.Tr.Get("error reading packet"), err) } if pktLen == 0 { if !seenStatus { return 0, nil, nil, errors.NewProtocolError(tr.Tr.Get("no status seen"), nil) } return 0, nil, nil, errors.NewProtocolError(tr.Tr.Get("unexpected flush packet"), nil) } else if !seenStatus { ok := false if strings.HasPrefix(s, "status ") { status, err = strconv.Atoi(s[7:]) ok = err == nil } if !ok { return 0, nil, nil, errors.NewProtocolError(tr.Tr.Get("expected status line, got %q", s), err) } seenStatus = true } else if pktLen == 1 { break } else { args = append(args, s) } } return status, args, pktlineReader(conn.pl), nil } // ReadStatusWithLines reads a status, arguments, and a set of text lines. func (conn *PktlineConnection) ReadStatusWithLines() (int, []string, []string, error) { args := make([]string, 0, 100) lines := make([]string, 0, 100) status := 0 seenDelim := false seenStatus := false for { s, pktLen, err := conn.pl.ReadPacketTextWithLength() if err != nil { return 0, nil, nil, errors.NewProtocolError(tr.Tr.Get("error reading packet"), err) } switch { case pktLen == 0: if !seenStatus { return 0, nil, nil, errors.NewProtocolError(tr.Tr.Get("no status seen"), nil) } return status, args, lines, nil case seenDelim: lines = append(lines, s) case !seenStatus: ok := false if strings.HasPrefix(s, "status ") { status, err = strconv.Atoi(s[7:]) ok = err == nil } if !ok { return 0, nil, nil, errors.NewProtocolError(tr.Tr.Get("expected status line, got %q", s), err) } seenStatus = true case pktLen == 1: if seenDelim { return 0, nil, nil, errors.NewProtocolError(tr.Tr.Get("unexpected delimiter packet"), nil) } seenDelim = true default: args = append(args, s) } } }