From 7308727a1c3ceee635908dbd9a0deb8baeb0a53f Mon Sep 17 00:00:00 2001 From: Justin Date: Sat, 30 May 2015 11:19:10 -0400 Subject: [PATCH] Adding current work on easyssh --- client.go | 146 +++++++++++++++++++ common.go | 292 +++++++++++++++++++++++++++++++++++++ doc.go | 56 +++++++ example_client_test.go | 63 ++++++++ example_handler_test.go | 29 ++++ example_tcpip_test.go | 46 ++++++ example_test.go | 66 +++++++++ server.go | 157 ++++++++++++++++++++ session.go | 313 ++++++++++++++++++++++++++++++++++++++++ tcpip.go | 173 ++++++++++++++++++++++ 10 files changed, 1341 insertions(+) create mode 100644 client.go create mode 100644 common.go create mode 100644 doc.go create mode 100644 example_client_test.go create mode 100644 example_handler_test.go create mode 100644 example_tcpip_test.go create mode 100644 example_test.go create mode 100644 server.go create mode 100644 session.go create mode 100644 tcpip.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..2b26c54 --- /dev/null +++ b/client.go @@ -0,0 +1,146 @@ +package easyssh + +import ( + "log" + "net" + + "golang.org/x/crypto/ssh" +) + +// Client wraps an SSH Client +type Client struct { + *ssh.Client +} + +// Dial starts an ssh connection to the provided server +func Dial(network, addr string, config *ssh.ClientConfig) (*Client, error) { + c, err := ssh.Dial(network, addr, config) + return &Client{c}, err +} + +// NewClient returns a new SSH Client. +func NewClient(c ssh.Conn, chans <-chan ssh.NewChannel, reqs <-chan *ssh.Request) *Client { + + client := ssh.NewClient(c, chans, reqs) + return &Client{client} + +} + +// LocalForward performs a port forwarding over the ssh connection - ssh -L. Client will bind to the local address, and will tunnel those requests to host addr +func (c *Client) LocalForward(laddr, raddr *net.TCPAddr) error { + + ln, err := net.ListenTCP("tcp", laddr) //tie to the client connection + if err != nil { + println(err.Error()) + return err + } + log.Println("Listening on address: ", ln.Addr().String()) + + quit := make(chan bool) + + go func() { // Handle incoming connections on this new listener + for { + select { + case <-quit: + + return + default: + conn, err := ln.Accept() + if err != nil { // Unable to accept new connection - listener likely closed + continue + } + go func(conn net.Conn) { + conn2, err := c.DialTCP("tcp", laddr, raddr) + + if err != nil { + return + } + go func(conn, conn2 net.Conn) { + + close := func() { + conn.Close() + conn2.Close() + + } + + go CopyReadWriters(conn, conn2, close) + + }(conn, conn2) + + }(conn) + } + + } + }() + + c.Wait() + + ln.Close() + quit <- true + + return nil +} + +// RemoteForward forwards a remote port - ssh -R +func (c *Client) RemoteForward(remote, local string) error { + ln, err := c.Listen("tcp", remote) + if err != nil { + return err + } + + quit := make(chan bool) + + go func() { // Handle incoming connections on this new listener + for { + select { + case <-quit: + + return + default: + conn, err := ln.Accept() + if err != nil { // Unable to accept new connection - listener likely closed + continue + } + + conn2, err := net.Dial("tcp", local) + if err != nil { + continue + } + + close := func() { + conn.Close() + conn2.Close() + + } + + go CopyReadWriters(conn, conn2, close) + + } + + } + }() + + c.Wait() + ln.Close() + quit <- true + + return nil +} + +// HandleOpenChannel requests that the remote end accept a channel request and if accepted, +// passes the newly opened channel and requests to the provided handler +func (c *Client) HandleOpenChannel(channelName string, handler ChannelMultipleRequestsHandler, data ...byte) error { + ch, reqs, err := c.OpenChannel(channelName, data) + if err != nil { + return err + } + handler.HandleMultipleRequests(reqs, c.Conn, channelName, ch) + return nil +} + +// HandleOpenChannelFunc requests that the remote end accept a channel request and if accepted, +// passes the newly opened channel and requests to the provided handler function +func (c *Client) HandleOpenChannelFunc(channelName string, handler ChannelMultipleRequestsHandlerFunc, data ...byte) error { + + return c.HandleOpenChannel(channelName, ChannelMultipleRequestsHandlerFunc(handler), data...) +} diff --git a/common.go b/common.go new file mode 100644 index 0000000..7d1e66a --- /dev/null +++ b/common.go @@ -0,0 +1,292 @@ +package easyssh + +import ( + "fmt" + "log" + "sync" + + "golang.org/x/crypto/ssh" +) + +// A ConnHandler is a top level SSH Manager. Objects implementing the ConnHandler are responsible for managing incoming Channels and Global Requests +type ConnHandler interface { + HandleSSHConn(ssh.Conn, <-chan ssh.NewChannel, <-chan *ssh.Request) +} + +// ConnHandlerFunc is an adapter that allows regular functions to act as SSH Connection Handlers +type ConnHandlerFunc func(ssh.Conn, <-chan ssh.NewChannel, <-chan *ssh.Request) + +// HandleSSHConn calls f(sshConn, chans, reqs) +func (f ConnHandlerFunc) HandleSSHConn(sshConn ssh.Conn, chans <-chan ssh.NewChannel, reqs <-chan *ssh.Request) { + f(sshConn, chans, reqs) +} + +// ChannelHandler handles channel requests for a given channel type +type ChannelHandler interface { + HandleChannel(newChannel ssh.NewChannel, channel ssh.Channel, reqs <-chan *ssh.Request, sshConn ssh.Conn) +} + +// ChannelHandlerFunc is an adapter that allows regular functions to act as SSH Channel Handlers +type ChannelHandlerFunc func(newChannel ssh.NewChannel, channel ssh.Channel, reqs <-chan *ssh.Request, sshConn ssh.Conn) + +// HandleChannel calls f(channelType, channel, reqs, sshConn) +func (f ChannelHandlerFunc) HandleChannel(newChannel ssh.NewChannel, channel ssh.Channel, reqs <-chan *ssh.Request, sshConn ssh.Conn) { + f(newChannel, channel, reqs, sshConn) +} + +// MultipleChannelsHandler handles a chan of all SSH channel requests for a connection +type MultipleChannelsHandler interface { + HandleChannels(chans <-chan ssh.NewChannel, sshConn ssh.Conn) +} + +// MultipleChannelsHandlerFunc is an adapter that allows regular functions to act as SSH Multiple Channels Handlers +type MultipleChannelsHandlerFunc func(chans <-chan ssh.NewChannel, sshConn ssh.Conn) + +// HandleChannels calls f(chans, sshConn) +func (f MultipleChannelsHandlerFunc) HandleChannels(chans <-chan ssh.NewChannel, sshConn ssh.Conn) { + f(chans, sshConn) +} + +// GlobalMultipleRequestsHandler handles global (not tied to a channel) out-of-band SSH Requests +type GlobalMultipleRequestsHandler interface { + HandleRequests(<-chan *ssh.Request, ssh.Conn) +} + +// GlobalMultipleRequestsHandlerFunc is an adaper to allow regular functions to act as a Global Requests Handler +type GlobalMultipleRequestsHandlerFunc func(<-chan *ssh.Request, ssh.Conn) + +// HandleRequests calls f(reqs, sshConn) +func (f GlobalMultipleRequestsHandlerFunc) HandleRequests(reqs <-chan *ssh.Request, sshConn ssh.Conn) { + f(reqs, sshConn) +} + +// DiscardGlobalMultipleRequests is a wrapper around ssh.DiscardRequests. Ignores ssh ServerConn +func DiscardGlobalMultipleRequests(reqs <-chan *ssh.Request, sshConn ssh.Conn) { + ssh.DiscardRequests(reqs) +} + +// DiscardRequest appropriately discards SSH Requests, returning responses to those that expect it +func DiscardRequest(req *ssh.Request) { + if req.WantReply { + req.Reply(false, nil) + } +} + +// GlobalRequestHandler handles global (not tied to a channel) out-of-band SSH Requests +type GlobalRequestHandler interface { + HandleRequest(*ssh.Request, ssh.Conn) +} + +// GlobalRequestHandlerFunc is an adaper to allow regular functions to act as a Global Request Handler +type GlobalRequestHandlerFunc func(*ssh.Request, ssh.Conn) + +// HandleRequest calls f(reqs, sshConn) +func (f GlobalRequestHandlerFunc) HandleRequest(req *ssh.Request, sshConn ssh.Conn) { + f(req, sshConn) +} + +// ChannelMultipleRequestsHandler handles tied to a channel out-of-band SSH Requests +type ChannelMultipleRequestsHandler interface { + HandleMultipleRequests(reqs <-chan *ssh.Request, sshConn ssh.Conn, channelType string, channel ssh.Channel) +} + +// ChannelMultipleRequestsHandlerFunc is an adaper to allow regular functions to act as a Channel Requests Handler +type ChannelMultipleRequestsHandlerFunc func(reqs <-chan *ssh.Request, sshConn ssh.Conn, channelType string, channel ssh.Channel) + +// HandleMultipleRequests calls f(reqs, sshConn, channelType, channel) +func (f ChannelMultipleRequestsHandlerFunc) HandleMultipleRequests(reqs <-chan *ssh.Request, sshConn ssh.Conn, channelType string, channel ssh.Channel) { + f(reqs, sshConn, channelType, channel) +} + +// DiscardChannelMultipleRequests is a wrapper around ssh.DiscardRequests. Ignores ssh ServerConn +func DiscardChannelMultipleRequests(reqs <-chan *ssh.Request, sshConn ssh.Conn, channelType string, channel ssh.Channel) { + ssh.DiscardRequests(reqs) +} + +// ChannelRequestHandler handles tied to a channel out-of-band SSH Requests +type ChannelRequestHandler interface { + HandleRequest(req *ssh.Request, sshConn ssh.Conn, channelType string, channel ssh.Channel) +} + +// ChannelRequestHandlerFunc is an adaper to allow regular functions to act as a Global Request Handler +type ChannelRequestHandlerFunc func(req *ssh.Request, sshConn ssh.Conn, channelType string, channel ssh.Channel) + +// HandleRequest calls f(reqs, sshConn, channelType, channel) +func (f ChannelRequestHandlerFunc) HandleRequest(req *ssh.Request, sshConn ssh.Conn, channelType string, channel ssh.Channel) { + f(req, sshConn, channelType, channel) +} + +// SSHConnHandler is an SSH Channel multiplexer. It matches Channel types and calls the handler for the corresponding type +type SSHConnHandler struct { + MultipleChannelsHandler + GlobalMultipleRequestsHandler +} + +// GlobalMultipleRequestsMux is an SSH Global Requests multiplexer. It matches Channel types and calls the handler for the corresponding type - can be used as GlobalMultipleRequestsHandler +type GlobalMultipleRequestsMux struct { + requestMutex sync.RWMutex + requests map[string]GlobalRequestHandler +} + +// NewGlobalMultipleRequestsMux creates and returns a GlobalMultipleRequestsHandler that performs multiplexing of request types with dispatching to GlobalRequestHandlers +func NewGlobalMultipleRequestsMux() *GlobalMultipleRequestsMux { + return &GlobalMultipleRequestsMux{requests: map[string]GlobalRequestHandler{}} +} + +// ChannelsMux is an SSH Channel multiplexer. It matches Channel types and calls the handler for the corresponding type - Can be used as ChannelsHandler +type ChannelsMux struct { + channelMutex sync.RWMutex + channels map[string]ChannelHandler +} + +// NewChannelsMux creates and returns a MultipleChannelsHandler that performs multiplexing of request types with dispatching to ChannelHandlers +func NewChannelsMux() *ChannelsMux { + return &ChannelsMux{channels: map[string]ChannelHandler{}} +} + +// NewSSHConnHandler creates and returns a basic working ConnHandler to provide a minimal "working" SSH server +func NewSSHConnHandler() *SSHConnHandler { + return &SSHConnHandler{MultipleChannelsHandler: NewChannelsMux(), GlobalMultipleRequestsHandler: NewGlobalMultipleRequestsMux()} +} + +// HandleSSHConn manages incoming channel and out-of-band requests. It discards out-of-band requests and dispatches channel requests if a ChannelHandler is registered for a given Channel Type +func (s *SSHConnHandler) HandleSSHConn(sshConn ssh.Conn, chans <-chan ssh.NewChannel, reqs <-chan *ssh.Request) { + go globalRequestsHandler{s}.HandleRequests(reqs, sshConn) + go channelsHandler{s}.HandleChannels(chans, sshConn) + +} + +// HandleRequest registers the GlobalRequestHandler for the given Channel Type. If a GlobalRequestHandler was already registered for the type, HandleRequest panics +func (s *GlobalMultipleRequestsMux) HandleRequest(requestType string, handler GlobalRequestHandler) { + s.requestMutex.Lock() + defer s.requestMutex.Unlock() + _, ok := s.requests[requestType] + if ok { + panic("easyssh: GlobalRequestHandler already registered for " + requestType) + } + s.requests[requestType] = handler +} + +// HandleRequestFunc registers the Channel Handler function for the provided Request Type +func (s *GlobalMultipleRequestsMux) HandleRequestFunc(requestType string, f GlobalRequestHandlerFunc) { + s.HandleRequest(requestType, GlobalRequestHandlerFunc(f)) + +} + +// HandleChannel registers the ChannelHandler for the given Channel Type. If a ChannelHandler was already registered for the type, HandleChannel panics +func (s *ChannelsMux) HandleChannel(channelType string, handler ChannelHandler) { + s.channelMutex.Lock() + defer s.channelMutex.Unlock() + _, ok := s.channels[channelType] + if ok { + panic("easyssh: ChannelHandler already registered for " + channelType) + } + s.channels[channelType] = handler + +} + +// HandleChannelFunc registers the Channel Handler function for the provided Channel Type +func (s *ChannelsMux) HandleChannelFunc(channelType string, f ChannelHandlerFunc) { + s.HandleChannel(channelType, ChannelHandlerFunc(f)) + +} + +// HandleRequests handles global out-of-band SSH Requests - +func (s *GlobalMultipleRequestsMux) HandleRequests(reqs <-chan *ssh.Request, sshConn ssh.Conn) { + for req := range reqs { + t := req.Type + s.requestMutex.RLock() + handler, ok := s.requests[t] + if !ok { + DiscardRequest(req) + } + + s.requestMutex.RUnlock() + + go handler.HandleRequest(req, sshConn) + } +} + +// HandleChannels acts a a mux for incoming channel requests +func (s *ChannelsMux) HandleChannels(chans <-chan ssh.NewChannel, sshConn ssh.Conn) { + for newChannel := range chans { + + log.Printf("Received channel: %v", newChannel.ChannelType()) + + // Check the type of channel + t := newChannel.ChannelType() + s.channelMutex.RLock() + handler, ok := s.channels[t] + + s.channelMutex.RUnlock() + if !ok { + log.Printf("Unknown channel type: %s", t) + + 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 + } + go handler.HandleChannel(newChannel, channel, requests, sshConn) + + } + +} + +// DefaultSSHConnHandler is an SSH Server Handler +var DefaultSSHConnHandler = &SSHConnHandler{} + +// DefaultMultipleChannelsHandler is the ChannelMux and is used to handle all incoming channel requests +var DefaultMultipleChannelsHandler = NewChannelsMux() + +// DefaultGlobalMultipleRequestsHandler is a GlobalMultipleRequestsHandler that by default discards all incoming global requests +var DefaultGlobalMultipleRequestsHandler = NewGlobalMultipleRequestsMux() + +// HandleRequest registers the given handler with the DefaultGlobalMultipleRequestsHandler +func HandleRequest(requestType string, handler GlobalRequestHandler) { + DefaultGlobalMultipleRequestsHandler.HandleRequest(requestType, handler) +} + +// HandleRequestFunc registers the given handler function with the DefaultGlobalMultipleRequestsHandler +func HandleRequestFunc(requestType string, handler GlobalRequestHandlerFunc) { + DefaultGlobalMultipleRequestsHandler.HandleRequestFunc(requestType, handler) +} + +// HandleChannel registers the given handler under the channelType with the DefaultMultipleChannelsHandler +func HandleChannel(channelType string, handler ChannelHandler) { + DefaultMultipleChannelsHandler.HandleChannel(channelType, handler) +} + +// HandleChannelFunc registers the given handler function under the channelType with the DefaultMultipleChannelsHandler +func HandleChannelFunc(channelType string, handler ChannelHandlerFunc) { + DefaultMultipleChannelsHandler.HandleChannelFunc(channelType, handler) +} + +type channelsHandler struct { + s *SSHConnHandler +} + +// HandleChannels is a wrapper, tests if the SSHConnHandler has a MultipleChannelsHandler, and if not uses the default one +func (s channelsHandler) HandleChannels(chans <-chan ssh.NewChannel, sshConn ssh.Conn) { + handler := s.s.MultipleChannelsHandler + if handler == nil { + handler = DefaultMultipleChannelsHandler + } + handler.HandleChannels(chans, sshConn) +} + +type globalRequestsHandler struct { + s *SSHConnHandler +} + +// HandleRequests is a wrapper, tests if the SSHConnHandler has a GlobalMultipleRequestsHandler, and if not uses the default one +func (s globalRequestsHandler) HandleRequests(reqs <-chan *ssh.Request, sshConn ssh.Conn) { + handler := s.s.GlobalMultipleRequestsHandler + if handler == nil { + handler = DefaultGlobalMultipleRequestsHandler + } + handler.HandleRequests(reqs, sshConn) +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..d8d516f --- /dev/null +++ b/doc.go @@ -0,0 +1,56 @@ +/* +Package easyssh provides a simple wrapper around the standard SSH library. Designed to be like net/http but for ssh. +Both server and client implementations are provided. + +Creating a client is similar to creating a normal ssh client + + client, err := easyssh.Dial("tcp", "localhost:2022", config) + if err != nil { + // Handle error + } + defer client.Close() + +Once a client is created, you can do a number of things with it: +Local Port Forwarding + + err = client.LocalForward("localhost:8000", "localhost:6060") + if err != nil { + // Handle error + } + +Remote Port Forwarding + + err = client.RemoteForward("localhost:8000", "localhost:6060") + if err != nil { + // Handle error + } + +Create a session - used for executing remote commands or getting a remote shell + + session, err := client.NewSession() + if err != nil { + // Handle error + } + out, err := session.Output("whoami") + if err != nil { + // Handle error + } + + +Getting started with an SSH server is easy with easyssh + + easyssh.HandleChannel(easyssh.SessionRequest, easyssh.SessionHandler()) + easyssh.HandleChannel(easyssh.DirectForwardRequest, easyssh.DirectPortForwardHandler()) + easyssh.HandleRequestFunc(easyssh.RemoteForwardRequest, easyssh.TCPIPForwardRequest) + + easyssh.ListenAndServe(":2022", sshServerConfig, nil) + + + + + +There are a lot of layers of ssh communication, and easyssh makes it easy to control at the level desired. + + +*/ +package easyssh diff --git a/example_client_test.go b/example_client_test.go new file mode 100644 index 0000000..f5dabd5 --- /dev/null +++ b/example_client_test.go @@ -0,0 +1,63 @@ +package easyssh_test + +import ( + "log" + + "golang.org/x/crypto/ssh" + + "dev.justinjudd.org/justin/easyssh" +) + +func ExampleDial() { + config := &ssh.ClientConfig{ + User: "test", + Auth: []ssh.AuthMethod{ + ssh.Password("test"), + }, + } + conn, err := easyssh.Dial("tcp", "localhost:2022", config) + if err != nil { + log.Fatalf("unable to connect: %s", err) + } + defer conn.Close() +} + +func ExampleClient_LocalForward() { + config := &ssh.ClientConfig{ + User: "test", + Auth: []ssh.AuthMethod{ + ssh.Password("test"), + }, + } + conn, err := easyssh.Dial("tcp", "localhost:2022", config) + if err != nil { + log.Fatalf("unable to connect: %s", err) + } + defer conn.Close() + + err = conn.LocalForward("localhost:8000", "localhost:6060") + if err != nil { + log.Fatalf("unable to forward local port: %s", err) + } + +} + +func ExampleClient_RemoteForward() { + config := &ssh.ClientConfig{ + User: "test", + Auth: []ssh.AuthMethod{ + ssh.Password("test"), + }, + } + conn, err := easyssh.Dial("tcp", "localhost:2022", config) + if err != nil { + log.Fatalf("unable to connect: %s", err) + } + defer conn.Close() + + err = conn.RemoteForward("localhost:8000", "localhost:6060") + if err != nil { + log.Fatalf("unable to forward local port: %s", err) + } + +} diff --git a/example_handler_test.go b/example_handler_test.go new file mode 100644 index 0000000..139d485 --- /dev/null +++ b/example_handler_test.go @@ -0,0 +1,29 @@ +package easyssh_test + +import ( + "dev.justinjudd.org/justin/easyssh" + + "golang.org/x/crypto/ssh" +) + +type testHandler struct{} + +func (testHandler) HandleChannel(nCh ssh.NewChannel, ch ssh.Channel, reqs <-chan *ssh.Request, conn *ssh.ServerConn) { + defer ch.Close() + // Do something +} + +func ExampleChannelsMux_HandleChannel() { + handler := easyssh.NewChannelsMux() + + handler.HandleChannel("test", testHandler{}) + + test2Handler := func(newChannel ssh.NewChannel, channel ssh.Channel, reqs <-chan *ssh.Request, sshConn *ssh.ServerConn) { + defer channel.Close() + ssh.DiscardRequests(reqs) + } + + handler.HandleChannelFunc("test2", test2Handler) + + handler.HandleChannel("anotherTest2", easyssh.ChannelHandlerFunc(test2Handler)) +} diff --git a/example_tcpip_test.go b/example_tcpip_test.go new file mode 100644 index 0000000..9df2ac9 --- /dev/null +++ b/example_tcpip_test.go @@ -0,0 +1,46 @@ +package easyssh_test + +import ( + "fmt" + "io/ioutil" + + "dev.justinjudd.org/justin/easyssh" + + "golang.org/x/crypto/ssh" +) + +func ExampleDirectPortForwardChannel() { + s := easyssh.Server{Addr: ":2022"} + + privateBytes, err := ioutil.ReadFile("id_rsa") + if err != nil { + // Failed to load private key (./id_rsa) + } + + private, err := ssh.ParsePrivateKey(privateBytes) + if err != nil { + // Failed to parse private key + } + + config := &ssh.ServerConfig{ + PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { + if c.User() == "test" && string(pass) == "test" { + return nil, nil + } + return nil, fmt.Errorf("password rejected for %s", c.User()) + }, + } + config.AddHostKey(private) + + s.Config = config + + handler := easyssh.NewServerHandler() + channelHandler := easyssh.NewChannelsMux() + + channelHandler.HandleChannel(easyssh.DirectForwardRequest, easyssh.DirectPortForwardHandler()) + handler.MultipleChannelsHandler = channelHandler + + s.Handler = handler + + s.ListenAndServe() +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..4a7a69f --- /dev/null +++ b/example_test.go @@ -0,0 +1,66 @@ +package easyssh_test + +import ( + "fmt" + "io/ioutil" + + "dev.justinjudd.org/justin/easyssh" + + "golang.org/x/crypto/ssh" +) + +func ExampleServer_ListenAndServe() { + s := easyssh.Server{Addr: ":2022"} + + privateBytes, err := ioutil.ReadFile("id_rsa") + if err != nil { + // Failed to load private key (./id_rsa) + } + + private, err := ssh.ParsePrivateKey(privateBytes) + if err != nil { + // Failed to parse private key + } + + config := &ssh.ServerConfig{ + PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { + if c.User() == "test" && string(pass) == "test" { + return nil, nil + } + return nil, fmt.Errorf("password rejected for %s", c.User()) + }, + } + config.AddHostKey(private) + + s.Config = config + + handler := easyssh.NewServerHandler() + channelHandler := easyssh.NewChannelsMux() + + channelHandler.HandleChannel(easyssh.SessionRequest, easyssh.SessionHandler()) + handler.MultipleChannelsHandler = channelHandler + + s.Handler = handler + + s.ListenAndServe() +} + +func ExampleListenAndServe() { + easyssh.HandleChannel(easyssh.SessionRequest, easyssh.SessionHandler()) + easyssh.HandleChannel(easyssh.DirectForwardRequest, easyssh.DirectPortForwardHandler()) + easyssh.HandleRequestFunc(easyssh.RemoteForwardRequest, easyssh.TCPIPForwardRequest) + + easyssh.ListenAndServe(":2022", config, nil) +} + +func ExampleChannelsMux_HandleChannelFunc() { + handler := easyssh.NewChannelsMux() + + testHandler := func(newChannel ssh.NewChannel, channel ssh.Channel, reqs <-chan *ssh.Request, sshConn *ssh.ServerConn) { + defer channel.Close() + ssh.DiscardRequests(reqs) + } + + handler.HandleChannelFunc("test", testHandler) + +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..d675926 --- /dev/null +++ b/server.go @@ -0,0 +1,157 @@ +package easyssh + +import ( + "log" + "net" + + "golang.org/x/crypto/ssh" +) + +// Server represents an SSH Server. The SSH ServerConfig must be provided +type Server struct { + Addr string + Config *ssh.ServerConfig + Handler ConnHandler + *ssh.ServerConn +} + +// ListenAndServe listens on the TCP address s.Addr and then calls Serve to handle requests on incoming connections. If s.Addr is blank, ":ssh" is used +func (s *Server) ListenAndServe() error { + addr := s.Addr + if addr == "" { + addr = ":ssh" + } + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + return s.Serve(ln.(*net.TCPListener)) +} + +// Serve accepts incoming connections on the provided listener.and reads global SSH Channel and Out-of-band requests and calls s,ConnHandler to handle them +func (s *Server) Serve(l net.Listener) error { + defer l.Close() + log.Print("SSH Server started listening on: ", l.Addr()) + for { + tcpConn, err := l.Accept() + if err != nil { + return err + } + + c, err := s.newConn(tcpConn) + if err != nil { + continue + } + go c.serve() + } +} + +// HandleOpenChannel requests that the remote end accept a channel request and if accepted, +// passes the newly opened channel and requests to the provided handler +func (s *Server) HandleOpenChannel(channelName string, handler ChannelMultipleRequestsHandler, data ...byte) error { + ch, reqs, err := s.OpenChannel(channelName, data) + if err != nil { + return err + } + handler.HandleMultipleRequests(reqs, s.ServerConn, channelName, ch) + return nil +} + +// HandleOpenChannelFunc requests that the remote end accept a channel request and if accepted, +// passes the newly opened channel and requests to the provided handler function +func (s *Server) HandleOpenChannelFunc(channelName string, handler ChannelMultipleRequestsHandlerFunc, data ...byte) error { + + return s.HandleOpenChannel(channelName, ChannelMultipleRequestsHandlerFunc(handler), data...) +} + +type conn struct { + server *Server + remoteAddr string + conn net.Conn +} + +func (c *conn) serve() { + sshConn, chans, reqs, err := ssh.NewServerConn(c.conn, c.server.Config) + if err != nil { + return + } + c.server.ServerConn = sshConn + log.Print("New ssh connection from: ", c.conn.RemoteAddr()) + + go func() { + sshConn.Wait() + log.Print("Closing ssh connection from: ", c.conn.RemoteAddr()) + if c.conn != nil { + c.conn.Close() + //c.conn = nil + } + }() + + // Use default ConnHandler if one isn't provided + serverHandler{c.server}.HandleSSHConn(sshConn, chans, reqs) +} + +func (s *Server) newConn(netConn net.Conn) (*conn, error) { + c := new(conn) + c.remoteAddr = netConn.RemoteAddr().String() + c.server = s + c.conn = netConn + return c, nil + +} + +// NewSessionServerHandler creates a ConnHandler to provide a more standard SSH server providing sessions +func NewSessionServerHandler() *SSHConnHandler { + s := SSHConnHandler{} + channelHandler := NewChannelsMux() + + channelHandler.HandleChannel(SessionRequest, SessionHandler()) + s.MultipleChannelsHandler = channelHandler + return &s +} + +// NewStandardSSHServerHandler returns a server handler that can deal with ssh sessions and both local and remote port forwarding +func NewStandardSSHServerHandler() *SSHConnHandler { + s := NewSSHConnHandler() + + chHandler := NewChannelsMux() + chHandler.HandleChannel(SessionRequest, SessionHandler()) + chHandler.HandleChannel(DirectForwardRequest, DirectPortForwardHandler()) + + s.MultipleChannelsHandler = chHandler + + globalHandler := NewGlobalMultipleRequestsMux() + globalHandler.HandleRequest(RemoteForwardRequest, TCPIPForwardRequestHandler()) + + s.GlobalMultipleRequestsHandler = globalHandler + return s + +} + +// ListenAndServe listens on the given tcp address addr and then calls Serve with handler. +// If handler is nil, the DefaultServerHandler is used. +func ListenAndServe(addr string, conf *ssh.ServerConfig, handler ConnHandler) error { + s := &Server{addr, conf, handler, nil} + return s.ListenAndServe() + +} + +// Serve accepts incoming SSH connections on the listener l. +// If handler is nil, the DefaultServerHandler is used. +func Serve(l net.Listener, conf *ssh.ServerConfig, handler ConnHandler) error { + s := &Server{Config: conf, Handler: handler} + return s.Serve(l) +} + +type serverHandler struct { + s *Server +} + +// ServeSSH is a wrapper, tests if the server has a ServerHandler, and if not uses the default one +func (s serverHandler) HandleSSHConn(conn *ssh.ServerConn, chans <-chan ssh.NewChannel, reqs <-chan *ssh.Request) { + handler := s.s.Handler + if handler == nil { + handler = DefaultSSHConnHandler + } + handler.HandleSSHConn(conn, chans, reqs) +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..3782454 --- /dev/null +++ b/session.go @@ -0,0 +1,313 @@ +package easyssh + +import ( + "encoding/binary" + "fmt" + "io" + "log" + "os" + "os/exec" + "sync" + "syscall" + "unsafe" + + "github.com/kr/pty" + "golang.org/x/crypto/ssh" +) + +// Request types used in sessions - RFC 4254 6.X +const ( + SessionRequest = "session" // RFC 4254 6.1 + PTYRequest = "pty-req" // RFC 4254 6.2 + X11Request = "x11-req" // RFC 4254 6.3.1 + X11ChannelRequest = "x11" // RFC 4254 6.3.2 + EnvironmentRequest = "env" // RFC 4254 6.4 + ShellRequest = "shell" // RFC 4254 6.5 + ExecRequest = "exec" // RFC 4254 6.5 + SubsystemRequest = "subsystem" // RFC 4254 6.5 + WindowDimensionChangeRequest = "window-change" // RFC 4254 6.7 + FlowControlRequest = "xon-off" // RFC 4254 6.8 + SignalRequest = "signal" // RFC 4254 6.9 + ExitStatusRequest = "exit-status" // RFC 4254 6.10 + ExitSignalRequest = "exit-signal" // RFC 4254 6.10 +) + +// SessionHandler returns a ChannelHandler that implements standard SSH Sessions for PTY, shell, and exec capabilities +func SessionHandler() ChannelHandler { return ChannelHandlerFunc(SessionChannel) } + +// SessionChannel acts as an SSH Session ChannelHandler +func SessionChannel(newChannel ssh.NewChannel, channel ssh.Channel, reqs <-chan *ssh.Request, sshConn ssh.Conn) { + // Get system shell + shell := os.Getenv("SHELL") + c := exec.Command(shell) + f, err := pty.Start(c) + if err != nil { + log.Printf("Unable to start shell: %s", shell) + return + } + + terminalModes := ssh.TerminalModes{} + // TODO: Find out if I should do anything with the terminal modes. Do any of the ptys/ttys know what to do with them + + close := func() { + channel.Close() + err := c.Wait() + if err != nil { + log.Printf("failed to exit bash (%s)", err) + } + log.Printf("session closed") + } + + go func(in <-chan *ssh.Request) { + + env := []string{} + var command *exec.Cmd + + for req := range in { + ok := false + + switch req.Type { + case PTYRequest: + + ok = true + pty := ptyReq{} + err = ssh.Unmarshal(req.Payload, &pty) + if err != nil { + log.Printf("Unable to decode pty request: %s", err.Error()) + } + + setWinsize(f.Fd(), pty.Width, pty.Height) + log.Printf("pty-req '%s'", pty.Term) + + type termModeStruct struct { + //Key byte + Key uint8 + Val uint32 + } + termModes := []termModeStruct{} + working := []byte(pty.TermModes) + for { + if len(working) < 5 { + break + } + tm := termModeStruct{} + + tm.Key = working[0] + tm.Val = binary.BigEndian.Uint32(working[1:5]) + + /* + + err = ssh.Unmarshal(working, &tm) + if err != nil { + fmt.Println(err.Error()) + break + } + */ + + termModes = append(termModes, tm) + terminalModes[tm.Key] = tm.Val + working = working[5:] + + } + go CopyReadWriters(channel, f, close) + + case WindowDimensionChangeRequest: + win := windowDimensionReq{} + err = ssh.Unmarshal(req.Payload, &win) + if err != nil { + log.Printf("Error reading window dimension change request: %s", err.Error()) + } + setWinsize(f.Fd(), win.Width, win.Height) + continue //no response according to RFC 4254 6.7 + case ShellRequest: // Shell requests should not have a payload - RFC 4254 6.7 + ok = true + go CopyReadWriters(channel, f, close) + + case ExecRequest: + ok = true + var cmd execRequest + err = ssh.Unmarshal(req.Payload, &cmd) + if err != nil { + continue + } + + command = exec.Command("sh", "-c", cmd.Command) // Let shell do the parsing + log.Printf("exec starting: %s", cmd.Command) + //c.Env = append(c.Env, env...) + + exitStatus := exitStatusReq{} + + fd, err := pty.Start(command) + if err != nil { + log.Printf("Unable to wrap exec command in pty\n") + return + } + + execClose := func() { + channel.Close() + log.Printf("exec finished: %s", cmd.Command) + } + + defer fd.Close() + go CopyReadWriters(channel, fd, execClose) + err = command.Wait() + + /* + command.Stdout = channel + command.Stderr = channel + */ + + //command.Stdin = channel // TODO: test how stdin works on exec on openssh server + //err = command.Run() + if err != nil { + log.Printf("Error running exec : %s", err.Error()) + e, ok := err.(*exec.ExitError) + errVal := 1 + if ok { + status := e.Sys().(syscall.WaitStatus) + if status.Exited() { + errVal = status.ExitStatus() + exitStatus.ExitStatus = uint32(errVal) + channel.SendRequest(ExitStatusRequest, false, ssh.Marshal(exitStatus)) + } else if status.Signaled() { // What is the difference between Siglnal and StopSignal + e := exitSignalReq{} + e.SignalName = status.Signal().String() + e.CoreDumped = status.CoreDump() + // TODO: Figure out other two fields + channel.SendRequest(ExitSignalRequest, false, ssh.Marshal(e)) + } + + } + + } else { + + channel.SendRequest(ExitStatusRequest, false, ssh.Marshal(exitStatus)) + } + + req.Reply(ok, nil) + close() + return + + case EnvironmentRequest: + ok = true + e := envReq{} + err = ssh.Unmarshal(req.Payload, &e) + if err != nil { + continue + } + + env = append(env, fmt.Sprintf("%s=%s", e.Name, e.Value)) + + case SignalRequest: + ok = true + sig := signalRequest{} + ssh.Unmarshal(req.Payload, &sig) + log.Println("Received Signal: ", sig.Signal) + + s := signalsMap[sig.Signal] + if command != nil { + + command.Process.Signal(s) + } else { + c.Process.Signal(s) + } + } + req.Reply(ok, nil) + + } + + }(reqs) + +} + +var signalsMap = map[ssh.Signal]os.Signal{ + ssh.SIGABRT: syscall.SIGABRT, + ssh.SIGALRM: syscall.SIGALRM, + ssh.SIGFPE: syscall.SIGFPE, + ssh.SIGHUP: syscall.SIGHUP, + ssh.SIGILL: syscall.SIGILL, + ssh.SIGINT: syscall.SIGINT, + ssh.SIGKILL: syscall.SIGKILL, + ssh.SIGPIPE: syscall.SIGPIPE, + ssh.SIGQUIT: syscall.SIGQUIT, + ssh.SIGSEGV: syscall.SIGSEGV, + ssh.SIGTERM: syscall.SIGTERM, + ssh.SIGUSR1: syscall.SIGUSR1, + ssh.SIGUSR2: syscall.SIGUSR2, +} + +// CopyReadWriters copies biderectionally - output from a to b, and output of b into a. Calls the close function when unable to copy in either direction +func CopyReadWriters(a, b io.ReadWriter, close func()) { + var once sync.Once + go func() { + io.Copy(a, b) + once.Do(close) + }() + + go func() { + io.Copy(b, a) + once.Do(close) + }() +} + +// windowDimension represents channel request for window dimension change - RFC 4254 6.7 +type windowDimensionReq struct { + Width uint32 + Height uint32 + WidthPixel uint32 + HeightPixel uint32 +} + +// ptyReq represents the channel request for a PTY. RFC 4254 6.2 +type ptyReq struct { + Term string + Width uint32 + Height uint32 + WidthPixel uint32 + HeightPixel uint32 + TermModes string +} + +// envReq represents an "env" channel request - RFC 4254 6.4 +type envReq struct { + Name string + Value string +} + +// execRequest represents an "exec" channel request - RFC 4254 6.5 +type execRequest struct { + Command string +} + +// signalRequest represents a "signal" session channel request - RFC 4254 6.9 +type signalRequest struct { + Signal ssh.Signal +} + +// exitStatusReq represents an exit status for "exec" requests - RFC 4254 6.10 +type exitStatusReq struct { + ExitStatus uint32 +} + +// exitSignalReq represents an exit signal for "exec" requests - RFC 4254 6.10 +type exitSignalReq struct { + SignalName string + CoreDumped bool + ErrorMessage string + LanguageTag string +} + +// winsize stores the Height and Width of a terminal in rows/columns and pixels - for syscall - http://linux.die.net/man/4/tty_ioctl +type winsize struct { + Row uint16 + Col uint16 + XPixel uint16 // unused + YPixel uint16 // unused +} + +// SetWinsize uses syscall to set pty window size +func setWinsize(fd uintptr, w, h uint32) { + log.Printf("Resize Window to %dx%d", w, h) + ws := &winsize{Col: uint16(w), Row: uint16(h)} + syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws))) +} diff --git a/tcpip.go b/tcpip.go new file mode 100644 index 0000000..fd7691a --- /dev/null +++ b/tcpip.go @@ -0,0 +1,173 @@ +package easyssh + +import ( + "encoding/binary" + "fmt" + "log" + "net" + "strconv" + + "golang.org/x/crypto/ssh" +) + +// Applicaple SSH Request types for Port Forwarding - RFC 4254 7.X +const ( + DirectForwardRequest = "direct-tcpip" // RFC 4254 7.2 + RemoteForwardRequest = "tcpip-forward" // RFC 4254 7.1 + ForwardedTCPReturnRequest = "forwarded-tcpip" // RFC 4254 7.2 + CancelRemoteForwardRequest = "cancel-tcpip-forward" // RFC 4254 7.1 +) + +// tcpipForward is structure for RFC 4254 7.1 "tcpip-forward" request +type tcpipForward struct { + Host string + Port uint32 +} + +// directForward is struxture for RFC 4254 7.2 - can be used for "forwarded-tcpip" and "direct-tcpip" +type directForward struct { + Host1 string + Port1 uint32 + Host2 string + Port2 uint32 +} + +func (p directForward) String() string { + return fmt.Sprintf("CONNECT: %s:%d FROM: %s:%d", p.Host1, p.Port1, p.Host2, p.Port2) +} + +// TCPIPForwardRequestHandler returns a GlobalRequestHandler that implements remote port forwarding - ssh -R +func TCPIPForwardRequestHandler() GlobalRequestHandler { + return GlobalRequestHandlerFunc(TCPIPForwardRequest) +} + +// TCPIPForwardRequest fulfills RFC 4254 7.1 "tcpip-forward" request +// +// TODO: Need to add state to handle "cancel-tcpip-forward" +func TCPIPForwardRequest(req *ssh.Request, sshConn ssh.Conn) { + + t := tcpipForward{} + reply := (t.Port == 0) && req.WantReply + ssh.Unmarshal(req.Payload, &t) + addr := fmt.Sprintf("%s:%d", t.Host, t.Port) + ln, err := net.Listen("tcp", addr) //tie to the client connection + + if err != nil { + log.Println("Unable to listen on address: ", addr) + return + } + log.Println("Listening on address: ", ln.Addr().String()) + + quit := make(chan bool) + + if reply { // Client sent port 0. let them know which port is actually being used + + _, port, err := getHostPortFromAddr(ln.Addr()) + if err != nil { + return + } + + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, uint32(port)) + t.Port = uint32(port) + req.Reply(true, b) + } else { + req.Reply(true, nil) + } + + go func() { // Handle incoming connections on this new listener + for { + select { + case <-quit: + + return + default: + conn, err := ln.Accept() + if err != nil { // Unable to accept new connection - listener likely closed + continue + } + go func(conn net.Conn) { + p := directForward{} + var err error + + var portnum int + p.Host1 = t.Host + p.Port1 = t.Port + p.Host2, portnum, err = getHostPortFromAddr(conn.RemoteAddr()) + if err != nil { + + return + } + + p.Port2 = uint32(portnum) + ch, reqs, err := sshConn.OpenChannel(ForwardedTCPReturnRequest, ssh.Marshal(p)) + if err != nil { + log.Println("Open forwarded Channel: ", err.Error()) + return + } + ssh.DiscardRequests(reqs) + go func(ch ssh.Channel, conn net.Conn) { + + close := func() { + ch.Close() + conn.Close() + + // log.Printf("forwarding closed") + } + + go CopyReadWriters(conn, ch, close) + + }(ch, conn) + + }(conn) + } + + } + + }() + sshConn.Wait() + log.Println("Stop forwarding/listening on ", ln.Addr()) + ln.Close() + quit <- true + +} + +func getHostPortFromAddr(addr net.Addr) (host string, port int, err error) { + host, portString, err := net.SplitHostPort(addr.String()) + if err != nil { + return + } + port, err = strconv.Atoi(portString) + return +} + +// DirectPortForwardHandler returns a ChannelHandler that implements standard SSH direct portforwarding +func DirectPortForwardHandler() ChannelHandler { return ChannelHandlerFunc(DirectPortForwardChannel) } + +// DirectPortForwardChannel acts as an SSH Direct Port Forwarder - ssh -L +// +// Should be to channel type - "direct-tcpip" - RFC 4254 7.2 +func DirectPortForwardChannel(newChannel ssh.NewChannel, channel ssh.Channel, reqs <-chan *ssh.Request, sshConn ssh.Conn) { + + p := directForward{} + ssh.Unmarshal(newChannel.ExtraData(), &p) + log.Println(p) + + go func(ch ssh.Channel, sshConn ssh.Conn) { + addr := fmt.Sprintf("%s:%d", p.Host1, p.Port1) + conn, err := net.Dial("tcp", addr) + if err != nil { + return + } + close := func() { + ch.Close() + conn.Close() + + //log.Printf("forwarding closed") + } + + go CopyReadWriters(conn, ch, close) + + }(channel, sshConn) + +}