Skip to content

Commit 6a47ae9

Browse files
committed
First impl
1 parent 35f8bf0 commit 6a47ae9

File tree

4 files changed

+285
-0
lines changed

4 files changed

+285
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## TODO
2+
3+
* [ ] Support authentication to sshd
4+
* [ ] Rate limiting and limite the num of connections
5+
* [ ] readline type stuff with tab completion, etc

local.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2016 Heptio Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sshexpose
16+
17+
import (
18+
"bufio"
19+
"io"
20+
"os"
21+
)
22+
23+
type localTerm struct {
24+
scanner *bufio.Scanner
25+
}
26+
27+
func (t *localTerm) ReadLine() (line string, err error) {
28+
more := t.scanner.Scan()
29+
if !more {
30+
if t.scanner.Err() != nil {
31+
return "", t.scanner.Err()
32+
}
33+
return "", io.EOF
34+
}
35+
return t.scanner.Text(), nil
36+
}
37+
38+
func (t *localTerm) StdOut() io.Writer {
39+
return os.Stdout
40+
}
41+
42+
func (t *localTerm) StdErr() io.Writer {
43+
return os.Stdout
44+
}
45+
46+
func (t *localTerm) Close() {}
47+
48+
// ServeLocal will serve an interactive CLI locally. This takes care of
49+
// providing a line based input method.
50+
func ServeLocal(icli ServeICLI) {
51+
t := localTerm{
52+
scanner: bufio.NewScanner(os.Stdin),
53+
}
54+
icli(&t)
55+
}

sshd.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2016 Heptio Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sshexpose
16+
17+
import (
18+
"fmt"
19+
"io"
20+
"io/ioutil"
21+
"log"
22+
"net"
23+
24+
"github.com/pkg/errors"
25+
"golang.org/x/crypto/ssh"
26+
"golang.org/x/crypto/ssh/terminal"
27+
)
28+
29+
type sshTerm struct {
30+
t *terminal.Terminal
31+
c ssh.Channel
32+
}
33+
34+
func (t *sshTerm) ReadLine() (line string, err error) {
35+
return t.t.ReadLine()
36+
}
37+
38+
func (t *sshTerm) StdOut() io.Writer {
39+
return t.t
40+
}
41+
42+
func (t *sshTerm) StdErr() io.Writer {
43+
return t.t
44+
}
45+
46+
func (t *sshTerm) Close() {
47+
t.c.Close()
48+
}
49+
50+
// SSHOptions defines the parameters for serving a REPL over SSH
51+
type SSHOptions struct {
52+
// ICLI defines the interactive CLI you want to serve
53+
ICLI ServeICLI
54+
55+
// Addr is an IPv4 IP:port pair that you wish to serve on
56+
Addr string
57+
58+
// PrivateHostKey is the host key to present when serving SSH.
59+
//
60+
// This can be generated with `ssh-keygen -t rsa -f my_host_key_rsa`
61+
PrivateHostKey string
62+
}
63+
64+
type sshServer struct {
65+
icli ServeICLI
66+
}
67+
68+
// ServeSSH will run an SSH server that accepts connections.
69+
//
70+
// Each incoming SSH session will result in a call to options.REPL.
71+
func ServeSSH(options SSHOptions) error {
72+
srv := &sshServer{
73+
icli: options.ICLI,
74+
}
75+
76+
log.Printf("Serving SSH on %v\n", options.Addr)
77+
config := &ssh.ServerConfig{
78+
NoClientAuth: true,
79+
}
80+
81+
privateBytes, err := ioutil.ReadFile(options.PrivateHostKey)
82+
if err != nil {
83+
return errors.Wrapf(err, "Failed to load private key (%s)", options.PrivateHostKey)
84+
}
85+
86+
private, err := ssh.ParsePrivateKey(privateBytes)
87+
if err != nil {
88+
return errors.Wrapf(err, "Failed to parse private key (%s)", options.PrivateHostKey)
89+
}
90+
91+
config.AddHostKey(private)
92+
93+
// Once a ServerConfig has been configured, connections can be accepted.
94+
listener, err := net.Listen("tcp", options.Addr)
95+
if err != nil {
96+
return errors.Wrapf(err, "Failed to listen on %s", options.Addr)
97+
}
98+
99+
for {
100+
tcpConn, err := listener.Accept()
101+
if err != nil {
102+
log.Printf("Failed to accept incoming connection (%s)", err)
103+
continue
104+
}
105+
// Before use, a handshake must be performed on the incoming net.Conn.
106+
sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, config)
107+
if err != nil {
108+
log.Printf("Failed to handshake (%s)", err)
109+
continue
110+
}
111+
112+
log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
113+
// Discard all global out-of-band Requests
114+
go ssh.DiscardRequests(reqs)
115+
// Accept all channels
116+
go srv.handleChannels(chans)
117+
}
118+
}
119+
120+
func (srv *sshServer) handleChannels(chans <-chan ssh.NewChannel) {
121+
// Service the incoming Channel channel in go routine
122+
for newChannel := range chans {
123+
go srv.handleChannel(newChannel)
124+
}
125+
}
126+
127+
type ptyReqMsg struct {
128+
Term string
129+
CharWidth uint32
130+
CharHeight uint32
131+
PixelWidth uint32
132+
PixelHeight uint32
133+
Modes string
134+
}
135+
136+
type windowChangeMsg struct {
137+
CharWidth uint32
138+
CharHeight uint32
139+
PixelWidth uint32
140+
PixelHeight uint32
141+
}
142+
143+
func (srv *sshServer) handleChannel(newChannel ssh.NewChannel) {
144+
// Since we're handling a shell, we expect a
145+
// channel type of "session". The also describes
146+
// "x11", "direct-tcpip" and "forwarded-tcpip"
147+
// channel types.
148+
if t := newChannel.ChannelType(); t != "session" {
149+
newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t))
150+
return
151+
}
152+
153+
connection, requests, err := newChannel.Accept()
154+
if err != nil {
155+
log.Printf("Could not accept channel (%s)", err)
156+
return
157+
}
158+
defer connection.Close()
159+
160+
t := &sshTerm{
161+
t: terminal.NewTerminal(connection, ""),
162+
c: connection,
163+
}
164+
165+
// Sessions have out-of-band requests such as "shell",
166+
// "pty-req" and "env". Here we handle only the
167+
// "shell" request.
168+
go func(in <-chan *ssh.Request) {
169+
for req := range in {
170+
switch req.Type {
171+
case "shell":
172+
req.Reply(true, nil)
173+
case "pty-req":
174+
var ptyReq ptyReqMsg
175+
err := ssh.Unmarshal(req.Payload, &ptyReq)
176+
if err != nil {
177+
log.Printf("Error parsing 'pty-req' payload")
178+
req.Reply(false, nil)
179+
}
180+
t.t.SetSize(int(ptyReq.CharWidth), int(ptyReq.CharHeight))
181+
req.Reply(true, nil)
182+
case "window-change":
183+
var windowChange windowChangeMsg
184+
err := ssh.Unmarshal(req.Payload, &windowChange)
185+
if err != nil {
186+
log.Printf("Error parsing 'window-change' payload")
187+
}
188+
t.t.SetSize(int(windowChange.CharWidth), int(windowChange.CharHeight))
189+
}
190+
}
191+
}(requests)
192+
193+
srv.icli(t)
194+
}

term.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2016 Heptio Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sshexpose
16+
17+
import "io"
18+
19+
// Term is a simple terminal abstraction.
20+
//
21+
// This is what programs should use for all input/output with a user. It will
22+
// work equally well over SSH or the local terminal.
23+
type Term interface {
24+
ReadLine() (line string, err error)
25+
StdOut() io.Writer
26+
StdErr() io.Writer
27+
Close()
28+
}
29+
30+
// ServeICLI should implement interactive CLI
31+
type ServeICLI func(t Term)

0 commit comments

Comments
 (0)