1// Package common implements the git pack protocol with a pluggable transport.
2// This is a low-level package to implement new transports. Use a concrete
3// implementation instead (e.g. http, file, ssh).
4//
5// A simple example of usage can be found in the file package.
6package common
7
8import (
9	"bufio"
10	"context"
11	"errors"
12	"fmt"
13	"io"
14	stdioutil "io/ioutil"
15	"strings"
16	"time"
17
18	"github.com/go-git/go-git/v5/plumbing/format/pktline"
19	"github.com/go-git/go-git/v5/plumbing/protocol/packp"
20	"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
21	"github.com/go-git/go-git/v5/plumbing/protocol/packp/sideband"
22	"github.com/go-git/go-git/v5/plumbing/transport"
23	"github.com/go-git/go-git/v5/utils/ioutil"
24)
25
26const (
27	readErrorSecondsTimeout = 10
28)
29
30var (
31	ErrTimeoutExceeded = errors.New("timeout exceeded")
32)
33
34// Commander creates Command instances. This is the main entry point for
35// transport implementations.
36type Commander interface {
37	// Command creates a new Command for the given git command and
38	// endpoint. cmd can be git-upload-pack or git-receive-pack. An
39	// error should be returned if the endpoint is not supported or the
40	// command cannot be created (e.g. binary does not exist, connection
41	// cannot be established).
42	Command(cmd string, ep *transport.Endpoint, auth transport.AuthMethod) (Command, error)
43}
44
45// Command is used for a single command execution.
46// This interface is modeled after exec.Cmd and ssh.Session in the standard
47// library.
48type Command interface {
49	// StderrPipe returns a pipe that will be connected to the command's
50	// standard error when the command starts. It should not be called after
51	// Start.
52	StderrPipe() (io.Reader, error)
53	// StdinPipe returns a pipe that will be connected to the command's
54	// standard input when the command starts. It should not be called after
55	// Start. The pipe should be closed when no more input is expected.
56	StdinPipe() (io.WriteCloser, error)
57	// StdoutPipe returns a pipe that will be connected to the command's
58	// standard output when the command starts. It should not be called after
59	// Start.
60	StdoutPipe() (io.Reader, error)
61	// Start starts the specified command. It does not wait for it to
62	// complete.
63	Start() error
64	// Close closes the command and releases any resources used by it. It
65	// will block until the command exits.
66	Close() error
67}
68
69// CommandKiller expands the Command interface, enabling it for being killed.
70type CommandKiller interface {
71	// Kill and close the session whatever the state it is. It will block until
72	// the command is terminated.
73	Kill() error
74}
75
76type client struct {
77	cmdr Commander
78}
79
80// NewClient creates a new client using the given Commander.
81func NewClient(runner Commander) transport.Transport {
82	return &client{runner}
83}
84
85// NewUploadPackSession creates a new UploadPackSession.
86func (c *client) NewUploadPackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
87	transport.UploadPackSession, error) {
88
89	return c.newSession(transport.UploadPackServiceName, ep, auth)
90}
91
92// NewReceivePackSession creates a new ReceivePackSession.
93func (c *client) NewReceivePackSession(ep *transport.Endpoint, auth transport.AuthMethod) (
94	transport.ReceivePackSession, error) {
95
96	return c.newSession(transport.ReceivePackServiceName, ep, auth)
97}
98
99type session struct {
100	Stdin   io.WriteCloser
101	Stdout  io.Reader
102	Command Command
103
104	isReceivePack bool
105	advRefs       *packp.AdvRefs
106	packRun       bool
107	finished      bool
108	firstErrLine  chan string
109}
110
111func (c *client) newSession(s string, ep *transport.Endpoint, auth transport.AuthMethod) (*session, error) {
112	cmd, err := c.cmdr.Command(s, ep, auth)
113	if err != nil {
114		return nil, err
115	}
116
117	stdin, err := cmd.StdinPipe()
118	if err != nil {
119		return nil, err
120	}
121
122	stdout, err := cmd.StdoutPipe()
123	if err != nil {
124		return nil, err
125	}
126
127	stderr, err := cmd.StderrPipe()
128	if err != nil {
129		return nil, err
130	}
131
132	if err := cmd.Start(); err != nil {
133		return nil, err
134	}
135
136	return &session{
137		Stdin:         stdin,
138		Stdout:        stdout,
139		Command:       cmd,
140		firstErrLine:  c.listenFirstError(stderr),
141		isReceivePack: s == transport.ReceivePackServiceName,
142	}, nil
143}
144
145func (c *client) listenFirstError(r io.Reader) chan string {
146	if r == nil {
147		return nil
148	}
149
150	errLine := make(chan string, 1)
151	go func() {
152		s := bufio.NewScanner(r)
153		if s.Scan() {
154			errLine <- s.Text()
155		} else {
156			close(errLine)
157		}
158
159		_, _ = io.Copy(stdioutil.Discard, r)
160	}()
161
162	return errLine
163}
164
165// AdvertisedReferences retrieves the advertised references from the server.
166func (s *session) AdvertisedReferences() (*packp.AdvRefs, error) {
167	if s.advRefs != nil {
168		return s.advRefs, nil
169	}
170
171	ar := packp.NewAdvRefs()
172	if err := ar.Decode(s.Stdout); err != nil {
173		if err := s.handleAdvRefDecodeError(err); err != nil {
174			return nil, err
175		}
176	}
177
178	// Some servers like jGit, announce capabilities instead of returning an
179	// packp message with a flush. This verifies that we received a empty
180	// adv-refs, even it contains capabilities.
181	if !s.isReceivePack && ar.IsEmpty() {
182		return nil, transport.ErrEmptyRemoteRepository
183	}
184
185	transport.FilterUnsupportedCapabilities(ar.Capabilities)
186	s.advRefs = ar
187	return ar, nil
188}
189
190func (s *session) handleAdvRefDecodeError(err error) error {
191	// If repository is not found, we get empty stdout and server writes an
192	// error to stderr.
193	if err == packp.ErrEmptyInput {
194		s.finished = true
195		if err := s.checkNotFoundError(); err != nil {
196			return err
197		}
198
199		return io.ErrUnexpectedEOF
200	}
201
202	// For empty (but existing) repositories, we get empty advertised-references
203	// message. But valid. That is, it includes at least a flush.
204	if err == packp.ErrEmptyAdvRefs {
205		// Empty repositories are valid for git-receive-pack.
206		if s.isReceivePack {
207			return nil
208		}
209
210		if err := s.finish(); err != nil {
211			return err
212		}
213
214		return transport.ErrEmptyRemoteRepository
215	}
216
217	// Some server sends the errors as normal content (git protocol), so when
218	// we try to decode it fails, we need to check the content of it, to detect
219	// not found errors
220	if uerr, ok := err.(*packp.ErrUnexpectedData); ok {
221		if isRepoNotFoundError(string(uerr.Data)) {
222			return transport.ErrRepositoryNotFound
223		}
224	}
225
226	return err
227}
228
229// UploadPack performs a request to the server to fetch a packfile. A reader is
230// returned with the packfile content. The reader must be closed after reading.
231func (s *session) UploadPack(ctx context.Context, req *packp.UploadPackRequest) (*packp.UploadPackResponse, error) {
232	if req.IsEmpty() {
233		return nil, transport.ErrEmptyUploadPackRequest
234	}
235
236	if err := req.Validate(); err != nil {
237		return nil, err
238	}
239
240	if _, err := s.AdvertisedReferences(); err != nil {
241		return nil, err
242	}
243
244	s.packRun = true
245
246	in := s.StdinContext(ctx)
247	out := s.StdoutContext(ctx)
248
249	if err := uploadPack(in, out, req); err != nil {
250		return nil, err
251	}
252
253	r, err := ioutil.NonEmptyReader(out)
254	if err == ioutil.ErrEmptyReader {
255		if c, ok := s.Stdout.(io.Closer); ok {
256			_ = c.Close()
257		}
258
259		return nil, transport.ErrEmptyUploadPackRequest
260	}
261
262	if err != nil {
263		return nil, err
264	}
265
266	rc := ioutil.NewReadCloser(r, s)
267	return DecodeUploadPackResponse(rc, req)
268}
269
270func (s *session) StdinContext(ctx context.Context) io.WriteCloser {
271	return ioutil.NewWriteCloserOnError(
272		ioutil.NewContextWriteCloser(ctx, s.Stdin),
273		s.onError,
274	)
275}
276
277func (s *session) StdoutContext(ctx context.Context) io.Reader {
278	return ioutil.NewReaderOnError(
279		ioutil.NewContextReader(ctx, s.Stdout),
280		s.onError,
281	)
282}
283
284func (s *session) onError(err error) {
285	if k, ok := s.Command.(CommandKiller); ok {
286		_ = k.Kill()
287	}
288
289	_ = s.Close()
290}
291
292func (s *session) ReceivePack(ctx context.Context, req *packp.ReferenceUpdateRequest) (*packp.ReportStatus, error) {
293	if _, err := s.AdvertisedReferences(); err != nil {
294		return nil, err
295	}
296
297	s.packRun = true
298
299	w := s.StdinContext(ctx)
300	if err := req.Encode(w); err != nil {
301		return nil, err
302	}
303
304	if err := w.Close(); err != nil {
305		return nil, err
306	}
307
308	if !req.Capabilities.Supports(capability.ReportStatus) {
309		// If we don't have report-status, we can only
310		// check return value error.
311		return nil, s.Command.Close()
312	}
313
314	r := s.StdoutContext(ctx)
315
316	var d *sideband.Demuxer
317	if req.Capabilities.Supports(capability.Sideband64k) {
318		d = sideband.NewDemuxer(sideband.Sideband64k, r)
319	} else if req.Capabilities.Supports(capability.Sideband) {
320		d = sideband.NewDemuxer(sideband.Sideband, r)
321	}
322	if d != nil {
323		d.Progress = req.Progress
324		r = d
325	}
326
327	report := packp.NewReportStatus()
328	if err := report.Decode(r); err != nil {
329		return nil, err
330	}
331
332	if err := report.Error(); err != nil {
333		defer s.Close()
334		return report, err
335	}
336
337	return report, s.Command.Close()
338}
339
340func (s *session) finish() error {
341	if s.finished {
342		return nil
343	}
344
345	s.finished = true
346
347	// If we did not run a upload/receive-pack, we close the connection
348	// gracefully by sending a flush packet to the server. If the server
349	// operates correctly, it will exit with status 0.
350	if !s.packRun {
351		_, err := s.Stdin.Write(pktline.FlushPkt)
352		return err
353	}
354
355	return nil
356}
357
358func (s *session) Close() (err error) {
359	err = s.finish()
360
361	defer ioutil.CheckClose(s.Command, &err)
362	return
363}
364
365func (s *session) checkNotFoundError() error {
366	t := time.NewTicker(time.Second * readErrorSecondsTimeout)
367	defer t.Stop()
368
369	select {
370	case <-t.C:
371		return ErrTimeoutExceeded
372	case line, ok := <-s.firstErrLine:
373		if !ok {
374			return nil
375		}
376
377		if isRepoNotFoundError(line) {
378			return transport.ErrRepositoryNotFound
379		}
380
381		return fmt.Errorf("unknown error: %s", line)
382	}
383}
384
385var (
386	githubRepoNotFoundErr      = "ERROR: Repository not found."
387	bitbucketRepoNotFoundErr   = "conq: repository does not exist."
388	localRepoNotFoundErr       = "does not appear to be a git repository"
389	gitProtocolNotFoundErr     = "ERR \n  Repository not found."
390	gitProtocolNoSuchErr       = "ERR no such repository"
391	gitProtocolAccessDeniedErr = "ERR access denied"
392	gogsAccessDeniedErr        = "Gogs: Repository does not exist or you do not have access"
393)
394
395func isRepoNotFoundError(s string) bool {
396	if strings.HasPrefix(s, githubRepoNotFoundErr) {
397		return true
398	}
399
400	if strings.HasPrefix(s, bitbucketRepoNotFoundErr) {
401		return true
402	}
403
404	if strings.HasSuffix(s, localRepoNotFoundErr) {
405		return true
406	}
407
408	if strings.HasPrefix(s, gitProtocolNotFoundErr) {
409		return true
410	}
411
412	if strings.HasPrefix(s, gitProtocolNoSuchErr) {
413		return true
414	}
415
416	if strings.HasPrefix(s, gitProtocolAccessDeniedErr) {
417		return true
418	}
419
420	if strings.HasPrefix(s, gogsAccessDeniedErr) {
421		return true
422	}
423
424	return false
425}
426
427var (
428	nak = []byte("NAK")
429	eol = []byte("\n")
430)
431
432// uploadPack implements the git-upload-pack protocol.
433func uploadPack(w io.WriteCloser, r io.Reader, req *packp.UploadPackRequest) error {
434	// TODO support multi_ack mode
435	// TODO support multi_ack_detailed mode
436	// TODO support acks for common objects
437	// TODO build a proper state machine for all these processing options
438
439	if err := req.UploadRequest.Encode(w); err != nil {
440		return fmt.Errorf("sending upload-req message: %s", err)
441	}
442
443	if err := req.UploadHaves.Encode(w, true); err != nil {
444		return fmt.Errorf("sending haves message: %s", err)
445	}
446
447	if err := sendDone(w); err != nil {
448		return fmt.Errorf("sending done message: %s", err)
449	}
450
451	if err := w.Close(); err != nil {
452		return fmt.Errorf("closing input: %s", err)
453	}
454
455	return nil
456}
457
458func sendDone(w io.Writer) error {
459	e := pktline.NewEncoder(w)
460
461	return e.Encodef("done\n")
462}
463
464// DecodeUploadPackResponse decodes r into a new packp.UploadPackResponse
465func DecodeUploadPackResponse(r io.ReadCloser, req *packp.UploadPackRequest) (
466	*packp.UploadPackResponse, error,
467) {
468	res := packp.NewUploadPackResponse(req)
469	if err := res.Decode(r); err != nil {
470		return nil, fmt.Errorf("error decoding upload-pack response: %s", err)
471	}
472
473	return res, nil
474}
475