pax_global_header 0000666 0000000 0000000 00000000064 14153472224 0014516 g ustar 00root root 0000000 0000000 52 comment=9a28fca26b79d66b212fcbcacb5014e492f56745
wish-0.1.1/ 0000775 0000000 0000000 00000000000 14153472224 0012467 5 ustar 00root root 0000000 0000000 wish-0.1.1/.github/ 0000775 0000000 0000000 00000000000 14153472224 0014027 5 ustar 00root root 0000000 0000000 wish-0.1.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14153472224 0016064 5 ustar 00root root 0000000 0000000 wish-0.1.1/.github/workflows/build.yml 0000664 0000000 0000000 00000000633 14153472224 0017710 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000714 14153472224 0017557 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001463 14153472224 0020710 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000231 14153472224 0014453 0 ustar 00root root 0000000 0000000 examples/*
!examples/bubbletea
examples/bubbletea/bubbletea
examples/bubbletea/.ssh
!examples/git
examples/git/git
examples/git/.ssh
examples/git/.repos
wish-0.1.1/.golangci.yml 0000664 0000000 0000000 00000000712 14153472224 0015053 0 ustar 00root root 0000000 0000000 run:
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/LICENSE 0000664 0000000 0000000 00000002070 14153472224 0013473 0 ustar 00root root 0000000 0000000 MIT 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.md 0000775 0000000 0000000 00000007035 14153472224 0013756 0 ustar 00root root 0000000 0000000 # Wish
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).
Charm热爱开源 • Charm loves open source
wish-0.1.1/accesscontrol/ 0000775 0000000 0000000 00000000000 14153472224 0015331 5 ustar 00root root 0000000 0000000 wish-0.1.1/accesscontrol/accesscontrol.go 0000664 0000000 0000000 00000001224 14153472224 0020521 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000003775 14153472224 0021575 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14153472224 0014632 5 ustar 00root root 0000000 0000000 wish-0.1.1/activeterm/activeterm.go 0000664 0000000 0000000 00000000662 14153472224 0017330 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000001152 14153472224 0020362 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14153472224 0014414 5 ustar 00root root 0000000 0000000 wish-0.1.1/bubbletea/tea.go 0000664 0000000 0000000 00000003717 14153472224 0015524 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14153472224 0014305 5 ustar 00root root 0000000 0000000 wish-0.1.1/examples/bubbletea/ 0000775 0000000 0000000 00000000000 14153472224 0016232 5 ustar 00root root 0000000 0000000 wish-0.1.1/examples/bubbletea/main.go 0000664 0000000 0000000 00000004364 14153472224 0017514 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14153472224 0015070 5 ustar 00root root 0000000 0000000 wish-0.1.1/examples/git/main.go 0000664 0000000 0000000 00000005133 14153472224 0016345 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14153472224 0013252 5 ustar 00root root 0000000 0000000 wish-0.1.1/git/git.go 0000664 0000000 0000000 00000011320 14153472224 0014361 0 ustar 00root root 0000000 0000000 package 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.mod 0000664 0000000 0000000 00000003470 14153472224 0013601 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000032211 14153472224 0013621 0 ustar 00root root 0000000 0000000 github.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/ 0000775 0000000 0000000 00000000000 14153472224 0014115 5 ustar 00root root 0000000 0000000 wish-0.1.1/logging/logging.go 0000664 0000000 0000000 00000001425 14153472224 0016074 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001102 14153472224 0017123 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003757 14153472224 0014525 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14153472224 0015052 5 ustar 00root root 0000000 0000000 wish-0.1.1/testsession/main.go 0000664 0000000 0000000 00000002323 14153472224 0016325 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000001735 14153472224 0013776 0 ustar 00root root 0000000 0000000 package 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
}