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}