mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-23 08:47:42 -05:00
628 lines
15 KiB
Go
628 lines
15 KiB
Go
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style
|
||
|
// license that can be found in the LICENSE file.
|
||
|
|
||
|
package ssh
|
||
|
|
||
|
// Session implements an interactive session described in
|
||
|
// "RFC 4254, section 6".
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/binary"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"sync"
|
||
|
)
|
||
|
|
||
|
type Signal string
|
||
|
|
||
|
// POSIX signals as listed in RFC 4254 Section 6.10.
|
||
|
const (
|
||
|
SIGABRT Signal = "ABRT"
|
||
|
SIGALRM Signal = "ALRM"
|
||
|
SIGFPE Signal = "FPE"
|
||
|
SIGHUP Signal = "HUP"
|
||
|
SIGILL Signal = "ILL"
|
||
|
SIGINT Signal = "INT"
|
||
|
SIGKILL Signal = "KILL"
|
||
|
SIGPIPE Signal = "PIPE"
|
||
|
SIGQUIT Signal = "QUIT"
|
||
|
SIGSEGV Signal = "SEGV"
|
||
|
SIGTERM Signal = "TERM"
|
||
|
SIGUSR1 Signal = "USR1"
|
||
|
SIGUSR2 Signal = "USR2"
|
||
|
)
|
||
|
|
||
|
var signals = map[Signal]int{
|
||
|
SIGABRT: 6,
|
||
|
SIGALRM: 14,
|
||
|
SIGFPE: 8,
|
||
|
SIGHUP: 1,
|
||
|
SIGILL: 4,
|
||
|
SIGINT: 2,
|
||
|
SIGKILL: 9,
|
||
|
SIGPIPE: 13,
|
||
|
SIGQUIT: 3,
|
||
|
SIGSEGV: 11,
|
||
|
SIGTERM: 15,
|
||
|
}
|
||
|
|
||
|
type TerminalModes map[uint8]uint32
|
||
|
|
||
|
// POSIX terminal mode flags as listed in RFC 4254 Section 8.
|
||
|
const (
|
||
|
tty_OP_END = 0
|
||
|
VINTR = 1
|
||
|
VQUIT = 2
|
||
|
VERASE = 3
|
||
|
VKILL = 4
|
||
|
VEOF = 5
|
||
|
VEOL = 6
|
||
|
VEOL2 = 7
|
||
|
VSTART = 8
|
||
|
VSTOP = 9
|
||
|
VSUSP = 10
|
||
|
VDSUSP = 11
|
||
|
VREPRINT = 12
|
||
|
VWERASE = 13
|
||
|
VLNEXT = 14
|
||
|
VFLUSH = 15
|
||
|
VSWTCH = 16
|
||
|
VSTATUS = 17
|
||
|
VDISCARD = 18
|
||
|
IGNPAR = 30
|
||
|
PARMRK = 31
|
||
|
INPCK = 32
|
||
|
ISTRIP = 33
|
||
|
INLCR = 34
|
||
|
IGNCR = 35
|
||
|
ICRNL = 36
|
||
|
IUCLC = 37
|
||
|
IXON = 38
|
||
|
IXANY = 39
|
||
|
IXOFF = 40
|
||
|
IMAXBEL = 41
|
||
|
ISIG = 50
|
||
|
ICANON = 51
|
||
|
XCASE = 52
|
||
|
ECHO = 53
|
||
|
ECHOE = 54
|
||
|
ECHOK = 55
|
||
|
ECHONL = 56
|
||
|
NOFLSH = 57
|
||
|
TOSTOP = 58
|
||
|
IEXTEN = 59
|
||
|
ECHOCTL = 60
|
||
|
ECHOKE = 61
|
||
|
PENDIN = 62
|
||
|
OPOST = 70
|
||
|
OLCUC = 71
|
||
|
ONLCR = 72
|
||
|
OCRNL = 73
|
||
|
ONOCR = 74
|
||
|
ONLRET = 75
|
||
|
CS7 = 90
|
||
|
CS8 = 91
|
||
|
PARENB = 92
|
||
|
PARODD = 93
|
||
|
TTY_OP_ISPEED = 128
|
||
|
TTY_OP_OSPEED = 129
|
||
|
)
|
||
|
|
||
|
// A Session represents a connection to a remote command or shell.
|
||
|
type Session struct {
|
||
|
// Stdin specifies the remote process's standard input.
|
||
|
// If Stdin is nil, the remote process reads from an empty
|
||
|
// bytes.Buffer.
|
||
|
Stdin io.Reader
|
||
|
|
||
|
// Stdout and Stderr specify the remote process's standard
|
||
|
// output and error.
|
||
|
//
|
||
|
// If either is nil, Run connects the corresponding file
|
||
|
// descriptor to an instance of ioutil.Discard. There is a
|
||
|
// fixed amount of buffering that is shared for the two streams.
|
||
|
// If either blocks it may eventually cause the remote
|
||
|
// command to block.
|
||
|
Stdout io.Writer
|
||
|
Stderr io.Writer
|
||
|
|
||
|
ch Channel // the channel backing this session
|
||
|
started bool // true once Start, Run or Shell is invoked.
|
||
|
copyFuncs []func() error
|
||
|
errors chan error // one send per copyFunc
|
||
|
|
||
|
// true if pipe method is active
|
||
|
stdinpipe, stdoutpipe, stderrpipe bool
|
||
|
|
||
|
// stdinPipeWriter is non-nil if StdinPipe has not been called
|
||
|
// and Stdin was specified by the user; it is the write end of
|
||
|
// a pipe connecting Session.Stdin to the stdin channel.
|
||
|
stdinPipeWriter io.WriteCloser
|
||
|
|
||
|
exitStatus chan error
|
||
|
}
|
||
|
|
||
|
// SendRequest sends an out-of-band channel request on the SSH channel
|
||
|
// underlying the session.
|
||
|
func (s *Session) SendRequest(name string, wantReply bool, payload []byte) (bool, error) {
|
||
|
return s.ch.SendRequest(name, wantReply, payload)
|
||
|
}
|
||
|
|
||
|
func (s *Session) Close() error {
|
||
|
return s.ch.Close()
|
||
|
}
|
||
|
|
||
|
// RFC 4254 Section 6.4.
|
||
|
type setenvRequest struct {
|
||
|
Name string
|
||
|
Value string
|
||
|
}
|
||
|
|
||
|
// Setenv sets an environment variable that will be applied to any
|
||
|
// command executed by Shell or Run.
|
||
|
func (s *Session) Setenv(name, value string) error {
|
||
|
msg := setenvRequest{
|
||
|
Name: name,
|
||
|
Value: value,
|
||
|
}
|
||
|
ok, err := s.ch.SendRequest("env", true, Marshal(&msg))
|
||
|
if err == nil && !ok {
|
||
|
err = errors.New("ssh: setenv failed")
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// RFC 4254 Section 6.2.
|
||
|
type ptyRequestMsg struct {
|
||
|
Term string
|
||
|
Columns uint32
|
||
|
Rows uint32
|
||
|
Width uint32
|
||
|
Height uint32
|
||
|
Modelist string
|
||
|
}
|
||
|
|
||
|
// RequestPty requests the association of a pty with the session on the remote host.
|
||
|
func (s *Session) RequestPty(term string, h, w int, termmodes TerminalModes) error {
|
||
|
var tm []byte
|
||
|
for k, v := range termmodes {
|
||
|
kv := struct {
|
||
|
Key byte
|
||
|
Val uint32
|
||
|
}{k, v}
|
||
|
|
||
|
tm = append(tm, Marshal(&kv)...)
|
||
|
}
|
||
|
tm = append(tm, tty_OP_END)
|
||
|
req := ptyRequestMsg{
|
||
|
Term: term,
|
||
|
Columns: uint32(w),
|
||
|
Rows: uint32(h),
|
||
|
Width: uint32(w * 8),
|
||
|
Height: uint32(h * 8),
|
||
|
Modelist: string(tm),
|
||
|
}
|
||
|
ok, err := s.ch.SendRequest("pty-req", true, Marshal(&req))
|
||
|
if err == nil && !ok {
|
||
|
err = errors.New("ssh: pty-req failed")
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// RFC 4254 Section 6.5.
|
||
|
type subsystemRequestMsg struct {
|
||
|
Subsystem string
|
||
|
}
|
||
|
|
||
|
// RequestSubsystem requests the association of a subsystem with the session on the remote host.
|
||
|
// A subsystem is a predefined command that runs in the background when the ssh session is initiated
|
||
|
func (s *Session) RequestSubsystem(subsystem string) error {
|
||
|
msg := subsystemRequestMsg{
|
||
|
Subsystem: subsystem,
|
||
|
}
|
||
|
ok, err := s.ch.SendRequest("subsystem", true, Marshal(&msg))
|
||
|
if err == nil && !ok {
|
||
|
err = errors.New("ssh: subsystem request failed")
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// RFC 4254 Section 6.9.
|
||
|
type signalMsg struct {
|
||
|
Signal string
|
||
|
}
|
||
|
|
||
|
// Signal sends the given signal to the remote process.
|
||
|
// sig is one of the SIG* constants.
|
||
|
func (s *Session) Signal(sig Signal) error {
|
||
|
msg := signalMsg{
|
||
|
Signal: string(sig),
|
||
|
}
|
||
|
|
||
|
_, err := s.ch.SendRequest("signal", false, Marshal(&msg))
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// RFC 4254 Section 6.5.
|
||
|
type execMsg struct {
|
||
|
Command string
|
||
|
}
|
||
|
|
||
|
// Start runs cmd on the remote host. Typically, the remote
|
||
|
// server passes cmd to the shell for interpretation.
|
||
|
// A Session only accepts one call to Run, Start or Shell.
|
||
|
func (s *Session) Start(cmd string) error {
|
||
|
if s.started {
|
||
|
return errors.New("ssh: session already started")
|
||
|
}
|
||
|
req := execMsg{
|
||
|
Command: cmd,
|
||
|
}
|
||
|
|
||
|
ok, err := s.ch.SendRequest("exec", true, Marshal(&req))
|
||
|
if err == nil && !ok {
|
||
|
err = fmt.Errorf("ssh: command %v failed", cmd)
|
||
|
}
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return s.start()
|
||
|
}
|
||
|
|
||
|
// Run runs cmd on the remote host. Typically, the remote
|
||
|
// server passes cmd to the shell for interpretation.
|
||
|
// A Session only accepts one call to Run, Start, Shell, Output,
|
||
|
// or CombinedOutput.
|
||
|
//
|
||
|
// The returned error is nil if the command runs, has no problems
|
||
|
// copying stdin, stdout, and stderr, and exits with a zero exit
|
||
|
// status.
|
||
|
//
|
||
|
// If the remote server does not send an exit status, an error of type
|
||
|
// *ExitMissingError is returned. If the command completes
|
||
|
// unsuccessfully or is interrupted by a signal, the error is of type
|
||
|
// *ExitError. Other error types may be returned for I/O problems.
|
||
|
func (s *Session) Run(cmd string) error {
|
||
|
err := s.Start(cmd)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return s.Wait()
|
||
|
}
|
||
|
|
||
|
// Output runs cmd on the remote host and returns its standard output.
|
||
|
func (s *Session) Output(cmd string) ([]byte, error) {
|
||
|
if s.Stdout != nil {
|
||
|
return nil, errors.New("ssh: Stdout already set")
|
||
|
}
|
||
|
var b bytes.Buffer
|
||
|
s.Stdout = &b
|
||
|
err := s.Run(cmd)
|
||
|
return b.Bytes(), err
|
||
|
}
|
||
|
|
||
|
type singleWriter struct {
|
||
|
b bytes.Buffer
|
||
|
mu sync.Mutex
|
||
|
}
|
||
|
|
||
|
func (w *singleWriter) Write(p []byte) (int, error) {
|
||
|
w.mu.Lock()
|
||
|
defer w.mu.Unlock()
|
||
|
return w.b.Write(p)
|
||
|
}
|
||
|
|
||
|
// CombinedOutput runs cmd on the remote host and returns its combined
|
||
|
// standard output and standard error.
|
||
|
func (s *Session) CombinedOutput(cmd string) ([]byte, error) {
|
||
|
if s.Stdout != nil {
|
||
|
return nil, errors.New("ssh: Stdout already set")
|
||
|
}
|
||
|
if s.Stderr != nil {
|
||
|
return nil, errors.New("ssh: Stderr already set")
|
||
|
}
|
||
|
var b singleWriter
|
||
|
s.Stdout = &b
|
||
|
s.Stderr = &b
|
||
|
err := s.Run(cmd)
|
||
|
return b.b.Bytes(), err
|
||
|
}
|
||
|
|
||
|
// Shell starts a login shell on the remote host. A Session only
|
||
|
// accepts one call to Run, Start, Shell, Output, or CombinedOutput.
|
||
|
func (s *Session) Shell() error {
|
||
|
if s.started {
|
||
|
return errors.New("ssh: session already started")
|
||
|
}
|
||
|
|
||
|
ok, err := s.ch.SendRequest("shell", true, nil)
|
||
|
if err == nil && !ok {
|
||
|
return errors.New("ssh: could not start shell")
|
||
|
}
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return s.start()
|
||
|
}
|
||
|
|
||
|
func (s *Session) start() error {
|
||
|
s.started = true
|
||
|
|
||
|
type F func(*Session)
|
||
|
for _, setupFd := range []F{(*Session).stdin, (*Session).stdout, (*Session).stderr} {
|
||
|
setupFd(s)
|
||
|
}
|
||
|
|
||
|
s.errors = make(chan error, len(s.copyFuncs))
|
||
|
for _, fn := range s.copyFuncs {
|
||
|
go func(fn func() error) {
|
||
|
s.errors <- fn()
|
||
|
}(fn)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Wait waits for the remote command to exit.
|
||
|
//
|
||
|
// The returned error is nil if the command runs, has no problems
|
||
|
// copying stdin, stdout, and stderr, and exits with a zero exit
|
||
|
// status.
|
||
|
//
|
||
|
// If the remote server does not send an exit status, an error of type
|
||
|
// *ExitMissingError is returned. If the command completes
|
||
|
// unsuccessfully or is interrupted by a signal, the error is of type
|
||
|
// *ExitError. Other error types may be returned for I/O problems.
|
||
|
func (s *Session) Wait() error {
|
||
|
if !s.started {
|
||
|
return errors.New("ssh: session not started")
|
||
|
}
|
||
|
waitErr := <-s.exitStatus
|
||
|
|
||
|
if s.stdinPipeWriter != nil {
|
||
|
s.stdinPipeWriter.Close()
|
||
|
}
|
||
|
var copyError error
|
||
|
for _ = range s.copyFuncs {
|
||
|
if err := <-s.errors; err != nil && copyError == nil {
|
||
|
copyError = err
|
||
|
}
|
||
|
}
|
||
|
if waitErr != nil {
|
||
|
return waitErr
|
||
|
}
|
||
|
return copyError
|
||
|
}
|
||
|
|
||
|
func (s *Session) wait(reqs <-chan *Request) error {
|
||
|
wm := Waitmsg{status: -1}
|
||
|
// Wait for msg channel to be closed before returning.
|
||
|
for msg := range reqs {
|
||
|
switch msg.Type {
|
||
|
case "exit-status":
|
||
|
wm.status = int(binary.BigEndian.Uint32(msg.Payload))
|
||
|
case "exit-signal":
|
||
|
var sigval struct {
|
||
|
Signal string
|
||
|
CoreDumped bool
|
||
|
Error string
|
||
|
Lang string
|
||
|
}
|
||
|
if err := Unmarshal(msg.Payload, &sigval); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Must sanitize strings?
|
||
|
wm.signal = sigval.Signal
|
||
|
wm.msg = sigval.Error
|
||
|
wm.lang = sigval.Lang
|
||
|
default:
|
||
|
// This handles keepalives and matches
|
||
|
// OpenSSH's behaviour.
|
||
|
if msg.WantReply {
|
||
|
msg.Reply(false, nil)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if wm.status == 0 {
|
||
|
return nil
|
||
|
}
|
||
|
if wm.status == -1 {
|
||
|
// exit-status was never sent from server
|
||
|
if wm.signal == "" {
|
||
|
// signal was not sent either. RFC 4254
|
||
|
// section 6.10 recommends against this
|
||
|
// behavior, but it is allowed, so we let
|
||
|
// clients handle it.
|
||
|
return &ExitMissingError{}
|
||
|
}
|
||
|
wm.status = 128
|
||
|
if _, ok := signals[Signal(wm.signal)]; ok {
|
||
|
wm.status += signals[Signal(wm.signal)]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return &ExitError{wm}
|
||
|
}
|
||
|
|
||
|
// ExitMissingError is returned if a session is torn down cleanly, but
|
||
|
// the server sends no confirmation of the exit status.
|
||
|
type ExitMissingError struct{}
|
||
|
|
||
|
func (e *ExitMissingError) Error() string {
|
||
|
return "wait: remote command exited without exit status or exit signal"
|
||
|
}
|
||
|
|
||
|
func (s *Session) stdin() {
|
||
|
if s.stdinpipe {
|
||
|
return
|
||
|
}
|
||
|
var stdin io.Reader
|
||
|
if s.Stdin == nil {
|
||
|
stdin = new(bytes.Buffer)
|
||
|
} else {
|
||
|
r, w := io.Pipe()
|
||
|
go func() {
|
||
|
_, err := io.Copy(w, s.Stdin)
|
||
|
w.CloseWithError(err)
|
||
|
}()
|
||
|
stdin, s.stdinPipeWriter = r, w
|
||
|
}
|
||
|
s.copyFuncs = append(s.copyFuncs, func() error {
|
||
|
_, err := io.Copy(s.ch, stdin)
|
||
|
if err1 := s.ch.CloseWrite(); err == nil && err1 != io.EOF {
|
||
|
err = err1
|
||
|
}
|
||
|
return err
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (s *Session) stdout() {
|
||
|
if s.stdoutpipe {
|
||
|
return
|
||
|
}
|
||
|
if s.Stdout == nil {
|
||
|
s.Stdout = ioutil.Discard
|
||
|
}
|
||
|
s.copyFuncs = append(s.copyFuncs, func() error {
|
||
|
_, err := io.Copy(s.Stdout, s.ch)
|
||
|
return err
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (s *Session) stderr() {
|
||
|
if s.stderrpipe {
|
||
|
return
|
||
|
}
|
||
|
if s.Stderr == nil {
|
||
|
s.Stderr = ioutil.Discard
|
||
|
}
|
||
|
s.copyFuncs = append(s.copyFuncs, func() error {
|
||
|
_, err := io.Copy(s.Stderr, s.ch.Stderr())
|
||
|
return err
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// sessionStdin reroutes Close to CloseWrite.
|
||
|
type sessionStdin struct {
|
||
|
io.Writer
|
||
|
ch Channel
|
||
|
}
|
||
|
|
||
|
func (s *sessionStdin) Close() error {
|
||
|
return s.ch.CloseWrite()
|
||
|
}
|
||
|
|
||
|
// StdinPipe returns a pipe that will be connected to the
|
||
|
// remote command's standard input when the command starts.
|
||
|
func (s *Session) StdinPipe() (io.WriteCloser, error) {
|
||
|
if s.Stdin != nil {
|
||
|
return nil, errors.New("ssh: Stdin already set")
|
||
|
}
|
||
|
if s.started {
|
||
|
return nil, errors.New("ssh: StdinPipe after process started")
|
||
|
}
|
||
|
s.stdinpipe = true
|
||
|
return &sessionStdin{s.ch, s.ch}, nil
|
||
|
}
|
||
|
|
||
|
// StdoutPipe returns a pipe that will be connected to the
|
||
|
// remote command's standard output when the command starts.
|
||
|
// There is a fixed amount of buffering that is shared between
|
||
|
// stdout and stderr streams. If the StdoutPipe reader is
|
||
|
// not serviced fast enough it may eventually cause the
|
||
|
// remote command to block.
|
||
|
func (s *Session) StdoutPipe() (io.Reader, error) {
|
||
|
if s.Stdout != nil {
|
||
|
return nil, errors.New("ssh: Stdout already set")
|
||
|
}
|
||
|
if s.started {
|
||
|
return nil, errors.New("ssh: StdoutPipe after process started")
|
||
|
}
|
||
|
s.stdoutpipe = true
|
||
|
return s.ch, nil
|
||
|
}
|
||
|
|
||
|
// StderrPipe returns a pipe that will be connected to the
|
||
|
// remote command's standard error when the command starts.
|
||
|
// There is a fixed amount of buffering that is shared between
|
||
|
// stdout and stderr streams. If the StderrPipe reader is
|
||
|
// not serviced fast enough it may eventually cause the
|
||
|
// remote command to block.
|
||
|
func (s *Session) StderrPipe() (io.Reader, error) {
|
||
|
if s.Stderr != nil {
|
||
|
return nil, errors.New("ssh: Stderr already set")
|
||
|
}
|
||
|
if s.started {
|
||
|
return nil, errors.New("ssh: StderrPipe after process started")
|
||
|
}
|
||
|
s.stderrpipe = true
|
||
|
return s.ch.Stderr(), nil
|
||
|
}
|
||
|
|
||
|
// newSession returns a new interactive session on the remote host.
|
||
|
func newSession(ch Channel, reqs <-chan *Request) (*Session, error) {
|
||
|
s := &Session{
|
||
|
ch: ch,
|
||
|
}
|
||
|
s.exitStatus = make(chan error, 1)
|
||
|
go func() {
|
||
|
s.exitStatus <- s.wait(reqs)
|
||
|
}()
|
||
|
|
||
|
return s, nil
|
||
|
}
|
||
|
|
||
|
// An ExitError reports unsuccessful completion of a remote command.
|
||
|
type ExitError struct {
|
||
|
Waitmsg
|
||
|
}
|
||
|
|
||
|
func (e *ExitError) Error() string {
|
||
|
return e.Waitmsg.String()
|
||
|
}
|
||
|
|
||
|
// Waitmsg stores the information about an exited remote command
|
||
|
// as reported by Wait.
|
||
|
type Waitmsg struct {
|
||
|
status int
|
||
|
signal string
|
||
|
msg string
|
||
|
lang string
|
||
|
}
|
||
|
|
||
|
// ExitStatus returns the exit status of the remote command.
|
||
|
func (w Waitmsg) ExitStatus() int {
|
||
|
return w.status
|
||
|
}
|
||
|
|
||
|
// Signal returns the exit signal of the remote command if
|
||
|
// it was terminated violently.
|
||
|
func (w Waitmsg) Signal() string {
|
||
|
return w.signal
|
||
|
}
|
||
|
|
||
|
// Msg returns the exit message given by the remote command
|
||
|
func (w Waitmsg) Msg() string {
|
||
|
return w.msg
|
||
|
}
|
||
|
|
||
|
// Lang returns the language tag. See RFC 3066
|
||
|
func (w Waitmsg) Lang() string {
|
||
|
return w.lang
|
||
|
}
|
||
|
|
||
|
func (w Waitmsg) String() string {
|
||
|
str := fmt.Sprintf("Process exited with status %v", w.status)
|
||
|
if w.signal != "" {
|
||
|
str += fmt.Sprintf(" from signal %v", w.signal)
|
||
|
}
|
||
|
if w.msg != "" {
|
||
|
str += fmt.Sprintf(". Reason was: %v", w.msg)
|
||
|
}
|
||
|
return str
|
||
|
}
|