You've already forked step-ca-cli
mirror of
https://github.com/smallstep/cli.git
synced 2026-10-24 11:19:19 +03:00
331 lines
8.0 KiB
Go
331 lines
8.0 KiB
Go
package sshutil
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/knownhosts"
|
|
"golang.org/x/term"
|
|
|
|
"github.com/smallstep/cli-utils/step"
|
|
)
|
|
|
|
// ProxyCommand replaces %%, %h, %p, and %r in the given command.
|
|
//
|
|
// %% A literal `%`.
|
|
// %h The remote hostname.
|
|
// %p The remote port.
|
|
// %r The remote username.
|
|
func ProxyCommand(cmd, user, host, port string) string {
|
|
cmd = strings.ReplaceAll(cmd, "%%", "%")
|
|
cmd = strings.ReplaceAll(cmd, "%h", host)
|
|
cmd = strings.ReplaceAll(cmd, "%p", port)
|
|
return strings.ReplaceAll(cmd, "%r", user)
|
|
}
|
|
|
|
// ShellOption is the type used to add new options to the shell.
|
|
type ShellOption func(s *Shell) error
|
|
|
|
// WithAuthMethod adds a new ssh.AuthMethod to the shell.
|
|
func WithAuthMethod(am ssh.AuthMethod) ShellOption {
|
|
return func(s *Shell) error {
|
|
s.authMethods = append(s.authMethods, am)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithSigner adds the given signer as an ssh.AuthMethod.
|
|
func WithSigner(signer ssh.Signer) ShellOption {
|
|
return func(s *Shell) error {
|
|
s.authMethods = append(s.authMethods, ssh.PublicKeys(signer))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithCertificate adds a signer with the given certificate as an
|
|
// ssh.AuthMethod.
|
|
func WithCertificate(cert *ssh.Certificate, priv interface{}) ShellOption {
|
|
return func(s *Shell) error {
|
|
signer, err := NewCertSigner(cert, priv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.authMethods = append(s.authMethods, ssh.PublicKeys(signer))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// withDefaultAuthMethod adds the ssh.Agent as an ssh.AuthMethod.
|
|
func withDefaultAuthMethod() ShellOption {
|
|
return func(s *Shell) error {
|
|
agent, err := DialAgent()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.authMethods = append(s.authMethods, agent.AuthMethod())
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// withDefaultDialer adds a direct connection dialer.
|
|
func withDefaultDialer(user, address string) ShellOption {
|
|
return func(s *Shell) error {
|
|
s.dialer = func(callback ssh.HostKeyCallback) (*ssh.Client, error) {
|
|
client, err := ssh.Dial("tcp", address, &ssh.ClientConfig{
|
|
User: user,
|
|
Auth: s.authMethods,
|
|
HostKeyCallback: callback,
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error connecting %s", address)
|
|
}
|
|
return client, nil
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func formatAddress(address string) string {
|
|
if _, _, err := net.SplitHostPort(address); err != nil {
|
|
address += ":22"
|
|
}
|
|
return address
|
|
}
|
|
|
|
// Shell implements a remote shell to an SSH server using x/crypto/ssh
|
|
type Shell struct {
|
|
user string
|
|
address string
|
|
authMethods []ssh.AuthMethod
|
|
client *ssh.Client
|
|
dialer func(callback ssh.HostKeyCallback) (*ssh.Client, error)
|
|
}
|
|
|
|
// NewShell initializes a new shell to the given address.
|
|
func NewShell(user, address string, opts ...ShellOption) (*Shell, error) {
|
|
address = formatAddress(address)
|
|
|
|
// Use known_host as HostKeyCallback
|
|
knownHosts, err := knownhosts.New(filepath.Join(step.Home(), ".ssh", "known_hosts"))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error reading known_hosts")
|
|
}
|
|
|
|
shell := &Shell{
|
|
user: user,
|
|
address: address,
|
|
}
|
|
if err := shell.apply(opts); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(shell.authMethods) == 0 {
|
|
if err := withDefaultAuthMethod()(shell); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if shell.dialer == nil {
|
|
if err := withDefaultDialer(user, address)(shell); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if shell.client, err = shell.dialer(knownHosts); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return shell, nil
|
|
}
|
|
|
|
func (s *Shell) apply(opts []ShellOption) error {
|
|
for _, fn := range opts {
|
|
if err := fn(s); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close finalizes the connection.
|
|
func (s *Shell) Close() error {
|
|
return s.client.Close()
|
|
}
|
|
|
|
// Run runs cmd on the remote host.
|
|
func (s *Shell) Run(cmd string) error {
|
|
// Create a session
|
|
session, err := s.client.NewSession()
|
|
if err != nil {
|
|
return errors.Wrap(err, "error creating a session")
|
|
}
|
|
defer session.Close()
|
|
|
|
return session.Run(cmd)
|
|
}
|
|
|
|
// RemoteShell starts a login shell on the remote host.
|
|
func (s *Shell) RemoteShell() error {
|
|
// Create a session
|
|
session, err := s.client.NewSession()
|
|
if err != nil {
|
|
return errors.Wrap(err, "error creating a session")
|
|
}
|
|
defer session.Close()
|
|
|
|
var fallback bool
|
|
if fd := int(os.Stdin.Fd()); term.IsTerminal(fd) { // #nosec G115 -- uintptr comes from file descriptor
|
|
// Put terminal in raw mode
|
|
if originalState, err := term.MakeRaw(fd); err != nil {
|
|
fallback = true
|
|
} else {
|
|
defer term.Restore(fd, originalState)
|
|
|
|
// Get terminal size
|
|
w, h, err := term.GetSize(fd)
|
|
if err != nil {
|
|
w, h = 80, 40
|
|
}
|
|
|
|
// Request pseudo terminal
|
|
if err := requestPty(session, h, w, ssh.TerminalModes{
|
|
ssh.ECHO: 1, // enable echoing
|
|
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
|
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
fallback = true
|
|
}
|
|
|
|
if fallback {
|
|
if err := requestPty(session, 40, 80, ssh.TerminalModes{
|
|
ssh.ECHO: 0, // disable echoing
|
|
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
|
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
|
}); err != nil {
|
|
return errors.Wrap(err, "error getting pseudo terminal")
|
|
}
|
|
}
|
|
|
|
stdin, err := session.StdinPipe()
|
|
if err != nil {
|
|
return errors.Wrap(err, "cannot setup stdin for session")
|
|
}
|
|
go io.Copy(stdin, os.Stdin)
|
|
|
|
stdout, err := session.StdoutPipe()
|
|
if err != nil {
|
|
return errors.Wrap(err, "cannot setup stdout for session")
|
|
}
|
|
go io.Copy(os.Stdout, stdout)
|
|
|
|
stderr, err := session.StderrPipe()
|
|
if err != nil {
|
|
return errors.Wrap(err, "cannot setup stderr for session")
|
|
}
|
|
go io.Copy(os.Stderr, stderr)
|
|
|
|
// Start remote shell
|
|
if err := session.Shell(); err != nil {
|
|
return errors.Wrap(err, "error starting remote shell")
|
|
}
|
|
|
|
return session.Wait()
|
|
}
|
|
|
|
// LocalForward creates a local listener in the bindAddress forwarding the
|
|
// packages to the remote hostAddress.
|
|
func (s *Shell) LocalForward(bindNetwork, bindAddress, hostNetwork, hostAddress string) error {
|
|
l, err := net.Listen(bindNetwork, bindAddress)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error listening on %s", bindAddress)
|
|
}
|
|
defer l.Close()
|
|
|
|
remote, err := s.client.Dial(hostNetwork, hostAddress)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error dialing %s", hostAddress)
|
|
}
|
|
defer remote.Close()
|
|
|
|
for {
|
|
local, err := l.Accept()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error connecting to %s", bindAddress)
|
|
}
|
|
handleConn(local, remote)
|
|
}
|
|
}
|
|
|
|
// RemoteForward creates a remote listener in the bindAddress and forwards the
|
|
// packages to the local hostAddress.
|
|
func (s *Shell) RemoteForward(bindNetwork, bindAddress, hostNetwork, hostAddress string) error {
|
|
l, err := s.client.Listen(bindNetwork, bindAddress)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error listening on %s", bindAddress)
|
|
}
|
|
defer l.Close()
|
|
|
|
for {
|
|
local, err := net.Dial(hostNetwork, hostAddress)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error dialing %s", hostAddress)
|
|
}
|
|
remote, err := l.Accept()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error connection to %s", bindAddress)
|
|
}
|
|
handleConn(remote, local)
|
|
}
|
|
}
|
|
|
|
func requestPty(session *ssh.Session, h, w int, modes ssh.TerminalModes) (err error) {
|
|
var terms []string
|
|
switch t := os.Getenv("TERM"); t {
|
|
case "", "xterm-256color":
|
|
terms = append(terms, "xterm-256color", "xterm")
|
|
case "xterm":
|
|
terms = append(terms, "xterm")
|
|
default:
|
|
terms = append(terms, t, "xterm-256color", "xterm")
|
|
}
|
|
for _, t := range terms {
|
|
if err = session.RequestPty(t, h, w, modes); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
return errors.Wrap(err, "error getting pseudo terminal")
|
|
}
|
|
|
|
func handleConn(local, remote net.Conn) {
|
|
chDone := make(chan bool)
|
|
|
|
// Start remote -> local data transfer
|
|
go func() {
|
|
_, err := io.Copy(local, remote)
|
|
if err != nil {
|
|
log.Println("error while copy remote->local:", err)
|
|
}
|
|
chDone <- true
|
|
}()
|
|
|
|
// Start local -> remote data transfer
|
|
go func() {
|
|
_, err := io.Copy(remote, local)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
chDone <- true
|
|
}()
|
|
|
|
<-chDone
|
|
}
|