feat: HTTP Server to support go get

Carlos A Becker created

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>

Change summary

server/config/config.go | 18 +++++++++++----
server/http.go          | 50 +++++++++++++++++++++++++++++++++++++++++++
server/server.go        | 31 +++++++++++++++++++++++---
3 files changed, 90 insertions(+), 9 deletions(-)

Detailed changes

server/config/config.go 🔗

@@ -55,6 +55,13 @@ type DBConfig struct {
 	SSLMode  bool   `env:"SSL_MODE" envDefault:"false"`
 }
 
+// HTTPConfig is the HTTP server config.
+type HTTPConfig struct {
+	Enabled bool   `env:"ENABLED" envDefault:"true"`
+	Port    int    `env:"PORT" envDefault:"8080"`
+	Domain  string `env:"DOMAIN" envDefault:"localhost"` // used for go get
+}
+
 // URL returns a database URL for the configuration.
 func (d *DBConfig) URL() *url.URL {
 	switch d.Driver {
@@ -88,9 +95,10 @@ func (d *DBConfig) URL() *url.URL {
 type Config struct {
 	Host string `env:"HOST" envDefault:"localhost"`
 
-	SSH SSHConfig `env:"SSH" envPrefix:"SSH_"`
-	Git GitConfig `env:"GIT" envPrefix:"GIT_"`
-	Db  DBConfig  `env:"DB" envPrefix:"DB_"`
+	SSH  SSHConfig  `env:"SSH" envPrefix:"SSH_"`
+	Git  GitConfig  `env:"GIT" envPrefix:"GIT_"`
+	HTTP HTTPConfig `env:"HTTP" envPrefix:"HTTP_"`
+	Db   DBConfig   `env:"DB" envPrefix:"DB_"`
 
 	ServerName string            `env:"SERVER_NAME" envDefault:"Soft Serve"`
 	AnonAccess proto.AccessLevel `env:"ANON_ACCESS" envDefault:"read-only"`
@@ -174,12 +182,12 @@ func DefaultConfig() *Config {
 		cfg.InitialAdminKeys[i] = pk
 	}
 	// init data path and db
-	if err := os.MkdirAll(cfg.RepoPath(), 0755); err != nil {
+	if err := os.MkdirAll(cfg.RepoPath(), 0o755); err != nil {
 		log.Fatalln(err)
 	}
 	switch cfg.Db.Driver {
 	case "sqlite":
-		if err := os.MkdirAll(filepath.Dir(cfg.DBPath()), 0755); err != nil {
+		if err := os.MkdirAll(filepath.Dir(cfg.DBPath()), 0o755); err != nil {
 			log.Fatalln(err)
 		}
 		db, err := sqlite.New(cfg.DBPath())

server/http.go 🔗

@@ -0,0 +1,50 @@
+package server
+
+import (
+	"html/template"
+	"log"
+	"net"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/charmbracelet/soft-serve/server/config"
+)
+
+func newHTTPServer(cfg *config.Config) *http.Server {
+	r := http.NewServeMux()
+	r.HandleFunc("/", repoIndexHandler(cfg))
+	return &http.Server{
+		Addr:              net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.HTTP.Port)),
+		Handler:           r,
+		ReadHeaderTimeout: time.Second * 10,
+		ReadTimeout:       time.Second * 10,
+		WriteTimeout:      time.Second * 10,
+		MaxHeaderBytes:    http.DefaultMaxHeaderBytes,
+	}
+}
+
+var repoIndexHTMLTpl = template.Must(template.New("index").Parse(`<!DOCTYPE html>
+<html lang="en">
+<head>
+ 	<meta name="go-import" content="{{ .Config.HTTP.Domain }}/{{ .Repo }} git ssh://{{ .Config.Host }}:{{ .Config.SSH.Port }}/{{ .Repo }}">
+</head>
+</html>`))
+
+func repoIndexHandler(cfg *config.Config) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		repo := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/")[0]
+		log.Println("serving index for", repo)
+		if err := repoIndexHTMLTpl.Execute(w, struct {
+			Repo   string
+			Config config.Config
+		}{
+			Repo:   repo,
+			Config: *cfg,
+		}); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	}
+}

server/server.go 🔗

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"log"
+	"net/http"
 	"time"
 
 	cm "github.com/charmbracelet/soft-serve/server/cmd"
@@ -21,9 +22,10 @@ import (
 
 // Server is the Soft Serve server.
 type Server struct {
-	SSHServer *ssh.Server
-	GitServer *daemon.Daemon
-	Config    *config.Config
+	SSHServer  *ssh.Server
+	GitServer  *daemon.Daemon
+	HTTPServer *http.Server
+	Config     *config.Config
 }
 
 // NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
@@ -63,7 +65,6 @@ func NewServer(cfg *config.Config) *Server {
 	} else {
 		opts = append(opts, wish.WithHostKeyPath(cfg.PrivateKeyPath()))
 	}
-	opts = append(opts)
 	sh, err := wish.NewServer(opts...)
 	if err != nil {
 		log.Fatalln(err)
@@ -82,6 +83,9 @@ func NewServer(cfg *config.Config) *Server {
 		}
 		s.GitServer = d
 	}
+	if cfg.HTTP.Enabled {
+		s.HTTPServer = newHTTPServer(cfg)
+	}
 	return s
 }
 
@@ -97,6 +101,15 @@ func (s *Server) Start() error {
 			return nil
 		})
 	}
+	if s.Config.HTTP.Enabled {
+		errg.Go(func() error {
+			log.Printf("Starting HTTP server on %s:%d", s.Config.Host, s.Config.HTTP.Port)
+			if err := s.HTTPServer.ListenAndServe(); err != http.ErrServerClosed {
+				return err
+			}
+			return nil
+		})
+	}
 	errg.Go(func() error {
 		log.Printf("Starting SSH server on %s:%d", s.Config.Host, s.Config.SSH.Port)
 		if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
@@ -115,6 +128,11 @@ func (s *Server) Shutdown(ctx context.Context) error {
 			return s.GitServer.Shutdown(ctx)
 		})
 	}
+	if s.Config.HTTP.Enabled {
+		errg.Go(func() error {
+			return s.HTTPServer.Shutdown(ctx)
+		})
+	}
 	errg.Go(func() error {
 		return s.SSHServer.Shutdown(ctx)
 	})
@@ -132,5 +150,10 @@ func (s *Server) Close() error {
 			return s.GitServer.Close()
 		})
 	}
+	if s.Config.HTTP.Enabled {
+		errg.Go(func() error {
+			return s.HTTPServer.Close()
+		})
+	}
 	return errg.Wait()
 }