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