pax_global_header00006660000000000000000000000064141534722240014516gustar00rootroot0000000000000052 comment=9a28fca26b79d66b212fcbcacb5014e492f56745 wish-0.1.1/000077500000000000000000000000001415347222400124675ustar00rootroot00000000000000wish-0.1.1/.github/000077500000000000000000000000001415347222400140275ustar00rootroot00000000000000wish-0.1.1/.github/workflows/000077500000000000000000000000001415347222400160645ustar00rootroot00000000000000wish-0.1.1/.github/workflows/build.yml000066400000000000000000000006331415347222400177100ustar00rootroot00000000000000name: Build on: push: pull_request: jobs: build: strategy: matrix: go-version: [~1.17, ^1] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Build run: go build -v ./... - name: Test run: go test -v ./... wish-0.1.1/.github/workflows/lint.yml000066400000000000000000000007141415347222400175570ustar00rootroot00000000000000name: lint on: push: pull_request: jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: # Optional: golangci-lint command line arguments. args: --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true wish-0.1.1/.github/workflows/soft-serve.yml000066400000000000000000000014631415347222400207100ustar00rootroot00000000000000name: Soft-Serve on: push: branches: - main jobs: soft-serve: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 with: fetch-depth: 0 - name: Push to Soft-Serve uses: charmbracelet/soft-serve-action@v1 with: server: "git.charm.sh" ssh-key: "${{ secrets.CHARM_SOFT_SERVE_KEY }}" name: "wish" - name: Push vendor to Soft-Serve env: SSH_AUTH_SOCK: /tmp/ssh_agent.sock run: | git config --global user.email "actions@github.com" git config --global user.name "Charmbracelet[bot]" git checkout -b vendor go mod vendor git add vendor git commit -m 'vendor' git push -f soft-serve vendor wish-0.1.1/.gitignore000066400000000000000000000002311415347222400144530ustar00rootroot00000000000000examples/* !examples/bubbletea examples/bubbletea/bubbletea examples/bubbletea/.ssh !examples/git examples/git/git examples/git/.ssh examples/git/.repos wish-0.1.1/.golangci.yml000066400000000000000000000007121415347222400150530ustar00rootroot00000000000000run: tests: false issues: include: - EXC0001 - EXC0005 - EXC0011 - EXC0012 - EXC0013 max-issues-per-linter: 0 max-same-issues: 0 linters: enable: - bodyclose - dupl - exportloopref - goconst - godot - godox - goimports - goprintffuncname - gosec - ifshort - misspell - prealloc - revive - rowserrcheck - sqlclosecheck - unconvert - unparam - whitespace wish-0.1.1/LICENSE000066400000000000000000000020701415347222400134730ustar00rootroot00000000000000MIT License Copyright (c) 2019-2021 Charmbracelet, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wish-0.1.1/README.md000077500000000000000000000070351415347222400137560ustar00rootroot00000000000000# Wish

A nice rendering of a star, anthropomorphized somewhat by means of a smile, with the words ‘Charm Wish’ next to it
Latest Release GoDoc Build Status

