Adding current work on easyssh

This commit is contained in:
Justin 2015-05-30 11:19:10 -04:00
parent bf216fa4fd
commit 7308727a1c
10 changed files with 1341 additions and 0 deletions

146
client.go Normal file
View File

@ -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...)
}

292
common.go Normal file
View File

@ -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)
}

56
doc.go Normal file
View File

@ -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

63
example_client_test.go Normal file
View File

@ -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)
}
}

29
example_handler_test.go Normal file
View File

@ -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))
}

46
example_tcpip_test.go Normal file
View File

@ -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()
}

66
example_test.go Normal file
View File

@ -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)
}

157
server.go Normal file
View File

@ -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)
}

313
session.go Normal file
View File

@ -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)))
}

173
tcpip.go Normal file
View File

@ -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)
}