From 40669604b54ed599d145be158280ad1cfd43919f Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Sun, 8 Jan 2023 09:04:05 -0300 Subject: [PATCH] fix: race condition, broken tests (#203) * test: fixing broken tests Signed-off-by: Carlos A Becker * fix: race daemon Signed-off-by: Carlos A Becker * refactor: rename, godocs Signed-off-by: Carlos A Becker * test: improve git test Signed-off-by: Carlos A Becker * fix: improvements Signed-off-by: Carlos A Becker * feat(deps): bump github.com/go-git/go-billy/v5 from 5.3.1 to 5.4.0 Bumps [github.com/go-git/go-billy/v5](https://github.com/go-git/go-billy) from 5.3.1 to 5.4.0. - [Release notes](https://github.com/go-git/go-billy/releases) - [Commits](https://github.com/go-git/go-billy/compare/v5.3.1...v5.4.0) --- updated-dependencies: - dependency-name: github.com/go-git/go-billy/v5 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * test: fix more tests * fix: gitignore Signed-off-by: Carlos A Becker Signed-off-by: Carlos A Becker Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .gitignore | 3 +- config/auth.go | 158 ----- config/auth_test.go | 669 ---------------------- config/config.go | 338 ----------- config/config_test.go | 35 -- config/defaults.go | 58 -- config/git.go | 299 ---------- go.mod | 4 +- go.sum | 6 +- server/config/config.go | 5 +- {config => server/config}/testdata/k1.pub | 0 server/git/daemon/daemon.go | 51 +- server/git/daemon/daemon_test.go | 12 +- server/server_test.go | 45 +- 14 files changed, 74 insertions(+), 1609 deletions(-) delete mode 100644 config/auth.go delete mode 100644 config/auth_test.go delete mode 100644 config/config.go delete mode 100644 config/config_test.go delete mode 100644 config/defaults.go delete mode 100644 config/git.go rename {config => server/config}/testdata/k1.pub (100%) diff --git a/.gitignore b/.gitignore index 515bc430e0f6d5e582fd3b6699bad8be9737f9eb..e23b9be24a88e9f71fc637190f29c96d76ccb291 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ soft data dist -testdata completions/ -manpages/ \ No newline at end of file +manpages/ diff --git a/config/auth.go b/config/auth.go deleted file mode 100644 index 1e444226a424cd41756335b80447eb5fc4db34c7..0000000000000000000000000000000000000000 --- a/config/auth.go +++ /dev/null @@ -1,158 +0,0 @@ -package config - -import ( - "log" - "strings" - - "github.com/charmbracelet/soft-serve/proto" - "github.com/gliderlabs/ssh" - gossh "golang.org/x/crypto/ssh" -) - -// Push registers Git push functionality for the given repo and key. -func (cfg *Config) Push(repo string, pk ssh.PublicKey) { - go func() { - err := cfg.Reload() - if err != nil { - log.Printf("error reloading after push: %s", err) - } - if cfg.Cfg.Callbacks != nil { - cfg.Cfg.Callbacks.Push(repo) - } - r, err := cfg.Source.GetRepo(repo) - if err != nil { - log.Printf("error getting repo after push: %s", err) - return - } - err = r.UpdateServerInfo() - if err != nil { - log.Printf("error updating server info after push: %s", err) - } - }() -} - -// Fetch registers Git fetch functionality for the given repo and key. -func (cfg *Config) Fetch(repo string, pk ssh.PublicKey) { - if cfg.Cfg.Callbacks != nil { - cfg.Cfg.Callbacks.Fetch(repo) - } -} - -// AuthRepo grants repo authorization to the given key. -func (cfg *Config) AuthRepo(repo string, pk ssh.PublicKey) proto.AccessLevel { - return cfg.accessForKey(repo, pk) -} - -// PasswordHandler returns whether or not password access is allowed. -func (cfg *Config) PasswordHandler(ctx ssh.Context, password string) bool { - return (cfg.AnonAccess != proto.NoAccess.String()) && cfg.AllowKeyless -} - -// KeyboardInteractiveHandler returns whether or not keyboard interactive is allowed. -func (cfg *Config) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.KeyboardInteractiveChallenge) bool { - return (cfg.AnonAccess != proto.NoAccess.String()) && cfg.AllowKeyless -} - -// PublicKeyHandler returns whether or not the given public key may access the -// repo. -func (cfg *Config) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool { - return cfg.accessForKey("", pk) != proto.NoAccess -} - -func (cfg *Config) anonAccessLevel() proto.AccessLevel { - cfg.mtx.RLock() - defer cfg.mtx.RUnlock() - switch cfg.AnonAccess { - case "no-access": - return proto.NoAccess - case "read-only": - return proto.ReadOnlyAccess - case "read-write": - return proto.ReadWriteAccess - case "admin-access": - return proto.AdminAccess - default: - return proto.NoAccess - } -} - -// accessForKey returns the access level for the given repo. -// -// If repo doesn't exist, then access is based on user's admin privileges, or -// config.AnonAccess. -// If repo exists, and private, then admins and collabs are allowed access. -// If repo exists, and not private, then access is based on config.AnonAccess. -func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) proto.AccessLevel { - anon := cfg.anonAccessLevel() - private := cfg.isPrivate(repo) - // Find user - if pk != nil { - for _, user := range cfg.Users { - for _, k := range user.PublicKeys { - apk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(k))) - if err != nil { - log.Printf("error: malformed authorized key: '%s'", k) - return proto.NoAccess - } - if ssh.KeysEqual(pk, apk) { - if user.Admin { - return proto.AdminAccess - } - u := user - if cfg.isCollab(repo, &u) { - if anon > proto.ReadWriteAccess { - return anon - } - return proto.ReadWriteAccess - } - if !private { - if anon > proto.ReadOnlyAccess { - return anon - } - return proto.ReadOnlyAccess - } - } - } - } - } - // Don't restrict access to private repos if no users are configured. - // Return anon access level. - if private && len(cfg.Users) > 0 { - return proto.NoAccess - } - return anon -} - -func (cfg *Config) findRepo(repo string) *RepoConfig { - for _, r := range cfg.Repos { - if r.Repo == repo { - return &r - } - } - return nil -} - -func (cfg *Config) isPrivate(repo string) bool { - if r := cfg.findRepo(repo); r != nil { - return r.Private - } - return false -} - -func (cfg *Config) isCollab(repo string, user *User) bool { - if user != nil { - for _, r := range user.CollabRepos { - if r == repo { - return true - } - } - if r := cfg.findRepo(repo); r != nil { - for _, c := range r.Collabs { - if c == user.Name { - return true - } - } - } - } - return false -} diff --git a/config/auth_test.go b/config/auth_test.go deleted file mode 100644 index 626ff0e8a180bc4db47378268b84c6c32216c0fc..0000000000000000000000000000000000000000 --- a/config/auth_test.go +++ /dev/null @@ -1,669 +0,0 @@ -package config - -import ( - "testing" - - "github.com/charmbracelet/soft-serve/proto" - "github.com/gliderlabs/ssh" - "github.com/matryer/is" -) - -func TestAuth(t *testing.T) { - adminKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMwLvyV3ouVrTysUYGoJdl5Vgn5BACKov+n9PlzfPwH a@b" - adminPk, _, _, _, _ := ssh.ParseAuthorizedKey([]byte(adminKey)) - dummyKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxIobhwtfdwN7m1TFt9wx3PsfvcAkISGPxmbmbauST8 a@b" - dummyPk, _, _, _, _ := ssh.ParseAuthorizedKey([]byte(dummyKey)) - cases := []struct { - name string - cfg Config - repo string - key ssh.PublicKey - access proto.AccessLevel - }{ - // Repo access - { - name: "anon access: no-access, anonymous user", - access: proto.NoAccess, - repo: "foo", - cfg: Config{ - AnonAccess: "no-access", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - }, - }, - { - name: "anon access: no-access, anonymous user with admin user", - access: proto.NoAccess, - repo: "foo", - cfg: Config{ - AnonAccess: "no-access", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: no-access, authd user", - key: dummyPk, - repo: "foo", - access: proto.ReadOnlyAccess, - cfg: Config{ - AnonAccess: "no-access", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, - { - name: "anon access: no-access, anonymous user with admin user", - key: dummyPk, - repo: "foo", - access: proto.NoAccess, - cfg: Config{ - AnonAccess: "no-access", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: no-access, admin user", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "no-access", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: read-only, anonymous user", - repo: "foo", - access: proto.ReadOnlyAccess, - cfg: Config{ - AnonAccess: "read-only", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - }, - }, - { - name: "anon access: read-only, authd user", - repo: "foo", - key: dummyPk, - access: proto.ReadOnlyAccess, - cfg: Config{ - AnonAccess: "read-only", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, - { - name: "anon access: read-only, admin user", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "read-only", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: read-write, anonymous user", - repo: "foo", - access: proto.ReadWriteAccess, - cfg: Config{ - AnonAccess: "read-write", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - }, - }, - { - name: "anon access: read-write, authd user", - repo: "foo", - key: dummyPk, - access: proto.ReadWriteAccess, - cfg: Config{ - AnonAccess: "read-write", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, { - name: "anon access: read-write, admin user", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "read-write", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: admin-access, anonymous user", - repo: "foo", - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "admin-access", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - }, - }, - { - name: "anon access: admin-access, authd user", - repo: "foo", - key: dummyPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "admin-access", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, { - name: "anon access: admin-access, admin user", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "admin-access", - Repos: []RepoConfig{ - { - Repo: "foo", - }, - }, - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - - // Collabs - { - name: "anon access: no-access, authd user, collab", - key: dummyPk, - repo: "foo", - access: proto.ReadWriteAccess, - cfg: Config{ - AnonAccess: "no-access", - Repos: []RepoConfig{ - { - Repo: "foo", - Collabs: []string{ - "user", - }, - }, - }, - Users: []User{ - { - Name: "user", - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, - { - name: "anon access: no-access, authd user, collab, private repo", - key: dummyPk, - repo: "foo", - access: proto.ReadWriteAccess, - cfg: Config{ - AnonAccess: "no-access", - Repos: []RepoConfig{ - { - Repo: "foo", - Private: true, - Collabs: []string{ - "user", - }, - }, - }, - Users: []User{ - { - Name: "user", - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, - { - name: "anon access: no-access, admin user, collab, private repo", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "no-access", - Repos: []RepoConfig{ - { - Repo: "foo", - Private: true, - Collabs: []string{ - "user", - }, - }, - }, - Users: []User{ - { - Name: "admin", - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: read-only, authd user, collab, private repo", - repo: "foo", - key: dummyPk, - access: proto.ReadWriteAccess, - cfg: Config{ - AnonAccess: "read-only", - Repos: []RepoConfig{ - { - Repo: "foo", - Private: true, - Collabs: []string{ - "user", - }, - }, - }, - Users: []User{ - { - Name: "user", - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, - { - name: "anon access: admin-access, anonymous user, collab", - repo: "foo", - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "admin-access", - Repos: []RepoConfig{ - { - Repo: "foo", - Collabs: []string{ - "user", - }, - }, - }, - }, - }, - { - name: "anon access: admin-access, authd user, collab", - repo: "foo", - key: dummyPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "admin-access", - Repos: []RepoConfig{ - { - Repo: "foo", - Collabs: []string{ - "user", - }, - }, - }, - Users: []User{ - { - Name: "user", - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, { - name: "anon access: admin-access, admin user, collab", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "admin-access", - Repos: []RepoConfig{ - { - Repo: "foo", - Collabs: []string{ - "user", - }, - }, - }, - Users: []User{ - { - Name: "admin", - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - - // New repo - { - name: "anon access: no-access, anonymous user, new repo", - access: proto.NoAccess, - repo: "foo", - cfg: Config{ - AnonAccess: "no-access", - }, - }, - { - name: "anon access: no-access, authd user, new repo", - key: dummyPk, - repo: "foo", - access: proto.ReadOnlyAccess, - cfg: Config{ - AnonAccess: "no-access", - Users: []User{ - { - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, - { - name: "anon access: no-access, authd user, new repo, with user", - key: dummyPk, - repo: "foo", - access: proto.NoAccess, - cfg: Config{ - AnonAccess: "no-access", - Users: []User{ - { - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: no-access, admin user, new repo", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "no-access", - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: read-only, anonymous user, new repo", - repo: "foo", - access: proto.ReadOnlyAccess, - cfg: Config{ - AnonAccess: "read-only", - }, - }, - { - name: "anon access: read-only, authd user, new repo", - repo: "foo", - key: dummyPk, - access: proto.ReadOnlyAccess, - cfg: Config{ - AnonAccess: "read-only", - Users: []User{ - { - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, - { - name: "anon access: read-only, admin user, new repo", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "read-only", - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: read-write, anonymous user, new repo", - repo: "foo", - access: proto.ReadWriteAccess, - cfg: Config{ - AnonAccess: "read-write", - }, - }, - { - name: "anon access: read-write, authd user, new repo", - repo: "foo", - key: dummyPk, - access: proto.ReadWriteAccess, - cfg: Config{ - AnonAccess: "read-write", - Users: []User{ - { - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, - { - name: "anon access: read-write, admin user, new repo", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "read-write", - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - { - name: "anon access: admin-access, anonymous user, new repo", - repo: "foo", - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "admin-access", - }, - }, - { - name: "anon access: admin-access, authd user, new repo", - repo: "foo", - key: dummyPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "admin-access", - Users: []User{ - { - PublicKeys: []string{ - dummyKey, - }, - }, - }, - }, - }, - { - name: "anon access: admin-access, admin user, new repo", - repo: "foo", - key: adminPk, - access: proto.AdminAccess, - cfg: Config{ - AnonAccess: "admin-access", - Users: []User{ - { - Admin: true, - PublicKeys: []string{ - adminKey, - }, - }, - }, - }, - }, - - // No users - { - name: "anon access: read-only, no users", - repo: "foo", - access: proto.ReadOnlyAccess, - cfg: Config{ - AnonAccess: "read-only", - }, - }, - { - name: "anon access: read-write, no users", - repo: "foo", - access: proto.ReadWriteAccess, - cfg: Config{ - AnonAccess: "read-write", - }, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - is := is.New(t) - al := c.cfg.accessForKey(c.repo, c.key) - is.Equal(al, c.access) - }) - } -} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index fea3deeac3b53b720cc0e46989f9dd59469216af..0000000000000000000000000000000000000000 --- a/config/config.go +++ /dev/null @@ -1,338 +0,0 @@ -package config - -import ( - "bytes" - "encoding/json" - "errors" - "io/fs" - "log" - "path/filepath" - "strings" - "sync" - "text/template" - "time" - - "golang.org/x/crypto/ssh" - "gopkg.in/yaml.v3" - - "fmt" - "os" - - "github.com/charmbracelet/soft-serve/git" - "github.com/charmbracelet/soft-serve/proto" - "github.com/charmbracelet/soft-serve/server/config" - "github.com/go-git/go-billy/v5/memfs" - ggit "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/go-git/go-git/v5/storage/memory" -) - -var ( - // ErrNoConfig is returned when a repo has no config file. - ErrNoConfig = errors.New("no config file found") -) - -const ( - defaultConfigRepo = "config" -) - -// Config is the Soft Serve configuration. -type Config struct { - Name string `yaml:"name" json:"name"` - Host string `yaml:"host" json:"host"` - Port int `yaml:"port" json:"port"` - AnonAccess string `yaml:"anon-access" json:"anon-access"` - AllowKeyless bool `yaml:"allow-keyless" json:"allow-keyless"` - Users []User `yaml:"users" json:"users"` - Repos []RepoConfig `yaml:"repos" json:"repos"` - Source *RepoSource `yaml:"-" json:"-"` - Cfg *config.Config `yaml:"-" json:"-"` - mtx sync.RWMutex -} - -// User contains user-level configuration for a repository. -type User struct { - Name string `yaml:"name" json:"name"` - Admin bool `yaml:"admin" json:"admin"` - PublicKeys []string `yaml:"public-keys" json:"public-keys"` - CollabRepos []string `yaml:"collab-repos" json:"collab-repos"` -} - -// RepoConfig is a repository configuration. -type RepoConfig struct { - Name string `yaml:"name" json:"name"` - Repo string `yaml:"repo" json:"repo"` - Note string `yaml:"note" json:"note"` - Private bool `yaml:"private" json:"private"` - Readme string `yaml:"readme" json:"readme"` - Collabs []string `yaml:"collabs" json:"collabs"` -} - -// NewConfig creates a new internal Config struct. -func NewConfig(cfg *config.Config) (*Config, error) { - var anonAccess string - var yamlUsers string - var displayHost string - host := cfg.Host - port := cfg.SSH.Port - - pks := make([]string, 0) - for _, k := range cfg.InitialAdminKeys { - if bts, err := os.ReadFile(k); err == nil { - // pk is a file, set its contents as pk - k = string(bts) - } - var pk = strings.TrimSpace(k) - if pk == "" { - continue - } - // it is a valid ssh key, nothing to do - if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk)); err != nil { - return nil, fmt.Errorf("invalid initial admin key %q: %w", k, err) - } - pks = append(pks, pk) - } - - rs := NewRepoSource(cfg.RepoPath()) - c := &Config{ - Cfg: cfg, - } - c.Host = host - c.Port = port - c.Source = rs - // Grant read-write access when no keys are provided. - if len(pks) == 0 { - anonAccess = proto.ReadWriteAccess.String() - } else { - anonAccess = proto.ReadOnlyAccess.String() - } - if host == "" { - displayHost = "localhost" - } else { - displayHost = host - } - yamlConfig := fmt.Sprintf(defaultConfig, - displayHost, - port, - anonAccess, - len(pks) == 0, - ) - if len(pks) == 0 { - yamlUsers = defaultUserConfig - } else { - var result string - for _, pk := range pks { - result += fmt.Sprintf(" - %s\n", pk) - } - yamlUsers = fmt.Sprintf(hasKeyUserConfig, result) - } - yaml := fmt.Sprintf("%s%s%s", yamlConfig, yamlUsers, exampleUserConfig) - err := c.createDefaultConfigRepo(yaml) - if err != nil { - return nil, err - } - return c, nil -} - -// readConfig reads the config file for the repo. All config files are stored in -// the config repo. -func (cfg *Config) readConfig(repo string, v interface{}) error { - cr, err := cfg.Source.GetRepo(defaultConfigRepo) - if err != nil { - return err - } - // Parse YAML files - var cy string - for _, ext := range []string{".yaml", ".yml"} { - cy, _, err = cr.LatestFile(repo + ext) - if err != nil && !errors.Is(err, git.ErrFileNotFound) { - return err - } else if err == nil { - break - } - } - // Parse JSON files - cj, _, err := cr.LatestFile(repo + ".json") - if err != nil && !errors.Is(err, git.ErrFileNotFound) { - return err - } - if cy != "" { - err = yaml.Unmarshal([]byte(cy), v) - if err != nil { - return err - } - } else if cj != "" { - err = json.Unmarshal([]byte(cj), v) - if err != nil { - return err - } - } else { - return ErrNoConfig - } - return nil -} - -// Reload reloads the configuration. -func (cfg *Config) Reload() error { - cfg.mtx.Lock() - defer cfg.mtx.Unlock() - err := cfg.Source.LoadRepos() - if err != nil { - return err - } - if err := cfg.readConfig(defaultConfigRepo, cfg); err != nil { - return fmt.Errorf("error reading config: %w", err) - } - // sanitize repo configs - repos := make(map[string]RepoConfig, 0) - for _, r := range cfg.Repos { - repos[r.Repo] = r - } - for _, r := range cfg.Source.AllRepos() { - var rc RepoConfig - repo := r.Repo() - if repo == defaultConfigRepo { - continue - } - if err := cfg.readConfig(repo, &rc); err != nil { - if !errors.Is(err, ErrNoConfig) { - log.Printf("error reading config: %v", err) - } - continue - } - repos[r.Repo()] = rc - } - cfg.Repos = make([]RepoConfig, 0, len(repos)) - for n, r := range repos { - r.Repo = n - cfg.Repos = append(cfg.Repos, r) - } - // Populate readmes and descriptions - for _, r := range cfg.Source.AllRepos() { - repo := r.Repo() - err = r.UpdateServerInfo() - if err != nil { - log.Printf("error updating server info for %s: %s", repo, err) - } - pat := "README*" - rp := "" - for _, rr := range cfg.Repos { - if repo == rr.Repo { - rp = rr.Readme - r.name = rr.Name - r.description = rr.Note - r.private = rr.Private - break - } - } - if rp != "" { - pat = rp - } - rm := "" - fc, fp, _ := r.LatestFile(pat) - rm = fc - if repo == "config" { - md, err := templatize(rm, cfg) - if err != nil { - return err - } - rm = md - } - r.SetReadme(rm, fp) - } - return nil -} - -func createFile(path string, content string) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer f.Close() - _, err = f.WriteString(content) - if err != nil { - return err - } - return f.Sync() -} - -func (cfg *Config) createDefaultConfigRepo(yaml string) error { - cn := defaultConfigRepo - rp := filepath.Join(cfg.Cfg.RepoPath(), cn) + ".git" - rs := cfg.Source - err := rs.LoadRepo(cn) - if errors.Is(err, fs.ErrNotExist) { - repo, err := ggit.PlainInit(rp, true) - if err != nil { - return err - } - repo, err = ggit.Clone(memory.NewStorage(), memfs.New(), &ggit.CloneOptions{ - URL: rp, - }) - if err != nil && err != transport.ErrEmptyRemoteRepository { - return err - } - wt, err := repo.Worktree() - if err != nil { - return err - } - rm, err := wt.Filesystem.Create("README.md") - if err != nil { - return err - } - _, err = rm.Write([]byte(defaultReadme)) - if err != nil { - return err - } - _, err = wt.Add("README.md") - if err != nil { - return err - } - cf, err := wt.Filesystem.Create("config.yaml") - if err != nil { - return err - } - _, err = cf.Write([]byte(yaml)) - if err != nil { - return err - } - _, err = wt.Add("config.yaml") - if err != nil { - return err - } - author := object.Signature{ - Name: "Soft Serve Server", - Email: "vt100@charm.sh", - When: time.Now(), - } - _, err = wt.Commit("Default init", &ggit.CommitOptions{ - All: true, - Author: &author, - Committer: &author, - }) - if err != nil { - return err - } - err = repo.Push(&ggit.PushOptions{}) - if err != nil { - return err - } - } else if err != nil { - return err - } - return cfg.Reload() -} - -func templatize(mdt string, tmpl interface{}) (string, error) { - t, err := template.New("readme").Parse(mdt) - if err != nil { - return "", err - } - buf := &bytes.Buffer{} - err = t.Execute(buf, tmpl) - if err != nil { - return "", err - } - return buf.String(), nil -} diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index 7616a3ca5cb488e2a4d92f1a1be33f559d5451e7..0000000000000000000000000000000000000000 --- a/config/config_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package config - -import ( - "testing" - - "github.com/charmbracelet/soft-serve/server/config" - "github.com/matryer/is" -) - -func TestMultipleInitialKeys(t *testing.T) { - cfg, err := NewConfig(&config.Config{ - DataPath: t.TempDir(), - InitialAdminKeys: []string{ - "testdata/k1.pub", - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxIobhwtfdwN7m1TFt9wx3PsfvcAkISGPxmbmbauST8 a@b", - }, - }) - is := is.New(t) - is.NoErr(err) - err = cfg.Reload() - is.NoErr(err) - is.Equal(cfg.Users[0].PublicKeys, []string{ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINMwLvyV3ouVrTysUYGoJdl5Vgn5BACKov+n9PlzfPwH a@b", - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxIobhwtfdwN7m1TFt9wx3PsfvcAkISGPxmbmbauST8 a@b", - }) // should have both keys -} - -func TestEmptyInitialKeys(t *testing.T) { - cfg, err := NewConfig(&config.Config{ - DataPath: t.TempDir(), - }) - is := is.New(t) - is.NoErr(err) - is.Equal(len(cfg.Users), 0) // should not have any users -} diff --git a/config/defaults.go b/config/defaults.go deleted file mode 100644 index 8e2a796fd14284e2653fecb7c15479e9bd0d6fa4..0000000000000000000000000000000000000000 --- a/config/defaults.go +++ /dev/null @@ -1,58 +0,0 @@ -package config - -const defaultReadme = "# Soft Serve\n\n Welcome! You can configure your Soft Serve server by cloning this repo and pushing changes.\n\n```\ngit clone ssh://{{.Host}}:{{.Port}}/config\n```" - -const defaultConfig = `# The name of the server to show in the TUI. -name: Soft Serve - -# The host and port to display in the TUI. You may want to change this if your -# server is accessible from a different host and/or port that what it's -# actually listening on (for example, if it's behind a reverse proxy). -host: %s -port: %d - -# Access level for anonymous users. Options are: admin-access, read-write, -# read-only, and no-access. -anon-access: %s - -# You can grant read-only access to users without private keys. Any password -# will be accepted. -allow-keyless: %t - -# Customize repo display in the menu. -repos: - - name: Home - repo: config - private: true - note: "Configuration and content repo for this server" - readme: README.md -` - -const hasKeyUserConfig = ` - -# Authorized users. Admins have full access to all repos. Private repos are only -# accessible by admins and collab users. Regular users can read public repos -# based on your anon-access setting. -users: - - name: Admin - admin: true - public-keys: -%s -` - -const defaultUserConfig = ` -#users: -# - name: Admin -# admin: true -# public-keys: -# - ssh-ed25519 AAAA... # redacted -# - ssh-rsa AAAAB3Nz... # redacted` - -const exampleUserConfig = ` -# - name: Example User -# collab-repos: -# - REPO -# public-keys: -# - ssh-ed25519 AAAA... # redacted -# - ssh-rsa AAAAB3Nz... # redacted -` diff --git a/config/git.go b/config/git.go deleted file mode 100644 index 00e2fac85d8d1dfa6ffb4ff77e65978e139c973d..0000000000000000000000000000000000000000 --- a/config/git.go +++ /dev/null @@ -1,299 +0,0 @@ -package config - -import ( - "errors" - "log" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/charmbracelet/soft-serve/git" - "github.com/gobwas/glob" - "github.com/golang/groupcache/lru" -) - -// ErrMissingRepo indicates that the requested repository could not be found. -var ErrMissingRepo = errors.New("missing repo") - -// Repo represents a Git repository. -type Repo struct { - name string - description string - path string - repository *git.Repository - readme string - readmePath string - head *git.Reference - headCommit string - refs []*git.Reference - patchCache *lru.Cache - private bool -} - -// open opens a Git repository. -func (rs *RepoSource) open(path string) (*Repo, error) { - rg, err := git.Open(path) - if err != nil { - return nil, err - } - r := &Repo{ - path: path, - repository: rg, - patchCache: lru.New(1000), - } - _, err = r.HEAD() - if err != nil { - return nil, err - } - _, err = r.References() - if err != nil { - return nil, err - } - return r, nil -} - -// IsBare returns true if the repository is a bare repository. -func (r *Repo) IsBare() bool { - return r.repository.IsBare -} - -// IsPrivate returns true if the repository is private. -func (r *Repo) IsPrivate() bool { - return r.private -} - -// Path returns the path to the repository. -func (r *Repo) Path() string { - return r.path -} - -// Repo returns the repository directory name. -func (r *Repo) Repo() string { - return strings.TrimSuffix(filepath.Base(r.path), ".git") -} - -// Name returns the name of the repository. -func (r *Repo) Name() string { - if r.name == "" { - return r.Repo() - } - return r.name -} - -// Description returns the description for a repository. -func (r *Repo) Description() string { - return r.description -} - -// Readme returns the readme and its path for the repository. -func (r *Repo) Readme() (readme string, path string) { - return r.readme, r.readmePath -} - -// SetReadme sets the readme for the repository. -func (r *Repo) SetReadme(readme, path string) { - r.readme = readme - r.readmePath = path -} - -// HEAD returns the reference for a repository. -func (r *Repo) HEAD() (*git.Reference, error) { - if r.head != nil { - return r.head, nil - } - h, err := r.repository.HEAD() - if err != nil { - return nil, err - } - r.head = h - return h, nil -} - -// GetReferences returns the references for a repository. -func (r *Repo) References() ([]*git.Reference, error) { - if r.refs != nil { - return r.refs, nil - } - refs, err := r.repository.References() - if err != nil { - return nil, err - } - r.refs = refs - return refs, nil -} - -// Tree returns the git tree for a given path. -func (r *Repo) Tree(ref *git.Reference, path string) (*git.Tree, error) { - return r.repository.TreePath(ref, path) -} - -// Diff returns the diff for a given commit. -func (r *Repo) Diff(commit *git.Commit) (*git.Diff, error) { - hash := commit.Hash.String() - c, ok := r.patchCache.Get(hash) - if ok { - return c.(*git.Diff), nil - } - diff, err := r.repository.Diff(commit) - if err != nil { - return nil, err - } - r.patchCache.Add(hash, diff) - return diff, nil -} - -// CountCommits returns the number of commits for a repository. -func (r *Repo) CountCommits(ref *git.Reference) (int64, error) { - tc, err := r.repository.CountCommits(ref) - if err != nil { - return 0, err - } - return tc, nil -} - -// Commit returns the commit for a given hash. -func (r *Repo) Commit(hash string) (*git.Commit, error) { - if hash == "HEAD" && r.headCommit != "" { - hash = r.headCommit - } - c, err := r.repository.CatFileCommit(hash) - if err != nil { - return nil, err - } - r.headCommit = c.ID.String() - return &git.Commit{ - Commit: c, - Hash: git.Hash(c.ID.String()), - }, nil -} - -// CommitsByPage returns the commits for a repository. -func (r *Repo) CommitsByPage(ref *git.Reference, page, size int) (git.Commits, error) { - return r.repository.CommitsByPage(ref, page, size) -} - -// Push pushes the repository to the remote. -func (r *Repo) Push(remote, branch string) error { - return r.repository.Push(remote, branch) -} - -// RepoSource is a reference to an on-disk repositories. -type RepoSource struct { - Path string - mtx sync.Mutex - repos map[string]*Repo -} - -// NewRepoSource creates a new RepoSource. -func NewRepoSource(repoPath string) *RepoSource { - err := os.MkdirAll(repoPath, os.ModeDir|os.FileMode(0700)) - if err != nil { - log.Fatal(err) - } - rs := &RepoSource{Path: repoPath} - rs.repos = make(map[string]*Repo, 0) - return rs -} - -// AllRepos returns all repositories for the given RepoSource. -func (rs *RepoSource) AllRepos() []*Repo { - rs.mtx.Lock() - defer rs.mtx.Unlock() - repos := make([]*Repo, 0, len(rs.repos)) - for _, r := range rs.repos { - repos = append(repos, r) - } - return repos -} - -// GetRepo returns a repository by name. -func (rs *RepoSource) GetRepo(name string) (*Repo, error) { - rs.mtx.Lock() - defer rs.mtx.Unlock() - if strings.HasSuffix(name, ".git") { - name = strings.TrimSuffix(name, ".git") - } - r, ok := rs.repos[name] - if !ok { - return nil, ErrMissingRepo - } - return r, nil -} - -// LoadRepo loads a repository from disk. -func (rs *RepoSource) LoadRepo(name string) error { - rs.mtx.Lock() - defer rs.mtx.Unlock() - if strings.HasSuffix(name, ".git") { - name = strings.TrimSuffix(name, ".git") - } - rp := filepath.Join(rs.Path, name) - if _, err := os.Stat(rp); os.IsNotExist(err) { - rp += ".git" - } - r, err := rs.open(rp) - if err != nil { - return err - } - if !r.IsBare() { - log.Printf("warning: %q is not a bare repository", r.Path()) - } else if r.IsBare() && !strings.HasSuffix(rp, ".git") { - log.Printf("warning: %q should be renamed to %q", r.Path(), r.Path()+".git") - } - rs.repos[name] = r - return nil -} - -// LoadRepos opens Git repositories. -func (rs *RepoSource) LoadRepos() error { - rd, err := os.ReadDir(rs.Path) - if err != nil { - return err - } - for _, de := range rd { - if !de.IsDir() { - log.Printf("warning: %q is not a directory", filepath.Join(rs.Path, de.Name())) - continue - } - if err := rs.LoadRepo(de.Name()); err != nil { - log.Printf("error opening repository %q: %s", de.Name(), err) - continue - } - } - return nil -} - -// LatestFile returns the contents of the latest file at the specified path in -// the repository and its file path. -func (r *Repo) LatestFile(pattern string) (string, string, error) { - g := glob.MustCompile(pattern) - dir := filepath.Dir(pattern) - t, err := r.repository.TreePath(r.head, dir) - if err != nil { - return "", "", err - } - ents, err := t.Entries() - if err != nil { - return "", "", err - } - for _, e := range ents { - fp := filepath.Join(dir, e.Name()) - if e.IsTree() { - continue - } - if g.Match(fp) { - bts, err := e.Contents() - if err != nil { - return "", "", err - } - return string(bts), fp, nil - } - } - return "", "", git.ErrFileNotFound -} - -// UpdateServerInfo updates the server info for the repository. -func (r *Repo) UpdateServerInfo() error { - return r.repository.UpdateServerInfo() -} diff --git a/go.mod b/go.mod index ca8dc670099bb4aa403cf857df16fff8dce67c77..3ab02571ec5837258fbbce3418af839f02fe46b5 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/charmbracelet/wish v0.7.0 github.com/dustin/go-humanize v1.0.0 github.com/gliderlabs/ssh v0.3.5 - github.com/go-git/go-billy/v5 v5.3.1 + github.com/go-git/go-billy/v5 v5.4.0 github.com/go-git/go-git/v5 v5.4.2 github.com/matryer/is v1.4.0 github.com/muesli/reflow v0.3.0 @@ -76,7 +76,7 @@ require ( github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/mod v0.3.0 // indirect golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect - golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect + golang.org/x/sys v0.3.0 // indirect golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect diff --git a/go.sum b/go.sum index d5bcaed27dd7faf6f0287c398dc2deec71e27a74..19c3462b552ed4a4f9f0761c8fab2b75c1e39f0b 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,9 @@ github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4x github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.4.0 h1:Vaw7LaSTRJOUric7pe4vnzBSgyuf2KrLsu2Y4ZpQBDE= +github.com/go-git/go-billy/v5 v5.4.0/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= @@ -254,8 +255,9 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= diff --git a/server/config/config.go b/server/config/config.go index ea4c773c859de251e802fd19b9881d52e56860d0..a40d6e2cbd825a2a49343d2117ba5a5eb8f6fe7d 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -176,7 +176,8 @@ func DefaultConfig() *Config { } if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)); err != nil { // Fatal if the key is invalid - log.Fatalf("invalid initial admin key %q: %v", k, err) + cwd, _ := os.Getwd() + log.Fatalf("invalid initial admin key %q: %v", filepath.Join(cwd, k), err) } // store the key in the config cfg.InitialAdminKeys[i] = pk @@ -197,7 +198,7 @@ func DefaultConfig() *Config { cfg.WithDB(db) } if err := cfg.createDefaultConfigRepoAndUsers(); err != nil { - log.Fatalln(err) + log.Fatalln("create default config and users", err) } return &cfg } diff --git a/config/testdata/k1.pub b/server/config/testdata/k1.pub similarity index 100% rename from config/testdata/k1.pub rename to server/config/testdata/k1.pub diff --git a/server/git/daemon/daemon.go b/server/git/daemon/daemon.go index d4a2a538b4dc534f68b20768273bf90710e4a8ea..c103c7eefc74d0a6111f3da5c5c2b09eb9502138 100644 --- a/server/git/daemon/daemon.go +++ b/server/git/daemon/daemon.go @@ -21,16 +21,49 @@ import ( // ErrServerClosed indicates that the server has been closed. var ErrServerClosed = errors.New("git: Server closed") +// connections synchronizes access to to a net.Conn pool. +type connections struct { + m map[net.Conn]struct{} + mu sync.Mutex +} + +func (m *connections) Add(c net.Conn) { + m.mu.Lock() + defer m.mu.Unlock() + m.m[c] = struct{}{} +} + +func (m *connections) Close(c net.Conn) { + m.mu.Lock() + defer m.mu.Unlock() + _ = c.Close() + delete(m.m, c) +} + +func (m *connections) Size() int { + m.mu.Lock() + defer m.mu.Unlock() + return len(m.m) +} + +func (m *connections) CloseAll() { + m.mu.Lock() + defer m.mu.Unlock() + for c := range m.m { + _ = c.Close() + delete(m.m, c) + } +} + // Daemon represents a Git daemon. type Daemon struct { listener net.Listener addr string finished chan struct{} - conns map[net.Conn]struct{} + conns connections cfg *config.Config wg sync.WaitGroup once sync.Once - mtx sync.RWMutex } // NewDaemon returns a new Git daemon. @@ -40,7 +73,7 @@ func NewDaemon(cfg *config.Config) (*Daemon, error) { addr: addr, finished: make(chan struct{}, 1), cfg: cfg, - conns: make(map[net.Conn]struct{}), + conns: connections{m: make(map[net.Conn]struct{})}, } listener, err := net.Listen("tcp", d.addr) if err != nil { @@ -83,7 +116,7 @@ func (d *Daemon) Start() error { } // Close connection if there are too many open connections. - if len(d.conns)+1 >= d.cfg.Git.MaxConnections { + if d.conns.Size()+1 >= d.cfg.Git.MaxConnections { log.Printf("git: max connections reached, closing %s", conn.RemoteAddr()) fatal(conn, git.ErrMaxConnections) continue @@ -117,10 +150,9 @@ func (d *Daemon) handleClient(conn net.Conn) { dur := time.Duration(d.cfg.Git.MaxTimeout) * time.Second c.maxDeadline = time.Now().Add(dur) } - d.conns[c] = struct{}{} + d.conns.Add(c) defer func() { - c.Close() - delete(d.conns, c) + d.conns.Close(c) }() readc := make(chan struct{}, 1) @@ -194,10 +226,7 @@ func (d *Daemon) handleClient(conn net.Conn) { func (d *Daemon) Close() error { d.once.Do(func() { close(d.finished) }) err := d.listener.Close() - for c := range d.conns { - c.Close() - delete(d.conns, c) - } + d.conns.CloseAll() return err } diff --git a/server/git/daemon/daemon_test.go b/server/git/daemon/daemon_test.go index bdc2e0b0bd153320a80c1557957bb0eacafe6f52..0b940163a0f9ebf51ceaa3a236e1f3a29596a579 100644 --- a/server/git/daemon/daemon_test.go +++ b/server/git/daemon/daemon_test.go @@ -2,12 +2,14 @@ package daemon import ( "bytes" + "errors" "io" "log" "net" "os" "strconv" "testing" + "time" "github.com/charmbracelet/soft-serve/server/config" "github.com/charmbracelet/soft-serve/server/git" @@ -26,7 +28,7 @@ func TestMain(m *testing.M) { 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_IDLE_TIMEOUT", "1") os.Setenv("SOFT_SERVE_GIT_PORT", strconv.Itoa(randomPort())) cfg := config.DefaultConfig() d, err := NewDaemon(cfg) @@ -39,14 +41,15 @@ func TestMain(m *testing.M) { log.Fatal(err) } }() - defer d.Close() - os.Exit(m.Run()) + code := 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") + _ = d.Close() + os.Exit(code) } func TestIdleTimeout(t *testing.T) { @@ -54,8 +57,9 @@ func TestIdleTimeout(t *testing.T) { if err != nil { t.Fatal(err) } + time.Sleep(2 * time.Second) out, err := readPktline(c) - if err != nil { + if err != nil && !errors.Is(err, io.EOF) { t.Fatalf("expected nil, got error: %v", err) } if out != git.ErrTimeout.Error() { diff --git a/server/server_test.go b/server/server_test.go index 02aa1a4cc9818395d27306f4259a568c491b79d9..d3811a0e4cc285cd92c5fb2b2df8d94b10f540b2 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3,7 +3,6 @@ package server import ( "fmt" "net" - "os" "path/filepath" "strconv" "strings" @@ -64,11 +63,8 @@ func TestPushRepo(t *testing.T) { func TestCloneRepo(t *testing.T) { is := is.New(t) _, cfg, pkPath := setupServer(t) - t.Log("starting server") - dst := t.TempDir() - t.Cleanup(func() { is.NoErr(os.RemoveAll(dst)) }) url := fmt.Sprintf("ssh://localhost:%d/config", cfg.SSH.Port) - t.Log("cloning repo") + t.Log("cloning repo", url) pk, err := gssh.NewPublicKeysFromFile("git", pkPath, "") is.NoErr(err) pk.HostKeyCallbackHelper = gssh.HostKeyCallbackHelper{ @@ -87,40 +83,31 @@ func randomPort() int { return addr.Addr().(*net.TCPAddr).Port } -func setupServer(t *testing.T) (*Server, *config.Config, string) { - t.Helper() - is := is.New(t) - pub, pkPath := createKeyPair(t) - dp := t.TempDir() - is.NoErr(os.Setenv("SOFT_SERVE_DATA_PATH", dp)) - is.NoErr(os.Setenv("SOFT_SERVE_INITIAL_ADMIN_KEY", authorizedKey(pub))) - is.NoErr(os.Setenv("SOFT_SERVE_GIT_ENABLED", "false")) - is.NoErr(os.Setenv("SOFT_SERVE_SSH_PORT", strconv.Itoa(randomPort()))) - // is.NoErr(os.Setenv("SOFT_SERVE_DB_DRIVER", "fake")) - t.Cleanup(func() { - is.NoErr(os.Unsetenv("SOFT_SERVE_DATA_PATH")) - is.NoErr(os.Unsetenv("SOFT_SERVE_SSH_PORT")) - is.NoErr(os.Unsetenv("SOFT_SERVE_INITIAL_ADMIN_KEY")) - is.NoErr(os.Unsetenv("SOFT_SERVE_GIT_ENABLED")) - // is.NoErr(os.Unsetenv("SOFT_SERVE_DB_DRIVER")) - is.NoErr(os.RemoveAll(dp)) - }) +func setupServer(tb testing.TB) (*Server, *config.Config, string) { + tb.Helper() + pub, pkPath := createKeyPair(tb) + dp := tb.TempDir() + tb.Setenv("SOFT_SERVE_DATA_PATH", dp) + tb.Setenv("SOFT_SERVE_INITIAL_ADMIN_KEY", authorizedKey(pub)) + tb.Setenv("SOFT_SERVE_GIT_ENABLED", "false") + tb.Setenv("SOFT_SERVE_SSH_PORT", strconv.Itoa(randomPort())) + // tb.Setenv("SOFT_SERVE_DB_DRIVER", "fake") cfg := config.DefaultConfig() //.WithDB(&fakedb.FakeDB{}) s := NewServer(cfg) go func() { - t.Log("starting server") + tb.Log("starting server") s.Start() }() - t.Cleanup(func() { + tb.Cleanup(func() { s.Close() }) return s, cfg, pkPath } -func createKeyPair(t *testing.T) (ssh.PublicKey, string) { - t.Helper() - is := is.New(t) - keyDir := t.TempDir() +func createKeyPair(tb testing.TB) (ssh.PublicKey, string) { + tb.Helper() + is := is.New(tb) + keyDir := tb.TempDir() kp, err := keygen.NewWithWrite(filepath.Join(keyDir, "id"), nil, keygen.Ed25519) is.NoErr(err) pubkey, _, _, _, err := ssh.ParseAuthorizedKey(kp.PublicKey())