From fb9c587be4848ab12d12346d5aa8f5e4bd912c68 Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 28 Dec 2014 23:06:09 -0500 Subject: [PATCH] Adding fully functional sshrpc code for basic use cases --- client.go | 73 ++++++++++++++++++++++++++++ doc.go | 4 ++ example_test.go | 47 +++++++++++++++++++ server.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++ server_test.go | 101 +++++++++++++++++++++++++++++++++++++++ testdata/keys.go | 30 ++++++++++++ 6 files changed, 375 insertions(+) create mode 100644 client.go create mode 100644 doc.go create mode 100644 example_test.go create mode 100644 server.go create mode 100644 server_test.go create mode 100644 testdata/keys.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..293cc48 --- /dev/null +++ b/client.go @@ -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 +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..b8e1fb0 --- /dev/null +++ b/doc.go @@ -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 diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..a744180 --- /dev/null +++ b/example_test.go @@ -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) +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..417b421 --- /dev/null +++ b/server.go @@ -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) + } +} diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..be78f11 --- /dev/null +++ b/server_test.go @@ -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) + } + +} diff --git a/testdata/keys.go b/testdata/keys.go new file mode 100644 index 0000000..62a7973 --- /dev/null +++ b/testdata/keys.go @@ -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----- +`)