1// Copyright 2011 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package ssh
6
7// Session implements an interactive session described in
8// "RFC 4254, section 6".
9
10import (
11	"bytes"
12	"encoding/binary"
13	"errors"
14	"fmt"
15	"io"
16	"io/ioutil"
17	"sync"
18)
19
20type Signal string
21
22// POSIX signals as listed in RFC 4254 Section 6.10.
23const (
24	SIGABRT Signal = "ABRT"
25	SIGALRM Signal = "ALRM"
26	SIGFPE  Signal = "FPE"
27	SIGHUP  Signal = "HUP"
28	SIGILL  Signal = "ILL"
29	SIGINT  Signal = "INT"
30	SIGKILL Signal = "KILL"
31	SIGPIPE Signal = "PIPE"
32	SIGQUIT Signal = "QUIT"
33	SIGSEGV Signal = "SEGV"
34	SIGTERM Signal = "TERM"
35	SIGUSR1 Signal = "USR1"
36	SIGUSR2 Signal = "USR2"
37)
38
39var signals = map[Signal]int{
40	SIGABRT: 6,
41	SIGALRM: 14,
42	SIGFPE:  8,
43	SIGHUP:  1,
44	SIGILL:  4,
45	SIGINT:  2,
46	SIGKILL: 9,
47	SIGPIPE: 13,
48	SIGQUIT: 3,
49	SIGSEGV: 11,
50	SIGTERM: 15,
51}
52
53type TerminalModes map[uint8]uint32
54
55// POSIX terminal mode flags as listed in RFC 4254 Section 8.
56const (
57	tty_OP_END    = 0
58	VINTR         = 1
59	VQUIT         = 2
60	VERASE        = 3
61	VKILL         = 4
62	VEOF          = 5
63	VEOL          = 6
64	VEOL2         = 7
65	VSTART        = 8
66	VSTOP         = 9
67	VSUSP         = 10
68	VDSUSP        = 11
69	VREPRINT      = 12
70	VWERASE       = 13
71	VLNEXT        = 14
72	VFLUSH        = 15
73	VSWTCH        = 16
74	VSTATUS       = 17
75	VDISCARD      = 18
76	IGNPAR        = 30
77	PARMRK        = 31
78	INPCK         = 32
79	ISTRIP        = 33
80	INLCR         = 34
81	IGNCR         = 35
82	ICRNL         = 36
83	IUCLC         = 37
84	IXON          = 38
85	IXANY         = 39
86	IXOFF         = 40
87	IMAXBEL       = 41
88	ISIG          = 50
89	ICANON        = 51
90	XCASE         = 52
91	ECHO          = 53
92	ECHOE         = 54
93	ECHOK         = 55
94	ECHONL        = 56
95	NOFLSH        = 57
96	TOSTOP        = 58
97	IEXTEN        = 59
98	ECHOCTL       = 60
99	ECHOKE        = 61
100	PENDIN        = 62
101	OPOST         = 70
102	OLCUC         = 71
103	ONLCR         = 72
104	OCRNL         = 73
105	ONOCR         = 74
106	ONLRET        = 75
107	CS7           = 90
108	CS8           = 91
109	PARENB        = 92
110	PARODD        = 93
111	TTY_OP_ISPEED = 128
112	TTY_OP_OSPEED = 129
113)
114
115// A Session represents a connection to a remote command or shell.
116type Session struct {
117	// Stdin specifies the remote process's standard input.
118	// If Stdin is nil, the remote process reads from an empty
119	// bytes.Buffer.
120	Stdin io.Reader
121
122	// Stdout and Stderr specify the remote process's standard
123	// output and error.
124	//
125	// If either is nil, Run connects the corresponding file
126	// descriptor to an instance of ioutil.Discard. There is a
127	// fixed amount of buffering that is shared for the two streams.
128	// If either blocks it may eventually cause the remote
129	// command to block.
130	Stdout io.Writer
131	Stderr io.Writer
132
133	ch        Channel // the channel backing this session
134	started   bool    // true once Start, Run or Shell is invoked.
135	copyFuncs []func() error
136	errors    chan error // one send per copyFunc
137
138	// true if pipe method is active
139	stdinpipe, stdoutpipe, stderrpipe bool
140
141	// stdinPipeWriter is non-nil if StdinPipe has not been called
142	// and Stdin was specified by the user; it is the write end of
143	// a pipe connecting Session.Stdin to the stdin channel.
144	stdinPipeWriter io.WriteCloser
145
146	exitStatus chan error
147}
148
149// SendRequest sends an out-of-band channel request on the SSH channel
150// underlying the session.
151func (s *Session) SendRequest(name string, wantReply bool, payload []byte) (bool, error) {
152	return s.ch.SendRequest(name, wantReply, payload)
153}
154
155func (s *Session) Close() error {
156	return s.ch.Close()
157}
158
159// RFC 4254 Section 6.4.
160type setenvRequest struct {
161	Name  string
162	Value string
163}
164
165// Setenv sets an environment variable that will be applied to any
166// command executed by Shell or Run.
167func (s *Session) Setenv(name, value string) error {
168	msg := setenvRequest{
169		Name:  name,
170		Value: value,
171	}
172	ok, err := s.ch.SendRequest("env", true, Marshal(&msg))
173	if err == nil && !ok {
174		err = errors.New("ssh: setenv failed")
175	}
176	return err
177}
178
179// RFC 4254 Section 6.2.
180type ptyRequestMsg struct {
181	Term     string
182	Columns  uint32
183	Rows     uint32
184	Width    uint32
185	Height   uint32
186	Modelist string
187}
188
189// RequestPty requests the association of a pty with the session on the remote host.
190func (s *Session) RequestPty(term string, h, w int, termmodes TerminalModes) error {
191	var tm []byte
192	for k, v := range termmodes {
193		kv := struct {
194			Key byte
195			Val uint32
196		}{k, v}
197
198		tm = append(tm, Marshal(&kv)...)
199	}
200	tm = append(tm, tty_OP_END)
201	req := ptyRequestMsg{
202		Term:     term,
203		Columns:  uint32(w),
204		Rows:     uint32(h),
205		Width:    uint32(w * 8),
206		Height:   uint32(h * 8),
207		Modelist: string(tm),
208	}
209	ok, err := s.ch.SendRequest("pty-req", true, Marshal(&req))
210	if err == nil && !ok {
211		err = errors.New("ssh: pty-req failed")
212	}
213	return err
214}
215
216// RFC 4254 Section 6.5.
217type subsystemRequestMsg struct {
218	Subsystem string
219}
220
221// RequestSubsystem requests the association of a subsystem with the session on the remote host.
222// A subsystem is a predefined command that runs in the background when the ssh session is initiated
223func (s *Session) RequestSubsystem(subsystem string) error {
224	msg := subsystemRequestMsg{
225		Subsystem: subsystem,
226	}
227	ok, err := s.ch.SendRequest("subsystem", true, Marshal(&msg))
228	if err == nil && !ok {
229		err = errors.New("ssh: subsystem request failed")
230	}
231	return err
232}
233
234// RFC 4254 Section 6.7.
235type ptyWindowChangeMsg struct {
236	Columns uint32
237	Rows    uint32
238	Width   uint32
239	Height  uint32
240}
241
242// WindowChange informs the remote host about a terminal window dimension change to h rows and w columns.
243func (s *Session) WindowChange(h, w int) error {
244	req := ptyWindowChangeMsg{
245		Columns: uint32(w),
246		Rows:    uint32(h),
247		Width:   uint32(w * 8),
248		Height:  uint32(h * 8),
249	}
250	_, err := s.ch.SendRequest("window-change", false, Marshal(&req))
251	return err
252}
253
254// RFC 4254 Section 6.9.
255type signalMsg struct {
256	Signal string
257}
258
259// Signal sends the given signal to the remote process.
260// sig is one of the SIG* constants.
261func (s *Session) Signal(sig Signal) error {
262	msg := signalMsg{
263		Signal: string(sig),
264	}
265
266	_, err := s.ch.SendRequest("signal", false, Marshal(&msg))
267	return err
268}
269
270// RFC 4254 Section 6.5.
271type execMsg struct {
272	Command string
273}
274
275// Start runs cmd on the remote host. Typically, the remote
276// server passes cmd to the shell for interpretation.
277// A Session only accepts one call to Run, Start or Shell.
278func (s *Session) Start(cmd string) error {
279	if s.started {
280		return errors.New("ssh: session already started")
281	}
282	req := execMsg{
283		Command: cmd,
284	}
285
286	ok, err := s.ch.SendRequest("exec", true, Marshal(&req))
287	if err == nil && !ok {
288		err = fmt.Errorf("ssh: command %v failed", cmd)
289	}
290	if err != nil {
291		return err
292	}
293	return s.start()
294}
295
296// Run runs cmd on the remote host. Typically, the remote
297// server passes cmd to the shell for interpretation.
298// A Session only accepts one call to Run, Start, Shell, Output,
299// or CombinedOutput.
300//
301// The returned error is nil if the command runs, has no problems
302// copying stdin, stdout, and stderr, and exits with a zero exit
303// status.
304//
305// If the remote server does not send an exit status, an error of type
306// *ExitMissingError is returned. If the command completes
307// unsuccessfully or is interrupted by a signal, the error is of type
308// *ExitError. Other error types may be returned for I/O problems.
309func (s *Session) Run(cmd string) error {
310	err := s.Start(cmd)
311	if err != nil {
312		return err
313	}
314	return s.Wait()
315}
316
317// Output runs cmd on the remote host and returns its standard output.
318func (s *Session) Output(cmd string) ([]byte, error) {
319	if s.Stdout != nil {
320		return nil, errors.New("ssh: Stdout already set")
321	}
322	var b bytes.Buffer
323	s.Stdout = &b
324	err := s.Run(cmd)
325	return b.Bytes(), err
326}
327
328type singleWriter struct {
329	b  bytes.Buffer
330	mu sync.Mutex
331}
332
333func (w *singleWriter) Write(p []byte) (int, error) {
334	w.mu.Lock()
335	defer w.mu.Unlock()
336	return w.b.Write(p)
337}
338
339// CombinedOutput runs cmd on the remote host and returns its combined
340// standard output and standard error.
341func (s *Session) CombinedOutput(cmd string) ([]byte, error) {
342	if s.Stdout != nil {
343		return nil, errors.New("ssh: Stdout already set")
344	}
345	if s.Stderr != nil {
346		return nil, errors.New("ssh: Stderr already set")
347	}
348	var b singleWriter
349	s.Stdout = &b
350	s.Stderr = &b
351	err := s.Run(cmd)
352	return b.b.Bytes(), err
353}
354
355// Shell starts a login shell on the remote host. A Session only
356// accepts one call to Run, Start, Shell, Output, or CombinedOutput.
357func (s *Session) Shell() error {
358	if s.started {
359		return errors.New("ssh: session already started")
360	}
361
362	ok, err := s.ch.SendRequest("shell", true, nil)
363	if err == nil && !ok {
364		return errors.New("ssh: could not start shell")
365	}
366	if err != nil {
367		return err
368	}
369	return s.start()
370}
371
372func (s *Session) start() error {
373	s.started = true
374
375	type F func(*Session)
376	for _, setupFd := range []F{(*Session).stdin, (*Session).stdout, (*Session).stderr} {
377		setupFd(s)
378	}
379
380	s.errors = make(chan error, len(s.copyFuncs))
381	for _, fn := range s.copyFuncs {
382		go func(fn func() error) {
383			s.errors <- fn()
384		}(fn)
385	}
386	return nil
387}
388
389// Wait waits for the remote command to exit.
390//
391// The returned error is nil if the command runs, has no problems
392// copying stdin, stdout, and stderr, and exits with a zero exit
393// status.
394//
395// If the remote server does not send an exit status, an error of type
396// *ExitMissingError is returned. If the command completes
397// unsuccessfully or is interrupted by a signal, the error is of type
398// *ExitError. Other error types may be returned for I/O problems.
399func (s *Session) Wait() error {
400	if !s.started {
401		return errors.New("ssh: session not started")
402	}
403	waitErr := <-s.exitStatus
404
405	if s.stdinPipeWriter != nil {
406		s.stdinPipeWriter.Close()
407	}
408	var copyError error
409	for _ = range s.copyFuncs {
410		if err := <-s.errors; err != nil && copyError == nil {
411			copyError = err
412		}
413	}
414	if waitErr != nil {
415		return waitErr
416	}
417	return copyError
418}
419
420func (s *Session) wait(reqs <-chan *Request) error {
421	wm := Waitmsg{status: -1}
422	// Wait for msg channel to be closed before returning.
423	for msg := range reqs {
424		switch msg.Type {
425		case "exit-status":
426			wm.status = int(binary.BigEndian.Uint32(msg.Payload))
427		case "exit-signal":
428			var sigval struct {
429				Signal     string
430				CoreDumped bool
431				Error      string
432				Lang       string
433			}
434			if err := Unmarshal(msg.Payload, &sigval); err != nil {
435				return err
436			}
437
438			// Must sanitize strings?
439			wm.signal = sigval.Signal
440			wm.msg = sigval.Error
441			wm.lang = sigval.Lang
442		default:
443			// This handles keepalives and matches
444			// OpenSSH's behaviour.
445			if msg.WantReply {
446				msg.Reply(false, nil)
447			}
448		}
449	}
450	if wm.status == 0 {
451		return nil
452	}
453	if wm.status == -1 {
454		// exit-status was never sent from server
455		if wm.signal == "" {
456			// signal was not sent either.  RFC 4254
457			// section 6.10 recommends against this
458			// behavior, but it is allowed, so we let
459			// clients handle it.
460			return &ExitMissingError{}
461		}
462		wm.status = 128
463		if _, ok := signals[Signal(wm.signal)]; ok {
464			wm.status += signals[Signal(wm.signal)]
465		}
466	}
467
468	return &ExitError{wm}
469}
470
471// ExitMissingError is returned if a session is torn down cleanly, but
472// the server sends no confirmation of the exit status.
473type ExitMissingError struct{}
474
475func (e *ExitMissingError) Error() string {
476	return "wait: remote command exited without exit status or exit signal"
477}
478
479func (s *Session) stdin() {
480	if s.stdinpipe {
481		return
482	}
483	var stdin io.Reader
484	if s.Stdin == nil {
485		stdin = new(bytes.Buffer)
486	} else {
487		r, w := io.Pipe()
488		go func() {
489			_, err := io.Copy(w, s.Stdin)
490			w.CloseWithError(err)
491		}()
492		stdin, s.stdinPipeWriter = r, w
493	}
494	s.copyFuncs = append(s.copyFuncs, func() error {
495		_, err := io.Copy(s.ch, stdin)
496		if err1 := s.ch.CloseWrite(); err == nil && err1 != io.EOF {
497			err = err1
498		}
499		return err
500	})
501}
502
503func (s *Session) stdout() {
504	if s.stdoutpipe {
505		return
506	}
507	if s.Stdout == nil {
508		s.Stdout = ioutil.Discard
509	}
510	s.copyFuncs = append(s.copyFuncs, func() error {
511		_, err := io.Copy(s.Stdout, s.ch)
512		return err
513	})
514}
515
516func (s *Session) stderr() {
517	if s.stderrpipe {
518		return
519	}
520	if s.Stderr == nil {
521		s.Stderr = ioutil.Discard
522	}
523	s.copyFuncs = append(s.copyFuncs, func() error {
524		_, err := io.Copy(s.Stderr, s.ch.Stderr())
525		return err
526	})
527}
528
529// sessionStdin reroutes Close to CloseWrite.
530type sessionStdin struct {
531	io.Writer
532	ch Channel
533}
534
535func (s *sessionStdin) Close() error {
536	return s.ch.CloseWrite()
537}
538
539// StdinPipe returns a pipe that will be connected to the
540// remote command's standard input when the command starts.
541func (s *Session) StdinPipe() (io.WriteCloser, error) {
542	if s.Stdin != nil {
543		return nil, errors.New("ssh: Stdin already set")
544	}
545	if s.started {
546		return nil, errors.New("ssh: StdinPipe after process started")
547	}
548	s.stdinpipe = true
549	return &sessionStdin{s.ch, s.ch}, nil
550}
551
552// StdoutPipe returns a pipe that will be connected to the
553// remote command's standard output when the command starts.
554// There is a fixed amount of buffering that is shared between
555// stdout and stderr streams. If the StdoutPipe reader is
556// not serviced fast enough it may eventually cause the
557// remote command to block.
558func (s *Session) StdoutPipe() (io.Reader, error) {
559	if s.Stdout != nil {
560		return nil, errors.New("ssh: Stdout already set")
561	}
562	if s.started {
563		return nil, errors.New("ssh: StdoutPipe after process started")
564	}
565	s.stdoutpipe = true
566	return s.ch, nil
567}
568
569// StderrPipe returns a pipe that will be connected to the
570// remote command's standard error when the command starts.
571// There is a fixed amount of buffering that is shared between
572// stdout and stderr streams. If the StderrPipe reader is
573// not serviced fast enough it may eventually cause the
574// remote command to block.
575func (s *Session) StderrPipe() (io.Reader, error) {
576	if s.Stderr != nil {
577		return nil, errors.New("ssh: Stderr already set")
578	}
579	if s.started {
580		return nil, errors.New("ssh: StderrPipe after process started")
581	}
582	s.stderrpipe = true
583	return s.ch.Stderr(), nil
584}
585
586// newSession returns a new interactive session on the remote host.
587func newSession(ch Channel, reqs <-chan *Request) (*Session, error) {
588	s := &Session{
589		ch: ch,
590	}
591	s.exitStatus = make(chan error, 1)
592	go func() {
593		s.exitStatus <- s.wait(reqs)
594	}()
595
596	return s, nil
597}
598
599// An ExitError reports unsuccessful completion of a remote command.
600type ExitError struct {
601	Waitmsg
602}
603
604func (e *ExitError) Error() string {
605	return e.Waitmsg.String()
606}
607
608// Waitmsg stores the information about an exited remote command
609// as reported by Wait.
610type Waitmsg struct {
611	status int
612	signal string
613	msg    string
614	lang   string
615}
616
617// ExitStatus returns the exit status of the remote command.
618func (w Waitmsg) ExitStatus() int {
619	return w.status
620}
621
622// Signal returns the exit signal of the remote command if
623// it was terminated violently.
624func (w Waitmsg) Signal() string {
625	return w.signal
626}
627
628// Msg returns the exit message given by the remote command
629func (w Waitmsg) Msg() string {
630	return w.msg
631}
632
633// Lang returns the language tag. See RFC 3066
634func (w Waitmsg) Lang() string {
635	return w.lang
636}
637
638func (w Waitmsg) String() string {
639	str := fmt.Sprintf("Process exited with status %v", w.status)
640	if w.signal != "" {
641		str += fmt.Sprintf(" from signal %v", w.signal)
642	}
643	if w.msg != "" {
644		str += fmt.Sprintf(". Reason was: %v", w.msg)
645	}
646	return str
647}
648