Skip to content

Commit ffa145d

Browse files
committed
Initial commit.
0 parents  commit ffa145d

15 files changed

+700
-0
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vendor/

Diff for: Gopkg.lock

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Gopkg.toml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
ignored = ["github.com/spf13/cobra"]
2+
3+
[[constraint]]
4+
branch = "master"
5+
name = "github.com/ftrvxmtrx/fd"
6+
7+
[[constraint]]
8+
branch = "master"
9+
name = "github.com/problame/go-rwccmd"
10+

Diff for: LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 Christian Schwarz
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See `example/` and https://godoc.org/github.com/problame/go-netssh

Diff for: context.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package netssh
2+
3+
import (
4+
"context"
5+
)
6+
7+
type contextKey int
8+
9+
const (
10+
contextKeyLog contextKey = iota
11+
)
12+
13+
type Logger interface {
14+
Printf(format string, args ...interface{})
15+
}
16+
17+
type discardLog struct {}
18+
func (d discardLog) Printf(format string, args ...interface{}) {}
19+
20+
func contextLog(ctx context.Context) (log Logger) {
21+
log, ok := ctx.Value(contextKeyLog).(Logger)
22+
if !ok {
23+
log = discardLog{}
24+
}
25+
return log
26+
}
27+
28+
func ContextWithLog(ctx context.Context, log Logger) context.Context {
29+
return context.WithValue(ctx, contextKeyLog, log)
30+
}

Diff for: dial.go

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package netssh
2+
3+
import (
4+
"net"
5+
"fmt"
6+
"context"
7+
"io"
8+
"bytes"
9+
"github.com/problame/go-rwccmd"
10+
)
11+
12+
type Endpoint struct {
13+
Host string
14+
User string
15+
Port uint16
16+
IdentityFile string
17+
SSHCommand string
18+
Options []string
19+
}
20+
21+
func (e Endpoint) CmdArgs() (cmd string, args []string, env []string) {
22+
23+
if e.SSHCommand != "" {
24+
cmd = e.SSHCommand
25+
} else {
26+
cmd = "ssh"
27+
}
28+
29+
args = make([]string, 0, 2*len(e.Options)+4)
30+
args = append(args,
31+
"-p", fmt.Sprintf("%d", e.Port),
32+
"-q",
33+
"-i", e.IdentityFile,
34+
"-o", "BatchMode=yes",
35+
)
36+
for _, option := range e.Options {
37+
args = append(args, "-o", option)
38+
}
39+
args = append(args, fmt.Sprintf("%s@%s", e.User, e.Host))
40+
41+
env = []string{}
42+
43+
return
44+
}
45+
46+
// FIXME: should conform to net.Conn one day, but deadlines as required by net.Conn are complicated:
47+
// it requires to keep the connection open when the deadline is exceeded, but rwcconn.Cmd does not provide Deadlines
48+
// for good reason, see their docs for details.
49+
type SSHConn struct {
50+
c *rwccmd.Cmd
51+
}
52+
53+
const go_network string = "SSH"
54+
55+
type addr struct {
56+
pid int
57+
}
58+
59+
func (a addr) Network() string {
60+
return go_network
61+
}
62+
63+
func (a addr) String() string {
64+
return fmt.Sprintf("pid=%d", a.pid)
65+
}
66+
67+
func (conn *SSHConn) LocalAddr() net.Addr {
68+
return addr{conn.c.Pid()}
69+
}
70+
71+
func (conn *SSHConn) RemoteAddr() net.Addr {
72+
return addr{conn.c.Pid()}
73+
}
74+
75+
func (conn *SSHConn) Read(p []byte) (int, error) {
76+
return conn.c.Read(p)
77+
}
78+
79+
func (conn *SSHConn) Write(p []byte) (int, error) {
80+
return conn.c.Write(p)
81+
}
82+
83+
func (conn *SSHConn) Close() (error) {
84+
return conn.c.Close()
85+
}
86+
87+
// Use at your own risk...
88+
func (conn *SSHConn) Cmd() *rwccmd.Cmd {
89+
return conn.c
90+
}
91+
92+
const bannerMessageLen = 31
93+
var messages = make(map[string][]byte)
94+
func mustMessage(str string) []byte {
95+
if len(str) > bannerMessageLen {
96+
panic("message length must be smaller than bannerMessageLen")
97+
}
98+
if _, ok := messages[str]; ok {
99+
panic("duplicate message")
100+
}
101+
var buf bytes.Buffer
102+
n, _ := buf.WriteString(str)
103+
if n != len(str) {
104+
panic("message must only contain ascii / 8-bit chars")
105+
}
106+
buf.Write(bytes.Repeat([]byte{0}, bannerMessageLen-n))
107+
return buf.Bytes()
108+
}
109+
var banner_msg = mustMessage("SSHCON_HELO")
110+
var proxy_error_msg = mustMessage("SSHCON_PROXY_ERROR")
111+
var begin_msg = mustMessage("SSHCON_BEGIN")
112+
113+
func Dial(ctx context.Context, endpoint Endpoint) (*SSHConn , error) {
114+
115+
sshCmd, sshArgs, sshEnv := endpoint.CmdArgs()
116+
cmd, err := rwccmd.CommandContext(ctx, sshCmd, sshArgs, sshEnv)
117+
if err != nil {
118+
return nil, err
119+
}
120+
if err = cmd.Start(); err != nil {
121+
return nil, err
122+
}
123+
124+
confErrChan := make(chan error)
125+
go func() {
126+
var buf bytes.Buffer
127+
if _, err := io.CopyN(&buf, cmd, int64(len(banner_msg))); err != nil {
128+
confErrChan <- fmt.Errorf("error reading banner: %s", err)
129+
return
130+
}
131+
resp := buf.Bytes()
132+
switch {
133+
case bytes.Equal(resp, banner_msg):
134+
break
135+
case bytes.Equal(resp, proxy_error_msg):
136+
confErrChan <- fmt.Errorf("proxy error, check remote configuration")
137+
return
138+
default:
139+
confErrChan <- fmt.Errorf("unknown banner message: %v", resp)
140+
return
141+
}
142+
buf.Reset()
143+
buf.Write(begin_msg)
144+
if _, err := io.Copy(cmd, &buf); err != nil {
145+
confErrChan <- fmt.Errorf("error sending begin message: %s", err)
146+
return
147+
}
148+
close(confErrChan)
149+
}()
150+
151+
select {
152+
case <-ctx.Done():
153+
return nil, ctx.Err()
154+
case err := <-confErrChan:
155+
if err != nil {
156+
return nil, err
157+
}
158+
}
159+
160+
return &SSHConn{cmd}, err
161+
}

