@@ -6,11 +6,10 @@ import (
"log"
"net"
"os"
+ "strconv"
"testing"
- "github.com/charmbracelet/soft-serve/proto"
"github.com/charmbracelet/soft-serve/server/config"
- "github.com/charmbracelet/soft-serve/server/db/fakedb"
"github.com/charmbracelet/soft-serve/server/git"
"github.com/go-git/go-git/v5/plumbing/format/pktline"
)
@@ -23,21 +22,13 @@ func TestMain(m *testing.M) {
log.Fatal(err)
}
defer os.RemoveAll(tmp)
- cfg := &config.Config{
- Host: "",
- DataPath: tmp,
- AnonAccess: proto.ReadOnlyAccess,
- Git: config.GitConfig{
- // Reduce the max read timeout to 3 second so we can test the timeout.
- IdleTimeout: 3,
- // Reduce the max timeout to 100 second so we can test the timeout.
- MaxTimeout: 100,
- // Reduce the max connections to 3 so we can test the timeout.
- MaxConnections: 3,
- Port: 9418,
- },
- }
- cfg = cfg.WithDB(&fakedb.FakeDB{})
+ os.Setenv("SOFT_SERVE_DATA_PATH", tmp)
+ os.Setenv("SOFT_SERVE_ANON_ACCESS", "read-only")
+ os.Setenv("SOFT_SERVE_GIT_MAX_CONNECTIONS", "3")
+ os.Setenv("SOFT_SERVE_GIT_MAX_TIMEOUT", "100")
+ os.Setenv("SOFT_SERVE_GIT_IDLE_TIMEOUT", "3")
+ os.Setenv("SOFT_SERVE_GIT_PORT", strconv.Itoa(randomPort()))
+ cfg := config.DefaultConfig()
d, err := NewDaemon(cfg)
if err != nil {
log.Fatal(err)
@@ -50,6 +41,12 @@ func TestMain(m *testing.M) {
}()
defer d.Close()
os.Exit(m.Run())
+ os.Unsetenv("SOFT_SERVE_DATA_PATH")
+ os.Unsetenv("SOFT_SERVE_ANON_ACCESS")
+ os.Unsetenv("SOFT_SERVE_GIT_MAX_CONNECTIONS")
+ os.Unsetenv("SOFT_SERVE_GIT_MAX_TIMEOUT")
+ os.Unsetenv("SOFT_SERVE_GIT_IDLE_TIMEOUT")
+ os.Unsetenv("SOFT_SERVE_GIT_PORT")
}
func TestIdleTimeout(t *testing.T) {
@@ -94,3 +91,9 @@ func readPktline(c net.Conn) (string, error) {
}
return string(pktout.Bytes()), nil
}
+
+func randomPort() int {
+ addr, _ := net.Listen("tcp", ":0") //nolint:gosec
+ _ = addr.Close()
+ return addr.Addr().(*net.TCPAddr).Port
+}
@@ -12,12 +12,18 @@ import (
"github.com/gliderlabs/ssh"
)
+// Auth is the interface that wraps both Access and Provider interfaces.
+type Auth interface {
+ proto.Access
+ proto.Provider
+}
+
// Middleware adds Git server functionality to the ssh.Server. Repos are stored
// in the specified repo directory. The provided Hooks implementation will be
// checked for access on a per repo basis for a ssh.Session public key.
// Hooks.Push and Hooks.Fetch will be called on successful completion of
// their commands.
-func Middleware(repoDir string, auth proto.Access) wish.Middleware {
+func Middleware(repoDir string, auth Auth) wish.Middleware {
return func(sh ssh.Handler) ssh.Handler {
return func(s ssh.Session) {
func() {
@@ -27,24 +33,29 @@ func Middleware(repoDir string, auth proto.Access) wish.Middleware {
// repo should be in the form of "repo.git"
repo := strings.TrimPrefix(cmd[1], "/")
repo = filepath.Clean(repo)
+ name := repo
if strings.Contains(repo, "/") {
log.Printf("invalid repo: %s", repo)
Fatal(s, fmt.Errorf("%s: %s", git.ErrInvalidRepo, "user repos not supported"))
return
}
pk := s.PublicKey()
- access := auth.AuthRepo(strings.TrimSuffix(repo, ".git"), pk)
+ access := auth.AuthRepo(name, pk)
// git bare repositories should end in ".git"
// https://git-scm.com/docs/gitrepository-layout
- if !strings.HasSuffix(repo, ".git") {
- repo += ".git"
- }
+ repo = strings.TrimSuffix(repo, ".git") + ".git"
switch gc {
case "git-receive-pack":
switch access {
case proto.ReadWriteAccess, proto.AdminAccess:
- err := git.GitPack(s, s, s.Stderr(), gc, repoDir, repo)
- if err != nil {
+ if _, err := auth.Open(name); err != nil {
+ if err := auth.Create(name, "", "", false); err != nil {
+ log.Printf("failed to create repo: %s", err)
+ Fatal(s, err)
+ return
+ }
+ }
+ if err := git.GitPack(s, s, s.Stderr(), gc, repoDir, repo); err != nil {
Fatal(s, git.ErrSystemMalfunction)
}
default:
@@ -52,6 +63,7 @@ func Middleware(repoDir string, auth proto.Access) wish.Middleware {
}
return
case "git-upload-archive", "git-upload-pack":
+ log.Printf("access %s", access)
switch access {
case proto.ReadOnlyAccess, proto.ReadWriteAccess, proto.AdminAccess:
// try to upload <repo>.git first, then <repo>
@@ -3,6 +3,7 @@ package ssh
import (
"fmt"
"net"
+ "net/mail"
"os"
"os/exec"
"path/filepath"
@@ -14,6 +15,7 @@ import (
"github.com/charmbracelet/soft-serve/proto"
"github.com/charmbracelet/wish"
"github.com/gliderlabs/ssh"
+ gossh "golang.org/x/crypto/ssh"
)
func TestGitMiddleware(t *testing.T) {
@@ -25,8 +27,6 @@ func TestGitMiddleware(t *testing.T) {
repoDir := t.TempDir()
hooks := &testHooks{
- pushes: []action{},
- fetches: []action{},
access: []accessDetails{
{pubkey, "repo1", proto.AdminAccess},
{pubkey, "repo2", proto.AdminAccess},
@@ -53,7 +53,6 @@ func TestGitMiddleware(t *testing.T) {
requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/repo1"))
requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit"))
requireNoError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "master"))
- requireHasAction(t, hooks.pushes, pubkey, "repo1")
})
t.Run("create repo on main", func(t *testing.T) {
@@ -62,7 +61,6 @@ func TestGitMiddleware(t *testing.T) {
requireNoError(t, runGitHelper(t, pkPath, cwd, "remote", "add", "origin", remote+"/repo2"))
requireNoError(t, runGitHelper(t, pkPath, cwd, "commit", "--allow-empty", "-m", "initial commit"))
requireNoError(t, runGitHelper(t, pkPath, cwd, "push", "origin", "main"))
- requireHasAction(t, hooks.pushes, pubkey, "repo2")
})
t.Run("create and clone repo", func(t *testing.T) {
@@ -74,9 +72,6 @@ func TestGitMiddleware(t *testing.T) {
cwd = t.TempDir()
requireNoError(t, runGitHelper(t, pkPath, cwd, "clone", remote+"/repo3"))
-
- requireHasAction(t, hooks.pushes, pubkey, "repo3")
- requireHasAction(t, hooks.fetches, pubkey, "repo3")
})
t.Run("clone repo that doesn't exist", func(t *testing.T) {
@@ -106,9 +101,6 @@ func TestGitMiddleware(t *testing.T) {
cwd = t.TempDir()
requireNoError(t, runGitHelper(t, pkPath, cwd, "clone", remote+"/repo7"))
-
- requireHasAction(t, hooks.pushes, pubkey, "repo7")
- requireHasAction(t, hooks.fetches, pubkey, "repo7")
})
}
@@ -153,6 +145,7 @@ func requireHasAction(t *testing.T, actions []action, key ssh.PublicKey, repo st
t.Helper()
for _, action := range actions {
+ t.Logf("action: %q", action.repo)
if repo == strings.TrimSuffix(action.repo, ".git") && ssh.KeysEqual(key, action.key) {
return
}
@@ -195,20 +188,98 @@ type testHooks struct {
access []accessDetails
}
+func (h *testHooks) Open(string) (proto.Repository, error) {
+ return nil, nil
+}
+
+func (h *testHooks) ListRepos() ([]proto.Metadata, error) {
+ return nil, nil
+}
+
func (h *testHooks) AuthRepo(repo string, key ssh.PublicKey) proto.AccessLevel {
for _, dets := range h.access {
- if dets.repo == repo && ssh.KeysEqual(key, dets.key) {
+ if strings.TrimSuffix(dets.repo, ".git") == repo && ssh.KeysEqual(key, dets.key) {
return dets.level
}
}
return proto.NoAccess
}
+type testUser struct{}
+
+func (u *testUser) Name() string {
+ return "test"
+}
+
+func (u *testUser) Email() *mail.Address {
+ return &mail.Address{
+ Name: "test",
+ Address: "test@wish",
+ }
+}
+
+func (u *testUser) IsAdmin() bool {
+ return false
+}
+
+func (u *testUser) Login() *string {
+ l := "test"
+ return &l
+}
+
+func (u *testUser) Password() *string {
+ return nil
+}
+
+func (u *testUser) PublicKeys() []gossh.PublicKey {
+ return nil
+}
+
+func (h *testHooks) User(pk ssh.PublicKey) (proto.User, error) {
+ return &testUser{}, nil
+}
+
+func (h *testHooks) IsAdmin(pk ssh.PublicKey) bool {
+ return false
+}
+
+func (h *testHooks) IsCollab(repo string, pk ssh.PublicKey) bool {
+ return false
+}
+
+func (h *testHooks) Create(name, projectName, description string, isPrivate bool) error {
+ return nil
+}
+
+func (h *testHooks) Delete(repo string) error {
+ return nil
+}
+
+func (h *testHooks) Rename(repo, name string) error {
+ return nil
+}
+
+func (h *testHooks) SetProjectName(repo, projectName string) error {
+ return nil
+}
+
+func (h *testHooks) SetDescription(repo, description string) error {
+ return nil
+}
+
+func (h *testHooks) SetPrivate(repo string, isPrivate bool) error {
+ return nil
+}
+
+func (h *testHooks) SetDefaultBranch(repo, branch string) error {
+ return nil
+}
+
func (h *testHooks) Push(repo string, key ssh.PublicKey) {
h.Lock()
defer h.Unlock()
- h.pushes = append(h.pushes, action{key, repo})
+ h.pushes = append(h.pushes, action{key, strings.TrimSuffix(repo, ".git")})
}
func (h *testHooks) Fetch(repo string, key ssh.PublicKey) {