daemon.go

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