diff --git a/go.mod b/go.mod index f81b8c1da..a1fae4825 100755 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da github.com/muesli/mango v0.1.0 github.com/muesli/roff v0.1.0 + github.com/spf13/cobra v1.4.0 ) require ( @@ -45,6 +46,7 @@ require ( github.com/go-git/gcfg v1.5.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/imdario/mergo v0.3.12 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -57,6 +59,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect github.com/yuin/goldmark v1.4.4 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect diff --git a/go.sum b/go.sum index 8dc59b6d4..85a6b542c 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,7 @@ github.com/charmbracelet/wish v0.3.1-0.20220405152319-bea68c3da3b1/go.mod h1:+Eg github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -76,6 +77,8 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= @@ -135,12 +138,17 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/server/cmd/cat.go b/server/cmd/cat.go new file mode 100644 index 000000000..18ff5678c --- /dev/null +++ b/server/cmd/cat.go @@ -0,0 +1,123 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/alecthomas/chroma/lexers" + gansi "github.com/charmbracelet/glamour/ansi" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/soft-serve/internal/git" + "github.com/charmbracelet/soft-serve/tui/common" + gitwish "github.com/charmbracelet/wish/git" + "github.com/muesli/termenv" + "github.com/spf13/cobra" +) + +var ( + lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239")) + lineBarStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("236")) + dirnameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AAFF")) + filenameStyle = lipgloss.NewStyle() + filemodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")) +) + +// CatCommand returns a command that prints the contents of a file. +func CatCommand() *cobra.Command { + var linenumber bool + var color bool + + catCmd := &cobra.Command{ + Use: "cat PATH", + Short: "Outputs the contents of the file at path.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ac, s := fromContext(cmd) + ps := strings.Split(args[0], "/") + rn := ps[0] + fp := strings.Join(ps[1:], "/") + auth := ac.AuthRepo(rn, s.PublicKey()) + if auth < gitwish.ReadOnlyAccess { + return ErrUnauthorized + } + var repo *git.Repo + repoExists := false + for _, rp := range ac.Source.AllRepos() { + if rp.Name() == rn { + repoExists = true + repo = rp + break + } + } + if !repoExists { + return ErrRepoNotFound + } + c, _, err := repo.LatestFile(fp) + if err != nil { + return err + } + if color { + c, err = withFormatting(fp, c) + if err != nil { + return err + } + } + if linenumber { + c = withLineNumber(c, color) + } + fmt.Fprint(s, c) + return nil + }, + } + catCmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers") + catCmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output") + + return catCmd +} + +func withLineNumber(s string, color bool) string { + lines := strings.Split(s, "\n") + // NB: len() is not a particularly safe way to count string width (because + // it's counting bytes instead of runes) but in this case it's okay + // because we're only dealing with digits, which are one byte each. + mll := len(fmt.Sprintf("%d", len(lines))) + for i, l := range lines { + digit := fmt.Sprintf("%*d", mll, i+1) + bar := "│" + if color { + digit = lineDigitStyle.Render(digit) + bar = lineBarStyle.Render(bar) + } + if i < len(lines)-1 || len(l) != 0 { + // If the final line was a newline we'll get an empty string for + // the final line, so drop the newline altogether. + lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l) + } + } + return strings.Join(lines, "\n") +} + +func withFormatting(p, c string) (string, error) { + zero := uint(0) + lang := "" + lexer := lexers.Match(p) + if lexer != nil && lexer.Config() != nil { + lang = lexer.Config().Name + } + formatter := &gansi.CodeBlockElement{ + Code: c, + Language: lang, + } + r := strings.Builder{} + styles := common.DefaultStyles() + styles.CodeBlock.Margin = &zero + rctx := gansi.NewRenderContext(gansi.Options{ + Styles: styles, + ColorProfile: termenv.TrueColor, + }) + err := formatter.Render(&r, rctx) + if err != nil { + return "", err + } + return r.String(), nil +} diff --git a/server/cmd/cmd.go b/server/cmd/cmd.go new file mode 100644 index 000000000..e71ab6535 --- /dev/null +++ b/server/cmd/cmd.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "fmt" + + appCfg "github.com/charmbracelet/soft-serve/internal/config" + "github.com/gliderlabs/ssh" + "github.com/spf13/cobra" +) + +var ( + // ErrUnauthorized is returned when the user is not authorized to perform action. + ErrUnauthorized = fmt.Errorf("Unauthorized") + // ErrRepoNotFound is returned when the repo is not found. + ErrRepoNotFound = fmt.Errorf("Repository not found") + // ErrFileNotFound is returned when the file is not found. + ErrFileNotFound = fmt.Errorf("File not found") + + usageTemplate = `Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.UseLine}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.UseLine}} [command] --help" for more information about a command.{{end}} +` +) + +// RootCommand is the root command for the server. +func RootCommand() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "ssh [-p PORT] HOST", + Long: "Soft Serve is a self-hostable Git server for the command line.", + Args: cobra.MinimumNArgs(1), + DisableFlagsInUseLine: true, + } + rootCmd.SetUsageTemplate(usageTemplate) + rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.AddCommand( + ReloadCommand(), + CatCommand(), + ListCommand(), + GitCommand(), + ) + + return rootCmd +} + +func fromContext(cmd *cobra.Command) (*appCfg.Config, ssh.Session) { + ctx := cmd.Context() + ac := ctx.Value("config").(*appCfg.Config) + s := ctx.Value("session").(ssh.Session) + return ac, s +} diff --git a/server/cmd/git.go b/server/cmd/git.go new file mode 100644 index 000000000..fd2c3be87 --- /dev/null +++ b/server/cmd/git.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "io" + "os/exec" + + "github.com/charmbracelet/soft-serve/internal/git" + gitwish "github.com/charmbracelet/wish/git" + "github.com/spf13/cobra" +) + +// GitCommand returns a command that handles Git operations. +func GitCommand() *cobra.Command { + gitCmd := &cobra.Command{ + Use: "git REPO COMMAND", + Short: "Perform Git operations on a repository.", + RunE: func(cmd *cobra.Command, args []string) error { + ac, s := fromContext(cmd) + auth := ac.AuthRepo("config", s.PublicKey()) + if auth < gitwish.AdminAccess { + return ErrUnauthorized + } + if len(args) < 1 { + return runGit(nil, s, s, "") + } + var repo *git.Repo + rn := args[0] + repoExists := false + for _, rp := range ac.Source.AllRepos() { + if rp.Name() == rn { + repoExists = true + repo = rp + break + } + } + if !repoExists { + return ErrRepoNotFound + } + return runGit(nil, s, s, repo.Path(), args[1:]...) + }, + } + gitCmd.Flags().SetInterspersed(false) + + return gitCmd +} + +func runGit(in io.Reader, out, err io.Writer, dir string, args ...string) error { + cmd := exec.Command("git", args...) + cmd.Stdin = in + cmd.Stdout = out + cmd.Stderr = err + cmd.Dir = dir + return cmd.Run() +} diff --git a/server/cmd/list.go b/server/cmd/list.go new file mode 100644 index 000000000..f07a2a68f --- /dev/null +++ b/server/cmd/list.go @@ -0,0 +1,81 @@ +package cmd + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/charmbracelet/soft-serve/git" + gitwish "github.com/charmbracelet/wish/git" + "github.com/spf13/cobra" +) + +// ListCommand returns a command that list file or directory at path. +func ListCommand() *cobra.Command { + lsCmd := &cobra.Command{ + Use: "ls PATH", + Aliases: []string{"list"}, + Short: "List file or directory at path.", + Args: cobra.RangeArgs(0, 1), + RunE: func(cmd *cobra.Command, args []string) error { + ac, s := fromContext(cmd) + rn := "" + path := "" + ps := []string{} + if len(args) > 0 { + path = filepath.Clean(args[0]) + ps = strings.Split(path, "/") + rn = ps[0] + auth := ac.AuthRepo(rn, s.PublicKey()) + if auth < gitwish.ReadOnlyAccess { + return ErrUnauthorized + } + } + if path == "" || path == "." || path == "/" { + for _, r := range ac.Source.AllRepos() { + fmt.Fprintln(s, r.Name()) + } + return nil + } + r, err := ac.Source.GetRepo(rn) + if err != nil { + return err + } + head, err := r.HEAD() + if err != nil { + return err + } + tree, err := r.Tree(head, "") + if err != nil { + return err + } + subpath := strings.Join(ps[1:], "/") + ents := git.Entries{} + te, err := tree.TreeEntry(subpath) + if err == git.ErrRevisionNotExist { + return ErrFileNotFound + } + if err != nil { + return err + } + if te.Type() == "tree" { + tree, err = tree.SubTree(subpath) + if err != nil { + return err + } + ents, err = tree.Entries() + if err != nil { + return err + } + } else { + ents = append(ents, te) + } + ents.Sort() + for _, ent := range ents { + fmt.Fprintf(s, "%s\t%d\t %s\n", ent.Mode(), ent.Size(), ent.Name()) + } + return nil + }, + } + return lsCmd +} diff --git a/server/cmd/reload.go b/server/cmd/reload.go new file mode 100644 index 000000000..7f2312ab6 --- /dev/null +++ b/server/cmd/reload.go @@ -0,0 +1,23 @@ +package cmd + +import ( + gitwish "github.com/charmbracelet/wish/git" + "github.com/spf13/cobra" +) + +// ReloadCommand returns a command that reloads the server configuration. +func ReloadCommand() *cobra.Command { + reloadCmd := &cobra.Command{ + Use: "reload", + Short: "Reloads the configuration", + RunE: func(cmd *cobra.Command, args []string) error { + ac, s := fromContext(cmd) + auth := ac.AuthRepo("config", s.PublicKey()) + if auth < gitwish.AdminAccess { + return ErrUnauthorized + } + return ac.Reload() + }, + } + return reloadCmd +} diff --git a/server/middleware.go b/server/middleware.go index cfe89a3fd..3dd615179 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -1,186 +1,48 @@ package server import ( + "context" "fmt" - "path/filepath" - "strings" - "github.com/alecthomas/chroma/lexers" - gansi "github.com/charmbracelet/glamour/ansi" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/soft-serve/git" appCfg "github.com/charmbracelet/soft-serve/internal/config" - "github.com/charmbracelet/soft-serve/tui/common" + "github.com/charmbracelet/soft-serve/server/cmd" "github.com/charmbracelet/wish" - gitwish "github.com/charmbracelet/wish/git" "github.com/gliderlabs/ssh" - ggit "github.com/gogs/git-module" - "github.com/muesli/termenv" ) -var ( - lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239")) - lineBarStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("236")) - dirnameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AAFF")) - filenameStyle = lipgloss.NewStyle() - filemodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")) -) - -// softServeMiddleware is a middleware that handles displaying files with the -// option of syntax highlighting and line numbers. -func softServeMiddleware(ac *appCfg.Config) wish.Middleware { +// softMiddleware is the Soft Serve middleware that handles SSH commands. +func softMiddleware(ac *appCfg.Config) wish.Middleware { return func(sh ssh.Handler) ssh.Handler { return func(s ssh.Session) { - _, _, active := s.Pty() - cmds := s.Command() - if !active && len(cmds) > 0 { - func() { - color := false - lineno := false - fp := filepath.Clean(cmds[0]) - ps := strings.Split(fp, "/") - repo := ps[0] - if repo == "config" { - return - } - repoExists := false - for _, rp := range ac.Source.AllRepos() { - if rp.Name() == repo { - repoExists = true - break - } - } - if !repoExists { - s.Write([]byte("repository not found")) - s.Exit(1) - return - } - auth := ac.AuthRepo(repo, s.PublicKey()) - if auth < gitwish.ReadOnlyAccess { - s.Write([]byte("unauthorized")) - s.Exit(1) - return - } - for _, op := range cmds[1:] { - if op == "-c" || op == "--color" { - color = true - } else if op == "-l" || op == "--lineno" || op == "--linenumber" { - lineno = true - } - } - rs, err := ac.Source.GetRepo(repo) - if err != nil { - _, _ = s.Write([]byte(err.Error())) - _ = s.Exit(1) - return - } - ref, err := rs.HEAD() - if err != nil { - _, _ = s.Write([]byte(err.Error())) - _ = s.Exit(1) - return - } - p := strings.Join(ps[1:], "/") - t, err := rs.Tree(ref, p) - if err != nil && err != ggit.ErrRevisionNotExist { - _, _ = s.Write([]byte(err.Error())) - _ = s.Exit(1) - return - } - if err == ggit.ErrRevisionNotExist { - _, _ = s.Write([]byte(git.ErrFileNotFound.Error())) - _ = s.Exit(1) - return - } - ents, err := t.Entries() - if err != nil { - fc, _, err := rs.LatestFile(p) - if err != nil { - _, _ = s.Write([]byte(err.Error())) - _ = s.Exit(1) - return - } - if color { - ffc, err := withFormatting(fp, fc) - if err != nil { - s.Write([]byte(err.Error())) - s.Exit(1) - return - } - fc = ffc - } - if lineno { - fc = withLineNumber(fc, color) - } - s.Write([]byte(fc)) - } else { - ents.Sort() - for _, e := range ents { - m := e.Mode() - if m == 0 { - s.Write([]byte(strings.Repeat(" ", 10))) - } else { - s.Write([]byte(filemodeStyle.Render(m.String()))) - } - s.Write([]byte(" ")) - if !e.IsTree() { - s.Write([]byte(filenameStyle.Render(e.Name()))) - } else { - s.Write([]byte(dirnameStyle.Render(e.Name()))) - } - s.Write([]byte("\n")) - } - } - }() - } - sh(s) - } - } -} + func() { + _, _, active := s.Pty() + if active { + return + } + ctx := s.Context() + ctx = context.WithValue(ctx, "config", ac) //nolint:revive + ctx = context.WithValue(ctx, "session", s) //nolint:revive -func withLineNumber(s string, color bool) string { - lines := strings.Split(s, "\n") - // NB: len() is not a particularly safe way to count string width (because - // it's counting bytes instead of runes) but in this case it's okay - // because we're only dealing with digits, which are one byte each. - mll := len(fmt.Sprintf("%d", len(lines))) - for i, l := range lines { - digit := fmt.Sprintf("%*d", mll, i+1) - bar := "│" - if color { - digit = lineDigitStyle.Render(digit) - bar = lineBarStyle.Render(bar) - } - if i < len(lines)-1 || len(l) != 0 { - // If the final line was a newline we'll get an empty string for - // the final line, so drop the newline altogether. - lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l) + use := "ssh" + port := ac.Cfg.Port + if port != 22 { + use += fmt.Sprintf(" -p%d", port) + } + use += fmt.Sprintf(" %s", ac.Cfg.Host) + cmd := cmd.RootCommand() + cmd.Use = use + cmd.SetIn(s) + cmd.SetOut(s) + cmd.SetErr(s) + cmd.SetArgs(s.Command()) + err := cmd.ExecuteContext(ctx) + if err != nil { + _, _ = s.Write([]byte(err.Error())) + _ = s.Exit(1) + return + } + }() + sh(s) } } - return strings.Join(lines, "\n") -} - -func withFormatting(p, c string) (string, error) { - zero := uint(0) - lang := "" - lexer := lexers.Match(p) - if lexer != nil && lexer.Config() != nil { - lang = lexer.Config().Name - } - formatter := &gansi.CodeBlockElement{ - Code: c, - Language: lang, - } - r := strings.Builder{} - styles := common.DefaultStyles() - styles.CodeBlock.Margin = &zero - rctx := gansi.NewRenderContext(gansi.Options{ - Styles: styles, - ColorProfile: termenv.TrueColor, - }) - err := formatter.Render(&r, rctx) - if err != nil { - return "", err - } - return r.String(), nil } diff --git a/server/middleware_test.go b/server/middleware_test.go index 9b8fe2661..94ac3ebbf 100644 --- a/server/middleware_test.go +++ b/server/middleware_test.go @@ -1,8 +1,10 @@ package server import ( + "os" "testing" + sconfig "github.com/charmbracelet/soft-serve/config" "github.com/charmbracelet/soft-serve/internal/config" "github.com/charmbracelet/wish/testsession" "github.com/gliderlabs/ssh" @@ -12,13 +14,21 @@ import ( var () func TestMiddleware(t *testing.T) { + t.Cleanup(func() { + os.RemoveAll("testmiddleware") + }) is := is.New(t) - appCfg, err := config.NewConfig(cfg) + appCfg, err := config.NewConfig(&sconfig.Config{ + Host: "localhost", + Port: 22223, + RepoPath: "testmiddleware/repos", + KeyPath: "testmiddleware/key", + }) is.NoErr(err) _ = testsession.New(t, &ssh.Server{ - Handler: softServeMiddleware(appCfg)(func(s ssh.Session) { + Handler: softMiddleware(appCfg)(func(s ssh.Session) { t.Run("TestCatConfig", func(t *testing.T) { - _, err := s.Write([]byte("config/config.json")) + _, err := s.Write([]byte("cat config/config.json")) if err == nil { t.Errorf("Expected error, got nil") } diff --git a/server/server.go b/server/server.go index 87bc47551..aa4063728 100644 --- a/server/server.go +++ b/server/server.go @@ -36,7 +36,7 @@ func NewServer(cfg *config.Config) *Server { mw := []wish.Middleware{ rm.MiddlewareWithLogger( cfg.ErrorLog, - softServeMiddleware(ac), + softMiddleware(ac), bm.Middleware(tui.SessionHandler(ac)), gm.Middleware(cfg.RepoPath, ac), lm.Middleware(),