wishlist

package module
v0.14.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 17, 2023 License: MIT Imports: 34 Imported by: 2

README

Wishlist

Latest Release GoDoc Build Status

The SSH directory ✨

Gif

With Wishlist you can have a single entry point for multiple SSH endpoints, whether they are Wish apps or not.

As a server, it can be used to start multiple SSH apps within a single package and list them over SSH. You can list apps provided elsewhere, too.

You can also use the wishlist command to list and connect to servers in your ~/.ssh/config or in a YAML configuration file.

Installation

Use your fave package manager:

# macOS or Linux
brew install charmbracelet/tap/wishlist

# Arch Linux (btw)
yay -S wishlist-bin
# or
yay -S wishlist

# Windows (with winget)
winget install wishlist

# Windows (with Scoop)
scoop bucket add https://github.com/charmbracelet/scoop-bucket.git
scoop install wishlist

# Nix
nix-env -iA nixpkgs.wishlist

# Debian/Ubuntu
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install wishlist

# Fedora/RHEL
echo '[charm]
name=Charm
baseurl=https://repo.charm.sh/yum/
enabled=1
gpgcheck=1
gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo
sudo yum install wishlist

Or download a pre-compiled binary or package from the releases page.

Or just build it yourself (requires Go 1.19+):

git clone https://github.com/charmbracelet/wishlist.git
cd wishlist
go build ./cmd/wishlist/

Usage

CLI
Remote

If you just want a directory of existing servers, you can use the wishlist CLI and a YAML config file. You can also just run it without any arguments to list the servers in your ~/.ssh/config. To start wishlist in server mode, you'll need to use the serve subcommand:

wishlist serve

Check the example config file file as well as wishlist server --help for details.

Local

If you want to explore your ~/.ssh/config, you can run wishlist in local mode with:

wishlist

Note that not all options are supported at this moment. Check the commented example config for reference.

Library

Wishlist is also available as a library, which allows you to start several apps within the same process. Check out the _example folder for a working example.

Auth

Local mode

When running in local mode, wishlist will first see if the current endpoint has an IdentityFile specified. If so, it'll try to use that. If not, it'll see if there's a SSH Agent available, and use it. Otherwise, it'll try the common key names in ~/.ssh.

Server mode

When running as a server, wishlist will first try to forward the current SSH Agent. If there's no agent, it'll create or use an existing ed25519 key present in .wishlist/client_ed25519. Password authentication is not supported at this moment.

Agent forwarding example
eval (ssh-agent)
ssh-add -k # adds all your pubkeys
ssh-add -l # should list the added keys

ssh \
  -o 'ForwardAgent=yes' \             # forwards the agent
  -o 'UserKnownHostsFile=/dev/null' \ # do not add to ~/.ssh/known_hosts, optional
  -p 2222 \                           # port
  foo.bar \                           # host
  -t list                             # optional, app name

You can also add this to your ~/.ssh/config, for instance:

Host wishlist
	HostName foo.bar
	Port 2222
	ForwardAgent yes
	UserKnownHostsFile /dev/null

Discovery

Wishlist can discover endpoints using Zeroconf, SRV Records, and Tailscale.

You can find a brief explanation and examples of all of them bellow.

Run wishlist --help to see all the options.

Tailscale

You can configure Wishlist to find all nodes in your tailnet and add them as endpoints:

wishlist --tailscale.net=your_tailnet_name --tailscale.key=tskey-api-abc123...

You can use the Hints to change the connection settings.

OAuth authentication

Tailscale API keys expire after 90 days. If you want something that doesn't require you to intervene every couple of months, use OAuth Clients:

Create a client here. The only scope needed is devices:read.

Instead of using --tailscale.key (or $TAILSCALE_KEY), set --tailscale.client.id and --tailscale.client.secret (or $TAILSCALE_CLIENT_ID and $TAILSCALE_CLIENT_SECRET, respectively).

Zeroconf/Avahi/mDNS/Bonjour

You can enable this using the --zeroconf.enabled flag:

wishlist --zeroconf.enabled

Optionally, you can also specify a timeout with --zeroconf.timeout and, which domain to look for with --zeroconf.domain.

Wishlist will look for _ssh._tcp services in the given domain.

You can use the Hints to change the connection settings.

SRV records

You can set Wishlist up to find nodes from DNS SRV records:

wishlist --srv.domain example.com

By default, Wishlist will set the name of the endpoint to the SRV target. You can, however, customize that with a TXT record in the following format:

wishlist.name full.address:22=thename

So, in this case, a SRV record pointing to full.address on port 22 will get the name thename.

Hints

You can use the hints key in the YAML configuration file to hint settings into discovered endpoints.

Check the example configuration file to learn what options are available.

If you're using a SSH configuration file as the Wishlist configuration file, it'll try to match the hosts with the rules in the given configuration. Otherwise, the services will simply be added to the list.

