Detailed changes
@@ -0,0 +1,20 @@
+package config
+
+import (
+ gm "github.com/charmbracelet/wish/git"
+ "github.com/gliderlabs/ssh"
+)
+
+func (cfg *Config) AuthRepo(repo string, pk ssh.PublicKey) gm.AccessLevel {
+ // TODO: check yaml for access rules
+ return gm.ReadWriteAccess
+}
+
+func (cfg *Config) PasswordHandler(ctx ssh.Context, password string) bool {
+ return cfg.AnonReadOnly && cfg.AllowNoKeys
+}
+
+func (cfg *Config) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
+ // TODO: check yaml for access rules
+ return true
+}
@@ -0,0 +1,164 @@
+package config
+
+import (
+ "log"
+
+ "gopkg.in/yaml.v2"
+
+ "fmt"
+ "os"
+ "path/filepath"
+ "soft-serve/git"
+
+ "github.com/gliderlabs/ssh"
+ gg "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+type Config struct {
+ Name string `yaml:"name"`
+ Host string `yaml:"host"`
+ Port int `yaml:"port"`
+ AnonReadOnly bool `yaml:"anon-access"`
+ AllowNoKeys bool `yaml:"allow-no-keys"`
+ Users []User `yaml:"users"`
+ Repos []Repo `yaml:"repos"`
+ Source *git.RepoSource
+}
+
+type User struct {
+ Name string `yaml:"name"`
+ Admin bool `yaml:"admin"`
+ PublicKey string `yaml:"pk"`
+ CollabRepos []string `yaml:"collab_repos"`
+}
+
+type Repo struct {
+ Name string `yaml:"name"`
+ Repo string `yaml:"repo"`
+ Note string `yaml:"note"`
+}
+
+func NewConfig(host string, port int, anon bool, pk string, rs *git.RepoSource) (*Config, error) {
+ cfg := &Config{}
+ cfg.Host = host
+ cfg.Port = port
+ cfg.AnonReadOnly = anon
+ cfg.Source = rs
+
+ var yamlUsers string
+ var h string
+ if host == "" {
+ h = "localhost"
+ } else {
+ h = host
+ }
+ yamlConfig := fmt.Sprintf(defaultConfig, h, port, anon)
+ if pk != "" {
+ yamlUsers = fmt.Sprintf(hasKeyUserConfig, pk)
+ } else {
+ yamlUsers = defaultUserConfig
+ }
+ yaml := fmt.Sprintf("%s%s%s", yamlConfig, yamlUsers, exampleUserConfig)
+ err := cfg.createDefaultConfigRepo(yaml)
+ if err != nil {
+ return nil, err
+ }
+ return cfg, nil
+}
+
+func (cfg *Config) Pushed(repo string, pk ssh.PublicKey) {
+ err := cfg.Reload()
+ if err != nil {
+ log.Printf("error reloading after push: %s", err)
+ }
+}
+
+func (cfg *Config) Reload() error {
+ err := cfg.Source.LoadRepos()
+ if err != nil {
+ return err
+ }
+ cr, err := cfg.Source.GetRepo("config")
+ if err != nil {
+ return err
+ }
+ cs, err := cr.LatestFile("config.yaml")
+ if err != nil {
+ return err
+ }
+ err = yaml.Unmarshal([]byte(cs), cfg)
+ if err != nil {
+ return fmt.Errorf("bad yaml in config.yaml: %s", err)
+ }
+ 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 := "config"
+ rs := cfg.Source
+ err := rs.LoadRepos()
+ if err != nil {
+ return err
+ }
+ _, err = rs.GetRepo(cn)
+ if err == git.ErrMissingRepo {
+ cr, err := rs.InitRepo(cn, false)
+ if err != nil {
+ return err
+ }
+
+ rp := filepath.Join(rs.Path, cn, "README.md")
+ err = createFile(rp, defaultReadme)
+ if err != nil {
+ return err
+ }
+ cp := filepath.Join(rs.Path, cn, "config.yaml")
+ err = createFile(cp, yaml)
+ if err != nil {
+ return err
+ }
+ wt, err := cr.Repository.Worktree()
+ if err != nil {
+ return err
+ }
+ _, err = wt.Add("README.md")
+ if err != nil {
+ return err
+ }
+ _, err = wt.Add("config.yaml")
+ if err != nil {
+ return err
+ }
+ _, err = wt.Commit("Default init", &gg.CommitOptions{
+ All: true,
+ Author: &object.Signature{
+ Name: "Soft Serve Server",
+ Email: "vt100@charm.sh",
+ },
+ })
+ if err != nil {
+ return err
+ }
+ err = rs.LoadRepos()
+ if err != nil {
+ return err
+ }
+ } else if err != nil {
+ return err
+ }
+ return nil
+}
@@ -0,0 +1,42 @@
+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## Repos\n\n{{ range .Repos }}* {{ .Name }}{{ if .Note }} - {{ .Note }} {{ end }}\n - `git clone ssh://{{$.Host}}:{{$.Port}}/{{.Repo}}`\n{{ end }}"
+
+const defaultConfig = `
+name: Soft Serve
+host: %s
+port: %d
+
+# Set the access level for anonymous users. Options are: read-write, read-only and no-access
+anon-access: %v
+
+# Allow read only even if they don't have private keys, any password will work
+allow-no-keys: false
+
+# Customize repo display in menu
+repos:
+ - name: Home
+ repo: config
+ note: "Configuration and content repo for this server"`
+
+const hasKeyUserConfig = `
+# Users can read all repos, and push to collab-repos, admin can push to all repos
+users:
+ - name: admin
+ admin: true
+ public-key: |
+ %s`
+
+const defaultUserConfig = `
+# users:
+# - name: admin
+# admin: true
+# public-key: |
+# KEY TEXT`
+
+const exampleUserConfig = `
+# - name: little-buddy
+# collab-repos:
+# - soft-serve
+# public-key: |
+# KEY TEXT`
@@ -19,6 +19,7 @@ require (
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.9.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
+ gopkg.in/yaml.v2 v2.3.0
)
require (
@@ -3,45 +3,62 @@ package main
import (
"fmt"
"log"
+ "soft-serve/config"
+ "soft-serve/git"
"soft-serve/tui"
- "time"
"github.com/charmbracelet/wish"
bm "github.com/charmbracelet/wish/bubbletea"
gm "github.com/charmbracelet/wish/git"
lm "github.com/charmbracelet/wish/logging"
+ "github.com/gliderlabs/ssh"
"github.com/meowgorithm/babyenv"
)
-type Config struct {
- Port int `env:"SOFT_SERVE_PORT" default:"23231"`
- Host string `env:"SOFT_SERVE_HOST" default:""`
- KeyPath string `env:"SOFT_SERVE_KEY_PATH" default:".ssh/soft_serve_server_ed25519"`
- RepoAuth string `env:"SOFT_SERVE_REPO_KEYS" default:""`
- RepoAuthFile string `env:"SOFT_SERVE_REPO_KEYS_PATH" default:".ssh/soft_serve_git_authorized_keys"`
- RepoPath string `env:"SOFT_SERVE_REPO_PATH" default:".repos"`
+type serverConfig struct {
+ Port int `env:"SOFT_SERVE_PORT" default:"23231"`
+ Host string `env:"SOFT_SERVE_HOST" default:""`
+ InitKey string `env:"SOFT_SERVE_REPO_KEY" default:""`
+ KeyPath string `env:"SOFT_SERVE_KEY_PATH" default:".ssh/soft_serve_server_ed25519"`
+ RepoPath string `env:"SOFT_SERVE_REPO_PATH" default:".repos"`
}
func main() {
- var cfg Config
- err := babyenv.Parse(&cfg)
+ var scfg serverConfig
+ var cfg *config.Config
+ var err error
+ err = babyenv.Parse(&scfg)
if err != nil {
log.Fatalln(err)
}
+ rs := git.NewRepoSource(scfg.RepoPath)
+ if scfg.InitKey == "" {
+ cfg, err = config.NewConfig(scfg.Host, scfg.Port, true, "", rs)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ } else {
+ cfg, err = config.NewConfig(scfg.Host, scfg.Port, false, scfg.InitKey, rs)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ }
s, err := wish.NewServer(
- wish.WithAddress(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)),
- wish.WithHostKeyPath(cfg.KeyPath),
+ ssh.PublicKeyAuth(cfg.PublicKeyHandler),
+ ssh.PasswordAuth(cfg.PasswordHandler),
+ wish.WithAddress(fmt.Sprintf("%s:%d", scfg.Host, scfg.Port)),
+ wish.WithHostKeyPath(scfg.KeyPath),
wish.WithMiddlewares(
- bm.Middleware(tui.SessionHandler(cfg.RepoPath, time.Second*5)),
- gm.Middleware(cfg.RepoPath, cfg.RepoAuth, cfg.RepoAuthFile),
+ bm.Middleware(tui.SessionHandler(cfg)),
+ gm.MiddlewareWithPushCallback(scfg.RepoPath, cfg, cfg.Pushed),
lm.Middleware(),
),
)
if err != nil {
log.Fatalln(err)
}
- log.Printf("Starting SSH server on %s:%d\n", cfg.Host, cfg.Port)
+ log.Printf("Starting SSH server on %s:%d\n", scfg.Host, scfg.Port)
err = s.ListenAndServe()
if err != nil {
log.Fatalln(err)
@@ -2,6 +2,7 @@ package tui
import (
"fmt"
+ "soft-serve/config"
"soft-serve/git"
"soft-serve/tui/bubbles/repo"
"soft-serve/tui/bubbles/selection"
@@ -22,13 +23,10 @@ const (
quitState
)
-type Config struct {
- Name string `json:"name"`
- Host string `json:"host"`
- Port int64 `json:"port"`
- ShowAllRepos bool `json:"show_all_repos"`
- Menu []MenuEntry `json:"menu"`
- RepoSource *git.RepoSource
+type SessionConfig struct {
+ Width int
+ Height int
+ InitialRepo string
}
type MenuEntry struct {
@@ -38,20 +36,13 @@ type MenuEntry struct {
bubble *repo.Bubble
}
-type SessionConfig struct {
- Width int
- Height int
- InitialRepo string
-}
-
type Bubble struct {
- config *Config
+ config *config.Config
styles *style.Styles
state sessionState
error string
width int
height int
- repoSource *git.RepoSource
initialRepo string
repoMenu []MenuEntry
repos []*git.Repo
@@ -60,17 +51,12 @@ type Bubble struct {
repoSelect *selection.Bubble
}
-func NewBubble(cfg *Config, sCfg *SessionConfig) *Bubble {
- var repoSource *git.RepoSource = nil
- if cfg != nil {
- repoSource = cfg.RepoSource
- }
+func NewBubble(cfg *config.Config, sCfg *SessionConfig) *Bubble {
b := &Bubble{
config: cfg,
styles: style.DefaultStyles(),
width: sCfg.Width,
height: sCfg.Height,
- repoSource: repoSource,
repoMenu: make([]MenuEntry, 0),
boxes: make([]tea.Model, 2),
initialRepo: sCfg.InitialRepo,
@@ -46,7 +46,7 @@ type Bubble struct {
// solution would be to (rename and) move this Bubble into the parent
// package.
Host string
- Port int64
+ Port int
}
func NewBubble(rs *git.RepoSource, name string, styles *style.Styles, width, wm, height, hm int, tmp interface{}) *Bubble {
@@ -3,6 +3,7 @@ package tui
import (
"fmt"
"log"
+ "soft-serve/config"
"soft-serve/tui/bubbles/repo"
"soft-serve/tui/bubbles/selection"
"time"
@@ -19,29 +20,27 @@ func (e errMsg) Error() string {
}
func (b *Bubble) setupCmd() tea.Msg {
- if b.config == nil || b.config.RepoSource == nil {
+ if b.config == nil || b.config.Source == nil {
return errMsg{err: fmt.Errorf("config not set")}
}
ct := time.Now()
lipgloss.SetColorProfile(termenv.ANSI256)
- b.repos = b.repoSource.AllRepos()
- mes := append([]MenuEntry{}, b.config.Menu...)
+ b.repos = b.config.Source.AllRepos()
+ mes := append([]MenuEntry{}, b.repoMenu...)
rs := make([]string, 0)
- if b.config.ShowAllRepos {
- OUTER:
- for _, r := range b.repos {
- for _, me := range mes {
- if r.Name == me.Repo {
- continue OUTER
- }
+OUTER:
+ for _, r := range b.repos {
+ for _, me := range mes {
+ if r.Name == me.Repo {
+ continue OUTER
}
- mes = append(mes, MenuEntry{Name: r.Name, Repo: r.Name})
}
+ mes = append(mes, MenuEntry{Name: r.Name, Repo: r.Name})
}
if len(mes) == 0 {
return errMsg{fmt.Errorf("no repos found")}
}
- var tmplConfig *Config
+ var tmplConfig *config.Config
for _, me := range mes {
if me.Repo == "config" {
tmplConfig = b.config
@@ -53,7 +52,7 @@ func (b *Bubble) setupCmd() tea.Msg {
lipgloss.Height(b.footerView()) +
b.styles.RepoBody.GetVerticalFrameSize() +
b.styles.App.GetVerticalMargins()
- rb := repo.NewBubble(b.repoSource, me.Repo, b.styles, width, boxLeftWidth, b.height, heightMargin, tmplConfig)
+ rb := repo.NewBubble(b.config.Source, me.Repo, b.styles, width, boxLeftWidth, b.height, heightMargin, tmplConfig)
rb.Host = b.config.Host
rb.Port = b.config.Port
initCmd := rb.Init()
@@ -1,52 +1,22 @@
package tui
import (
- "encoding/json"
"fmt"
- "log"
- "soft-serve/git"
- "time"
+ "soft-serve/config"
tea "github.com/charmbracelet/bubbletea"
"github.com/gliderlabs/ssh"
)
-func SessionHandler(reposPath string, repoPoll time.Duration) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
- rs := git.NewRepoSource(reposPath)
- // createDefaultConfigRepo runs rs.LoadRepos()
- err := createDefaultConfigRepo(rs)
- if err != nil {
- if err != nil {
- log.Fatalf("cannot create config repo: %s", err)
- }
- }
- appCfg, err := loadConfig(rs)
- if err != nil {
- log.Printf("cannot load config: %s", err)
- }
-
+func SessionHandler(cfg *config.Config) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
cmd := s.Command()
- // reload repos and config on git push
- if len(cmd) > 0 && cmd[0] == "git-receive-pack" {
- ct := time.Now()
- err := rs.LoadRepos()
- if err != nil {
- log.Printf("cannot load repos: %s", err)
- }
- cfg, err := loadConfig(rs)
- if err != nil {
- log.Printf("cannot load config: %s", err)
- }
- appCfg = cfg
- log.Printf("Repo bubble loaded in %s", time.Since(ct))
- }
- cfg := &SessionConfig{}
+ scfg := &SessionConfig{}
switch len(cmd) {
case 0:
- cfg.InitialRepo = ""
+ scfg.InitialRepo = ""
case 1:
- cfg.InitialRepo = cmd[0]
+ scfg.InitialRepo = cmd[0]
default:
return nil, nil
}
@@ -55,26 +25,8 @@ func SessionHandler(reposPath string, repoPoll time.Duration) func(ssh.Session)
fmt.Println("not active")
return nil, nil
}
- cfg.Width = pty.Window.Width
- cfg.Height = pty.Window.Height
- return NewBubble(appCfg, cfg), []tea.ProgramOption{tea.WithAltScreen()}
- }
-}
-
-func loadConfig(rs *git.RepoSource) (*Config, error) {
- cfg := &Config{}
- cfg.RepoSource = rs
- cr, err := rs.GetRepo("config")
- if err != nil {
- return nil, err
- }
- cs, err := cr.LatestFile("config.json")
- if err != nil {
- return nil, err
- }
- err = json.Unmarshal([]byte(cs), cfg)
- if err != nil {
- return nil, fmt.Errorf("bad json in config.json: %s", err)
+ scfg.Width = pty.Window.Width
+ scfg.Height = pty.Window.Height
+ return NewBubble(cfg, scfg), []tea.ProgramOption{tea.WithAltScreen()}
}
- return cfg, nil
}