Skip to content

Commit 6a8a3ae

Browse files
committed
Merge pull request #3 from shazow/pty-req
Add pty shell support
2 parents fcf92ec + c5193c0 commit 6a8a3ae

File tree

2 files changed

+179
-22
lines changed

2 files changed

+179
-22
lines changed

execd.go

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"code.google.com/p/go.crypto/ssh"
2222
"github.com/flynn/go-shlex"
23+
"github.com/kr/pty"
2324
)
2425

2526
var host = flag.String("h", "", "host ip to listen on")
@@ -49,7 +50,7 @@ func exitStatus(err error) (exitStatusMsg, error) {
4950
return exitStatusMsg{0}, nil
5051
}
5152

52-
func attachCmd(cmd *exec.Cmd, stdout, stderr io.Writer, stdin io.Reader) (*sync.WaitGroup, error) {
53+
func attachCmd(cmd *exec.Cmd, stdout io.Writer, stderr io.Writer, stdin io.Reader) (*sync.WaitGroup, error) {
5354
var wg sync.WaitGroup
5455
wg.Add(2)
5556

@@ -61,6 +62,7 @@ func attachCmd(cmd *exec.Cmd, stdout, stderr io.Writer, stdin io.Reader) (*sync.
6162
go func() {
6263
io.Copy(stdinIn, stdin)
6364
stdinIn.Close()
65+
// FIXME: Do we care that this is not part of the WaitGroup?
6466
}()
6567
}
6668

@@ -85,6 +87,27 @@ func attachCmd(cmd *exec.Cmd, stdout, stderr io.Writer, stdin io.Reader) (*sync.
8587
return &wg, nil
8688
}
8789

90+
func attachShell(cmd *exec.Cmd, stdout io.Writer, stdin io.Reader) (*os.File, *sync.WaitGroup, error) {
91+
var wg sync.WaitGroup
92+
wg.Add(2)
93+
94+
// Note that pty merges stdout and stderr.
95+
cmdPty, err := pty.Start(cmd)
96+
if err != nil {
97+
return nil, nil, err
98+
}
99+
go func() {
100+
io.Copy(stdout, cmdPty)
101+
wg.Done()
102+
}()
103+
go func() {
104+
io.Copy(cmdPty, stdin)
105+
wg.Done()
106+
}()
107+
108+
return cmdPty, &wg, nil
109+
}
110+
88111
func addKey(conf *ssh.ServerConfig, block *pem.Block) (err error) {
89112
var key interface{}
90113
switch block.Type {
@@ -247,18 +270,31 @@ func handleChannel(conn *ssh.ServerConn, newChan ssh.NewChannel, execHandler []s
247270
log.Println("newChan.Accept failed:", err)
248271
return
249272
}
250-
defer ch.Close()
273+
274+
assert := func(at string, err error) bool {
275+
if err != nil {
276+
log.Printf("%s failed: %s", at, err)
277+
ch.Stderr().Write([]byte("Internal error.\n"))
278+
return true
279+
}
280+
return false
281+
}
282+
283+
var stdout, stderr io.Writer
284+
if *debug {
285+
stdout = io.MultiWriter(ch, os.Stdout)
286+
stderr = io.MultiWriter(ch.Stderr(), os.Stdout)
287+
} else {
288+
stdout = ch
289+
stderr = ch.Stderr()
290+
}
291+
292+
var ptyShell *os.File
293+
251294
for req := range reqs {
252295
switch req.Type {
253296
case "exec":
254-
assert := func(at string, err error) bool {
255-
if err != nil {
256-
log.Printf("%s failed: %s", at, err)
257-
ch.Stderr().Write([]byte("Internal error.\n"))
258-
return true
259-
}
260-
return false
261-
}
297+
defer ch.Close()
262298

263299
if req.WantReply {
264300
req.Reply(true, nil)
@@ -287,14 +323,6 @@ func handleChannel(conn *ssh.ServerConn, newChan ssh.NewChannel, execHandler []s
287323
cmd.Env = append(cmd.Env, "USER="+conn.Permissions.Extensions["user"])
288324
}
289325
cmd.Env = append(cmd.Env, "SSH_ORIGINAL_COMMAND="+cmdline)
290-
var stdout, stderr io.Writer
291-
if *debug {
292-
stdout = io.MultiWriter(ch, os.Stdout)
293-
stderr = io.MultiWriter(ch.Stderr(), os.Stdout)
294-
} else {
295-
stdout = ch
296-
stderr = ch.Stderr()
297-
}
298326
done, err := attachCmd(cmd, stdout, stderr, ch)
299327
if assert("attachCmd", err) {
300328
return
@@ -310,12 +338,52 @@ func handleChannel(conn *ssh.ServerConn, newChan ssh.NewChannel, execHandler []s
310338
_, err = ch.SendRequest("exit-status", false, ssh.Marshal(&status))
311339
assert("sendExit", err)
312340
return
313-
case "env":
314-
if req.WantReply {
341+
case "pty-req":
342+
width, height, okSize := parsePtyRequest(req.Payload)
343+
344+
var cmd *exec.Cmd
345+
if *shell {
346+
cmd = exec.Command(os.Getenv("SHELL"))
347+
} else {
348+
cmd = exec.Command(execHandler[0], execHandler[1:]...)
349+
}
350+
if !*env {
351+
cmd.Env = []string{}
352+
}
353+
if conn.Permissions != nil {
354+
// Using Permissions.Extensions as a way to get state from PublicKeyCallback
355+
if conn.Permissions.Extensions["environ"] != "" {
356+
cmd.Env = append(cmd.Env, strings.Split(conn.Permissions.Extensions["environ"], "\n")...)
357+
}
358+
cmd.Env = append(cmd.Env, "USER="+conn.Permissions.Extensions["user"])
359+
}
360+
ptyShell, _, err := attachShell(cmd, stdout, ch)
361+
if assert("attachShell", err) {
362+
ch.Close()
363+
return
364+
}
365+
if okSize {
366+
setWinsize(ptyShell.Fd(), width, height)
315367
req.Reply(true, nil)
316368
}
317-
default:
318-
return
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)
382+
}
383+
}
384+
385+
if req.WantReply {
386+
req.Reply(true, nil)
319387
}
320388
}
321389
}

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)