The difference is that if a hints themselves won't show in the TUI, as of hosts in the SSH configuration will.

Running it

Wishlist will read and store all its information in a .wishlist folder in the current working directory:

  • the server keys
  • the client keys
  • known hosts
  • config files

Config files may be provided in either YAML or SSH Config formats:

The config files are tried in the following order:

  • the -config flag in either YAML or SSH config formats
  • .wishlist/config.yaml
  • .wishlist/config.yml
  • .wishlist/config
  • [[user config dir]]/wishlist/config.yaml[^1]
  • [[user config dir]]/wishlist/config.yml[^1]
  • [[user config dir]]/wishlist/config[^1]
  • $HOME/.ssh/config
  • /etc/ssh/ssh_config

[^1]: i.e. [[user config dir]]: On Unix systems, it will be $XDG_CONFIG_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else $HOME/.config. On Darwin, it will be $HOME/Library/Application Support. On Windows, it will be %AppData%. On Plan 9, it will be $home/lib.

The first one that is loaded and parsed without errors will be used. This means that if you have your common used hosts in your ~/.ssh/config, you can simply run wishlist and get it running right away. It also means that if you don't want that, you can pass a path to -config, and it can be either a YAML, or a SSH config file.

Using the binary
wishlist
Using Docker
mkdir .wishlist
$EDITOR .wishlist/config.yaml # either an YAML or a SSH config
docker run \
  -p 2222:22 \
  -v $PWD/.wishlist:/.wishlist \
  docker.io/charmcli/wishlist:latest
Supported SSH Options

Not all SSH options are currently supported. Here's a list of the ones that are:

  • User
  • Hostname
  • Port
  • IdentityFiles
  • ForwardAgent
  • RequestTTY
  • RemoteCommand
  • SendEnv
  • SetEnv
  • ConnectTimeout
  • Include
  • PreferredAuthentications
  • ProxyJump

Acknowledgments

The gif above shows a lot of Maas Lalani’s confeTTY.

Feedback

We’d love to hear your thoughts on this project. Feel free to drop us a note!

License

MIT


Part of Charm.

The Charm logo

Charm热爱开源 • Charm loves open source

Documentation

Overview

Package wishlist provides a library and binary to list and connect to SSH apps.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FirstNonEmpty added in v0.9.0

func FirstNonEmpty(ss ...string) string

FirstNonEmpty returns the first non-empty string of the list.

func Serve

func Serve(config *Config) error

Serve serves wishlist with the given config.

Types

type Config

type Config struct {
	Listen       string                              `yaml:"listen"`    // Address to listen on.
	Port         int64                               `yaml:"port"`      // Port to start the first server on.
	Endpoints    []*Endpoint                         `yaml:"endpoints"` // Endpoints to list.
	Hints        []EndpointHint                      `yaml:"hints"`     // Endpoints hints to apply to discovered hosts.
	Factory      func(Endpoint) (*ssh.Server, error) `yaml:"-"`         // Factory used to create the SSH server for the given endpoint.
	Users        []User                              `yaml:"users"`     // Users allowed to access the list.
	Metrics      Metrics                             `yaml:"metrics"`   // Metrics configuration.
	EndpointChan chan []*Endpoint                    `yaml:"-"`         // Channel to update the endpoints. Used only in server mode.
	// contains filtered or unexported fields
}

Config represents the wishlist configuration.

type Endpoint

type Endpoint struct {
	Name                     string            `yaml:"name"`                      // Endpoint name.
	Address                  string            `yaml:"address"`                   // Endpoint address in the `host:port` format, if empty, will be the same address as the list, increasing the port number.
	User                     string            `yaml:"user"`                      // User to authenticate as.
	ForwardAgent             bool              `yaml:"forward_agent"`             // ForwardAgent defines whether to forward the current agent. Anologous to SSH's config ForwardAgent.
	RequestTTY               bool              `yaml:"request_tty"`               // RequestTTY defines whether to request a TTY. Anologous to SSH's config RequestTTY.
	RemoteCommand            string            `yaml:"remote_command"`            // RemoteCommand defines whether to request a TTY. Anologous to SSH's config RemoteCommand.
	Desc                     string            `yaml:"description"`               // Description describes an optional description of the item.
	Link                     Link              `yaml:"link"`                      // Links can be used to add a link to the item description using OSC8.
	ProxyJump                string            `yaml:"proxy_jump"`                // Analogous to SSH's ProxyJump
	SendEnv                  []string          `yaml:"send_env"`                  // Analogous to SSH's SendEnv
	SetEnv                   []string          `yaml:"set_env"`                   // Analogous to SSH's SetEnv
	PreferredAuthentications []string          `yaml:"preferred_authentications"` // Analogous to SSH's PreferredAuthentications
	IdentityFiles            []string          `yaml:"identity_files"`            // IdentityFiles is only used when in local mode.
	Timeout                  time.Duration     `yaml:"connect_timeout"`           // Connection timeout.
	Middlewares              []wish.Middleware `yaml:"-"`                         // wish middlewares you can use in the factory method.
}

