0c6cc324cc6cead9bce636c7cf61af560a81fa91

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

feat(server): http server

* Git dumb transport for public repos
* Go Import response

Diff

This diff is truncated to protect this page.

  1diff --git a/go.mod b/go.mod
  2index 28f483877d5e5cefe4c48df2d8313ba9e8a97950..f2088542e92c527d1979083e70d2c4994907d09f 100644
  3--- a/go.mod
  4+++ b/go.mod
  5@@ -29,6 +29,7 @@ require (
  6 	github.com/muesli/mango-cobra v1.2.0
  7 	github.com/muesli/roff v0.1.0
  8 	github.com/spf13/cobra v1.6.1
  9+	goji.io v2.0.2+incompatible
 10 	golang.org/x/crypto v0.7.0
 11 	golang.org/x/sync v0.1.0
 12 )
 13diff --git a/go.sum b/go.sum
 14index 626d920b1c3db6dedfe4381536bd047e47879539..04e8a16f6787ffec10cbf2d570ba0392ee91e189 100644
 15--- a/go.sum
 16+++ b/go.sum
 17@@ -180,6 +180,8 @@ github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
 18 github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 19 github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
 20 github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
 21+goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c=
 22+goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=
 23 golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 24 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 25 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 26diff --git a/server/config/config.go b/server/config/config.go
 27index 8a3de7e1c5313333245a07b0fde349a1281d1e66..e005cdea7a879bbe6711a0f98d785f48465dad8a 100644
 28--- a/server/config/config.go
 29+++ b/server/config/config.go
 30@@ -14,6 +14,9 @@ type SSHConfig struct {
 31 	// ListenAddr is the address on which the SSH server will listen.
 32 	ListenAddr string `env:"LISTEN_ADDR" envDefault:":23231"`
 33 
 34+	// PublicURL is the public URL of the SSH server.
 35+	PublicURL string `env:"PUBLIC_URL" envDefault:"ssh://localhost:23231"`
 36+
 37 	// KeyPath is the path to the SSH server's private key.
 38 	KeyPath string `env:"KEY_PATH"`
 39 
 40@@ -39,14 +42,29 @@ type GitConfig struct {
 41 	MaxConnections int `env:"MAX_CONNECTIONS" envDefault:"32"`
 42 }
 43 
 44+// HTTPConfig is the HTTP configuration for the server.
 45+type HTTPConfig struct {
 46+	// ListenAddr is the address on which the HTTP server will listen.
 47+	ListenAddr string `env:"LISTEN_ADDR" envDefault:":8080"`
 48+
 49+	// PublicURL is the public URL of the HTTP server.
 50+	PublicURL string `env:"PUBLIC_URL" envDefault:"http://localhost:8080"`
 51+}
 52+
 53 // Config is the configuration for Soft Serve.
 54 type Config struct {
 55+	// Name is the name of the server.
 56+	Name string `env:"NAME" envDefault:"Soft Serve"`
 57+
 58 	// SSH is the configuration for the SSH server.
 59 	SSH SSHConfig `envPrefix:"SSH_"`
 60 
 61 	// Git is the configuration for the Git daemon.
 62 	Git GitConfig `envPrefix:"GIT_"`
 63 
 64+	// HTTP is the configuration for the HTTP server.
 65+	HTTP HTTPConfig `envPrefix:"HTTP_"`
 66+
 67 	// InitialAdminKeys is a list of public keys that will be added to the list of admins.
 68 	InitialAdminKeys []string `env:"INITIAL_ADMIN_KEY" envSeparator:"\n"`
 69 
 70diff --git a/server/daemon.go b/server/daemon.go
 71index d1f0c438724dabc7b7dc04acc4261a2761d85907..789f1e9bd39b2adc7fd4ce7152d3b1844e2f4195 100644
 72--- a/server/daemon.go
 73+++ b/server/daemon.go
 74@@ -3,7 +3,7 @@ package server
 75 import (
 76 	"bytes"
 77 	"context"
 78-	"errors"
 79+	"fmt"
 80 	"io"
 81 	"net"
 82 	"path/filepath"
 83@@ -16,7 +16,7 @@ import (
 84 )
 85 
 86 // ErrServerClosed indicates that the server has been closed.
 87-var ErrServerClosed = errors.New("git: Server closed")
 88+var ErrServerClosed = fmt.Errorf("git: %w", net.ErrClosed)
 89 
 90 // connections synchronizes access to to a net.Conn pool.
 91 type connections struct {
 92diff --git a/server/http.go b/server/http.go
 93new file mode 100644
 94index 0000000000000000000000000000000000000000..fdc58c551f394e920852a1fe47cb337501b956ef
 95--- /dev/null
 96+++ b/server/http.go
 97@@ -0,0 +1,183 @@
 98+package server
 99+
100+import (
101+	"context"
102+	"fmt"
103+	"net/http"
104+	"net/url"
105+	"os"
106+	"path/filepath"
107+	"strings"
108+	"text/template"
109+	"time"
110+
111+	"github.com/charmbracelet/soft-serve/server/backend"
112+	"github.com/charmbracelet/soft-serve/server/config"
113+	"github.com/dustin/go-humanize"
114+	"goji.io"
115+	"goji.io/pat"
116+	"goji.io/pattern"
117+)
118+
119+// logWriter is a wrapper around http.ResponseWriter that allows us to capture
120+// the HTTP status code and bytes written to the response.
121+type logWriter struct {
122+	http.ResponseWriter
123+	code, bytes int
124+}
125+
126+func (r *logWriter) Write(p []byte) (int, error) {
127+	written, err := r.ResponseWriter.Write(p)
128+	r.bytes += written
129+	return written, err
130+}
131+
132+// Note this is generally only called when sending an HTTP error, so it's
133+// important to set the `code` value to 200 as a default
134+func (r *logWriter) WriteHeader(code int) {
135+	r.code = code
136+	r.ResponseWriter.WriteHeader(code)
137+}
138+
139+func loggingMiddleware(next http.Handler) http.Handler {
140+	logger := logger.WithPrefix("server.http")
141+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
142+		start := time.Now()
143+		writer := &logWriter{code: http.StatusOK, ResponseWriter: w}
144+		logger.Debug("request",
145+			"method", r.Method,
146+			"uri", r.RequestURI,
147+			"addr", r.RemoteAddr)
148+		next.ServeHTTP(writer, r)
149+		elapsed := time.Since(start)
150+		logger.Debug("response",
151+			"status", fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code)),
152+			"bytes", humanize.Bytes(uint64(writer.bytes)),
153+			"time", elapsed)
154+	})
155+}
156+
157+// HTTPServer is an http server.
158+type HTTPServer struct {
159+	cfg        *config.Config
160+	server     *http.Server
161+	dirHandler http.Handler
162+}
163+
164+func NewHTTPServer(cfg *config.Config) (*HTTPServer, error) {
165+	mux := goji.NewMux()
166+	s := &HTTPServer{
167+		cfg:        cfg,
168+		dirHandler: http.FileServer(http.Dir(cfg.Backend.RepositoryStorePath())),
169+		server: &http.Server{
170+			Addr:              cfg.HTTP.ListenAddr,
171+			Handler:           mux,
172+			ReadHeaderTimeout: time.Second * 10,
173+			ReadTimeout:       time.Second * 10,
174+			WriteTimeout:      time.Second * 10,
175+			MaxHeaderBytes:    http.DefaultMaxHeaderBytes,
176+		},
177+	}
178+
179+	mux.Use(loggingMiddleware)
180+	mux.HandleFunc(pat.Get("/:repo"), s.repoIndexHandler)
181+	mux.HandleFunc(pat.Get("/:repo/*"), s.dumbGitHandler)
182+	return s, nil
183+}
184+
185+// Close closes the HTTP server.
186+func (s *HTTPServer) Close() error {
187+	return s.server.Close()
188+}
189+
190+// ListenAndServe starts the HTTP server.
191+func (s *HTTPServer) ListenAndServe() error {
192+	return s.server.ListenAndServe()
193+}
194+
195+// Shutdown gracefully shuts down the HTTP server.
196+func (s *HTTPServer) Shutdown(ctx context.Context) error {
197diff --git a/server/server.go b/server/server.go
198index b52d09f28c08f5c71f3621055f0f2166307cc1ca..f1c703af2a1e195c530a13a87db7d369e052b22a 100644
199--- a/server/server.go
200+++ b/server/server.go
201@@ -2,6 +2,7 @@ package server
202 
203 import (
204 	"context"
205+	"net/http"
206 
207 	"github.com/charmbracelet/log"
208 
209@@ -17,11 +18,12 @@ var (
210 
211 // Server is the Soft Serve server.
212 type Server struct {
213-	SSHServer *SSHServer
214-	GitDaemon *GitDaemon
215-	Config    *config.Config
216-	Backend   backend.Backend
217-	Access    backend.AccessMethod
218+	SSHServer  *SSHServer
219+	GitDaemon  *GitDaemon
220+	HTTPServer *HTTPServer
221+	Config     *config.Config
222+	Backend    backend.Backend
223+	Access     backend.AccessMethod
224 }
225 
226 // NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
227@@ -46,6 +48,11 @@ func NewServer(cfg *config.Config) (*Server, error) {
228 		return nil, err
229 	}
230 
231+	srv.HTTPServer, err = NewHTTPServer(cfg)
232+	if err != nil {
233+		return nil, err
234+	}
235+
236 	return srv, nil
237 }
238 
239@@ -59,6 +66,13 @@ func (s *Server) Start() error {
240 		}
241 		return nil
242 	})
243+	errg.Go(func() error {
244+		log.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
245+		if err := s.HTTPServer.ListenAndServe(); err != http.ErrServerClosed {
246+			return err
247+		}
248+		return nil
249+	})
250 	errg.Go(func() error {
251 		log.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
252 		if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
253@@ -75,6 +89,9 @@ func (s *Server) Shutdown(ctx context.Context) error {
254 	errg.Go(func() error {
255 		return s.GitDaemon.Shutdown(ctx)
256 	})
257+	errg.Go(func() error {
258+		return s.HTTPServer.Shutdown(ctx)
259+	})
260 	errg.Go(func() error {
261 		return s.SSHServer.Shutdown(ctx)
262 	})
263@@ -84,11 +101,8 @@ func (s *Server) Shutdown(ctx context.Context) error {
264 // Close closes the SSH server.
265 func (s *Server) Close() error {
266 	var errg errgroup.Group
267-	errg.Go(func() error {
268-		return s.SSHServer.Close()
269-	})
270-	errg.Go(func() error {
271-		return s.GitDaemon.Close()
272-	})
273+	errg.Go(s.GitDaemon.Close)
274+	errg.Go(s.HTTPServer.Close)
275+	errg.Go(s.SSHServer.Close)
276 	return errg.Wait()
277 }
278diff --git a/server/ssh.go b/server/ssh.go
279index c0f3529fd25958b3b5a827ee6a56d16542154a29..25dcdaed86d01b6e9c3d18263f27bab7037aeafd 100644
280--- a/server/ssh.go
281+++ b/server/ssh.go
282@@ -1,7 +1,9 @@
283 package server
284 
285 import (
286+	"context"
287 	"errors"
288+	"net"
289 	"path/filepath"
290 	"strings"
291 	"time"
292@@ -21,7 +23,7 @@ import (
293 
294 // SSHServer is a SSH server that implements the git protocol.
295 type SSHServer struct {
296-	*ssh.Server
297+	srv *ssh.Server
298 	cfg *config.Config
299 }
300 
301@@ -35,7 +37,7 @@ func NewSSHServer(cfg *config.Config) (*SSHServer, error) {
302 			logger,
303 			// BubbleTea middleware.
304 			bm.MiddlewareWithProgramHandler(SessionHandler(cfg), termenv.ANSI256),
305-			// Command middleware must come after the git middleware.
306+			// CLI middleware.
307 			cm.Middleware(cfg),
308 			// Git middleware.
309 			s.Middleware(cfg),
310@@ -43,7 +45,7 @@ func NewSSHServer(cfg *config.Config) (*SSHServer, error) {
311 			lm.MiddlewareWithLogger(logger),
312 		),
313 	}
314-	s.Server, err = wish.NewServer(
315+	s.srv, err = wish.NewServer(
316 		ssh.PublicKeyAuth(s.PublicKeyHandler),
317 		ssh.KeyboardInteractiveAuth(s.KeyboardInteractiveHandler),
318 		wish.WithAddress(cfg.SSH.ListenAddr),
319@@ -55,15 +57,35 @@ func NewSSHServer(cfg *config.Config) (*SSHServer, error) {
320 	}
321 
322 	if cfg.SSH.MaxTimeout > 0 {
323-		s.Server.MaxTimeout = time.Duration(cfg.SSH.MaxTimeout) * time.Second
324+		s.srv.MaxTimeout = time.Duration(cfg.SSH.MaxTimeout) * time.Second
325 	}
326 	if cfg.SSH.IdleTimeout > 0 {
327-		s.Server.IdleTimeout = time.Duration(cfg.SSH.IdleTimeout) * time.Second
328+		s.srv.IdleTimeout = time.Duration(cfg.SSH.IdleTimeout) * time.Second
329 	}
330 
331 	return s, nil
332 }
333 
334+// ListenAndServe starts the SSH server.
335+func (s *SSHServer) ListenAndServe() error {
336+	return s.srv.ListenAndServe()
337+}
338+
339+// Serve starts the SSH server on the given net.Listener.
340+func (s *SSHServer) Serve(l net.Listener) error {
341+	return s.srv.Serve(l)
342+}
343+
344+// Close closes the SSH server.
345+func (s *SSHServer) Close() error {
346+	return s.srv.Close()
347+}
348+
349+// Shutdown gracefully shuts down the SSH server.
350+func (s *SSHServer) Shutdown(ctx context.Context) error {
351+	return s.srv.Shutdown(ctx)
352+}
353+
354 // PublicKeyAuthHandler handles public key authentication.
355 func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
356 	return s.cfg.Access.AccessLevel("", pk) > backend.NoAccess
357diff --git a/ui/common/utils.go b/ui/common/utils.go
358index de8fbe8a6316795c5f581fae471f3287122d1894..ae7c3839a6197e3245b96a5d703fd94594005f85 100644
359--- a/ui/common/utils.go
360+++ b/ui/common/utils.go
361@@ -15,10 +15,6 @@ func TruncateString(s string, max int) string {
362 }
363 
364 // RepoURL returns the URL of the repository.
365-func RepoURL(host string, port string, name string) string {
366-	p := ""
367-	if port != "22" {
368-		p += ":" + port
369-	}
370-	return fmt.Sprintf("git clone ssh://%s/%s", host+p, name)
371+func RepoURL(publicURL, name string) string {
372+	return fmt.Sprintf("git clone %s/%s", publicURL, name)
373 }
374diff --git a/ui/pages/repo/empty.go b/ui/pages/repo/empty.go
375index bddab29f15fc476c9f7b974ecc877c6e294dcacc..d5230c42a5da1179bf4d2e5ef6e1c6abfeb83838 100644
376--- a/ui/pages/repo/empty.go
377+++ b/ui/pages/repo/empty.go
378@@ -8,10 +8,7 @@ import (
379 )
380 
381 func defaultEmptyRepoMsg(cfg *config.Config, repo string) string {
382-	host := cfg.Backend.ServerHost()
383-	if cfg.Backend.ServerPort() != "22" {
384-		host = fmt.Sprintf("%s:%s", host, cfg.Backend.ServerPort())
385-	}
386+	publicURL := cfg.SSH.PublicURL
387 	repo = strings.TrimSuffix(repo, ".git")
388 	return fmt.Sprintf(`# Quick Start
389 
390@@ -20,7 +17,7 @@ Get started by cloning this repository, add your files, commit, and push.
391 ## Clone this repository.
392 
393 `+"```"+`sh
394-git clone ssh://%[1]s/%[2]s.git
395+git clone %[1]s/%[2]s.git
396 `+"```"+`
397 
398 ## Creating a new repository on the command line
399@@ -31,15 +28,15 @@ git init
400 git add README.md
401 git branch -M main
402 git commit -m "first commit"
403-git remote add origin ssh://%[1]s/%[2]s.git
404+git remote add origin %[1]s/%[2]s.git
405 git push -u origin main
406 `+"```"+`
407 
408 ## Pushing an existing repository from the command line
409 
410 `+"```"+`sh
411-git remote add origin ssh://%[1]s/%[2]s.git
412+git remote add origin %[1]s/%[2]s.git
413 git push -u origin main
414 `+"```"+`
415-`, host, repo)
416+`, publicURL, repo)
417 }
418diff --git a/ui/pages/repo/repo.go b/ui/pages/repo/repo.go
419index 16c2af44fb00434276618356071e7cf24c04a500..75f3ac38ad302cc3af82ef9633141fe11479ffbb 100644
420--- a/ui/pages/repo/repo.go
421+++ b/ui/pages/repo/repo.go
422@@ -235,10 +235,8 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
423 		}
424 	case CopyURLMsg:
425 		if cfg := r.common.Config(); cfg != nil {
426-			host := cfg.Backend.ServerHost()
427-			port := cfg.Backend.ServerPort()
428 			r.common.Copy.Copy(
429-				common.RepoURL(host, port, r.selectedRepo.Name()),
430+				common.RepoURL(cfg.SSH.PublicURL, r.selectedRepo.Name()),
431 			)
432 		}
433 	case ResetURLMsg:
434@@ -342,7 +340,7 @@ func (r *Repo) headerView() string {
435 		Align(lipgloss.Right)
436 	var url string
437 	if cfg := r.common.Config(); cfg != nil {
438-		url = common.RepoURL(cfg.Backend.ServerHost(), cfg.Backend.ServerPort(), r.selectedRepo.Name())
439+		url = common.RepoURL(cfg.SSH.PublicURL, r.selectedRepo.Name())
440 	}
441 	if !r.copyURL.IsZero() && r.copyURL.Add(time.Second).After(time.Now()) {
442 		url = "copied!"
443diff --git a/ui/pages/selection/item.go b/ui/pages/selection/item.go
444index 596ddcef2a287768cc19ccfbc17562e8086da33a..83bf3e6c3cdb17f60d1580de113059ea1c15f078 100644
445--- a/ui/pages/selection/item.go
446+++ b/ui/pages/selection/item.go
447@@ -68,7 +68,7 @@ func NewItem(repo backend.Repository, cfg *config.Config) (Item, error) {
448 	return Item{
449 		repo:       repo,
450 		lastUpdate: lastUpdate,
451-		cmd:        common.RepoURL(cfg.Backend.ServerHost(), cfg.Backend.ServerPort(), repo.Name()),
452+		cmd:        common.RepoURL(cfg.SSH.PublicURL, repo.Name()),
453 	}, nil
454 }
455 
456diff --git a/ui/ui.go b/ui/ui.go
457index 29f4cde45d7097ecf56f1beb826bb2f9fb9b9189..78b92b8434249be1791defa69f5c0f85a097c764 100644
458--- a/ui/ui.go
459+++ b/ui/ui.go
460@@ -52,10 +52,7 @@ type UI struct {
461 
462 // New returns a new UI model.
463 func New(c common.Common, initialRepo string) *UI {
464-	var serverName string
465-	if cfg := c.Config(); cfg != nil {
466-		serverName = cfg.Backend.ServerName()
467-	}
468+	serverName := c.Config().Name
469 	h := header.New(c, serverName)
470 	ui := &UI{
471 		serverName:  serverName,