1package server
  2
  3import (
  4	"bytes"
  5	"context"
  6	"errors"
  7	"io"
  8	"net"
  9	"path/filepath"
 10	"sync"
 11	"time"
 12
 13	"github.com/charmbracelet/soft-serve/server/backend"
 14	"github.com/charmbracelet/soft-serve/server/config"
 15	"github.com/go-git/go-git/v5/plumbing/format/pktline"
 16)
 17
 18// ErrServerClosed indicates that the server has been closed.
 19var ErrServerClosed = errors.New("git: Server closed")
 20
 21// connections synchronizes access to to a net.Conn pool.
 22type connections struct {
 23	m  map[net.Conn]struct{}
 24	mu sync.Mutex
 25}
 26
 27func (m *connections) Add(c net.Conn) {
 28	m.mu.Lock()
 29	defer m.mu.Unlock()
 30	m.m[c] = struct{}{}
 31}
 32
 33func (m *connections) Close(c net.Conn) {
 34	m.mu.Lock()
 35	defer m.mu.Unlock()
 36	_ = c.Close()
 37	delete(m.m, c)
 38}
 39
 40func (m *connections) Size() int {
 41	m.mu.Lock()
 42	defer m.mu.Unlock()
 43	return len(m.m)
 44}
 45
 46func (m *connections) CloseAll() {
 47	m.mu.Lock()
 48	defer m.mu.Unlock()
 49	for c := range m.m {
 50		_ = c.Close()
 51		delete(m.m, c)
 52	}
 53}
 54
 55// GitDaemon represents a Git daemon.
 56type GitDaemon struct {
 57	listener net.Listener
 58	addr     string
 59	finished chan struct{}
 60	conns    connections
 61	cfg      *config.Config
 62	wg       sync.WaitGroup
 63	once     sync.Once
 64}
 65
 66// NewDaemon returns a new Git daemon.
 67func NewGitDaemon(cfg *config.Config) (*GitDaemon, error) {
 68	addr := cfg.Git.ListenAddr
 69	d := &GitDaemon{
 70		addr:     addr,
 71		finished: make(chan struct{}, 1),
 72		cfg:      cfg,
 73		conns:    connections{m: make(map[net.Conn]struct{})},
 74	}
 75	listener, err := net.Listen("tcp", d.addr)
 76	if err != nil {
 77		return nil, err
 78	}
 79	d.listener = listener
 80	return d, nil
 81}
 82
 83// Start starts the Git TCP daemon.
 84func (d *GitDaemon) Start() error {
 85	defer d.listener.Close() // nolint: errcheck
 86
 87	d.wg.Add(1)
 88	defer d.wg.Done()
 89
 90	var tempDelay time.Duration
 91	for {
 92		conn, err := d.listener.Accept()
 93		if err != nil {
 94			select {
 95			case <-d.finished:
 96				return ErrServerClosed
 97			default:
 98				logger.Debugf("git: error accepting connection: %v", err)
 99			}
100			if ne, ok := err.(net.Error); ok && ne.Temporary() {
101				if tempDelay == 0 {
102					tempDelay = 5 * time.Millisecond
103				} else {
104					tempDelay *= 2
105				}
106				if max := 1 * time.Second; tempDelay > max {
107					tempDelay = max
108				}
109				time.Sleep(tempDelay)
110				continue
111			}
112			return err
113		}
114
115		// Close connection if there are too many open connections.
116		if d.conns.Size()+1 >= d.cfg.Git.MaxConnections {
117			logger.Debugf("git: max connections reached, closing %s", conn.RemoteAddr())
118			fatal(conn, ErrMaxConnections)
119			continue
120		}
121
122		d.wg.Add(1)
123		go func() {
124			d.handleClient(conn)
125			d.wg.Done()
126		}()
127	}
128}
129
130func fatal(c net.Conn, err error) {
131	WritePktline(c, err)
132	if err := c.Close(); err != nil {
133		logger.Debugf("git: error closing connection: %v", err)
134	}
135}
136
137// handleClient handles a git protocol client.
138func (d *GitDaemon) handleClient(conn net.Conn) {
139	ctx, cancel := context.WithCancel(context.Background())
140	idleTimeout := time.Duration(d.cfg.Git.IdleTimeout) * time.Second
141	c := &serverConn{
142		Conn:          conn,
143		idleTimeout:   idleTimeout,
144		closeCanceler: cancel,
145	}
146	if d.cfg.Git.MaxTimeout > 0 {
147		dur := time.Duration(d.cfg.Git.MaxTimeout) * time.Second
148		c.maxDeadline = time.Now().Add(dur)
149	}
150	d.conns.Add(c)
151	defer func() {
152		d.conns.Close(c)
153	}()
154
155	readc := make(chan struct{}, 1)
156	s := pktline.NewScanner(c)
157	go func() {
158		if !s.Scan() {
159			if err := s.Err(); err != nil {
160				if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
161					fatal(c, ErrTimeout)
162				} else {
163					logger.Debugf("git: error scanning pktline: %v", err)
164					fatal(c, ErrSystemMalfunction)
165				}
166			}
167			return
168		}
169		readc <- struct{}{}
170	}()
171
172	select {
173	case <-ctx.Done():
174		if err := ctx.Err(); err != nil {
175			logger.Debugf("git: connection context error: %v", err)
176		}
177		return
178	case <-readc:
179		line := s.Bytes()
180		split := bytes.SplitN(line, []byte{' '}, 2)
181		if len(split) != 2 {
182			fatal(c, ErrInvalidRequest)
183			return
184		}
185
186		var gitPack func(io.Reader, io.Writer, io.Writer, string) error
187		cmd := string(split[0])
188		switch cmd {
189		case UploadPackBin:
190			gitPack = UploadPack
191		case UploadArchiveBin:
192			gitPack = UploadArchive
193		default:
194			fatal(c, ErrInvalidRequest)
195			return
196		}
197
198		opts := bytes.Split(split[1], []byte{'\x00'})
199		if len(opts) == 0 {
200			fatal(c, ErrInvalidRequest)
201			return
202		}
203
204		name := sanitizeRepoName(string(opts[0]))
205		logger.Debugf("git: connect %s %s %s", c.RemoteAddr(), cmd, name)
206		defer logger.Debugf("git: disconnect %s %s %s", c.RemoteAddr(), cmd, name)
207		// git bare repositories should end in ".git"
208		// https://git-scm.com/docs/gitrepository-layout
209		repo := name + ".git"
210		// FIXME: determine repositories path
211		reposDir := filepath.Join(d.cfg.DataPath, "repos")
212		if err := ensureWithin(reposDir, repo); err != nil {
213			fatal(c, err)
214			return
215		}
216
217		auth := d.cfg.Access.AccessLevel(name, nil)
218		if auth < backend.ReadOnlyAccess {
219			fatal(c, ErrNotAuthed)
220			return
221		}
222
223		if err := gitPack(c, c, c, filepath.Join(reposDir, repo)); err != nil {
224			fatal(c, err)
225			return
226		}
227	}
228}
229
230// Close closes the underlying listener.
231func (d *GitDaemon) Close() error {
232	d.once.Do(func() { close(d.finished) })
233	err := d.listener.Close()
234	d.conns.CloseAll()
235	return err
236}
237
238// Shutdown gracefully shuts down the daemon.
239func (d *GitDaemon) Shutdown(ctx context.Context) error {
240	d.once.Do(func() { close(d.finished) })
241	err := d.listener.Close()
242	finished := make(chan struct{}, 1)
243	go func() {
244		d.wg.Wait()
245		finished <- struct{}{}
246	}()
247	select {
248	case <-ctx.Done():
249		return ctx.Err()
250	case <-finished:
251		return err
252	}
253}
254
255type serverConn struct {
256	net.Conn
257
258	idleTimeout   time.Duration
259	maxDeadline   time.Time
260	closeCanceler context.CancelFunc
261}
262
263func (c *serverConn) Write(p []byte) (n int, err error) {
264	c.updateDeadline()
265	n, err = c.Conn.Write(p)
266	if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
267		c.closeCanceler()
268	}
269	return
270}
271
272func (c *serverConn) Read(b []byte) (n int, err error) {
273	c.updateDeadline()
274	n, err = c.Conn.Read(b)
275	if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
276		c.closeCanceler()
277	}
278	return
279}
280
281func (c *serverConn) Close() (err error) {
282	err = c.Conn.Close()
283	if c.closeCanceler != nil {
284		c.closeCanceler()
285	}
286	return
287}
288
289func (c *serverConn) updateDeadline() {
290	switch {
291	case c.idleTimeout > 0:
292		idleDeadline := time.Now().Add(c.idleTimeout)
293		if idleDeadline.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() {
294			c.Conn.SetDeadline(idleDeadline)
295			return
296		}
297		fallthrough
298	default:
299		c.Conn.SetDeadline(c.maxDeadline)
300	}
301}