Endpoint represents an endpoint to list. If it has a Handler, wishlist will start an SSH server on the given address.

func (Endpoint) Authentications added in v0.12.0

func (e Endpoint) Authentications() []string

Authentications returns either the client preferred authentications or the default publickey,keyboard-interactive.

func (Endpoint) Environment added in v0.6.0

func (e Endpoint) Environment(hostenv ...string) map[string]string

Environment evaluates SendEnv and SetEnv into the env map that should be set into the session. Optionally you can pass a list existing environment variables (e.g. os.Environ()), and the ones allowed by SendEnv will be set as well. As on OpenSSH, envs set via SetEnv take precedence over the ones from hostenv.

func (Endpoint) ShouldListen

func (e Endpoint) ShouldListen() bool

ShouldListen returns true if we should start a server for this endpoint.

func (*Endpoint) String

func (e *Endpoint) String() string

String returns the endpoint in a friendly string format.

func (Endpoint) Valid

func (e Endpoint) Valid() bool

Valid returns true if the endpoint is valid.

type EndpointHint added in v0.12.0

type EndpointHint struct {
	Match                    string        `yaml:"match"`
	Port                     string        `yaml:"port"`
	User                     string        `yaml:"user"`
	ForwardAgent             *bool         `yaml:"forward_agent"`
	RequestTTY               *bool         `yaml:"request_tty"`
	RemoteCommand            string        `yaml:"remote_command"`
	Desc                     string        `yaml:"description"`
	Link                     Link          `yaml:"link"`
	ProxyJump                string        `yaml:"proxy_jump"`
	SendEnv                  []string      `yaml:"send_env"`
	SetEnv                   []string      `yaml:"set_env"`
	PreferredAuthentications []string      `yaml:"preferred_authentications"`
	IdentityFiles            []string      `yaml:"identity_files"`
	Timeout                  time.Duration `yaml:"connect_timeout"`
}

EndpointHint can be used to match a discovered endpoint (through zeroconf for example) and set additional options into it.

type ItemWrapper added in v0.6.0

type ItemWrapper struct {
	// contains filtered or unexported fields
}

ItemWrapper wrappes an Endpoint and a set of descriptors and acts as a list.Item.

func (ItemWrapper) Description added in v0.6.0

func (i ItemWrapper) Description() string

Description to abide the list.Item interface.

func (ItemWrapper) FilterValue added in v0.6.0

func (i ItemWrapper) FilterValue() string

FilterValue to abide the list.Item interface.

func (ItemWrapper) Title added in v0.6.0

func (i ItemWrapper) Title() string

Title to abide the list.Item interface.

type Link struct {
	Name string `yaml:"name"`
	URL  string `yaml:"url"`
}

Link defines an item link.

func (Link) String added in v0.6.0

func (l Link) String() string

type ListModel added in v0.4.0

type ListModel struct {
	// contains filtered or unexported fields
}

ListModel main wishlist model.

func NewListing added in v0.4.0

func NewListing(endpoints []*Endpoint, client SSHClient) *ListModel

NewListing creates a new listing model for the given endpoints and SSH session. If session is nil, it is assume to be a local listing.

func (*ListModel) Init added in v0.4.0

func (m *ListModel) Init() tea.Cmd

Init comply with tea.Model interface.

func (*ListModel) SetItems added in v0.4.0

func (m *ListModel) SetItems(endpoints []*Endpoint) tea.Cmd

SetItems allows to update the listing items.

func (*ListModel) Update added in v0.4.0

func (m *ListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update comply with tea.Model interface.

func (*ListModel) View added in v0.4.0

func (m *ListModel) View() string

View comply with tea.Model interface.

type Metrics added in v0.9.0

type Metrics struct {
	Enabled bool   `yaml:"enabled"`
	Name    string `yaml:"name"`
	Address string `yaml:"address"`
}

Metrics configuration.

type SSHClient added in v0.2.0

type SSHClient interface {
	For(e *Endpoint) tea.ExecCommand
}

SSHClient is a SSH client.

func NewLocalSSHClient added in v0.2.0

func NewLocalSSHClient() SSHClient

NewLocalSSHClient returns a SSH Client for local usage.

type SetEndpointsMsg added in v0.5.0

type SetEndpointsMsg struct {
	Endpoints []*Endpoint
}

SetEndpointsMsg can be used to update the listed wishlist endpoints.

type User

type User struct {
	Name       string   `yaml:"name"`
	PublicKeys []string `yaml:"public-keys"`
}

User contains user-level configuration for a repository.

Directories

Path Synopsis
cmd
Package sshconfig can parse a SSH config file into a list of endpoints.
Package sshconfig can parse a SSH config file into a list of endpoints.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL