Skip to content

Commit b00824f

Browse files
authored
Merge pull request #1 from caarlos0/safe-writer
feat: replace newlines with crlf newlines when in a pty
2 parents 3f61eab + fd0cfe4 commit b00824f

File tree

6 files changed

+128
-28
lines changed

6 files changed

+128
-28
lines changed

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ go 1.12
55
require (
66
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
77
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
8-
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
98
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
109
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
1110
)

go.sum

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
11
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
22
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
3-
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
4-
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
53
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
64
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
7-
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
85
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
9-
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
10-
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
116
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
127
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
138
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14-
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
15-
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
16-
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
179
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
1810
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
19-
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
2011
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
21-
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
2212
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
2313
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
24-
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
2514
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
26-
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
27-
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
2815
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

pty.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package ssh
2+
3+
import (
4+
"bytes"
5+
"io"
6+
)
7+
8+
// NewPtyWriter creates a writer that handles when the session has a active
9+
// PTY, replacing the \n with \r\n.
10+
func NewPtyWriter(w io.Writer) io.Writer {
11+
return ptyWriter{
12+
w: w,
13+
}
14+
}
15+
16+
var _ io.Writer = ptyWriter{}
17+
18+
type ptyWriter struct {
19+
w io.Writer
20+
}
21+
22+
func (w ptyWriter) Write(p []byte) (int, error) {
23+
m := len(p)
24+
// normalize \n to \r\n when pty is accepted.
25+
// this is a hardcoded shortcut since we don't support terminal modes.
26+
p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1)
27+
p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1)
28+
n, err := w.w.Write(p)
29+
if n > m {
30+
n = m
31+
}
32+
return n, err
33+
}
34+
35+
// NewPtyReadWriter return an io.ReadWriter that delegates the read to the
36+
// given io.ReadWriter, and the writes to a ptyWriter.
37+
func NewPtyReadWriter(rw io.ReadWriter) io.ReadWriter {
38+
return readWriterDelegate{
39+
w: NewPtyWriter(rw),
40+
r: rw,
41+
}
42+
}
43+
44+
var _ io.ReadWriter = readWriterDelegate{}
45+
46+
type readWriterDelegate struct {
47+
w io.Writer
48+
r io.Reader
49+
}
50+
51+
func (rw readWriterDelegate) Read(p []byte) (n int, err error) {
52+
return rw.r.Read(p)
53+
}
54+
55+
func (rw readWriterDelegate) Write(p []byte) (n int, err error) {
56+
return rw.w.Write(p)
57+
}

pty_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package ssh_test
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/gliderlabs/ssh"
8+
)
9+
10+
func TestNewPtyWriter(t *testing.T) {
11+
in := "\nfoo\r\nbar\nmore text\rmore\r\r\r\nfoo\n\n"
12+
out := "\r\nfoo\r\nbar\r\nmore text\rmore\r\r\r\nfoo\r\n\r\n"
13+
var b bytes.Buffer
14+
n, err := ssh.NewPtyWriter(&b).Write([]byte(in))
15+
if err != nil {
16+
t.Error("did not expect an error", err)
17+
}
18+
if out != b.String() {
19+
t.Errorf("outputs do not match, expected %q got %q", out, b.String())
20+
}
21+
if n != len(in) {
22+
t.Errorf("expected to write %d bytes, wrote %d", len(in), n)
23+
}
24+
}

session.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package ssh
22

33
import (
4-
"bytes"
54
"errors"
65
"fmt"
6+
"io"
77
"net"
88
"sync"
99

@@ -82,6 +82,12 @@ type Session interface {
8282
// the request handling loop. Registering nil will unregister the channel.
8383
// During the time that no channel is registered, breaks are ignored.
8484
Break(c chan<- bool)
85+
86+
// Stderr returns an io.ReadWriter that writes to this channel
87+
// with the extended data type set to stderr. Stderr may
88+
// safely be read and written from a different goroutine than
89+
// Read and Write respectively.
90+
Stderr() io.ReadWriter
8591
}
8692

8793
// maxSigBufSize is how many signals will be buffered
@@ -127,18 +133,16 @@ type session struct {
127133
breakCh chan<- bool
128134
}
129135

130-
func (sess *session) Write(p []byte) (n int, err error) {
136+
func (sess *session) Stderr() io.ReadWriter {
131137
if sess.pty != nil {
132-
m := len(p)
133-
// normalize \n to \r\n when pty is accepted.
134-
// this is a hardcoded shortcut since we don't support terminal modes.
135-
p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1)
136-
p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1)
137-
n, err = sess.Channel.Write(p)
138-
if n > m {
139-
n = m
140-
}
141-
return
138+
return NewPtyReadWriter(sess.Channel.Stderr())
139+
}
140+
return sess.Channel.Stderr()
141+
}
142+
143+
func (sess *session) Write(p []byte) (int, error) {
144+
if sess.pty != nil {
145+
return NewPtyWriter(sess.Channel).Write(p)
142146
}
143147
return sess.Channel.Write(p)
144148
}
@@ -242,7 +246,7 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
242246
continue
243247
}
244248

245-
var payload = struct{ Value string }{}
249+
payload := struct{ Value string }{}
246250
gossh.Unmarshal(req.Payload, &payload)
247251
sess.rawCmd = payload.Value
248252

@@ -267,7 +271,7 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
267271
continue
268272
}
269273

270-
var payload = struct{ Value string }{}
274+
payload := struct{ Value string }{}
271275
gossh.Unmarshal(req.Payload, &payload)
272276
sess.subsystem = payload.Value
273277

session_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"net"
88
"testing"
9+
"time"
910

1011
gossh "golang.org/x/crypto/ssh"
1112
)
@@ -228,6 +229,34 @@ func TestPty(t *testing.T) {
228229
<-done
229230
}
230231

232+
func TestPtyWriter(t *testing.T) {
233+
t.Parallel()
234+
term := "xterm"
235+
winWidth := 40
236+
winHeight := 80
237+
session, _, cleanup := newTestSession(t, &Server{
238+
Handler: func(s Session) {
239+
_, _ = fmt.Fprintln(s, "foo\nbar")
240+
time.Sleep(10 * time.Millisecond)
241+
_, _ = fmt.Fprintln(s.Stderr(), "many\nerrors")
242+
_ = s.Exit(0)
243+
},
244+
}, nil)
245+
defer cleanup()
246+
if err := session.RequestPty(term, winHeight, winWidth, gossh.TerminalModes{}); err != nil {
247+
t.Fatalf("expected nil but got %v", err)
248+
}
249+
bts, err := session.CombinedOutput("")
250+
if err != nil {
251+
t.Fatalf("expected nil but got %v", err)
252+
}
253+
254+
expected := "foo\r\nbar\r\nmany\r\nerrors\r\n"
255+
if expected != string(bts) {
256+
t.Fatalf("expected output to be %q, got %q", expected, string(bts))
257+
}
258+
}
259+
231260
func TestPtyResize(t *testing.T) {
232261
t.Parallel()
233262
winch0 := Window{40, 80}

0 commit comments

Comments
 (0)