0c6cc324cc6cead9bce636c7cf61af560a81fa91
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Date
Message
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,