Skip to content

Commit c5193c0

Browse files
committed
Close shell properly on exit, and return exit code. Update winsize.
1 parent 0750d87 commit c5193c0

File tree

2 files changed

+117
-16
lines changed

2 files changed

+117
-16
lines changed

execd.go

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ func attachCmd(cmd *exec.Cmd, stdout io.Writer, stderr io.Writer, stdin io.Reade
8787
return &wg, nil
8888
}
8989

90-
func attachShell(cmd *exec.Cmd, stdout io.Writer, stdin io.Reader) (*sync.WaitGroup, error) {
90+
func attachShell(cmd *exec.Cmd, stdout io.Writer, stdin io.Reader) (*os.File, *sync.WaitGroup, error) {
9191
var wg sync.WaitGroup
9292
wg.Add(2)
9393

9494
// Note that pty merges stdout and stderr.
9595
cmdPty, err := pty.Start(cmd)
9696
if err != nil {
97-
return nil, err
97+
return nil, nil, err
9898
}
9999
go func() {
100100
io.Copy(stdout, cmdPty)
@@ -105,7 +105,7 @@ func attachShell(cmd *exec.Cmd, stdout io.Writer, stdin io.Reader) (*sync.WaitGr
105105
wg.Done()
106106
}()
107107

108-
return &wg, nil
108+
return cmdPty, &wg, nil
109109
}
110110

111111
func addKey(conf *ssh.ServerConfig, block *pem.Block) (err error) {
@@ -270,7 +270,6 @@ func handleChannel(conn *ssh.ServerConn, newChan ssh.NewChannel, execHandler []s
270270
log.Println("newChan.Accept failed:", err)
271271
return
272272
}
273-
defer ch.Close()
274273

275274
assert := func(at string, err error) bool {
276275
if err != nil {
@@ -290,11 +289,13 @@ func handleChannel(conn *ssh.ServerConn, newChan ssh.NewChannel, execHandler []s
290289
stderr = ch.Stderr()
291290
}
292291

293-
// FIXME: We currently bail out as soon as a req is matched. Technically ssh
294-
// can send multiple requests out of band, which we might want to handle?
292+
var ptyShell *os.File
293+
295294
for req := range reqs {
296295
switch req.Type {
297296
case "exec":
297+
defer ch.Close()
298+
298299
if req.WantReply {
299300
req.Reply(true, nil)
300301
}
@@ -338,9 +339,8 @@ func handleChannel(conn *ssh.ServerConn, newChan ssh.NewChannel, execHandler []s
338339
assert("sendExit", err)
339340
return
340341
case "pty-req":
341-
// TODO: Handle "shell" which technically comes after "pty-req".
342-
// TODO: Handle "window-change" for pty
343-
// TODO: Parse payload to set initial window dimensions
342+
width, height, okSize := parsePtyRequest(req.Payload)
343+
344344
var cmd *exec.Cmd
345345
if *shell {
346346
cmd = exec.Command(os.Getenv("SHELL"))
@@ -357,17 +357,29 @@ func handleChannel(conn *ssh.ServerConn, newChan ssh.NewChannel, execHandler []s
357357
}
358358
cmd.Env = append(cmd.Env, "USER="+conn.Permissions.Extensions["user"])
359359
}
360-
done, err := attachShell(cmd, stdout, ch)
360+
ptyShell, _, err := attachShell(cmd, stdout, ch)
361361
if assert("attachShell", err) {
362+
ch.Close()
362363
return
363364
}
364-
done.Wait()
365-
_, err = exitStatus(cmd.Wait())
366-
if assert("exitStatus", err) {
367-
return
365+
if okSize {
366+
setWinsize(ptyShell.Fd(), width, height)
367+
req.Reply(true, nil)
368+
}
369+
370+
go func() {
371+
status, err := exitStatus(cmd.Wait())
372+
if !assert("exitStatus", err) {
373+
_, err := ch.SendRequest("exit-status", false, ssh.Marshal(&status))
374+
assert("sendExit", err)
375+
}
376+
ch.Close()
377+
}()
378+
case "window-change":
379+
width, height, okSize := parsePtyRequest(req.Payload)
380+
if okSize {
381+
setWinsize(ptyShell.Fd(), width, height)
368382
}
369-
// FIXME: Do we want to send anything back before closing?
370-
return
371383
}
372384

373385
if req.WantReply {

pty.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package main
2+
3+
import (
4+
"encoding/binary"
5+
"syscall"
6+
"unsafe"
7+
)
8+
9+
//// winsize is borrowed from https://github.com/creack/termios/blob/master/win/win.go
10+
11+
// winsize stores the Heighty and Width of a terminal.
12+
type winsize struct {
13+
Height uint16
14+
Width uint16
15+
x uint16 // unused
16+
y uint16 // unused
17+
}
18+
19+
func setWinsize(fd uintptr, width int, height int) {
20+
ws := &winsize{Width: uint16(width), Height: uint16(height)}
21+
syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws)))
22+
}
23+
24+
//// Helpers below are borrowed from go.crypto circa 2011:
25+
26+
// parsePtyRequest parses the payload of the pty-req message and extracts the
27+
// dimensions of the terminal. See RFC 4254, section 6.2.
28+
func parsePtyRequest(s []byte) (width, height int, ok bool) {
29+
_, s, ok = parseString(s)
30+
if !ok {
31+
return
32+
}
33+
width32, s, ok := parseUint32(s)
34+
if !ok {
35+
return
36+
}
37+
height32, _, ok := parseUint32(s)
38+
width = int(width32)
39+
height = int(height32)
40+
if width < 1 {
41+
ok = false
42+
}
43+
if height < 1 {
44+
ok = false
45+
}
46+
return
47+
}
48+
49+
func parseWinchRequest(s []byte) (width, height int, ok bool) {
50+
width32, s, ok := parseUint32(s)
51+
if !ok {
52+
return
53+
}
54+
height32, s, ok := parseUint32(s)
55+
if !ok {
56+
return
57+
}
58+
59+
width = int(width32)
60+
height = int(height32)
61+
if width < 1 {
62+
ok = false
63+
}
64+
if height < 1 {
65+
ok = false
66+
}
67+
return
68+
}
69+
70+
func parseString(in []byte) (out string, rest []byte, ok bool) {
71+
if len(in) < 4 {
72+
return
73+
}
74+
length := binary.BigEndian.Uint32(in)
75+
if uint32(len(in)) < 4+length {
76+
return
77+
}
78+
out = string(in[4 : 4+length])
79+
rest = in[4+length:]
80+
ok = true
81+
return
82+
}
83+
84+
func parseUint32(in []byte) (uint32, []byte, bool) {
85+
if len(in) < 4 {
86+
return 0, nil, false
87+
}
88+
return binary.BigEndian.Uint32(in), in[4:], true
89+
}

0 commit comments

Comments
 (0)