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/go-git/go-git/v5/plumbing/format/pktline"
16)
17
18// ErrServerClosed indicates that the server has been closed.
19var ErrServerClosed = fmt.Errorf("git: %w", net.ErrClosed)
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 reposDir := d.cfg.Backend.RepositoryStorePath()
211 if err := ensureWithin(reposDir, repo); err != nil {
212 fatal(c, err)
213 return
214 }
215
216 auth := d.cfg.Access.AccessLevel(name, nil)
217 if auth < backend.ReadOnlyAccess {
218 fatal(c, ErrNotAuthed)
219 return
220 }
221
222 if err := gitPack(c, c, c, filepath.Join(reposDir, repo)); err != nil {
223 fatal(c, err)
224 return
225 }
226 }
227}
228
229// Close closes the underlying listener.
230func (d *GitDaemon) Close() error {
231 d.once.Do(func() { close(d.finished) })
232 err := d.listener.Close()
233 d.conns.CloseAll()
234 return err
235}
236
237// Shutdown gracefully shuts down the daemon.
238func (d *GitDaemon) Shutdown(ctx context.Context) error {
239 d.once.Do(func() { close(d.finished) })
240 err := d.listener.Close()
241 finished := make(chan struct{}, 1)
242 go func() {
243 d.wg.Wait()
244 finished <- struct{}{}
245 }()
246 select {
247 case <-ctx.Done():
248 return ctx.Err()
249 case <-finished:
250 return err
251 }
252}
253
254type serverConn struct {
255 net.Conn
256
257 idleTimeout time.Duration
258 maxDeadline time.Time
259 closeCanceler context.CancelFunc
260}
261
262func (c *serverConn) Write(p []byte) (n int, err error) {
263 c.updateDeadline()
264 n, err = c.Conn.Write(p)
265 if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
266 c.closeCanceler()
267 }
268 return
269}
270
271func (c *serverConn) Read(b []byte) (n int, err error) {
272 c.updateDeadline()
273 n, err = c.Conn.Read(b)
274 if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
275 c.closeCanceler()
276 }
277 return
278}
279
280func (c *serverConn) Close() (err error) {
281 err = c.Conn.Close()
282 if c.closeCanceler != nil {
283 c.closeCanceler()
284 }
285 return
286}
287
288func (c *serverConn) updateDeadline() {
289 switch {
290 case c.idleTimeout > 0:
291 idleDeadline := time.Now().Add(c.idleTimeout)
292 if idleDeadline.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() {
293 c.Conn.SetDeadline(idleDeadline)
294 return
295 }
296 fallthrough
297 default:
298 c.Conn.SetDeadline(c.maxDeadline)
299 }
300}