.gitignore 🔗
@@ -1,6 +1,5 @@
soft
data
dist
-testdata
completions/
-manpages/
+manpages/
Carlos Alexandro Becker and dependabot[bot] created
* test: fixing broken tests
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
* fix: race daemon
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
* refactor: rename, godocs
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
* test: improve git test
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
* fix: improvements
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
* 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] <support@github.com>
* test: fix more tests
* fix: gitignore
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
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
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(+), 1,609 deletions(-)
@@ -1,6 +1,5 @@
soft
data
dist
-testdata
completions/
-manpages/
+manpages/
@@ -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
-}
@@ -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)
- })
- }
-}
@@ -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
-}
@@ -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
-}
@@ -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
-`
@@ -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()
-}
@@ -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
@@ -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=
@@ -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
}
@@ -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
}
@@ -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() {
@@ -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())