Diff for: example/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
exampleident
2+
exampleident.pub
3+
example

Diff for: example/README.md

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# How to run the example
2+
3+
```
4+
cd $GOPATH/github.com/problame/go-netssh/example
5+
go build
6+
# confirm the following questions with empty passphrase
7+
ssh-keygen -t ed25519 -f exampleident
8+
PUBKEY=$(cat exampleident.pub)
9+
ENTRY="command=\"$(pwd)/example proxy --log /tmp/netssh_proxy.log\",restrict $PUBKEY"
10+
echo
11+
echo "Add the following line to your authorized_keys file: "
12+
echo
13+
echo $ENTRY
14+
```
15+
16+
Then, in one terminal, run
17+
18+
```
19+
./example serve
20+
```
21+
22+
And in another
23+
24+
```
25+
touch /tmp/netssh_proxy.log
26+
tail -f
27+
```
28+
29+
And in another
30+
31+
```
32+
./example connect --ssh.user $(id -nu) --ssh.identity exampleident --ssh.host localhost
33+
```
34+
35+
You should see log activity in all 3 terminals.

Diff for: example/cmd/connect.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package cmd
2+
3+
import (
4+
5+
"github.com/spf13/cobra"
6+
"log"
7+
"io"
8+
"io/ioutil"
9+
"time"
10+
"context"
11+
"os"
12+
"github.com/problame/go-netssh"
13+
"math"
14+
"github.com/problame/go-rwccmd"
15+
)
16+
17+
var connectArgs struct {
18+
killSSHDuration time.Duration
19+
waitBeforeRequestDuration time.Duration
20+
responseTimeout time.Duration
21+
endpoint netssh.Endpoint
22+
23+
}
24+
25+
var connectCmd = &cobra.Command{
26+
Use: "connect",
27+
Short: "connect to server over SSH using proxy",
28+
Run: func(cmd *cobra.Command, args []string) {
29+
30+
log := log.New(os.Stdout, "", log.Ltime|log.Lmicroseconds|log.Lshortfile)
31+
32+
ctx := netssh.ContextWithLog(context.TODO(), log)
33+
ctx = rwccmd.ContextWithLog(ctx, log)
34+
outstream, err := netssh.Dial(ctx, connectArgs.endpoint)
35+
if err != nil {
36+
log.Panic(err)
37+
}
38+
defer func() {
39+
log.Printf("closing connection in defer")
40+
err := outstream.Close()
41+
if err != nil {
42+
log.Printf("error closing connection in defer: %s", err)
43+
}
44+
}()
45+
46+
if connectArgs.killSSHDuration != 0 {
47+
go func(){
48+
time.Sleep(connectArgs.killSSHDuration)
49+
log.Printf("killing ssh process")
50+
if err := outstream.Cmd().Kill(); err != nil {
51+
log.Printf("error killing ssh process: %s", err)
52+
}
53+
}()
54+
}
55+
56+
time.Sleep(connectArgs.waitBeforeRequestDuration)
57+
58+
var dl time.Time
59+
if connectArgs.responseTimeout > 0 {
60+
dl = time.Now().Add(connectArgs.responseTimeout)
61+
} else {
62+
dl = time.Time{}
63+
}
64+
outstream.Cmd().CloseAtDeadline(dl)
65+
66+
log.Print("writing request")
67+
n, err := outstream.Write([]byte("b\n"))
68+
if n != 2 || err != nil {
69+
log.Panic(err)
70+
}
71+
log.Print("read response")
72+
_, err = io.CopyN(ioutil.Discard, outstream, int64(Bytecount))
73+
if err != nil {
74+
log.Panic(err)
75+
}
76+
log.Print("writing close request")
77+
n, err = outstream.Write([]byte("a\n"))
78+
if n != 2 || err != nil {
79+
log.Panic(err)
80+
}
81+
},
82+
}
83+
84+
func init() {
85+
RootCmd.AddCommand(connectCmd)
86+
connectCmd.Flags().DurationVar(&connectArgs.killSSHDuration, "killSSH",0, "")
87+
connectCmd.Flags().DurationVar(&connectArgs.waitBeforeRequestDuration, "wait",0, "")
88+
connectCmd.Flags().DurationVar(&connectArgs.responseTimeout, "responseTimeout",math.MaxInt64, "")
89+
connectCmd.Flags().StringVar(&connectArgs.endpoint.Host, "ssh.host", "", "")
90+
connectCmd.Flags().StringVar(&connectArgs.endpoint.User, "ssh.user", "", "")
91+
connectCmd.Flags().StringVar(&connectArgs.endpoint.IdentityFile, "ssh.identity", "", "")
92+
connectCmd.Flags().Uint16Var(&connectArgs.endpoint.Port, "ssh.port", 22, "")
93+
}

0 commit comments

Comments
 (0)