Detailed changes
@@ -57,16 +57,12 @@ func main() {
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
- log.Printf("Starting SSH server on %s:%d", cfg.BindAddr, cfg.Port)
go func() {
- if err := s.Start(); err != nil {
- log.Fatalln(err)
- }
+ s.Start()
}()
<-done
- log.Printf("Stopping SSH server on %s:%d", cfg.BindAddr, cfg.Port)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer func() { cancel() }()
if err := s.Shutdown(ctx); err != nil {
@@ -1,6 +1,7 @@
package config
import (
+ "crypto/tls"
"log"
"path/filepath"
@@ -17,12 +18,14 @@ type Callbacks interface {
// Config is the configuration for Soft Serve.
type Config struct {
BindAddr string `env:"SOFT_SERVE_BIND_ADDRESS" envDefault:""`
- Port int `env:"SOFT_SERVE_PORT" envDefault:"23231"`
+ SSHPort int `env:"SOFT_SERVE_SSH_PORT" envDefault:"23231"`
+ HTTPPort int `env:"SOFT_SERVE_HTTP_PORT" envDefault:"23232"`
KeyPath string `env:"SOFT_SERVE_KEY_PATH"`
RepoPath string `env:"SOFT_SERVE_REPO_PATH" envDefault:".repos"`
InitialAdminKeys []string `env:"SOFT_SERVE_INITIAL_ADMIN_KEY" envSeparator:"\n"`
Callbacks Callbacks
ErrorLog *log.Logger
+ TLSConfig *tls.Config
}
// DefaultConfig returns a Config with the values populated with the defaults
@@ -50,3 +53,8 @@ func (c *Config) WithErrorLogger(logger *log.Logger) *Config {
c.ErrorLog = logger
return c
}
+
+func (c *Config) WithTLSConfig(t *tls.Config) *Config {
+ c.TLSConfig = t
+ return c
+}
@@ -22,6 +22,8 @@ require (
gopkg.in/yaml.v2 v2.4.0
)
+require goji.io v2.0.2+incompatible
+
require (
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f // indirect
@@ -141,6 +141,8 @@ github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
+goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c=
+goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
@@ -52,7 +52,7 @@ func NewConfig(cfg *config.Config) (*Config, error) {
var yamlUsers string
var displayHost string
host := cfg.BindAddr
- port := cfg.Port
+ port := cfg.SSHPort
pks := make([]string, 0)
for _, k := range cfg.InitialAdminKeys {
@@ -0,0 +1,77 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/charmbracelet/soft-serve/config"
+ appCfg "github.com/charmbracelet/soft-serve/internal/config"
+ "github.com/charmbracelet/wish/git"
+ "goji.io"
+ "goji.io/pat"
+ "goji.io/pattern"
+)
+
+type HTTPServer struct {
+ server *http.Server
+ gitHandler http.Handler
+ cfg *config.Config
+ ac *appCfg.Config
+}
+
+func NewHTTPServer(cfg *config.Config, ac *appCfg.Config) *HTTPServer {
+ h := goji.NewMux()
+ s := &HTTPServer{
+ cfg: cfg,
+ ac: ac,
+ gitHandler: http.FileServer(http.Dir(cfg.RepoPath)),
+ server: &http.Server{
+ Addr: fmt.Sprintf(":%d", cfg.HTTPPort),
+ Handler: h,
+ TLSConfig: cfg.TLSConfig,
+ },
+ }
+ h.HandleFunc(pat.Get("/:repo"), s.handleGit)
+ h.HandleFunc(pat.Get("/:repo/*"), s.handleGit)
+ return s
+}
+
+func (s *HTTPServer) Start() error {
+ if s.cfg.TLSConfig != nil {
+ return s.server.ListenAndServeTLS("", "")
+ }
+ return s.server.ListenAndServe()
+}
+
+func (s *HTTPServer) Shutdown(ctx context.Context) error {
+ return s.server.Shutdown(ctx)
+}
+
+func (s *HTTPServer) handleGit(w http.ResponseWriter, r *http.Request) {
+ ua := r.Header.Get("User-Agent")
+ repo := pat.Param(r, "repo")
+ access := s.ac.AuthRepo(repo, nil)
+ path := pattern.Path(r.Context())
+ stat, err := os.Stat(filepath.Join(s.cfg.RepoPath, repo, path))
+ // Restrict access to files
+ if err != nil || stat.IsDir() {
+ http.NotFound(w, r)
+ return
+ }
+ if !strings.HasPrefix(strings.ToLower(ua), "git") {
+ http.Error(w, fmt.Sprintf("%d Bad Request", http.StatusBadRequest), http.StatusBadRequest)
+ return
+ }
+ if access < git.ReadOnlyAccess || !s.ac.AllowKeyless {
+ http.Error(w, fmt.Sprintf("%d Unauthorized", http.StatusUnauthorized), http.StatusUnauthorized)
+ return
+ }
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+ r.URL.Path = fmt.Sprintf("/%s/%s", repo, path)
+ s.gitHandler.ServeHTTP(w, r)
+}
@@ -2,26 +2,18 @@ package server
import (
"context"
- "fmt"
"log"
"github.com/charmbracelet/soft-serve/config"
appCfg "github.com/charmbracelet/soft-serve/internal/config"
- "github.com/charmbracelet/soft-serve/internal/tui"
-
- "github.com/charmbracelet/wish"
- bm "github.com/charmbracelet/wish/bubbletea"
- gm "github.com/charmbracelet/wish/git"
- lm "github.com/charmbracelet/wish/logging"
- rm "github.com/charmbracelet/wish/recover"
- "github.com/gliderlabs/ssh"
)
// Server is the Soft Serve server.
type Server struct {
- SSHServer *ssh.Server
- Config *config.Config
- config *appCfg.Config
+ HTTPServer *HTTPServer
+ SSHServer *SSHServer
+ Cfg *config.Config
+ ac *appCfg.Config
}
// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
@@ -34,43 +26,38 @@ func NewServer(cfg *config.Config) *Server {
if err != nil {
log.Fatal(err)
}
- mw := []wish.Middleware{
- rm.MiddlewareWithLogger(
- cfg.ErrorLog,
- softServeMiddleware(ac),
- bm.Middleware(tui.SessionHandler(ac)),
- gm.Middleware(cfg.RepoPath, ac),
- lm.Middleware(),
- ),
- }
- s, err := wish.NewServer(
- ssh.PublicKeyAuth(ac.PublicKeyHandler),
- ssh.PasswordAuth(ac.PasswordHandler),
- wish.WithAddress(fmt.Sprintf("%s:%d", cfg.BindAddr, cfg.Port)),
- wish.WithHostKeyPath(cfg.KeyPath),
- wish.WithMiddleware(mw...),
- )
- if err != nil {
- log.Fatalln(err)
- }
return &Server{
- SSHServer: s,
- Config: cfg,
- config: ac,
+ HTTPServer: NewHTTPServer(cfg, ac),
+ SSHServer: NewSSHServer(cfg, ac),
+ Cfg: cfg,
+ ac: ac,
}
}
// Reload reloads the server configuration.
func (srv *Server) Reload() error {
- return srv.config.Reload()
+ return srv.ac.Reload()
}
-// Start starts the SSH server.
-func (srv *Server) Start() error {
- return srv.SSHServer.ListenAndServe()
+func (s *Server) Start() {
+ go func() {
+ log.Printf("Starting HTTP server on %s:%d", s.Cfg.BindAddr, s.Cfg.HTTPPort)
+ if err := s.HTTPServer.Start(); err != nil {
+ log.Fatal(err)
+ }
+ }()
+ log.Printf("Starting SSH server on %s:%d", s.Cfg.BindAddr, s.Cfg.SSHPort)
+ if err := s.SSHServer.Start(); err != nil {
+ log.Fatal(err)
+ }
}
-// Shutdown lets the server gracefully shutdown.
-func (srv *Server) Shutdown(ctx context.Context) error {
- return srv.SSHServer.Shutdown(ctx)
+func (s *Server) Shutdown(ctx context.Context) error {
+ log.Printf("Stopping SSH server on %s:%d", s.Cfg.BindAddr, s.Cfg.SSHPort)
+ err := s.SSHServer.Shutdown(ctx)
+ if err != nil {
+ return err
+ }
+ log.Printf("Stopping HTTP server on %s:%d", s.Cfg.BindAddr, s.Cfg.SSHPort)
+ return s.HTTPServer.Shutdown(ctx)
}
@@ -0,0 +1,65 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ "github.com/charmbracelet/soft-serve/config"
+ appCfg "github.com/charmbracelet/soft-serve/internal/config"
+ "github.com/charmbracelet/soft-serve/internal/tui"
+ "github.com/charmbracelet/wish"
+ bm "github.com/charmbracelet/wish/bubbletea"
+ gm "github.com/charmbracelet/wish/git"
+ lm "github.com/charmbracelet/wish/logging"
+ rm "github.com/charmbracelet/wish/recover"
+ "github.com/gliderlabs/ssh"
+)
+
+type SSHServer struct {
+ server *ssh.Server
+ cfg *config.Config
+ ac *appCfg.Config
+}
+
+// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
+// server key-pair will be created if none exists. An initial admin SSH public
+// key can be provided with authKey. If authKey is provided, access will be
+// restricted to that key. If authKey is not provided, the server will be
+// publicly writable until configured otherwise by cloning the `config` repo.
+func NewSSHServer(cfg *config.Config, ac *appCfg.Config) *SSHServer {
+ mw := []wish.Middleware{
+ rm.MiddlewareWithLogger(
+ cfg.ErrorLog,
+ softServeMiddleware(ac),
+ bm.Middleware(tui.SessionHandler(ac)),
+ gm.Middleware(cfg.RepoPath, ac),
+ lm.Middleware(),
+ ),
+ }
+ s, err := wish.NewServer(
+ ssh.PublicKeyAuth(ac.PublicKeyHandler),
+ ssh.PasswordAuth(ac.PasswordHandler),
+ wish.WithAddress(fmt.Sprintf("%s:%d", cfg.BindAddr, cfg.SSHPort)),
+ wish.WithHostKeyPath(cfg.KeyPath),
+ wish.WithMiddleware(mw...),
+ )
+ if err != nil {
+ log.Fatalln(err)
+ }
+ return &SSHServer{
+ server: s,
+ cfg: cfg,
+ ac: ac,
+ }
+}
+
+// Start starts the SSH server.
+func (s *SSHServer) Start() error {
+ return s.server.ListenAndServe()
+}
+
+// Shutdown lets the server gracefully shutdown.
+func (s *SSHServer) Shutdown(ctx context.Context) error {
+ return s.server.Shutdown(ctx)
+}