Make SSH apps, just like that! 💫 SSH is an excellent platform to build remotely accessible applications on. It offers secure communication without the hassle of HTTPS certificates, it has user identification with SSH keys and it's accessible from anywhere with a terminal. Powerful protocols like Git work over SSH and you can even render TUIs directly over an SSH connection. Wish is an SSH server with sensible defaults and a collection of middleware that makes building SSH apps easy. Wish is built on [gliderlabs/ssh][gliderlabs/ssh] and should be easy to integrate into any existing projects. ## Middleware ### Bubble Tea The [`bubbletea`](bubbletea) middleware makes it easy to serve any [Bubble Tea][bubbletea] application over SSH. Each SSH session will get their own `tea.Program` with the SSH pty input and output connected. Client window dimension and resize messages are also natively handled by the `tea.Program`. You can see a demo of the Wish middleware in action at: `ssh git.charm.sh` ### Git The [`git`](git) middleware adds `git` server functionality to any ssh server. It supports repo creation on initial push and custom public key based auth. This middleware requires that `git` is installed on the server. ### Logging The [`logging`](logging) middleware provides basic connection logging. Connects are logged with the remote address, invoked command, TERM setting, window dimensions and if the auth was public key based. Disconnect will log the remote address and connection duration. ### Access Control Not all applications will support general SSH connections. To restrict access to supported methods, you can use the [`activeterm`](activeterm) middleware to only allow connections with active terminals connected and the [`accesscontrol`](accesscontrol) middleware that lets you specify allowed commands. ## Default Server Wish includes the ability to easily create an always authenticating default SSH server with automatic server key generation. ## Examples There are examples for a standalone [Bubble Tea application](examples/bubbletea) and [Git server](examples/git) in the [examples](examples) folder. To see a more real-world application that combines Bubble Tea and Git, you can take a look at [Soft Serve](https://github.com/charmbracelet/soft-serve) which uses Git as a CMS and provides a TUI over SSH for interacting with pushed repos. [bubbletea]: https://github.com/charmbracelet/bubbletea [gliderlabs/ssh]: https://github.com/gliderlabs/ssh ## License [MIT](https://github.com/charmbracelet/wish/raw/main/LICENSE) *** Part of [Charm](https://charm.sh). The Charm logo Charm热爱开源 • Charm loves open source wish-0.1.1/accesscontrol/000077500000000000000000000000001415347222400153315ustar00rootroot00000000000000wish-0.1.1/accesscontrol/accesscontrol.go000066400000000000000000000012241415347222400205210ustar00rootroot00000000000000// Package accesscontrol provides a middleware that allows you to restrict the commands the user can execute. package accesscontrol import ( "github.com/charmbracelet/wish" "github.com/gliderlabs/ssh" ) // Middleware will exit 1 connections trying to execute commands that are not allowed. // If no allowed commands are provided, no commands will be allowed. func Middleware(cmds ...string) wish.Middleware { return func(sh ssh.Handler) ssh.Handler { return func(s ssh.Session) { if len(s.Command()) == 0 { sh(s) return } for _, cmd := range cmds { if s.Command()[0] == cmd { sh(s) return } } s.Exit(1) } } } wish-0.1.1/accesscontrol/accesscontrol_test.go000066400000000000000000000037751415347222400215750ustar00rootroot00000000000000package accesscontrol_test import ( "bytes" "io" "testing" "github.com/charmbracelet/wish/accesscontrol" "github.com/charmbracelet/wish/testsession" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" ) const out = "hello world" func TestMiddleware(t *testing.T) { requireEmpty := func(tb testing.TB, s string) { tb.Helper() if s != "" { tb.Errorf("expected output to be empty, got %q", s) } } requireOutput := func(tb testing.TB, s string) { tb.Helper() if out != s { t.Errorf("expected %q, got %q", out, s) } } t.Run("no allowed cmds no cmd", func(t *testing.T) { var b bytes.Buffer if err := setup(t, &b).Run(""); err != nil { t.Error(err) } requireOutput(t, b.String()) }) t.Run("no allowed cmds with cmd", func(t *testing.T) { var b bytes.Buffer if err := setup(t, &b).Run("echo"); err == nil { t.Errorf("should have errored") } requireEmpty(t, b.String()) }) t.Run("allowed cmds no cmd", func(t *testing.T) { var b bytes.Buffer if err := setup(t, &b, "echo").Run(""); err != nil { t.Error(err) } requireOutput(t, b.String()) }) t.Run("allowed cmds with allowed cmd", func(t *testing.T) { var b bytes.Buffer if err := setup(t, &b, "echo").Run("echo"); err != nil { t.Error(err) } requireOutput(t, b.String()) }) t.Run("allowed cmds with disallowed cmd", func(t *testing.T) { var b bytes.Buffer if err := setup(t, &b, "echo").Run("cat"); err == nil { t.Error(err) } requireEmpty(t, b.String()) }) t.Run("allowed cmds with allowed cmd followed disallowed cmd", func(t *testing.T) { var b bytes.Buffer if err := setup(t, &b, "echo").Run("cat echo"); err == nil { t.Error(err) } requireEmpty(t, b.String()) }) } func setup(t *testing.T, w io.Writer, allowedCmds ...string) *gossh.Session { session, _, cleanup := testsession.New(t, &ssh.Server{ Handler: accesscontrol.Middleware(allowedCmds...)(func(s ssh.Session) { s.Write([]byte(out)) }), }, nil) t.Cleanup(cleanup) session.Stdout = w return session } wish-0.1.1/activeterm/000077500000000000000000000000001415347222400146325ustar00rootroot00000000000000wish-0.1.1/activeterm/activeterm.go000066400000000000000000000006621415347222400173300ustar00rootroot00000000000000// Package activeterm provides a middleware to block inactive PTYs. package activeterm import ( "github.com/charmbracelet/wish" "github.com/gliderlabs/ssh" ) // Middleware will exit 1 connections trying with no active terminals. func Middleware() wish.Middleware { return func(sh ssh.Handler) ssh.Handler { return func(s ssh.Session) { _, _, active := s.Pty() if !active { s.Exit(1) return } sh(s) } } } wish-0.1.1/activeterm/activeterm_test.go000066400000000000000000000011521415347222400203620ustar00rootroot00000000000000package activeterm_test import ( "testing" "github.com/charmbracelet/wish/activeterm" "github.com/charmbracelet/wish/testsession" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" ) func TestMiddleware(t *testing.T) { t.Run("inactive term", func(t *testing.T) { if err := setup(t).Run(""); err == nil { t.Errorf("tests should be an inactive pty") } }) } func setup(t *testing.T) *gossh.Session { session, _, cleanup := testsession.New(t, &ssh.Server{ Handler: activeterm.Middleware()(func(s ssh.Session) { s.Write([]byte("hello")) }), }, nil) t.Cleanup(cleanup) return session } wish-0.1.1/bubbletea/000077500000000000000000000000001415347222400144145ustar00rootroot00000000000000wish-0.1.1/bubbletea/tea.go000066400000000000000000000037171415347222400155240ustar00rootroot00000000000000package bubbletea import ( "log" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/wish" "github.com/gliderlabs/ssh" "github.com/muesli/termenv" ) // BubbleTeaHander is the function Bubble Tea apps implement to hook into the // SSH Middleware. This will create a new tea.Program for every connection and // start it with the tea.ProgramOptions returned. type BubbleTeaHandler func(ssh.Session) (tea.Model, []tea.ProgramOption) // Middleware takes a BubbleTeaHandler and hooks the input and output for the // ssh.Session into the tea.Program. It also captures window resize events and // sends them to the tea.Program as tea.WindowSizeMsgs. By default a 256 color // profile will be used when rendering with Lip Gloss. func Middleware(bth BubbleTeaHandler) wish.Middleware { return MiddlewareWithColorProfile(bth, termenv.ANSI256) } // MiddlewareWithColorProfile allows you to specify the number of colors // returned by the server when using Lip Gloss. The number of colors supported // by an SSH client's terminal cannot be detected by the server but this will // allow for manually setting the color profile on all SSH connections. func MiddlewareWithColorProfile(bth BubbleTeaHandler, cp termenv.Profile) wish.Middleware { return func(sh ssh.Handler) ssh.Handler { lipgloss.SetColorProfile(cp) return func(s ssh.Session) { errc := make(chan error, 1) m, opts := bth(s) if m != nil { opts = append(opts, tea.WithInput(s), tea.WithOutput(s)) p := tea.NewProgram(m, opts...) _, windowChanges, _ := s.Pty() go func() { for { select { case <-s.Context().Done(): if p != nil { p.Quit() } case w := <-windowChanges: if p != nil { p.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height}) } case err := <-errc: if err != nil { log.Print(err) } } } }() errc <- p.Start() } sh(s) } } } wish-0.1.1/examples/000077500000000000000000000000001415347222400143055ustar00rootroot00000000000000wish-0.1.1/examples/bubbletea/000077500000000000000000000000001415347222400162325ustar00rootroot00000000000000wish-0.1.1/examples/bubbletea/main.go000066400000000000000000000043641415347222400175140ustar00rootroot00000000000000package main // An example Bubble Tea server. This will put an ssh session into alt screen // and continually print up to date terminal information. import ( "fmt" "log" "os" "os/signal" "syscall" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/wish" bm "github.com/charmbracelet/wish/bubbletea" lm "github.com/charmbracelet/wish/logging" "github.com/gliderlabs/ssh" ) const host = "localhost" const port = 23234 func main() { s, err := wish.NewServer( wish.WithAddress(fmt.Sprintf("%s:%d", host, port)), wish.WithHostKeyPath(".ssh/term_info_ed25519"), wish.WithMiddleware( bm.Middleware(teaHandler), lm.Middleware(), ), ) if err != nil { log.Fatalln(err) } done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) log.Printf("Starting SSH server on %s:%d", host, port) go func() { if err = s.ListenAndServe(); err != nil { log.Fatalln(err) } }() <-done log.Println("Stopping SSH server") if err := s.Close(); err != nil { log.Fatalln(err) } } // You can wire any Bubble Tea model up to the middleware with a function that // handles the incoming ssh.Session. Here we just grab the terminal info and // pass it to the new model. You can also return tea.ProgramOptions (such as // teaw.WithAltScreen) on a session by session basis func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { pty, _, active := s.Pty() if !active { fmt.Println("no active terminal, skipping") return nil, nil } m := model{ term: pty.Term, width: pty.Window.Width, height: pty.Window.Height, } return m, []tea.ProgramOption{tea.WithAltScreen()} } // Just a generic tea.Model to demo terminal information of ssh. type model struct { term string width int height int } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.height = msg.Height m.width = msg.Width case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": return m, tea.Quit } } return m, nil } func (m model) View() string { s := "Your term is %s\n" s += "Your window size is x: %d y: %d\n\n" s += "Press 'q' to quit\n" return fmt.Sprintf(s, m.term, m.width, m.height) } wish-0.1.1/examples/git/000077500000000000000000000000001415347222400150705ustar00rootroot00000000000000wish-0.1.1/examples/git/main.go000066400000000000000000000051331415347222400163450ustar00rootroot00000000000000package main // An example git server. This will list all available repos if you ssh // directly to the server. To test `ssh -p 23233 localhost` once it's running. import ( "fmt" "io/fs" "log" "os" "os/signal" "syscall" "github.com/charmbracelet/wish" gm "github.com/charmbracelet/wish/git" lm "github.com/charmbracelet/wish/logging" "github.com/gliderlabs/ssh" ) const port = 23233 const host = "localhost" const repoDir = ".repos" type app struct { access gm.AccessLevel } func (a app) AuthRepo(repo string, pk ssh.PublicKey) gm.AccessLevel { return a.access } func (a app) Push(repo string, pk ssh.PublicKey) { log.Printf("pushed %s", repo) } func (a app) Fetch(repo string, pk ssh.PublicKey) { log.Printf("fetch %s", repo) } func passHandler(ctx ssh.Context, password string) bool { return false } func pkHandler(ctx ssh.Context, key ssh.PublicKey) bool { return true } func main() { // A simple GitHooks implementation to allow global read write access. a := app{gm.ReadWriteAccess} s, err := wish.NewServer( ssh.PublicKeyAuth(pkHandler), ssh.PasswordAuth(passHandler), wish.WithAddress(fmt.Sprintf("%s:%d", host, port)), wish.WithHostKeyPath(".ssh/git_server_ed25519"), wish.WithMiddleware( gm.Middleware(repoDir, a), gitListMiddleware, lm.Middleware(), ), ) if err != nil { log.Fatalln(err) } done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) log.Printf("Starting SSH server on %s:%d", host, port) go func() { if err = s.ListenAndServe(); err != nil { log.Fatalln(err) } }() <-done log.Println("Stopping SSH server") if err := s.Close(); err != nil { log.Fatalln(err) } } // Normally we would use a Bubble Tea program for the TUI but for simplicity, // we'll just write a list of the pushed repos to the terminal and exit the ssh // session. func gitListMiddleware(h ssh.Handler) ssh.Handler { return func(s ssh.Session) { // Git will have a command included so only run this if there are no // commands passed to ssh. if len(s.Command()) == 0 { des, err := os.ReadDir(repoDir) if err != nil && err != fs.ErrNotExist { log.Println(err) } if len(des) > 0 { fmt.Fprintf(s, "\n### Repo Menu ###\n\n") } for _, de := range des { fmt.Fprintf(s, "• %s - ", de.Name()) fmt.Fprintf(s, "git clone ssh://%s:%d/%s\n", host, port, de.Name()) } fmt.Fprintf(s, "\n\n### Add some repos! ###\n\n") fmt.Fprintf(s, "> cd some_repo\n") fmt.Fprintf(s, "> git remote add wish_test ssh://%s:%d/some_repo\n", host, port) fmt.Fprintf(s, "> git push wish_test\n\n\n") } h(s) } } wish-0.1.1/git/000077500000000000000000000000001415347222400132525ustar00rootroot00000000000000wish-0.1.1/git/git.go000066400000000000000000000113201415347222400143610ustar00rootroot00000000000000package git import ( "context" "fmt" "os" "os/exec" "path/filepath" "github.com/charmbracelet/wish" "github.com/gliderlabs/ssh" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" ) // ErrNotAuthed represents unauthorized access. var ErrNotAuthed = fmt.Errorf("you are not authorized to do this") // ErrSystemMalfunction represents a general system error returned to clients. var ErrSystemMalfunction = fmt.Errorf("something went wrong") // AccessLevel is the level of access allowed to a repo. type AccessLevel int const ( NoAccess AccessLevel = iota ReadOnlyAccess ReadWriteAccess AdminAccess ) // GitHooks is an interface that allows for custom authorization // implementations and post push/fetch notifications. Prior to git access, // AuthRepo will be called with the ssh.Session public key and the repo name. // Implementers return the appropriate AccessLevel. type GitHooks interface { AuthRepo(string, ssh.PublicKey) AccessLevel Push(string, ssh.PublicKey) Fetch(string, ssh.PublicKey) } // Middleware adds Git server functionality to the ssh.Server. Repos are stored // in the specified repo directory. The provided GitHooks implementation will be // checked for access on a per repo basis for a ssh.Session public key. // GitHooks.Push and GitHooks.Fetch will be called on successful completion of // their commands. func Middleware(repoDir string, gh GitHooks) wish.Middleware { return func(sh ssh.Handler) ssh.Handler { return func(s ssh.Session) { cmd := s.Command() if len(cmd) == 2 { gc := cmd[0] repo := cmd[1] // cmd[1] will be `/REPO` if len(repo) > 0 && repo[0] == '/' { repo = repo[1:] } pk := s.PublicKey() access := gh.AuthRepo(repo, pk) switch gc { case "git-receive-pack": switch access { case ReadWriteAccess, AdminAccess: err := gitReceivePack(s, gc, repoDir, repo) if err != nil { fatalGit(s, ErrSystemMalfunction) } else { gh.Push(repo, pk) } default: fatalGit(s, ErrNotAuthed) } case "git-upload-archive", "git-upload-pack": switch access { case ReadOnlyAccess, ReadWriteAccess, AdminAccess: err := gitUploadPack(s, gc, repoDir, repo) if err != nil { fatalGit(s, ErrSystemMalfunction) } else { gh.Fetch(repo, pk) } default: fatalGit(s, ErrNotAuthed) } } } sh(s) } } } func gitReceivePack(s ssh.Session, gitCmd string, repoDir string, repo string) error { ctx := s.Context() err := ensureRepo(ctx, repoDir, repo) if err != nil { return err } rp := filepath.Join(repoDir, repo) err = runCmd(s, "./", gitCmd, rp) if err != nil { return err } err = ensureDefaultBranch(s, rp) if err != nil { return err } err = runCmd(s, rp, "git", "update-server-info") if err != nil { return err } return nil } func gitUploadPack(s ssh.Session, gitCmd string, repoDir string, repo string) error { rp := filepath.Join(repoDir, repo) if exists, err := fileExists(rp); exists && err == nil { err = runCmd(s, "./", gitCmd, rp) if err != nil { return err } } return nil } func fileExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return true, err } func fatalGit(s ssh.Session, err error) { // hex length includes 4 byte length prefix and ending newline msg := err.Error() pktLine := fmt.Sprintf("%04x%s\n", len(msg)+5, msg) _, _ = s.Write([]byte(pktLine)) s.Exit(1) } func ensureRepo(ctx context.Context, dir string, repo string) error { exists, err := fileExists(dir) if err != nil { return err } if !exists { err = os.MkdirAll(dir, os.ModeDir|os.FileMode(0700)) if err != nil { return err } } rp := filepath.Join(dir, repo) exists, err = fileExists(rp) if err != nil { return err } if !exists { _, err := git.PlainInit(rp, true) if err != nil { return err } } return nil } func runCmd(s ssh.Session, dir, name string, args ...string) error { usi := exec.CommandContext(s.Context(), name, args...) usi.Dir = dir usi.Stdout = s usi.Stdin = s err := usi.Run() if err != nil { return err } return nil } func ensureDefaultBranch(s ssh.Session, repoPath string) error { r, err := git.PlainOpen(repoPath) if err != nil { return err } brs, err := r.Branches() if err != nil { return err } defer brs.Close() fb, err := brs.Next() if err != nil { return err } // Rename the default branch to the first branch available _, err = r.Head() if err == plumbing.ErrReferenceNotFound { err = runCmd(s, repoPath, "git", "branch", "-M", fb.Name().Short()) if err != nil { return err } } if err != nil && err != plumbing.ErrReferenceNotFound { return err } return nil } wish-0.1.1/go.mod000066400000000000000000000034701415347222400136010ustar00rootroot00000000000000module github.com/charmbracelet/wish go 1.17 require ( github.com/charmbracelet/bubbletea v0.19.0 github.com/charmbracelet/keygen v0.1.2 github.com/charmbracelet/lipgloss v0.4.0 github.com/gliderlabs/ssh v0.3.3 github.com/go-git/go-git/v5 v5.4.2 github.com/muesli/termenv v0.9.0 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 ) require ( github.com/Microsoft/go-winio v0.4.16 // indirect github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/containerd/console v1.0.2 // indirect github.com/emirpasic/gods v1.12.0 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.3.1 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.14-0.20210829144114-504425e14f74 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/xanzy/ssh-agent v0.3.0 // indirect golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) wish-0.1.1/go.sum000066400000000000000000000322111415347222400136210ustar00rootroot00000000000000github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/charmbracelet/bubbletea v0.19.0 h1:1gz4rbxl3qZik/oP8QW2vUtul2gO8RDDzmoLGERpTQc= github.com/charmbracelet/bubbletea v0.19.0/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= github.com/charmbracelet/keygen v0.1.2 h1:Gr/gdIOjDIxCTRVXpwa9tsXPoJPS2eGNehPoMnZLvTQ= github.com/charmbracelet/keygen v0.1.2/go.mod h1:kFQ3Cvop12fXWX1K29vxDxV9x8ujG4wBSXq//GySSSk= github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g= github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= 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= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA= github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= 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-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= github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 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/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= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14-0.20210829144114-504425e14f74 h1:3kEhu34+VLPo2YgQ1PXHLQRgMQKtBmq+MmMYEBHGX7U= github.com/mattn/go-isatty v0.0.14-0.20210829144114-504425e14f74/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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/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= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= wish-0.1.1/logging/000077500000000000000000000000001415347222400141155ustar00rootroot00000000000000wish-0.1.1/logging/logging.go000066400000000000000000000014251415347222400160740ustar00rootroot00000000000000package logging import ( "log" "time" "github.com/charmbracelet/wish" "github.com/gliderlabs/ssh" ) // Middleware provides basic connection logging. Connects are logged with the // remote address, invoked command, TERM setting, window dimensions and if the // auth was public key based. Disconnect will log the remote address and // connection duration. func Middleware() wish.Middleware { return func(sh ssh.Handler) ssh.Handler { return func(s ssh.Session) { ct := time.Now() hpk := s.PublicKey() != nil pty, _, _ := s.Pty() log.Printf("%s connect %s %v %v %s %v %v\n", s.User(), s.RemoteAddr().String(), hpk, s.Command(), pty.Term, pty.Window.Width, pty.Window.Height) sh(s) log.Printf("%s disconnect %s\n", s.RemoteAddr().String(), time.Since(ct)) } } } wish-0.1.1/logging/logging_test.go000066400000000000000000000011021415347222400171230ustar00rootroot00000000000000package logging_test import ( "testing" "github.com/charmbracelet/wish/logging" "github.com/charmbracelet/wish/testsession" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" ) func TestMiddleware(t *testing.T) { t.Run("inactive term", func(t *testing.T) { if err := setup(t).Run(""); err != nil { t.Error(err) } }) } func setup(t *testing.T) *gossh.Session { session, _, cleanup := testsession.New(t, &ssh.Server{ Handler: logging.Middleware()(func(s ssh.Session) { s.Write([]byte("hello")) }), }, nil) t.Cleanup(cleanup) return session } wish-0.1.1/options.go000066400000000000000000000037571415347222400145250ustar00rootroot00000000000000package wish import ( "os" "path/filepath" "strings" "github.com/charmbracelet/keygen" "github.com/gliderlabs/ssh" ) // WithAddress returns an ssh.Option that sets the address to listen on. func WithAddress(addr string) ssh.Option { return func(s *ssh.Server) error { s.Addr = addr return nil } } // WithVersion returns an ssh.Option that sets the server version. func WithVersion(version string) ssh.Option { return func(s *ssh.Server) error { s.Version = version return nil } } // WithMiddleware composes the provided Middleware and return a ssh.Option. // This useful if you manually create an ssh.Server and want to set the // Server.Handler. // Notice that middlewares are composed from first to last, which means the last one is executed first. func WithMiddleware(mw ...Middleware) ssh.Option { return func(s *ssh.Server) error { h := func(s ssh.Session) {} for _, m := range mw { h = m(h) } s.Handler = h return nil } } // WithHostKeyFile returns an ssh.Option that sets the path to the private. func WithHostKeyPath(path string) ssh.Option { if _, err := os.Stat(path); os.IsNotExist(err) { kps := strings.Split(path, string(filepath.Separator)) kp := strings.Join(kps[:len(kps)-1], string(filepath.Separator)) n := strings.TrimSuffix(kps[len(kps)-1], "_ed25519") _, err := keygen.NewWithWrite(kp, n, nil, keygen.Ed25519) if err != nil { return func(*ssh.Server) error { return err } } path = filepath.Join(kp, n+"_ed25519") } return ssh.HostKeyFile(path) } // WithHostKeyPEM returns an ssh.Option that sets the host key from a PEM block. func WithHostKeyPEM(pem []byte) ssh.Option { return ssh.HostKeyPEM(pem) } // WithPublicKeyAuth returns an ssh.Option that sets the public key auth handler. func WithPublicKeyAuth(h ssh.PublicKeyHandler) ssh.Option { return ssh.PublicKeyAuth(h) } // WithPasswordAuth returns an ssh.Option that sets the password auth handler. func WithPasswordAuth(p ssh.PasswordHandler) ssh.Option { return ssh.PasswordAuth(p) } wish-0.1.1/testsession/000077500000000000000000000000001415347222400150525ustar00rootroot00000000000000wish-0.1.1/testsession/main.go000066400000000000000000000023231415347222400163250ustar00rootroot00000000000000// more or less copied from gliderlabs/ssh tests package testsession import ( "fmt" "net" "testing" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" ) func New(tb testing.TB, srv *ssh.Server, cfg *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) { tb.Helper() l := newLocalListener() go srv.Serve(l) return newClientSession(tb, l.Addr().String(), cfg) } func newLocalListener() net.Listener { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { panic(fmt.Sprintf("failed to listen on a port: %v", err)) } } return l } func newClientSession(tb testing.TB, addr string, config *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) { tb.Helper() if config == nil { config = &gossh.ClientConfig{ User: "testuser", Auth: []gossh.AuthMethod{ gossh.Password("testpass"), }, } } if config.HostKeyCallback == nil { config.HostKeyCallback = gossh.InsecureIgnoreHostKey() } client, err := gossh.Dial("tcp", addr, config) if err != nil { tb.Fatal(err) } session, err := client.NewSession() if err != nil { tb.Fatal(err) } return session, client, func() { session.Close() client.Close() } } wish-0.1.1/wish.go000066400000000000000000000017351415347222400137760ustar00rootroot00000000000000package wish import ( "github.com/charmbracelet/keygen" "github.com/gliderlabs/ssh" ) // Middleware is a function that takes an ssh.Handler and returns an // ssh.Handler. Implementations should call the provided handler argument. type Middleware func(ssh.Handler) ssh.Handler // NewServer is returns a default SSH server with the provided Middleware. A // new SSH key pair of type ed25519 will be created if one does not exist. By // default this server will accept all incoming connections, password and // public key. func NewServer(ops ...ssh.Option) (*ssh.Server, error) { s := &ssh.Server{} // Some sensible defaults s.Version = "OpenSSH_7.6p1" for _, op := range ops { if err := s.SetOption(op); err != nil { return nil, err } } if len(s.HostSigners) == 0 { k, err := keygen.New("", "", nil, keygen.Ed25519) if err != nil { return nil, err } err = s.SetOption(WithHostKeyPEM(k.PrivateKeyPEM)) if err != nil { return nil, err } } return s, nil }