Adding fully functional sshrpc code for basic use cases
This commit is contained in:
parent
e7a07d46cd
commit
fb9c587be4
73
client.go
Normal file
73
client.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package sshrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/rpc"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sshrpcSession struct {
|
||||||
|
*ssh.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sshrpcSession) Read(p []byte) (n int, err error) {
|
||||||
|
pipe, err := s.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return pipe.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sshrpcSession) Write(p []byte) (n int, err error) {
|
||||||
|
pipe, err := s.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return pipe.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
*rpc.Client
|
||||||
|
Config *ssh.ClientConfig
|
||||||
|
Subsystem string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient() *Client {
|
||||||
|
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
User: "test",
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.Password("test"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{nil, config, "sshrpc"}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Connect(address string) {
|
||||||
|
|
||||||
|
sshClient, err := ssh.Dial("tcp", address, c.Config)
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to dial: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each ClientConn can support multiple interactive sessions,
|
||||||
|
// represented by a Session.
|
||||||
|
sshSession, err := sshClient.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to create session: " + err.Error())
|
||||||
|
}
|
||||||
|
//defer sshSession.Close()
|
||||||
|
|
||||||
|
err = sshSession.RequestSubsystem(c.Subsystem)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Unable to start subsystem:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
session := sshrpcSession{sshSession}
|
||||||
|
c.Client = rpc.NewClient(session)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
4
doc.go
Normal file
4
doc.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
Package sshrpc provides RPC access over SSH. It uses the built-in RPC(net/rpc) library, so no RPC methods need to be rewritten.
|
||||||
|
*/
|
||||||
|
package sshrpc
|
47
example_test.go
Normal file
47
example_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package sshrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExampleServer struct{}
|
||||||
|
|
||||||
|
func (s *ExampleServer) Hello(name *string, out *string) error {
|
||||||
|
*out = fmt.Sprintf("Hello %s", *name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleServer_StartServer() {
|
||||||
|
|
||||||
|
s := NewServer()
|
||||||
|
privateBytes, err := ioutil.ReadFile("id_rsa")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to load private key (./id_rsa)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private, err := ssh.ParsePrivateKey(privateBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to parse private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Config.AddHostKey(private)
|
||||||
|
s.Register(new(ExampleServer))
|
||||||
|
s.StartServer("localhost:2022")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Connect() {
|
||||||
|
|
||||||
|
client := NewClient()
|
||||||
|
client.Connect("localhost:2022")
|
||||||
|
defer client.Close()
|
||||||
|
var reply string
|
||||||
|
err := client.Call("ExampleServer.Hello", "Example Name", &reply)
|
||||||
|
if err != nil {
|
||||||
|
//Handle Error
|
||||||
|
}
|
||||||
|
fmt.Println(reply)
|
||||||
|
}
|
120
server.go
Normal file
120
server.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package sshrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/rpc"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
*rpc.Server
|
||||||
|
Config *ssh.ServerConfig
|
||||||
|
Subsystem string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer() *Server {
|
||||||
|
c := &ssh.ServerConfig{
|
||||||
|
// NoClientAuth: true,
|
||||||
|
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||||
|
// Should use constant-time compare (or better, salt+hash) in a production setting.
|
||||||
|
if c.User() == "test" && string(pass) == "test" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("password rejected for %q", c.User())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &Server{rpc.NewServer(), c, "sshrpc"}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) StartServer(address string) {
|
||||||
|
|
||||||
|
// Once a ServerConfig has been configured, connections can be accepted.
|
||||||
|
listener, err := net.Listen("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to listen on ", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept all connections
|
||||||
|
log.Print("listening on ", address)
|
||||||
|
for {
|
||||||
|
tcpConn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to accept incoming connection (%s)", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Before use, a handshake must be performed on the incoming net.Conn.
|
||||||
|
sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, s.Config)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to handshake (%s)", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("new ssh connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
|
||||||
|
// Print incoming out-of-band Requests
|
||||||
|
go s.handleRequests(reqs)
|
||||||
|
// Accept all channels
|
||||||
|
go s.handleChannels(chans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleRequests(reqs <-chan *ssh.Request) {
|
||||||
|
for req := range reqs {
|
||||||
|
log.Printf("recieved out-of-band request: %+v", req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleChannels(chans <-chan ssh.NewChannel) {
|
||||||
|
// Service the incoming Channel channel.
|
||||||
|
for newChannel := range chans {
|
||||||
|
// Channels have a type, depending on the application level
|
||||||
|
// protocol intended. In the case of a shell, the type is
|
||||||
|
// "session" and ServerShell may be used to present a simple
|
||||||
|
// terminal interface.
|
||||||
|
if t := newChannel.ChannelType(); t != "session" {
|
||||||
|
newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
channel, requests, err := newChannel.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not accept channel (%s)", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
close := func() {
|
||||||
|
channel.Close()
|
||||||
|
log.Printf("session closed")
|
||||||
|
}
|
||||||
|
defer close()
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Sessions have out-of-band requests such as "shell", "pty-req" and "env"
|
||||||
|
go func(in <-chan *ssh.Request) {
|
||||||
|
for req := range in {
|
||||||
|
ok := false
|
||||||
|
switch req.Type {
|
||||||
|
|
||||||
|
case "subsystem":
|
||||||
|
ok = true
|
||||||
|
log.Printf("subsystem '%s'", req.Payload)
|
||||||
|
switch string(req.Payload[4:]) {
|
||||||
|
case s.Subsystem:
|
||||||
|
go s.ServeConn(channel)
|
||||||
|
log.Printf("Started SSH RPC")
|
||||||
|
default:
|
||||||
|
log.Printf("Unknown subsystem: %s", req.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
log.Printf("declining %s request...", req.Type)
|
||||||
|
}
|
||||||
|
req.Reply(ok, nil)
|
||||||
|
}
|
||||||
|
}(requests)
|
||||||
|
}
|
||||||
|
}
|
101
server_test.go
Normal file
101
server_test.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package sshrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dev.justinjudd.org/justin/sshrpc/testdata"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SimpleServer struct{}
|
||||||
|
|
||||||
|
func (s *SimpleServer) Hello(name *string, out *string) error {
|
||||||
|
log.Println("Name: ", *name)
|
||||||
|
*out = fmt.Sprintf("Hello %s", *name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleServer(t *testing.T) {
|
||||||
|
s := NewServer()
|
||||||
|
|
||||||
|
private, err := ssh.ParsePrivateKey(testdata.ServerRSAKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to parse private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Config.AddHostKey(private)
|
||||||
|
|
||||||
|
s.Register(new(SimpleServer))
|
||||||
|
t.Log("preparing server")
|
||||||
|
|
||||||
|
go s.StartServer("localhost:2022")
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
t.Log("preparing client")
|
||||||
|
client := NewClient()
|
||||||
|
client.Connect("localhost:2022")
|
||||||
|
defer client.Close()
|
||||||
|
var reply string
|
||||||
|
err = client.Call("SimpleServer.Hello", "Test Name", &reply)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to make rpc call: %s", err.Error())
|
||||||
|
}
|
||||||
|
log.Println("Reply: ", reply)
|
||||||
|
if reply != "Hello Test Name" {
|
||||||
|
t.Errorf("Simple Server Test Failed: Expected 'Hello Test Name', Recieved: '%s'", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdvancedServer struct{}
|
||||||
|
|
||||||
|
type AdvancedType struct {
|
||||||
|
Ints []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AdvancedServer) SortInts(req *AdvancedType, out *AdvancedType) error {
|
||||||
|
sort.Ints(req.Ints)
|
||||||
|
*out = *req
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdvancedServer(t *testing.T) {
|
||||||
|
s := NewServer()
|
||||||
|
s.Subsystem = "Advanced"
|
||||||
|
|
||||||
|
private, err := ssh.ParsePrivateKey(testdata.ServerRSAKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to parse private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Config.AddHostKey(private)
|
||||||
|
|
||||||
|
s.Register(new(AdvancedServer))
|
||||||
|
t.Log("preparing server")
|
||||||
|
|
||||||
|
go s.StartServer("localhost:3022")
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
t.Log("preparing client")
|
||||||
|
client := NewClient()
|
||||||
|
client.Subsystem = "Advanced"
|
||||||
|
client.Connect("localhost:3022")
|
||||||
|
defer client.Close()
|
||||||
|
var reply AdvancedType
|
||||||
|
unsorted := AdvancedType{[]int{1, 3, 5, 7, 2, 4, 6}}
|
||||||
|
err = client.Call("AdvancedServer.SortInts", unsorted, &reply)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to make rpc call: %s", err.Error())
|
||||||
|
}
|
||||||
|
log.Println("Reply: ", reply)
|
||||||
|
if !sort.IntsAreSorted(reply.Ints) {
|
||||||
|
t.Errorf("Advanced Server Test Failed: Expected '{1,2,3,4,5,6,7}', Recieved: '%v'", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
30
testdata/keys.go
vendored
Normal file
30
testdata/keys.go
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package testdata
|
||||||
|
|
||||||
|
var ServerRSAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKCAQEA53Phba8WOqAG0cgtlQjBFq8Uv3rauuxTgATiAcHOYsNendNL
|
||||||
|
svlzBx8syC6gfePksn4o06YcnBx9P7bZ5TF7H41Smwc+IQ6WNuxCvDFTvkaWGkLH
|
||||||
|
gP7QFd2HOW6UtGZEij6KWXq13TWmKgKPBvYE8ZF2U1ogOgZafGLHda6PT+D3cwvq
|
||||||
|
4skdr4nI/VFZ1qAQ4Necxomubs/p+KBA31aaxIKkOrt7tXofiY0Ht1pk1p6iMr5q
|
||||||
|
wLiumUBaIvHi8jZ4GFTEq+wAy0T6qKzkURIRLULQ+xweUKgpYEnK3SwHCx9nUOLv
|
||||||
|
pq7lIIDc0/tXm+bKn137aZ1HpMw8hk3FLAv4lQIDAQABAoIBAQDB8luzcqUY6SHt
|
||||||
|
xmVT3msW3A1xyrvhKMlrnCvatxF/tyDg9b8YMWmPTTNUZ6X69+2bGJaTjD2rH0Bh
|
||||||
|
FJMQOF9o+B6BZBdO0g9T5LSjSF5ZMoLzbIQK9vtdZy26FYysgonqoi+CfY5J2Hvt
|
||||||
|
9gjuF1fbKT5S6FHa7ZoDYy6q0WSr1ErI/aCOzpsSLohGv4RuC84qLorSkAqRIJpm
|
||||||
|
8BTs5m7PGx9lhI4LIDS5C3Oda0fml4Z4IVD9hpmAs635cbTMOKcA1CLEAlVJYIrN
|
||||||
|
CSOA3cqGW2nk8dPpP0P2FA3IWG+UbNc38OVkupmAQqNZ7kDxFueYbwj61jVYK4CS
|
||||||
|
t+BQrNsBAoGBAPprbDqd/EKg4EzmY2+cgxY4yD6RlLukydeBD6wrniy8e4/QfrU1
|
||||||
|
Rs0gwCk6I2/QOwdoiw1S7mKgA7s8ichJQ67HCoppMMA8RnVZbx/KBuKqYA30EVSQ
|
||||||
|
dHbhm3C8feiNbkc46+vWDZonwlS45Y7MaQKum+14SppN8pscHbAG5MjVAoGBAOyc
|
||||||
|
QZ/nkpAt5rGczmWtUFDtrDrrD+V5ksi8qnB1VTRrySmMMBt1pnC6JN87HSwk4hZG
|
||||||
|
1pu3tyktmqkHcKqU+K6n3TpNTH3dRuVGWOZ1ib8Vt25nd911qGTvElpZfDLW5Njl
|
||||||
|
+AsmPXDUOYxEFLQaKBWAvyOYp3c7BDvhQHaWllDBAoGBANZmDn2JMach8aglQFEY
|
||||||
|
oSrvZpIbNkoJZj4541824O/QV8HjcfhXKs0JEzy46AodL8zB9vtrW2nZMhimVhjC
|
||||||
|
kU4cX6vtL64GbRSfg4KmB4sc76xCoGvUWcJGmjzFRM9L93THCUYbN/4ZuEmtG+1M
|
||||||
|
mUOQlzOTX9wIjIO8aLaC0HIZAoGAKUi+XpM+TG/l37m3faA28lf2BDW9iVGkHehl
|
||||||
|
aMfgPQxNhjVSs4fcqbCg/F5JIcmxtSdZDMSKbeHqKXIF442osnjRrfmMzi1M0HZs
|
||||||
|
zpFVnoTAg8AD9x0va6UXM7KHbCt4tKuzkuZyM/yjqei7IA2sTswvDZv2JGSkwXn1
|
||||||
|
EHwH8EECgYBMQC7w7Lv1DIIFsp5ZmqsGZ8SSLyMtSrZUhnsZ7Z+nX6UNzbnhrw73
|
||||||
|
t57eaAau2wZ0cEg0lDDyE2RsYJTF5riv6SGjE91QElK4mP7TCm/Y2bgv2MLamWa9
|
||||||
|
zbZDpwviYOm7e4Zt2CsTdYQ3RX5ugYXOxQZJdtZVtSViV4NRoxzaDQ==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
`)
|
Loading…
Reference in New Issue
Block a user