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}