Skip to content

Commit 421a3be

Browse files
committed
soft-server http git server
Serves a dumb git server
1 parent 4c3bbb5 commit 421a3be

File tree

8 files changed

+151
-39
lines changed

8 files changed

+151
-39
lines changed

config/config.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"crypto/tls"
45
"log"
56

67
"github.com/meowgorithm/babyenv"
@@ -16,10 +17,13 @@ type Callbacks interface {
1617
// Config is the configuration for the soft-serve.
1718
type Config struct {
1819
Host string `env:"SOFT_SERVE_HOST" default:""`
19-
Port int `env:"SOFT_SERVE_PORT" default:"23231"`
20+
SSHPort int `env:"SOFT_SERVE_SSH_PORT" default:"23231"`
21+
HTTPPort int `env:"SOFT_SERVE_HTTP_PORT" default:"23232"`
22+
HTTPScheme string `env:"SOFT_SERVE_HTTP_SCHEME" default:"http"`
2023
KeyPath string `env:"SOFT_SERVE_KEY_PATH" default:".ssh/soft_serve_server_ed25519"`
2124
RepoPath string `env:"SOFT_SERVE_REPO_PATH" default:".repos"`
2225
InitialAdminKey string `env:"SOFT_SERVE_INITIAL_ADMIN_KEY" default:""`
26+
TLSConfig *tls.Config
2327
Callbacks Callbacks
2428
}
2529

go.mod

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ require (
2222

2323
require github.com/go-git/go-billy/v5 v5.3.1
2424

25+
require (
26+
github.com/go-git/go-billy/v5 v5.3.1
27+
goji.io v2.0.2+incompatible
28+
)
29+
2530
require (
2631
github.com/Microsoft/go-winio v0.4.16 // indirect
2732
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ github.com/yuin/goldmark v1.3.3 h1:37BdQwPx8VOSic8eDSWee6QL9mRpZRm9VJp/QugNrW0=
144144
github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
145145
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
146146
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
147+
goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c=
148+
goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=
147149
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
148150
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
149151
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=

internal/config/config.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func NewConfig(cfg *config.Config) (*Config, error) {
4747
var yamlUsers string
4848
var displayHost string
4949
host := cfg.Host
50-
port := cfg.Port
50+
port := cfg.SSHPort
5151
pk := cfg.InitialAdminKey
5252
rs := git.NewRepoSource(cfg.RepoPath)
5353
c := &Config{

main.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"log"
5+
"strings"
56

67
"github.com/charmbracelet/soft/config"
78
"github.com/charmbracelet/soft/server"
@@ -10,7 +11,8 @@ import (
1011
func main() {
1112
cfg := config.DefaultConfig()
1213
s := server.NewServer(cfg)
13-
log.Printf("Starting SSH server on %s:%d\n", cfg.Host, cfg.Port)
14+
log.Printf("Starting SSH server on %s:%d\n", cfg.Host, cfg.SSHPort)
15+
log.Printf("Starting %s server on %s:%d\n", strings.ToUpper(cfg.HTTPScheme), cfg.Host, cfg.HTTPPort)
1416
err := s.Start()
1517
if err != nil {
1618
log.Fatalln(err)

server/http.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/charmbracelet/soft/config"
10+
appCfg "github.com/charmbracelet/soft/internal/config"
11+
"github.com/charmbracelet/wish/git"
12+
"goji.io"
13+
"goji.io/pat"
14+
"goji.io/pattern"
15+
)
16+
17+
type HTTPServer struct {
18+
server *http.Server
19+
gitHandler http.Handler
20+
cfg *config.Config
21+
ac *appCfg.Config
22+
}
23+
24+
func NewHTTPServer(cfg *config.Config, ac *appCfg.Config) *HTTPServer {
25+
h := goji.NewMux()
26+
s := &HTTPServer{
27+
cfg: cfg,
28+
ac: ac,
29+
gitHandler: http.FileServer(http.Dir(cfg.RepoPath)),
30+
server: &http.Server{
31+
Addr: fmt.Sprintf(":%d", cfg.HTTPPort),
32+
Handler: h,
33+
TLSConfig: cfg.TLSConfig,
34+
},
35+
}
36+
h.HandleFunc(pat.Get("/:repo"), s.handleGit)
37+
h.HandleFunc(pat.Get("/:repo/*"), s.handleGit)
38+
return s
39+
}
40+
41+
func (s *HTTPServer) Start() error {
42+
if s.cfg.HTTPScheme == "https" {
43+
return s.server.ListenAndServeTLS("", "")
44+
} else {
45+
return s.server.ListenAndServe()
46+
}
47+
}
48+
49+
func (s *HTTPServer) handleGit(w http.ResponseWriter, r *http.Request) {
50+
repo := pat.Param(r, "repo")
51+
access := s.ac.AuthRepo(repo, nil)
52+
if access < git.ReadOnlyAccess || !s.ac.AllowKeyless {
53+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
54+
return
55+
}
56+
path := pattern.Path(r.Context())
57+
stat, err := os.Stat(filepath.Join(s.cfg.RepoPath, repo, path))
58+
// Restrict access to files
59+
if err != nil || stat.IsDir() {
60+
http.NotFound(w, r)
61+
return
62+
}
63+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
64+
w.Header().Set("X-Content-Type-Options", "nosniff")
65+
r.URL.Path = fmt.Sprintf("/%s/%s", repo, path)
66+
s.gitHandler.ServeHTTP(w, r)
67+
}

server/server.go

+15-36
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,41 @@
11
package server
22

33
import (
4-
"fmt"
54
"log"
65

76
"github.com/charmbracelet/soft/config"
87
appCfg "github.com/charmbracelet/soft/internal/config"
9-
"github.com/charmbracelet/soft/internal/tui"
10-
11-
"github.com/charmbracelet/wish"
12-
bm "github.com/charmbracelet/wish/bubbletea"
13-
gm "github.com/charmbracelet/wish/git"
14-
lm "github.com/charmbracelet/wish/logging"
15-
"github.com/gliderlabs/ssh"
168
)
179

1810
type Server struct {
19-
SSHServer *ssh.Server
20-
Config *config.Config
21-
config *appCfg.Config
11+
HTTPServer *HTTPServer
12+
SSHServer *SSHServer
13+
Config *config.Config
14+
ac *appCfg.Config
2215
}
2316

24-
// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
25-
// server key-pair will be created if none exists. An initial admin SSH public
26-
// key can be provided with authKey. If authKey is provided, access will be
27-
// restricted to that key. If authKey is not provided, the server will be
28-
// publicly writable until configured otherwise by cloning the `config` repo.
2917
func NewServer(cfg *config.Config) *Server {
3018
ac, err := appCfg.NewConfig(cfg)
3119
if err != nil {
3220
log.Fatal(err)
3321
}
34-
mw := []wish.Middleware{
35-
bm.Middleware(tui.SessionHandler(ac)),
36-
gm.Middleware(cfg.RepoPath, ac),
37-
lm.Middleware(),
38-
}
39-
s, err := wish.NewServer(
40-
ssh.PublicKeyAuth(ac.PublicKeyHandler),
41-
ssh.PasswordAuth(ac.PasswordHandler),
42-
wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)),
43-
wish.WithHostKeyPath(cfg.KeyPath),
44-
wish.WithMiddleware(mw...),
45-
)
46-
if err != nil {
47-
log.Fatalln(err)
48-
}
4922
return &Server{
50-
SSHServer: s,
51-
Config: cfg,
52-
config: ac,
23+
HTTPServer: NewHTTPServer(cfg, ac),
24+
SSHServer: NewSSHServer(cfg, ac),
25+
Config: cfg,
26+
ac: ac,
5327
}
5428
}
5529

5630
func (srv *Server) Reload() error {
57-
return srv.config.Reload()
31+
return srv.ac.Reload()
5832
}
5933

6034
func (srv *Server) Start() error {
61-
return srv.SSHServer.ListenAndServe()
35+
go func() {
36+
if err := srv.HTTPServer.Start(); err != nil {
37+
log.Fatal(err)
38+
}
39+
}()
40+
return srv.SSHServer.Start()
6241
}

server/ssh.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
"github.com/charmbracelet/soft/config"
8+
appCfg "github.com/charmbracelet/soft/internal/config"
9+
"github.com/charmbracelet/soft/internal/tui"
10+
"github.com/charmbracelet/wish"
11+
bm "github.com/charmbracelet/wish/bubbletea"
12+
gm "github.com/charmbracelet/wish/git"
13+
lm "github.com/charmbracelet/wish/logging"
14+
"github.com/gliderlabs/ssh"
15+
)
16+
17+
type SSHServer struct {
18+
s *ssh.Server
19+
cfg *config.Config
20+
ac *appCfg.Config
21+
}
22+
23+
// NewSSHServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
24+
// server key-pair will be created if none exists. An initial admin SSH public
25+
// key can be provided with authKey. If authKey is provided, access will be
26+
// restricted to that key. If authKey is not provided, the server will be
27+
// publicly writable until configured otherwise by cloning the `config` repo.
28+
func NewSSHServer(cfg *config.Config, ac *appCfg.Config) *SSHServer {
29+
mw := []wish.Middleware{
30+
bm.Middleware(tui.SessionHandler(ac)),
31+
gm.Middleware(cfg.RepoPath, ac),
32+
lm.Middleware(),
33+
}
34+
s, err := wish.NewServer(
35+
ssh.PublicKeyAuth(ac.PublicKeyHandler),
36+
ssh.PasswordAuth(ac.PasswordHandler),
37+
wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.SSHPort)),
38+
wish.WithHostKeyPath(cfg.KeyPath),
39+
wish.WithMiddleware(mw...),
40+
)
41+
if err != nil {
42+
log.Fatalln(err)
43+
}
44+
return &SSHServer{
45+
s: s,
46+
cfg: cfg,
47+
ac: ac,
48+
}
49+
}
50+
51+
func (s *SSHServer) Start() error {
52+
return s.s.ListenAndServe()
53+
}

0 commit comments

Comments
 (0)