313 lines
7.9 KiB
Go
313 lines
7.9 KiB
Go
package easyssh
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"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 {
|
|
logger.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 {
|
|
logger.Printf("failed to exit bash (%s)", err)
|
|
}
|
|
logger.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 {
|
|
logger.Printf("Unable to decode pty request: %s", err.Error())
|
|
}
|
|
|
|
setWinsize(f.Fd(), pty.Width, pty.Height)
|
|
logger.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 {
|
|
logger.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
|
|
logger.Printf("exec starting: %s", cmd.Command)
|
|
//c.Env = append(c.Env, env...)
|
|
|
|
exitStatus := exitStatusReq{}
|
|
|
|
fd, err := pty.Start(command)
|
|
if err != nil {
|
|
logger.Printf("Unable to wrap exec command in pty\n")
|
|
return
|
|
}
|
|
|
|
execClose := func() {
|
|
channel.Close()
|
|
logger.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 {
|
|
logger.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)
|
|
logger.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) {
|
|
logger.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)))
|
|
}
|