e78725f7895eed0ec0bf48b851fccb975f570f6d
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- GitHub <noreply@github.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/cmd/soft/root.go b/cmd/soft/root.go
2index 273c9d6a3e7f54649bf1dab8927724cec5665567..f78c2260645f98a2bd979d70daf43bd0a759556a 100644
3--- a/cmd/soft/root.go
4+++ b/cmd/soft/root.go
5@@ -54,6 +54,9 @@ func init() {
6 func main() {
7 logger := NewDefaultLogger()
8
9+ // Set global logger
10+ log.SetDefault(logger)
11+
12 // Set the max number of processes to the number of CPUs
13 // This is useful when running soft serve in a container
14 if _, err := maxprocs.Set(maxprocs.Logger(logger.Debugf)); err != nil {
15diff --git a/git/repo.go b/git/repo.go
16index 98b81b24c56cf8f4450b9e7c30baeb80bb538a83..8c0b87c43dc642c6b107f7ddbb37014861b187a8 100644
17--- a/git/repo.go
18+++ b/git/repo.go
19@@ -200,13 +200,6 @@ func (r *Repository) CommitsByPage(ref *Reference, page, size int) (Commits, err
20 return commits, nil
21 }
22
23-// UpdateServerInfo updates the repository server info.
24-func (r *Repository) UpdateServerInfo() error {
25- cmd := git.NewCommand("update-server-info")
26- _, err := cmd.RunInDir(r.Path)
27- return err
28-}
29-
30 // Config returns the config value for the given key.
31 func (r *Repository) Config(key string, opts ...ConfigOptions) (string, error) {
32 dir, err := gitDir(r.Repository)
33diff --git a/git/server.go b/git/server.go
34new file mode 100644
35index 0000000000000000000000000000000000000000..e868b1a29fd018edaeb54c88c84c7dd8655ef9c9
36--- /dev/null
37+++ b/git/server.go
38@@ -0,0 +1,18 @@
39+package git
40+
41+import (
42+ "context"
43+
44+ "github.com/gogs/git-module"
45+)
46+
47+// UpdateServerInfo updates the server info file for the given repo path.
48+func UpdateServerInfo(ctx context.Context, path string) error {
49+ if !isGitDir(path) {
50+ return ErrNotAGitRepository
51+ }
52+
53+ cmd := git.NewCommand("update-server-info").WithContext(ctx).WithTimeout(-1)
54+ _, err := cmd.RunInDir(path)
55+ return err
56+}
57diff --git a/git/utils.go b/git/utils.go
58index 2b3d287285f8abf0a80a2c3084f2e97a246ce6ab..3710e172de7772b6c28c2a7e2f6806343c940028 100644
59--- a/git/utils.go
60+++ b/git/utils.go
61@@ -1,6 +1,7 @@
62 package git
63
64 import (
65+ "os"
66 "path/filepath"
67
68 "github.com/gobwas/glob"
69@@ -49,3 +50,25 @@ func LatestFile(repo *Repository, pattern string) (string, string, error) {
70 }
71 return "", "", ErrFileNotFound
72 }
73+
74+// Returns true if path is a directory containing an `objects` directory and a
75+// `HEAD` file.
76+func isGitDir(path string) bool {
77+ stat, err := os.Stat(filepath.Join(path, "objects"))
78+ if err != nil {
79+ return false
80+ }
81+ if !stat.IsDir() {
82+ return false
83+ }
84+
85+ stat, err = os.Stat(filepath.Join(path, "HEAD"))
86+ if err != nil {
87+ return false
88+ }
89+ if stat.IsDir() {
90+ return false
91+ }
92+
93+ return true
94+}
95diff --git a/internal/log/log.go b/internal/log/log.go
96index 7389b80fb9e073174653271fe505282369df91b5..b6c4b1443d19a5bda654a22428efb1fb70cbe16d 100644
97--- a/internal/log/log.go
98+++ b/internal/log/log.go
99@@ -32,6 +32,10 @@ func NewDefaultLogger() *log.Logger {
100
101 if debug, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_DEBUG")); debug {
102 logger.SetLevel(log.DebugLevel)
103+
104+ if verbose, _ := strconv.ParseBool(os.Getenv("SOFT_SERVE_VERBOSE")); verbose {
105+ logger.SetReportCaller(true)
106+ }
107 }
108
109 logger.SetTimeFormat(cfg.Log.TimeFormat)
110diff --git a/server/backend/sqlite/hooks.go b/server/backend/sqlite/hooks.go
111index ff39046f42207e5deda43d3c6845391bd734ce1b..972b3f31d9be55b4dd2547c82e4cbc797dd4110f 100644
112--- a/server/backend/sqlite/hooks.go
113+++ b/server/backend/sqlite/hooks.go
114@@ -36,16 +36,6 @@ func (d *SqliteBackend) PostUpdate(stdout io.Writer, stderr io.Writer, repo stri
115
116 var wg sync.WaitGroup
117
118- // Update server info
119- wg.Add(1)
120- go func() {
121- defer wg.Done()
122- if err := updateServerInfo(d, repo); err != nil {
123- d.logger.Error("error updating server-info", "repo", repo, "err", err)
124- return
125- }
126- }()
127-
128 // Populate last-modified file.
129 wg.Add(1)
130 go func() {
131@@ -59,20 +49,6 @@ func (d *SqliteBackend) PostUpdate(stdout io.Writer, stderr io.Writer, repo stri
132 wg.Wait()
133 }
134
135-func updateServerInfo(d *SqliteBackend, repo string) error {
136- rr, err := d.Repository(repo)
137- if err != nil {
138- return err
139- }
140-
141- r, err := rr.Open()
142- if err != nil {
143- return err
144- }
145-
146- return r.UpdateServerInfo()
147-}
148-
149 func populateLastModified(d *SqliteBackend, repo string) error {
150 var rr *Repo
151 _rr, err := d.Repository(repo)
152diff --git a/server/backend/sqlite/sqlite.go b/server/backend/sqlite/sqlite.go
153index 91527e3fcc0a82b8b35e8f1cba9eab55477cd8bb..3273373ea54e180ce3821f9178f2a8d81e5c6031 100644
154--- a/server/backend/sqlite/sqlite.go
155+++ b/server/backend/sqlite/sqlite.go
156@@ -151,17 +151,12 @@ func (d *SqliteBackend) CreateRepository(name string, opts backend.RepositoryOpt
157 return err
158 }
159
160- rr, err := git.Init(rp, true)
161+ _, err := git.Init(rp, true)
162 if err != nil {
163 d.logger.Debug("failed to create repository", "err", err)
164 return err
165 }
166
167- if err := rr.UpdateServerInfo(); err != nil {
168- d.logger.Debug("failed to update server info", "err", err)
169- return err
170- }
171-
172 return nil
173 }); err != nil {
174 d.logger.Debug("failed to create repository in database", "err", err)
175diff --git a/server/config/config.go b/server/config/config.go
176index 2dca4fada1dfb5ce10e2c2cba76693113ebf76a2..c8b9fa9bf159e24edcb4ce52f89374a66d6eb925 100644
177--- a/server/config/config.go
178+++ b/server/config/config.go
179@@ -114,6 +114,40 @@ type Config struct {
180 Backend backend.Backend `yaml:"-"`
181 }
182
183+// Environ returns the config as a list of environment variables.
184+func (c *Config) Environ() []string {
185+ envs := []string{}
186+ if c == nil {
187+ return envs
188+ }
189+
190+ // TODO: do this dynamically
191+ envs = append(envs, []string{
192+ fmt.Sprintf("SOFT_SERVE_NAME=%s", c.Name),
193+ fmt.Sprintf("SOFT_SERVE_DATA_PATH=%s", c.DataPath),
194+ fmt.Sprintf("SOFT_SERVE_INITIAL_ADMIN_KEYS=%s", strings.Join(c.InitialAdminKeys, "\n")),
195+ fmt.Sprintf("SOFT_SERVE_SSH_LISTEN_ADDR=%s", c.SSH.ListenAddr),
196+ fmt.Sprintf("SOFT_SERVE_SSH_PUBLIC_URL=%s", c.SSH.PublicURL),
197+ fmt.Sprintf("SOFT_SERVE_SSH_KEY_PATH=%s", c.SSH.KeyPath),
198+ fmt.Sprintf("SOFT_SERVE_SSH_CLIENT_KEY_PATH=%s", c.SSH.ClientKeyPath),
199+ fmt.Sprintf("SOFT_SERVE_SSH_MAX_TIMEOUT=%d", c.SSH.MaxTimeout),
200+ fmt.Sprintf("SOFT_SERVE_SSH_IDLE_TIMEOUT=%d", c.SSH.IdleTimeout),
201+ fmt.Sprintf("SOFT_SERVE_GIT_LISTEN_ADDR=%s", c.Git.ListenAddr),
202+ fmt.Sprintf("SOFT_SERVE_GIT_MAX_TIMEOUT=%d", c.Git.MaxTimeout),
203+ fmt.Sprintf("SOFT_SERVE_GIT_IDLE_TIMEOUT=%d", c.Git.IdleTimeout),
204+ fmt.Sprintf("SOFT_SERVE_GIT_MAX_CONNECTIONS=%d", c.Git.MaxConnections),
205+ fmt.Sprintf("SOFT_SERVE_HTTP_LISTEN_ADDR=%s", c.HTTP.ListenAddr),
206+ fmt.Sprintf("SOFT_SERVE_HTTP_TLS_KEY_PATH=%s", c.HTTP.TLSKeyPath),
207+ fmt.Sprintf("SOFT_SERVE_HTTP_TLS_CERT_PATH=%s", c.HTTP.TLSCertPath),
208+ fmt.Sprintf("SOFT_SERVE_HTTP_PUBLIC_URL=%s", c.HTTP.PublicURL),
209+ fmt.Sprintf("SOFT_SERVE_STATS_LISTEN_ADDR=%s", c.Stats.ListenAddr),
210+ fmt.Sprintf("SOFT_SERVE_LOG_FORMAT=%s", c.Log.Format),
211+ fmt.Sprintf("SOFT_SERVE_LOG_TIME_FORMAT=%s", c.Log.TimeFormat),
212+ }...)
213+
214+ return envs
215+}
216+
217 func parseConfig(path string) (*Config, error) {
218 dataPath := filepath.Dir(path)
219 cfg := &Config{
220diff --git a/server/daemon/conn.go b/server/daemon/conn.go
221new file mode 100644
222index 0000000000000000000000000000000000000000..090d76aeecc3ff3ab847e036753ddfdaa3c3705b
223--- /dev/null
224+++ b/server/daemon/conn.go
225@@ -0,0 +1,105 @@
226+package daemon
227+
228+import (
229+ "context"
230+ "errors"
231+ "net"
232+ "sync"
233+ "time"
234+)
235+
236+// connections is a synchronizes access to to a net.Conn pool.
237+type connections struct {
238+ m map[net.Conn]struct{}
239+ mu sync.Mutex
240+}
241+
242+func (m *connections) Add(c net.Conn) {
243+ m.mu.Lock()
244+ defer m.mu.Unlock()
245+ m.m[c] = struct{}{}
246+}
247+
248+func (m *connections) Close(c net.Conn) error {
249+ m.mu.Lock()
250+ defer m.mu.Unlock()
251+ err := c.Close()
252+ delete(m.m, c)
253+ return err
254+}
255+
256+func (m *connections) Size() int {
257+ m.mu.Lock()
258+ defer m.mu.Unlock()
259+ return len(m.m)
260+}
261+
262+func (m *connections) CloseAll() error {
263+ m.mu.Lock()
264+ defer m.mu.Unlock()
265+ var err error
266+ for c := range m.m {
267+ err = errors.Join(err, c.Close())
268+ delete(m.m, c)
269+ }
270+
271+ return err
272+}
273+
274+// serverConn is a wrapper around a net.Conn that closes the connection when
275+// the one of the timeouts is reached.
276+type serverConn struct {
277+ net.Conn
278+
279+ initTimeout time.Duration
280+ idleTimeout time.Duration
281+ maxDeadline time.Time
282+ closeCanceler context.CancelFunc
283+}
284+
285+var _ net.Conn = (*serverConn)(nil)
286+
287+func (c *serverConn) Write(p []byte) (n int, err error) {
288+ c.updateDeadline()
289+ n, err = c.Conn.Write(p)
290+ if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
291+ c.closeCanceler()
292+ }
293+ return
294+}
295+
296+func (c *serverConn) Read(b []byte) (n int, err error) {
297+ c.updateDeadline()
298+ n, err = c.Conn.Read(b)
299+ if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
300+ c.closeCanceler()
301+ }
302+ return
303+}
304+
305+func (c *serverConn) Close() (err error) {
306+ err = c.Conn.Close()
307+ if c.closeCanceler != nil {
308+ c.closeCanceler()
309+ }
310+ return
311+}
312+
313+func (c *serverConn) updateDeadline() {
314+ switch {
315+ case c.initTimeout > 0:
316+ initTimeout := time.Now().Add(c.initTimeout)
317+ c.initTimeout = 0
318+ if initTimeout.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() {
319+ c.Conn.SetDeadline(initTimeout)
320+ return
321+ }
322+ case c.idleTimeout > 0:
323+ idleDeadline := time.Now().Add(c.idleTimeout)
324+ if idleDeadline.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() {
325diff --git a/server/daemon/daemon.go b/server/daemon/daemon.go
326index 944f0eedb9c955dfd1914f3236541b28a5ac38a2..1820c0e3e2c141f79488dac97537e5655a2ad4d5 100644
327--- a/server/daemon/daemon.go
328+++ b/server/daemon/daemon.go
329@@ -6,6 +6,7 @@ import (
330 "fmt"
331 "net"
332 "path/filepath"
333+ "strings"
334 "sync"
335 "time"
336
337@@ -41,40 +42,6 @@ var (
338 ErrServerClosed = fmt.Errorf("git: %w", net.ErrClosed)
339 )
340
341-// connections synchronizes access to to a net.Conn pool.
342-type connections struct {
343- m map[net.Conn]struct{}
344- mu sync.Mutex
345-}
346-
347-func (m *connections) Add(c net.Conn) {
348- m.mu.Lock()
349- defer m.mu.Unlock()
350- m.m[c] = struct{}{}
351-}
352-
353-func (m *connections) Close(c net.Conn) {
354- m.mu.Lock()
355- defer m.mu.Unlock()
356- _ = c.Close()
357- delete(m.m, c)
358-}
359-
360-func (m *connections) Size() int {
361- m.mu.Lock()
362- defer m.mu.Unlock()
363- return len(m.m)
364-}
365-
366-func (m *connections) CloseAll() {
367- m.mu.Lock()
368- defer m.mu.Unlock()
369- for c := range m.m {
370- _ = c.Close()
371- delete(m.m, c)
372- }
373-}
374-
375 // GitDaemon represents a Git daemon.
376 type GitDaemon struct {
377 ctx context.Context
378@@ -213,26 +180,53 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
379 return
380 }
381
382- gitPack := git.UploadPack
383- counter := uploadPackGitCounter
384- cmd := string(split[0])
385- switch cmd {
386- case git.UploadPackBin:
387- gitPack = git.UploadPack
388- case git.UploadArchiveBin:
389- gitPack = git.UploadArchive
390+ var handler git.ServiceHandler
391+ var counter *prometheus.CounterVec
392+ service := git.Service(split[0])
393+ switch service {
394+ case git.UploadPackService:
395+ handler = git.UploadPack
396+ counter = uploadPackGitCounter
397+ case git.UploadArchiveService:
398+ handler = git.UploadArchive
399 counter = uploadArchiveGitCounter
400 default:
401 d.fatal(c, git.ErrInvalidRequest)
402 return
403 }
404
405- opts := bytes.Split(split[1], []byte{'\x00'})
406- if len(opts) == 0 {
407- d.fatal(c, git.ErrInvalidRequest)
408+ opts := bytes.SplitN(split[1], []byte{0}, 3)
409+ if len(opts) < 2 {
410+ d.fatal(c, git.ErrInvalidRequest) // nolint: errcheck
411 return
412 }
413
414+ host := strings.TrimPrefix(string(opts[1]), "host=")
415+ extraParams := map[string]string{}
416+
417+ if len(opts) > 2 {
418+ buf := bytes.TrimPrefix(opts[2], []byte{0})
419+ for _, o := range bytes.Split(buf, []byte{0}) {
420+ opt := string(o)
421+ if opt == "" {
422+ continue
423+ }
424+
425+ kv := strings.SplitN(opt, "=", 2)
426+ if len(kv) != 2 {
427+ d.logger.Errorf("git: invalid option %q", opt)
428+ continue
429diff --git a/server/git/git.go b/server/git/git.go
430index ef8affe207a0cf4993976cbcc28fe91e5e9d512f..8f8ae3d7c6ecd30e68c55c0d53d5029f18a5445a 100644
431--- a/server/git/git.go
432+++ b/server/git/git.go
433@@ -5,16 +5,12 @@ import (
434 "errors"
435 "fmt"
436 "io"
437- "os"
438- "os/exec"
439 "path/filepath"
440 "strings"
441
442 "github.com/charmbracelet/log"
443 "github.com/charmbracelet/soft-serve/git"
444- "github.com/charmbracelet/soft-serve/server/config"
445 "github.com/go-git/go-git/v5/plumbing/format/pktline"
446- "golang.org/x/sync/errgroup"
447 )
448
449 var (
450@@ -38,112 +34,6 @@ var (
451 ErrTimeout = errors.New("I/O timeout reached")
452 )
453
454-// Git protocol commands.
455-const (
456- ReceivePackBin = "git-receive-pack"
457- UploadPackBin = "git-upload-pack"
458- UploadArchiveBin = "git-upload-archive"
459-)
460-
461-// UploadPack runs the git upload-pack protocol against the provided repo.
462-func UploadPack(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoDir string, envs ...string) error {
463- exists, err := fileExists(repoDir)
464- if !exists {
465- return ErrInvalidRepo
466- }
467- if err != nil {
468- return err
469- }
470- return RunGit(ctx, in, out, er, "", envs, UploadPackBin[4:], repoDir)
471-}
472-
473-// UploadArchive runs the git upload-archive protocol against the provided repo.
474-func UploadArchive(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoDir string, envs ...string) error {
475- exists, err := fileExists(repoDir)
476- if !exists {
477- return ErrInvalidRepo
478- }
479- if err != nil {
480- return err
481- }
482- return RunGit(ctx, in, out, er, "", envs, UploadArchiveBin[4:], repoDir)
483-}
484-
485-// ReceivePack runs the git receive-pack protocol against the provided repo.
486-func ReceivePack(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoDir string, envs ...string) error {
487- if err := RunGit(ctx, in, out, er, "", envs, ReceivePackBin[4:], repoDir); err != nil {
488- return err
489- }
490- return EnsureDefaultBranch(ctx, in, out, er, repoDir)
491-}
492-
493-// RunGit runs a git command in the given repo.
494-func RunGit(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, dir string, envs []string, args ...string) error {
495- cfg := config.FromContext(ctx)
496- logger := log.FromContext(ctx).WithPrefix("rungit")
497- c := exec.CommandContext(ctx, "git", args...)
498- c.Dir = dir
499- c.Env = append(os.Environ(), envs...)
500- c.Env = append(c.Env, "PATH="+os.Getenv("PATH"))
501- c.Env = append(c.Env, "SOFT_SERVE_DEBUG="+os.Getenv("SOFT_SERVE_DEBUG"))
502- if cfg != nil {
503- c.Env = append(c.Env, "SOFT_SERVE_LOG_FORMAT="+cfg.Log.Format)
504- c.Env = append(c.Env, "SOFT_SERVE_LOG_TIME_FORMAT="+cfg.Log.TimeFormat)
505- }
506-
507- stdin, err := c.StdinPipe()
508- if err != nil {
509- logger.Error("failed to get stdin pipe", "err", err)
510- return err
511- }
512-
513- stdout, err := c.StdoutPipe()
514- if err != nil {
515- logger.Error("failed to get stdout pipe", "err", err)
516- return err
517- }
518-
519- stderr, err := c.StderrPipe()
520- if err != nil {
521- logger.Error("failed to get stderr pipe", "err", err)
522- return err
523- }
524-
525- if err := c.Start(); err != nil {
526- logger.Error("failed to start command", "err", err)
527- return err
528- }
529-
530- errg, ctx := errgroup.WithContext(ctx)
531-
532- // stdin
533diff --git a/server/git/service.go b/server/git/service.go
534new file mode 100644
535index 0000000000000000000000000000000000000000..0730018403018a31336f01f8ddd6f38d70eccd9b
536--- /dev/null
537+++ b/server/git/service.go
538@@ -0,0 +1,186 @@
539+package git
540+
541+import (
542+ "context"
543+ "errors"
544+ "fmt"
545+ "io"
546+ "os"
547+ "os/exec"
548+ "strings"
549+
550+ "github.com/charmbracelet/log"
551+ "golang.org/x/sync/errgroup"
552+)
553+
554+// Service is a Git daemon service.
555+type Service string
556+
557+const (
558+ // UploadPackService is the upload-pack service.
559+ UploadPackService Service = "git-upload-pack"
560+ // UploadArchiveService is the upload-archive service.
561+ UploadArchiveService Service = "git-upload-archive"
562+ // ReceivePackService is the receive-pack service.
563+ ReceivePackService Service = "git-receive-pack"
564+)
565+
566+// String returns the string representation of the service.
567+func (s Service) String() string {
568+ return string(s)
569+}
570+
571+// Name returns the name of the service.
572+func (s Service) Name() string {
573+ return strings.TrimPrefix(s.String(), "git-")
574+}
575+
576+// Handler is the service handler.
577+func (s Service) Handler(ctx context.Context, cmd ServiceCommand) error {
578+ switch s {
579+ case UploadPackService, UploadArchiveService, ReceivePackService:
580+ return gitServiceHandler(ctx, s, cmd)
581+ default:
582+ return fmt.Errorf("unsupported service: %s", s)
583+ }
584+}
585+
586+// ServiceHandler is a git service command handler.
587+type ServiceHandler func(ctx context.Context, cmd ServiceCommand) error
588+
589+// gitServiceHandler is the default service handler using the git binary.
590+func gitServiceHandler(ctx context.Context, svc Service, scmd ServiceCommand) error {
591+ cmd := exec.CommandContext(ctx, "git", "-c", "uploadpack.allowFilter=true", svc.Name()) // nolint: gosec
592+ cmd.Dir = scmd.Dir
593+ if len(scmd.Args) > 0 {
594+ cmd.Args = append(cmd.Args, scmd.Args...)
595+ }
596+
597+ cmd.Args = append(cmd.Args, ".")
598+
599+ cmd.Env = os.Environ()
600+ if len(scmd.Env) > 0 {
601+ cmd.Env = append(cmd.Env, scmd.Env...)
602+ }
603+
604+ if scmd.CmdFunc != nil {
605+ scmd.CmdFunc(cmd)
606+ }
607+
608+ var (
609+ err error
610+ stdin io.WriteCloser
611+ stdout io.ReadCloser
612+ stderr io.ReadCloser
613+ )
614+
615+ if scmd.Stdin != nil {
616+ stdin, err = cmd.StdinPipe()
617+ if err != nil {
618+ return err
619+ }
620+ }
621+
622+ if scmd.Stdout != nil {
623+ stdout, err = cmd.StdoutPipe()
624+ if err != nil {
625+ return err
626+ }
627+ }
628+
629+ if scmd.Stderr != nil {
630+ stderr, err = cmd.StderrPipe()
631+ if err != nil {
632+ return err
633+ }
634+ }
635+
636+ log.Debugf("git service command in %q: %s", cmd.Dir, cmd.String())
637+ if err := cmd.Start(); err != nil {
638diff --git a/server/ssh/ssh.go b/server/ssh/ssh.go
639index 42cd7f5f543044881d79af9725e9c4a55211d277..8e98d0ebc0938776d031615daf8090ff6244f9cc 100644
640--- a/server/ssh/ssh.go
641+++ b/server/ssh/ssh.go
642@@ -216,13 +216,13 @@ func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
643 return func(s ssh.Session) {
644 func() {
645 start := time.Now()
646- cmd := s.Command()
647+ cmdLine := s.Command()
648 ctx := s.Context()
649 be := ss.be.WithContext(ctx)
650- if len(cmd) >= 2 && strings.HasPrefix(cmd[0], "git") {
651- gc := cmd[0]
652+
653+ if len(cmdLine) >= 2 && strings.HasPrefix(cmdLine[0], "git") {
654 // repo should be in the form of "repo.git"
655- name := utils.SanitizeRepo(cmd[1])
656+ name := utils.SanitizeRepo(cmdLine[1])
657 pk := s.PublicKey()
658 ak := backend.MarshalAuthorizedKey(pk)
659 access := cfg.Backend.AccessLevelByPublicKey(name, pk)
660@@ -240,12 +240,27 @@ func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
661 "SOFT_SERVE_REPO_NAME=" + name,
662 "SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repo),
663 "SOFT_SERVE_PUBLIC_KEY=" + ak,
664+ "SOFT_SERVE_USERNAME=" + ctx.User(),
665 }
666
667- ss.logger.Debug("git middleware", "cmd", gc, "access", access.String())
668+ // Add ssh session & config environ
669+ envs = append(envs, s.Environ()...)
670+ envs = append(envs, cfg.Environ()...)
671+
672 repoDir := filepath.Join(reposDir, repo)
673- switch gc {
674- case git.ReceivePackBin:
675+ service := git.Service(cmdLine[0])
676+ cmd := git.ServiceCommand{
677+ Stdin: s,
678+ Stdout: s,
679+ Stderr: s.Stderr(),
680+ Env: envs,
681+ Dir: repoDir,
682+ }
683+
684+ ss.logger.Debug("git middleware", "cmd", service, "access", access.String())
685+
686+ switch service {
687+ case git.ReceivePackService:
688 receivePackCounter.WithLabelValues(name).Inc()
689 defer func() {
690 receivePackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
691@@ -262,20 +277,27 @@ func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
692 }
693 createRepoCounter.WithLabelValues(name).Inc()
694 }
695- if err := git.ReceivePack(s.Context(), s, s, s.Stderr(), repoDir, envs...); err != nil {
696+
697+ if err := git.ReceivePack(ctx, cmd); err != nil {
698+ sshFatal(s, git.ErrSystemMalfunction)
699+ }
700+
701+ if err := git.EnsureDefaultBranch(ctx, cmd); err != nil {
702 sshFatal(s, git.ErrSystemMalfunction)
703 }
704+
705+ receivePackCounter.WithLabelValues(name).Inc()
706 return
707- case git.UploadPackBin, git.UploadArchiveBin:
708+ case git.UploadPackService, git.UploadArchiveService:
709 if access < backend.ReadOnlyAccess {
710 sshFatal(s, git.ErrNotAuthed)
711 return
712 }
713
714- gitPack := git.UploadPack
715- switch gc {
716- case git.UploadArchiveBin:
717- gitPack = git.UploadArchive
718+ handler := git.UploadPack
719+ switch service {
720+ case git.UploadArchiveService:
721+ handler = git.UploadArchive
722 uploadArchiveCounter.WithLabelValues(name).Inc()
723 defer func() {
724 uploadArchiveSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
725@@ -285,10 +307,9 @@ func (ss *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
726 defer func() {
727 uploadPackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
728 }()
729-
730 }
731
732- err := gitPack(ctx, s, s, s.Stderr(), repoDir, envs...)
733+ err := handler(ctx, cmd)
734 if errors.Is(err, git.ErrInvalidRepo) {
735 sshFatal(s, git.ErrInvalidRepo)
736 } else if err != nil {
737diff --git a/server/web/git.go b/server/web/git.go
738new file mode 100644
739index 0000000000000000000000000000000000000000..2ca9265912dc2af5c94f2d629ec27b230b811afa
740--- /dev/null
741+++ b/server/web/git.go
742@@ -0,0 +1,459 @@
743+package web
744+
745+import (
746+ "bytes"
747+ "compress/gzip"
748+ "context"
749+ "fmt"
750+ "io"
751+ "net/http"
752+ "os"
753+ "path/filepath"
754+ "regexp"
755+ "strings"
756+ "time"
757+
758+ "github.com/charmbracelet/log"
759+ gitb "github.com/charmbracelet/soft-serve/git"
760+ "github.com/charmbracelet/soft-serve/server/backend"
761+ "github.com/charmbracelet/soft-serve/server/config"
762+ "github.com/charmbracelet/soft-serve/server/git"
763+ "github.com/charmbracelet/soft-serve/server/utils"
764+ "github.com/prometheus/client_golang/prometheus"
765+ "github.com/prometheus/client_golang/prometheus/promauto"
766+ "goji.io/pat"
767+ "goji.io/pattern"
768+)
769+
770+// GitRoute is a route for git services.
771+type GitRoute struct {
772+ method string
773+ pattern *regexp.Regexp
774+ handler http.HandlerFunc
775+
776+ cfg *config.Config
777+ be backend.Backend
778+ logger *log.Logger
779+}
780+
781+var _ Route = GitRoute{}
782+
783+// Match implements goji.Pattern.
784+func (g GitRoute) Match(r *http.Request) *http.Request {
785+ if g.method != r.Method {
786+ return nil
787+ }
788+
789+ re := g.pattern
790+ ctx := r.Context()
791+ if m := re.FindStringSubmatch(r.URL.Path); m != nil {
792+ file := strings.Replace(r.URL.Path, m[1]+"/", "", 1)
793+ repo := utils.SanitizeRepo(m[1]) + ".git"
794+
795+ var service git.Service
796+ switch {
797+ case strings.HasSuffix(r.URL.Path, git.UploadPackService.String()):
798+ service = git.UploadPackService
799+ case strings.HasSuffix(r.URL.Path, git.ReceivePackService.String()):
800+ service = git.ReceivePackService
801+ }
802+
803+ ctx = context.WithValue(ctx, pattern.Variable("service"), service.String())
804+ ctx = context.WithValue(ctx, pattern.Variable("dir"), filepath.Join(g.cfg.DataPath, "repos", repo))
805+ ctx = context.WithValue(ctx, pattern.Variable("repo"), repo)
806+ ctx = context.WithValue(ctx, pattern.Variable("file"), file)
807+
808+ if g.cfg != nil {
809+ ctx = config.WithContext(ctx, g.cfg)
810+ }
811+
812+ if g.be != nil {
813+ ctx = backend.WithContext(ctx, g.be.WithContext(ctx))
814+ }
815+
816+ if g.logger != nil {
817+ ctx = log.WithContext(ctx, g.logger)
818+ }
819+
820+ return r.WithContext(ctx)
821+ }
822+
823+ return nil
824+}
825+
826+// ServeHTTP implements http.Handler.
827+func (g GitRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) {
828+ g.handler(w, r)
829+}
830+
831+var (
832+ gitHttpReceiveCounter = promauto.NewCounterVec(prometheus.CounterOpts{
833+ Namespace: "soft_serve",
834+ Subsystem: "http",
835+ Name: "git_receive_pack_total",
836+ Help: "The total number of git push requests",
837+ }, []string{"repo"})
838+
839+ gitHttpUploadCounter = promauto.NewCounterVec(prometheus.CounterOpts{
840+ Namespace: "soft_serve",
841+ Subsystem: "http",
842diff --git a/server/web/goget.go b/server/web/goget.go
843new file mode 100644
844index 0000000000000000000000000000000000000000..7e7c8c9d61faefd0b8bc629751e1d7328fa1c042
845--- /dev/null
846+++ b/server/web/goget.go
847@@ -0,0 +1,94 @@
848+package web
849+
850+import (
851+ "net/http"
852+ "net/url"
853+ "path"
854+ "text/template"
855+
856+ "github.com/charmbracelet/soft-serve/server/backend"
857+ "github.com/charmbracelet/soft-serve/server/config"
858+ "github.com/charmbracelet/soft-serve/server/utils"
859+ "github.com/prometheus/client_golang/prometheus"
860+ "github.com/prometheus/client_golang/prometheus/promauto"
861+ "goji.io/pattern"
862+)
863+
864+var goGetCounter = promauto.NewCounterVec(prometheus.CounterOpts{
865+ Namespace: "soft_serve",
866+ Subsystem: "http",
867+ Name: "go_get_total",
868+ Help: "The total number of go get requests",
869+}, []string{"repo"})
870+
871+var repoIndexHTMLTpl = template.Must(template.New("index").Parse(`<!DOCTYPE html>
872+<html lang="en">
873+<head>
874+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
875+ <meta http-equiv="refresh" content="0; url=https://godoc.org/{{ .ImportRoot }}/{{.Repo}}">
876+ <meta name="go-import" content="{{ .ImportRoot }}/{{ .Repo }} git {{ .Config.HTTP.PublicURL }}/{{ .Repo }}">
877+</head>
878+<body>
879+Redirecting to docs at <a href="https://godoc.org/{{ .ImportRoot }}/{{ .Repo }}">godoc.org/{{ .ImportRoot }}/{{ .Repo }}</a>...
880+</body>
881+</html>`))
882+
883+// GoGetHandler handles go get requests.
884+type GoGetHandler struct {
885+ cfg *config.Config
886+ be backend.Backend
887+}
888+
889+var _ http.Handler = (*GoGetHandler)(nil)
890+
891+func (g GoGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
892+ repo := pattern.Path(r.Context())
893+ repo = utils.SanitizeRepo(repo)
894+ be := g.be.WithContext(r.Context())
895+
896+ // Handle go get requests.
897+ //
898+ // Always return a 200 status code, even if the repo doesn't exist.
899+ //
900+ // https://golang.org/cmd/go/#hdr-Remote_import_paths
901+ // https://go.dev/ref/mod#vcs-branch
902+ if r.URL.Query().Get("go-get") == "1" {
903+ repo := repo
904+ importRoot, err := url.Parse(g.cfg.HTTP.PublicURL)
905+ if err != nil {
906+ http.Error(w, err.Error(), http.StatusInternalServerError)
907+ return
908+ }
909+
910+ // find the repo
911+ for {
912+ if _, err := be.Repository(repo); err == nil {
913+ break
914+ }
915+
916+ if repo == "" || repo == "." || repo == "/" {
917+ return
918+ }
919+
920+ repo = path.Dir(repo)
921+ }
922+
923+ if err := repoIndexHTMLTpl.Execute(w, struct {
924+ Repo string
925+ Config *config.Config
926+ ImportRoot string
927+ }{
928+ Repo: url.PathEscape(repo),
929+ Config: g.cfg,
930+ ImportRoot: importRoot.Host,
931+ }); err != nil {
932+ http.Error(w, err.Error(), http.StatusInternalServerError)
933+ return
934+ }
935+
936+ goGetCounter.WithLabelValues(repo).Inc()
937+ return
938+ }
939+
940+ http.NotFound(w, r)
941+}
942diff --git a/server/web/http.go b/server/web/http.go
943index b932f94162bf6c2c6119e451b682d2a132c7e233..7ff375cab3132d377d2f92e0ce5184dc94ce43a0 100644
944--- a/server/web/http.go
945+++ b/server/web/http.go
946@@ -2,103 +2,31 @@ package web
947
948 import (
949 "context"
950- "fmt"
951 "net/http"
952- "net/url"
953- "path"
954- "path/filepath"
955- "regexp"
956- "strings"
957- "text/template"
958 "time"
959
960- "github.com/charmbracelet/log"
961 "github.com/charmbracelet/soft-serve/server/backend"
962 "github.com/charmbracelet/soft-serve/server/config"
963- "github.com/charmbracelet/soft-serve/server/utils"
964- "github.com/dustin/go-humanize"
965- "github.com/prometheus/client_golang/prometheus"
966- "github.com/prometheus/client_golang/prometheus/promauto"
967- "goji.io"
968- "goji.io/pat"
969- "goji.io/pattern"
970 )
971
972-var (
973- gitHttpCounter = promauto.NewCounterVec(prometheus.CounterOpts{
974- Namespace: "soft_serve",
975- Subsystem: "http",
976- Name: "git_fetch_pull_total",
977- Help: "The total number of git fetch/pull requests",
978- }, []string{"repo", "file"})
979-
980- goGetCounter = promauto.NewCounterVec(prometheus.CounterOpts{
981- Namespace: "soft_serve",
982- Subsystem: "http",
983- Name: "go_get_total",
984- Help: "The total number of go get requests",
985- }, []string{"repo"})
986-)
987-
988-// logWriter is a wrapper around http.ResponseWriter that allows us to capture
989-// the HTTP status code and bytes written to the response.
990-type logWriter struct {
991- http.ResponseWriter
992- code, bytes int
993-}
994-
995-func (r *logWriter) Write(p []byte) (int, error) {
996- written, err := r.ResponseWriter.Write(p)
997- r.bytes += written
998- return written, err
999-}
1000-
1001-// Note this is generally only called when sending an HTTP error, so it's
1002-// important to set the `code` value to 200 as a default
1003-func (r *logWriter) WriteHeader(code int) {
1004- r.code = code
1005- r.ResponseWriter.WriteHeader(code)
1006-}
1007-
1008-func (s *HTTPServer) loggingMiddleware(next http.Handler) http.Handler {
1009- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1010- start := time.Now()
1011- writer := &logWriter{code: http.StatusOK, ResponseWriter: w}
1012- s.logger.Debug("request",
1013- "method", r.Method,
1014- "uri", r.RequestURI,
1015- "addr", r.RemoteAddr)
1016- next.ServeHTTP(writer, r)
1017- elapsed := time.Since(start)
1018- s.logger.Debug("response",
1019- "status", fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code)),
1020- "bytes", humanize.Bytes(uint64(writer.bytes)),
1021- "time", elapsed)
1022- })
1023-}
1024-
1025 // HTTPServer is an http server.
1026 type HTTPServer struct {
1027- ctx context.Context
1028- cfg *config.Config
1029- be backend.Backend
1030- server *http.Server
1031- dirHandler http.Handler
1032- logger *log.Logger
1033+ ctx context.Context
1034+ cfg *config.Config
1035+ be backend.Backend
1036+ server *http.Server
1037 }
1038
1039+// NewHTTPServer creates a new HTTP server.
1040 func NewHTTPServer(ctx context.Context) (*HTTPServer, error) {
1041 cfg := config.FromContext(ctx)
1042- mux := goji.NewMux()
1043 s := &HTTPServer{
1044- ctx: ctx,
1045- cfg: cfg,
1046diff --git a/server/web/logging.go b/server/web/logging.go
1047new file mode 100644
1048index 0000000000000000000000000000000000000000..f0f43a05c832cc98b2edc24ede2c83254b6d1b95
1049--- /dev/null
1050+++ b/server/web/logging.go
1051@@ -0,0 +1,84 @@
1052+package web
1053+
1054+import (
1055+ "bufio"
1056+ "fmt"
1057+ "net"
1058+ "net/http"
1059+ "time"
1060+
1061+ "github.com/charmbracelet/log"
1062+ "github.com/dustin/go-humanize"
1063+)
1064+
1065+// logWriter is a wrapper around http.ResponseWriter that allows us to capture
1066+// the HTTP status code and bytes written to the response.
1067+type logWriter struct {
1068+ http.ResponseWriter
1069+ code, bytes int
1070+}
1071+
1072+var _ http.ResponseWriter = (*logWriter)(nil)
1073+
1074+var _ http.Flusher = (*logWriter)(nil)
1075+
1076+var _ http.Hijacker = (*logWriter)(nil)
1077+
1078+var _ http.CloseNotifier = (*logWriter)(nil)
1079+
1080+// Write implements http.ResponseWriter.
1081+func (r *logWriter) Write(p []byte) (int, error) {
1082+ written, err := r.ResponseWriter.Write(p)
1083+ r.bytes += written
1084+ return written, err
1085+}
1086+
1087+// Note this is generally only called when sending an HTTP error, so it's
1088+// important to set the `code` value to 200 as a default.
1089+func (r *logWriter) WriteHeader(code int) {
1090+ r.code = code
1091+ r.ResponseWriter.WriteHeader(code)
1092+}
1093+
1094+// Flush implements http.Flusher.
1095+func (r *logWriter) Flush() {
1096+ if f, ok := r.ResponseWriter.(http.Flusher); ok {
1097+ f.Flush()
1098+ }
1099+}
1100+
1101+// CloseNotify implements http.CloseNotifier.
1102+func (r *logWriter) CloseNotify() <-chan bool {
1103+ if cn, ok := r.ResponseWriter.(http.CloseNotifier); ok {
1104+ return cn.CloseNotify()
1105+ }
1106+ return nil
1107+}
1108+
1109+// Hijack implements http.Hijacker.
1110+func (r *logWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
1111+ if h, ok := r.ResponseWriter.(http.Hijacker); ok {
1112+ return h.Hijack()
1113+ }
1114+ return nil, nil, fmt.Errorf("http.Hijacker not implemented")
1115+}
1116+
1117+// NewLoggingMiddleware returns a new logging middleware.
1118+func NewLoggingMiddleware(logger *log.Logger) func(http.Handler) http.Handler {
1119+ return func(next http.Handler) http.Handler {
1120+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1121+ start := time.Now()
1122+ writer := &logWriter{code: http.StatusOK, ResponseWriter: w}
1123+ logger.Debug("request",
1124+ "method", r.Method,
1125+ "uri", r.RequestURI,
1126+ "addr", r.RemoteAddr)
1127+ next.ServeHTTP(writer, r)
1128+ elapsed := time.Since(start)
1129+ logger.Debug("response",
1130+ "status", fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code)),
1131+ "bytes", humanize.Bytes(uint64(writer.bytes)),
1132+ "time", elapsed)
1133+ })
1134+ }
1135+}
1136diff --git a/server/web/server.go b/server/web/server.go
1137new file mode 100644
1138index 0000000000000000000000000000000000000000..ea15e778fbd2b4d131c75f9bd6f6f37b939e6be7
1139--- /dev/null
1140+++ b/server/web/server.go
1141@@ -0,0 +1,40 @@
1142+// Package server is the reusable server
1143+package web
1144+
1145+import (
1146+ "context"
1147+ "net/http"
1148+
1149+ "github.com/charmbracelet/log"
1150+ "github.com/charmbracelet/soft-serve/server/backend"
1151+ "github.com/charmbracelet/soft-serve/server/config"
1152+ "goji.io"
1153+ "goji.io/pat"
1154+)
1155+
1156+// Route is an interface for a route.
1157+type Route interface {
1158+ http.Handler
1159+ goji.Pattern
1160+}
1161+
1162+// NewRouter returns a new HTTP router.
1163+func NewRouter(ctx context.Context) *goji.Mux {
1164+ mux := goji.NewMux()
1165+ cfg := config.FromContext(ctx)
1166+ be := backend.FromContext(ctx)
1167+ logger := log.FromContext(ctx).WithPrefix("http")
1168+
1169+ // Middlewares
1170+ mux.Use(NewLoggingMiddleware(logger))
1171+
1172+ // Git routes
1173+ for _, service := range gitRoutes(ctx, logger) {
1174+ mux.Handle(service, service)
1175+ }
1176+
1177+ // go-get handler
1178+ mux.Handle(pat.Get("/*"), GoGetHandler{cfg, be})
1179+
1180+ return mux
1181+}