]>
Commit | Line | Data |
---|---|---|
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 | ||
5 | package ssh | |
6 | ||
7 | // Session implements an interactive session described in | |
8 | // "RFC 4254, section 6". | |
9 | ||
10 | import ( | |
11 | "bytes" | |
12 | "encoding/binary" | |
13 | "errors" | |
14 | "fmt" | |
15 | "io" | |
16 | "io/ioutil" | |
17 | "sync" | |
18 | ) | |
19 | ||
20 | type Signal string | |
21 | ||
22 | // POSIX signals as listed in RFC 4254 Section 6.10. | |
23 | const ( | |
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 | ||
39 | var 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 | ||
53 | type TerminalModes map[uint8]uint32 | |
54 | ||
55 | // POSIX terminal mode flags as listed in RFC 4254 Section 8. | |
56 | const ( | |
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. | |
116 | type 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. | |
151 | func (s *Session) SendRequest(name string, wantReply bool, payload []byte) (bool, error) { | |
152 | return s.ch.SendRequest(name, wantReply, payload) | |
153 | } | |
154 | ||
155 | func (s *Session) Close() error { | |
156 | return s.ch.Close() | |
157 | } | |
158 | ||
159 | // RFC 4254 Section 6.4. | |
160 | type 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. | |
167 | func (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. | |
180 | type 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. | |
190 | func (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. | |
217 | type 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 | |
223 | func (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.9. | |
235 | type signalMsg struct { | |
236 | Signal string | |
237 | } | |
238 | ||
239 | // Signal sends the given signal to the remote process. | |
240 | // sig is one of the SIG* constants. | |
241 | func (s *Session) Signal(sig Signal) error { | |
242 | msg := signalMsg{ | |
243 | Signal: string(sig), | |
244 | } | |
245 | ||
246 | _, err := s.ch.SendRequest("signal", false, Marshal(&msg)) | |
247 | return err | |
248 | } | |
249 | ||
250 | // RFC 4254 Section 6.5. | |
251 | type execMsg struct { | |
252 | Command string | |
253 | } | |
254 | ||
255 | // Start runs cmd on the remote host. Typically, the remote | |
256 | // server passes cmd to the shell for interpretation. | |
257 | // A Session only accepts one call to Run, Start or Shell. | |
258 | func (s *Session) Start(cmd string) error { | |
259 | if s.started { | |
260 | return errors.New("ssh: session already started") | |
261 | } | |
262 | req := execMsg{ | |
263 | Command: cmd, | |
264 | } | |
265 | ||
266 | ok, err := s.ch.SendRequest("exec", true, Marshal(&req)) | |
267 | if err == nil && !ok { | |
268 | err = fmt.Errorf("ssh: command %v failed", cmd) | |
269 | } | |
270 | if err != nil { | |
271 | return err | |
272 | } | |
273 | return s.start() | |
274 | } | |
275 | ||
276 | // Run runs cmd on the remote host. Typically, the remote | |
277 | // server passes cmd to the shell for interpretation. | |
278 | // A Session only accepts one call to Run, Start, Shell, Output, | |
279 | // or CombinedOutput. | |
280 | // | |
281 | // The returned error is nil if the command runs, has no problems | |
282 | // copying stdin, stdout, and stderr, and exits with a zero exit | |
283 | // status. | |
284 | // | |
285 | // If the remote server does not send an exit status, an error of type | |
286 | // *ExitMissingError is returned. If the command completes | |
287 | // unsuccessfully or is interrupted by a signal, the error is of type | |
288 | // *ExitError. Other error types may be returned for I/O problems. | |
289 | func (s *Session) Run(cmd string) error { | |
290 | err := s.Start(cmd) | |
291 | if err != nil { | |
292 | return err | |
293 | } | |
294 | return s.Wait() | |
295 | } | |
296 | ||
297 | // Output runs cmd on the remote host and returns its standard output. | |
298 | func (s *Session) Output(cmd string) ([]byte, error) { | |
299 | if s.Stdout != nil { | |
300 | return nil, errors.New("ssh: Stdout already set") | |
301 | } | |
302 | var b bytes.Buffer | |
303 | s.Stdout = &b | |
304 | err := s.Run(cmd) | |
305 | return b.Bytes(), err | |
306 | } | |
307 | ||
308 | type singleWriter struct { | |
309 | b bytes.Buffer | |
310 | mu sync.Mutex | |
311 | } | |
312 | ||
313 | func (w *singleWriter) Write(p []byte) (int, error) { | |
314 | w.mu.Lock() | |
315 | defer w.mu.Unlock() | |
316 | return w.b.Write(p) | |
317 | } | |
318 | ||
319 | // CombinedOutput runs cmd on the remote host and returns its combined | |
320 | // standard output and standard error. | |
321 | func (s *Session) CombinedOutput(cmd string) ([]byte, error) { | |
322 | if s.Stdout != nil { | |
323 | return nil, errors.New("ssh: Stdout already set") | |
324 | } | |
325 | if s.Stderr != nil { | |
326 | return nil, errors.New("ssh: Stderr already set") | |
327 | } | |
328 | var b singleWriter | |
329 | s.Stdout = &b | |
330 | s.Stderr = &b | |
331 | err := s.Run(cmd) | |
332 | return b.b.Bytes(), err | |
333 | } | |
334 | ||
335 | // Shell starts a login shell on the remote host. A Session only | |
336 | // accepts one call to Run, Start, Shell, Output, or CombinedOutput. | |
337 | func (s *Session) Shell() error { | |
338 | if s.started { | |
339 | return errors.New("ssh: session already started") | |
340 | } | |
341 | ||
342 | ok, err := s.ch.SendRequest("shell", true, nil) | |
343 | if err == nil && !ok { | |
344 | return errors.New("ssh: could not start shell") | |
345 | } | |
346 | if err != nil { | |
347 | return err | |
348 | } | |
349 | return s.start() | |
350 | } | |
351 | ||
352 | func (s *Session) start() error { | |
353 | s.started = true | |
354 | ||
355 | type F func(*Session) | |
356 | for _, setupFd := range []F{(*Session).stdin, (*Session).stdout, (*Session).stderr} { | |
357 | setupFd(s) | |
358 | } | |
359 | ||
360 | s.errors = make(chan error, len(s.copyFuncs)) | |
361 | for _, fn := range s.copyFuncs { | |
362 | go func(fn func() error) { | |
363 | s.errors <- fn() | |
364 | }(fn) | |
365 | } | |
366 | return nil | |
367 | } | |
368 | ||
369 | // Wait waits for the remote command to exit. | |
370 | // | |
371 | // The returned error is nil if the command runs, has no problems | |
372 | // copying stdin, stdout, and stderr, and exits with a zero exit | |
373 | // status. | |
374 | // | |
375 | // If the remote server does not send an exit status, an error of type | |
376 | // *ExitMissingError is returned. If the command completes | |
377 | // unsuccessfully or is interrupted by a signal, the error is of type | |
378 | // *ExitError. Other error types may be returned for I/O problems. | |
379 | func (s *Session) Wait() error { | |
380 | if !s.started { | |
381 | return errors.New("ssh: session not started") | |
382 | } | |
383 | waitErr := <-s.exitStatus | |
384 | ||
385 | if s.stdinPipeWriter != nil { | |
386 | s.stdinPipeWriter.Close() | |
387 | } | |
388 | var copyError error | |
389 | for _ = range s.copyFuncs { | |
390 | if err := <-s.errors; err != nil && copyError == nil { | |
391 | copyError = err | |
392 | } | |
393 | } | |
394 | if waitErr != nil { | |
395 | return waitErr | |
396 | } | |
397 | return copyError | |
398 | } | |
399 | ||
400 | func (s *Session) wait(reqs <-chan *Request) error { | |
401 | wm := Waitmsg{status: -1} | |
402 | // Wait for msg channel to be closed before returning. | |
403 | for msg := range reqs { | |
404 | switch msg.Type { | |
405 | case "exit-status": | |
406 | wm.status = int(binary.BigEndian.Uint32(msg.Payload)) | |
407 | case "exit-signal": | |
408 | var sigval struct { | |
409 | Signal string | |
410 | CoreDumped bool | |
411 | Error string | |
412 | Lang string | |
413 | } | |
414 | if err := Unmarshal(msg.Payload, &sigval); err != nil { | |
415 | return err | |
416 | } | |
417 | ||
418 | // Must sanitize strings? | |
419 | wm.signal = sigval.Signal | |
420 | wm.msg = sigval.Error | |
421 | wm.lang = sigval.Lang | |
422 | default: | |
423 | // This handles keepalives and matches | |
424 | // OpenSSH's behaviour. | |
425 | if msg.WantReply { | |
426 | msg.Reply(false, nil) | |
427 | } | |
428 | } | |
429 | } | |
430 | if wm.status == 0 { | |
431 | return nil | |
432 | } | |
433 | if wm.status == -1 { | |
434 | // exit-status was never sent from server | |
435 | if wm.signal == "" { | |
436 | // signal was not sent either. RFC 4254 | |
437 | // section 6.10 recommends against this | |
438 | // behavior, but it is allowed, so we let | |
439 | // clients handle it. | |
440 | return &ExitMissingError{} | |
441 | } | |
442 | wm.status = 128 | |
443 | if _, ok := signals[Signal(wm.signal)]; ok { | |
444 | wm.status += signals[Signal(wm.signal)] | |
445 | } | |
446 | } | |
447 | ||
448 | return &ExitError{wm} | |
449 | } | |
450 | ||
451 | // ExitMissingError is returned if a session is torn down cleanly, but | |
452 | // the server sends no confirmation of the exit status. | |
453 | type ExitMissingError struct{} | |
454 | ||
455 | func (e *ExitMissingError) Error() string { | |
456 | return "wait: remote command exited without exit status or exit signal" | |
457 | } | |
458 | ||
459 | func (s *Session) stdin() { | |
460 | if s.stdinpipe { | |
461 | return | |
462 | } | |
463 | var stdin io.Reader | |
464 | if s.Stdin == nil { | |
465 | stdin = new(bytes.Buffer) | |
466 | } else { | |
467 | r, w := io.Pipe() | |
468 | go func() { | |
469 | _, err := io.Copy(w, s.Stdin) | |
470 | w.CloseWithError(err) | |
471 | }() | |
472 | stdin, s.stdinPipeWriter = r, w | |
473 | } | |
474 | s.copyFuncs = append(s.copyFuncs, func() error { | |
475 | _, err := io.Copy(s.ch, stdin) | |
476 | if err1 := s.ch.CloseWrite(); err == nil && err1 != io.EOF { | |
477 | err = err1 | |
478 | } | |
479 | return err | |
480 | }) | |
481 | } | |
482 | ||
483 | func (s *Session) stdout() { | |
484 | if s.stdoutpipe { | |
485 | return | |
486 | } | |
487 | if s.Stdout == nil { | |
488 | s.Stdout = ioutil.Discard | |
489 | } | |
490 | s.copyFuncs = append(s.copyFuncs, func() error { | |
491 | _, err := io.Copy(s.Stdout, s.ch) | |
492 | return err | |
493 | }) | |
494 | } | |
495 | ||
496 | func (s *Session) stderr() { | |
497 | if s.stderrpipe { | |
498 | return | |
499 | } | |
500 | if s.Stderr == nil { | |
501 | s.Stderr = ioutil.Discard | |
502 | } | |
503 | s.copyFuncs = append(s.copyFuncs, func() error { | |
504 | _, err := io.Copy(s.Stderr, s.ch.Stderr()) | |
505 | return err | |
506 | }) | |
507 | } | |
508 | ||
509 | // sessionStdin reroutes Close to CloseWrite. | |
510 | type sessionStdin struct { | |
511 | io.Writer | |
512 | ch Channel | |
513 | } | |
514 | ||
515 | func (s *sessionStdin) Close() error { | |
516 | return s.ch.CloseWrite() | |
517 | } | |
518 | ||
519 | // StdinPipe returns a pipe that will be connected to the | |
520 | // remote command's standard input when the command starts. | |
521 | func (s *Session) StdinPipe() (io.WriteCloser, error) { | |
522 | if s.Stdin != nil { | |
523 | return nil, errors.New("ssh: Stdin already set") | |
524 | } | |
525 | if s.started { | |
526 | return nil, errors.New("ssh: StdinPipe after process started") | |
527 | } | |
528 | s.stdinpipe = true | |
529 | return &sessionStdin{s.ch, s.ch}, nil | |
530 | } | |
531 | ||
532 | // StdoutPipe returns a pipe that will be connected to the | |
533 | // remote command's standard output when the command starts. | |
534 | // There is a fixed amount of buffering that is shared between | |
535 | // stdout and stderr streams. If the StdoutPipe reader is | |
536 | // not serviced fast enough it may eventually cause the | |
537 | // remote command to block. | |
538 | func (s *Session) StdoutPipe() (io.Reader, error) { | |
539 | if s.Stdout != nil { | |
540 | return nil, errors.New("ssh: Stdout already set") | |
541 | } | |
542 | if s.started { | |
543 | return nil, errors.New("ssh: StdoutPipe after process started") | |
544 | } | |
545 | s.stdoutpipe = true | |
546 | return s.ch, nil | |
547 | } | |
548 | ||
549 | // StderrPipe returns a pipe that will be connected to the | |
550 | // remote command's standard error when the command starts. | |
551 | // There is a fixed amount of buffering that is shared between | |
552 | // stdout and stderr streams. If the StderrPipe reader is | |
553 | // not serviced fast enough it may eventually cause the | |
554 | // remote command to block. | |
555 | func (s *Session) StderrPipe() (io.Reader, error) { | |
556 | if s.Stderr != nil { | |
557 | return nil, errors.New("ssh: Stderr already set") | |
558 | } | |
559 | if s.started { | |
560 | return nil, errors.New("ssh: StderrPipe after process started") | |
561 | } | |
562 | s.stderrpipe = true | |
563 | return s.ch.Stderr(), nil | |
564 | } | |
565 | ||
566 | // newSession returns a new interactive session on the remote host. | |
567 | func newSession(ch Channel, reqs <-chan *Request) (*Session, error) { | |
568 | s := &Session{ | |
569 | ch: ch, | |
570 | } | |
571 | s.exitStatus = make(chan error, 1) | |
572 | go func() { | |
573 | s.exitStatus <- s.wait(reqs) | |
574 | }() | |
575 | ||
576 | return s, nil | |
577 | } | |
578 | ||
579 | // An ExitError reports unsuccessful completion of a remote command. | |
580 | type ExitError struct { | |
581 | Waitmsg | |
582 | } | |
583 | ||
584 | func (e *ExitError) Error() string { | |
585 | return e.Waitmsg.String() | |
586 | } | |
587 | ||
588 | // Waitmsg stores the information about an exited remote command | |
589 | // as reported by Wait. | |
590 | type Waitmsg struct { | |
591 | status int | |
592 | signal string | |
593 | msg string | |
594 | lang string | |
595 | } | |
596 | ||
597 | // ExitStatus returns the exit status of the remote command. | |
598 | func (w Waitmsg) ExitStatus() int { | |
599 | return w.status | |
600 | } | |
601 | ||
602 | // Signal returns the exit signal of the remote command if | |
603 | // it was terminated violently. | |
604 | func (w Waitmsg) Signal() string { | |
605 | return w.signal | |
606 | } | |
607 | ||
608 | // Msg returns the exit message given by the remote command | |
609 | func (w Waitmsg) Msg() string { | |
610 | return w.msg | |
611 | } | |
612 | ||
613 | // Lang returns the language tag. See RFC 3066 | |
614 | func (w Waitmsg) Lang() string { | |
615 | return w.lang | |
616 | } | |
617 | ||
618 | func (w Waitmsg) String() string { | |
619 | str := fmt.Sprintf("Process exited with status %v", w.status) | |
620 | if w.signal != "" { | |
621 | str += fmt.Sprintf(" from signal %v", w.signal) | |
622 | } | |
623 | if w.msg != "" { | |
624 | str += fmt.Sprintf(". Reason was: %v", w.msg) | |
625 | } | |
626 | return str | |
627 | } |