1package server
  2
  3import (
  4	"bytes"
  5	"context"
  6	"fmt"
  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/charmbracelet/soft-serve/server/utils"
 16	"github.com/go-git/go-git/v5/plumbing/format/pktline"
 17)
 18
 19// ErrServerClosed indicates that the server has been closed.
 20var ErrServerClosed = fmt.Errorf("git: %w", net.ErrClosed)
 21
 22// connections synchronizes access to to a net.Conn pool.
 23type connections struct {
 24	m  map[net.Conn]struct{}
 25	mu sync.Mutex
 26}
 27
 28func (m *connections) Add(c net.Conn) {
 29	m.mu.Lock()
 30	defer m.mu.Unlock()
 31	m.m[c] = struct{}{}
 32}
 33
 34func (m *connections) Close(c net.Conn) {
 35	m.mu.Lock()
 36	defer m.mu.Unlock()
 37	_ = c.Close()
 38	delete(m.m, c)
 39}
 40
 41func (m *connections) Size() int {
 42	m.mu.Lock()
 43	defer m.mu.Unlock()
 44	return len(m.m)
 45}
 46
 47func (m *connections) CloseAll() {
 48	m.mu.Lock()
 49	defer m.mu.Unlock()
 50	for c := range m.m {
 51		_ = c.Close()
 52		delete(m.m, c)
 53	}
 54}
 55
 56// GitDaemon represents a Git daemon.
 57type GitDaemon struct {
 58	listener net.Listener
 59	addr     string
 60	finished chan struct{}
 61	conns    connections
 62	cfg      *config.Config
 63	wg       sync.WaitGroup
 64	once     sync.Once
 65}
 66
 67// NewDaemon returns a new Git daemon.
 68func NewGitDaemon(cfg *config.Config) (*GitDaemon, error) {
 69	addr := cfg.Git.ListenAddr
 70	d := &GitDaemon{
 71		addr:     addr,
 72		finished: make(chan struct{}, 1),
 73		cfg:      cfg,
 74		conns:    connections{m: make(map[net.Conn]struct{})},
 75	}
 76	listener, err := net.Listen("tcp", d.addr)
 77	if err != nil {
 78		return nil, err
 79	}
 80	d.listener = listener
 81	return d, nil
 82}
 83
 84// Start starts the Git TCP daemon.
 85func (d *GitDaemon) Start() error {
 86	defer d.listener.Close() // nolint: errcheck
 87
 88	d.wg.Add(1)
 89	defer d.wg.Done()
 90
 91	var tempDelay time.Duration
 92	for {
 93		conn, err := d.listener.Accept()
 94		if err != nil {
 95			select {
 96			case <-d.finished:
 97				return ErrServerClosed
 98			default:
 99				logger.Debugf("git: error accepting connection: %v", err)
100			}
101			if ne, ok := err.(net.Error); ok && ne.Temporary() {
102				if tempDelay == 0 {
103					tempDelay = 5 * time.Millisecond
104				} else {
105					tempDelay *= 2
106				}
107				if max := 1 * time.Second; tempDelay > max {
108					tempDelay = max
109				}
110				time.Sleep(tempDelay)
111				continue
112			}
113			return err
114		}
115
116		// Close connection if there are too many open connections.
117		if d.conns.Size()+1 >= d.cfg.Git.MaxConnections {
118			logger.Debugf("git: max connections reached, closing %s", conn.RemoteAddr())
119			fatal(conn, ErrMaxConnections)
120			continue
121		}
122
123		d.wg.Add(1)
124		go func() {
125			d.handleClient(conn)
126			d.wg.Done()
127		}()
128	}
129}
130
131func fatal(c net.Conn, err error) {
132	WritePktline(c, err)
133	if err := c.Close(); err != nil {
134		logger.Debugf("git: error closing connection: %v", err)
135	}
136}
137
138// handleClient handles a git protocol client.
139func (d *GitDaemon) handleClient(conn net.Conn) {
140	ctx, cancel := context.WithCancel(context.Background())
141	idleTimeout := time.Duration(d.cfg.Git.IdleTimeout) * time.Second
142	c := &serverConn{
143		Conn:          conn,
144		idleTimeout:   idleTimeout,
145		closeCanceler: cancel,
146	}
147	if d.cfg.Git.MaxTimeout > 0 {
148		dur := time.Duration(d.cfg.Git.MaxTimeout) * time.Second
149		c.maxDeadline = time.Now().Add(dur)
150	}
151	d.conns.Add(c)
152	defer func() {
153		d.conns.Close(c)
154	}()
155
156	readc := make(chan struct{}, 1)
157	s := pktline.NewScanner(c)
158	go func() {
159		if !s.Scan() {
160			if err := s.Err(); err != nil {
161				if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
162					fatal(c, ErrTimeout)
163				} else {
164					logger.Debugf("git: error scanning pktline: %v", err)
165					fatal(c, ErrSystemMalfunction)
166				}
167			}
168			return
169		}
170		readc <- struct{}{}
171	}()
172
173	select {
174	case <-ctx.Done():
175		if err := ctx.Err(); err != nil {
176			logger.Debugf("git: connection context error: %v", err)
177		}
178		return
179	case <-readc:
180		line := s.Bytes()
181		split := bytes.SplitN(line, []byte{' '}, 2)
182		if len(split) != 2 {
183			fatal(c, ErrInvalidRequest)
184			return
185		}
186
187		var gitPack func(io.Reader, io.Writer, io.Writer, string) error
188		cmd := string(split[0])
189		switch cmd {
190		case UploadPackBin:
191			gitPack = UploadPack
192		case UploadArchiveBin:
193			gitPack = UploadArchive
194		default:
195			fatal(c, ErrInvalidRequest)
196			return
197		}
198
199		opts := bytes.Split(split[1], []byte{'\x00'})
200		if len(opts) == 0 {
201			fatal(c, ErrInvalidRequest)
202			return
203		}
204
205		name := utils.SanitizeRepo(string(opts[0]))
206		logger.Debugf("git: connect %s %s %s", c.RemoteAddr(), cmd, name)
207		defer logger.Debugf("git: disconnect %s %s %s", c.RemoteAddr(), cmd, name)
208		// git bare repositories should end in ".git"
209		// https://git-scm.com/docs/gitrepository-layout
210		repo := name + ".git"
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.Backend.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}