Initial commit
This commit is contained in:
664
application/commands/ssh.go
Normal file
664
application/commands/ssh.go
Normal file
@@ -0,0 +1,664 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/niruix/sshwifty/application/command"
|
||||
"github.com/niruix/sshwifty/application/log"
|
||||
"github.com/niruix/sshwifty/application/network"
|
||||
"github.com/niruix/sshwifty/application/rw"
|
||||
)
|
||||
|
||||
// Server -> client signal Consts
|
||||
const (
|
||||
SSHServerRemoteStdOut = 0x00
|
||||
SSHServerRemoteStdErr = 0x01
|
||||
SSHServerConnectFailed = 0x02
|
||||
SSHServerConnectSucceed = 0x03
|
||||
SSHServerConnectVerifyFingerprint = 0x04
|
||||
SSHServerConnectRequestCredential = 0x05
|
||||
)
|
||||
|
||||
// Client -> server signal consts
|
||||
const (
|
||||
SSHClientStdIn = 0x00
|
||||
SSHClientResize = 0x01
|
||||
SSHClientRespondFingerprint = 0x02
|
||||
SSHClientRespondCredential = 0x03
|
||||
)
|
||||
|
||||
const (
|
||||
sshCredentialMaxSize = 4096
|
||||
)
|
||||
|
||||
// Error codes
|
||||
const (
|
||||
SSHRequestErrorBadUserName = command.StreamError(0x01)
|
||||
SSHRequestErrorBadRemoteAddress = command.StreamError(0x02)
|
||||
SSHRequestErrorBadAuthMethod = command.StreamError(0x03)
|
||||
)
|
||||
|
||||
// Auth methods
|
||||
const (
|
||||
SSHAuthMethodNone byte = 0x00
|
||||
SSHAuthMethodPasspharse byte = 0x01
|
||||
SSHAuthMethodPrivateKey byte = 0x02
|
||||
)
|
||||
|
||||
type sshAuthMethodBuilder func(b []byte) []ssh.AuthMethod
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrSSHAuthCancelled = errors.New(
|
||||
"Authenication has been cancelled")
|
||||
|
||||
ErrSSHInvalidAuthMethod = errors.New(
|
||||
"Invalid auth method")
|
||||
|
||||
ErrSSHInvalidAddress = errors.New(
|
||||
"Invalid address")
|
||||
|
||||
ErrSSHRemoteFingerprintVerificationCancelled = errors.New(
|
||||
"Server Fingerprint verification process has been cancelled")
|
||||
|
||||
ErrSSHRemoteFingerprintRefused = errors.New(
|
||||
"Server Fingerprint has been refused")
|
||||
|
||||
ErrSSHRemoteConnUnavailable = errors.New(
|
||||
"Remote SSH connection is unavailable")
|
||||
|
||||
ErrSSHUnexpectedFingerprintVerificationRespond = errors.New(
|
||||
"Unexpected fingerprint verification respond")
|
||||
|
||||
ErrSSHUnexpectedCredentialDataRespond = errors.New(
|
||||
"Unexpected credential data respond")
|
||||
|
||||
ErrSSHCredentialDataTooLarge = errors.New(
|
||||
"Credential was too large")
|
||||
|
||||
ErrSSHUnknownClientSignal = errors.New(
|
||||
"Unknown client signal")
|
||||
)
|
||||
|
||||
type sshRemoteConn struct {
|
||||
writer io.Writer
|
||||
closer func() error
|
||||
session *ssh.Session
|
||||
}
|
||||
|
||||
func (s sshRemoteConn) isValid() bool {
|
||||
return s.writer != nil && s.closer != nil && s.session != nil
|
||||
}
|
||||
|
||||
type sshClient struct {
|
||||
w command.StreamResponder
|
||||
l log.Logger
|
||||
dial network.Dial
|
||||
dialTimeout time.Duration
|
||||
remoteCloseWait sync.WaitGroup
|
||||
credentialReceive chan []byte
|
||||
credentialProcessed bool
|
||||
credentialReceiveClosed bool
|
||||
fingerprintVerifyResultReceive chan bool
|
||||
fingerprintProcessed bool
|
||||
fingerprintVerifyResultReceiveClosed bool
|
||||
remoteConnReceive chan sshRemoteConn
|
||||
remoteConn sshRemoteConn
|
||||
}
|
||||
|
||||
func newSSH(
|
||||
l log.Logger,
|
||||
w command.StreamResponder,
|
||||
dial network.Dial,
|
||||
) command.FSMMachine {
|
||||
return &sshClient{
|
||||
w: w,
|
||||
l: l,
|
||||
dial: dial,
|
||||
dialTimeout: 10 * time.Second,
|
||||
remoteCloseWait: sync.WaitGroup{},
|
||||
credentialReceive: make(chan []byte, 1),
|
||||
credentialProcessed: false,
|
||||
credentialReceiveClosed: false,
|
||||
fingerprintVerifyResultReceive: make(chan bool, 1),
|
||||
fingerprintProcessed: false,
|
||||
fingerprintVerifyResultReceiveClosed: false,
|
||||
remoteConnReceive: make(chan sshRemoteConn, 1),
|
||||
remoteConn: sshRemoteConn{},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sshClient) Bootup(
|
||||
r *rw.LimitedReader,
|
||||
b []byte,
|
||||
) (command.FSMState, command.FSMError) {
|
||||
// User name
|
||||
userName, userNameErr := ParseString(r.Read, b)
|
||||
|
||||
if userNameErr != nil {
|
||||
return nil, command.ToFSMError(
|
||||
userNameErr, SSHRequestErrorBadUserName)
|
||||
}
|
||||
|
||||
userNameStr := string(userName.Data())
|
||||
|
||||
// Address
|
||||
addr, addrErr := ParseAddress(r.Read, b)
|
||||
|
||||
if addrErr != nil {
|
||||
return nil, command.ToFSMError(
|
||||
addrErr, SSHRequestErrorBadRemoteAddress)
|
||||
}
|
||||
|
||||
addrStr := addr.String()
|
||||
|
||||
if len(addrStr) <= 0 {
|
||||
return nil, command.ToFSMError(
|
||||
ErrSSHInvalidAddress, SSHRequestErrorBadRemoteAddress)
|
||||
}
|
||||
|
||||
// Auth method
|
||||
rData, rErr := rw.FetchOneByte(r.Fetch)
|
||||
|
||||
if rErr != nil {
|
||||
return nil, command.ToFSMError(
|
||||
rErr, SSHRequestErrorBadAuthMethod)
|
||||
}
|
||||
|
||||
authMethodBuilder, authMethodBuilderErr := d.buildAuthMethod(rData[0])
|
||||
|
||||
if authMethodBuilderErr != nil {
|
||||
return nil, command.ToFSMError(
|
||||
authMethodBuilderErr, SSHRequestErrorBadAuthMethod)
|
||||
}
|
||||
|
||||
d.remoteCloseWait.Add(1)
|
||||
go d.remote(userNameStr, addrStr, authMethodBuilder)
|
||||
|
||||
return d.local, command.NoFSMError()
|
||||
}
|
||||
|
||||
func (d *sshClient) buildAuthMethod(
|
||||
methodType byte) (sshAuthMethodBuilder, error) {
|
||||
switch methodType {
|
||||
case SSHAuthMethodNone:
|
||||
return func(b []byte) []ssh.AuthMethod {
|
||||
return nil
|
||||
}, nil
|
||||
|
||||
case SSHAuthMethodPasspharse:
|
||||
return func(b []byte) []ssh.AuthMethod {
|
||||
return []ssh.AuthMethod{
|
||||
ssh.PasswordCallback(func() (string, error) {
|
||||
wErr := d.w.SendManual(
|
||||
SSHServerConnectRequestCredential,
|
||||
b[d.w.HeaderSize():],
|
||||
)
|
||||
|
||||
if wErr != nil {
|
||||
return "", wErr
|
||||
}
|
||||
|
||||
passpharseBytes, passpharseReceived := <-d.credentialReceive
|
||||
|
||||
if !passpharseReceived {
|
||||
return "", ErrSSHAuthCancelled
|
||||
}
|
||||
|
||||
return string(passpharseBytes), nil
|
||||
}),
|
||||
}
|
||||
}, nil
|
||||
|
||||
case SSHAuthMethodPrivateKey:
|
||||
return func(b []byte) []ssh.AuthMethod {
|
||||
return []ssh.AuthMethod{
|
||||
ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
|
||||
wErr := d.w.SendManual(
|
||||
SSHServerConnectRequestCredential,
|
||||
b[d.w.HeaderSize():],
|
||||
)
|
||||
|
||||
if wErr != nil {
|
||||
return nil, wErr
|
||||
}
|
||||
|
||||
privateKeyBytes, privateKeyReceived := <-d.credentialReceive
|
||||
|
||||
if !privateKeyReceived {
|
||||
return nil, ErrSSHAuthCancelled
|
||||
}
|
||||
|
||||
signer, signerErr := ssh.ParsePrivateKey(privateKeyBytes)
|
||||
|
||||
if signerErr != nil {
|
||||
return nil, signerErr
|
||||
}
|
||||
|
||||
return []ssh.Signer{signer}, signerErr
|
||||
}),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, ErrSSHInvalidAuthMethod
|
||||
}
|
||||
|
||||
func (d *sshClient) comfirmRemoteFingerprint(
|
||||
hostname string,
|
||||
remote net.Addr,
|
||||
key ssh.PublicKey,
|
||||
buf []byte,
|
||||
) error {
|
||||
fgp := ssh.FingerprintSHA256(key)
|
||||
fgpLen := copy(buf[d.w.HeaderSize():], fgp)
|
||||
|
||||
wErr := d.w.SendManual(
|
||||
SSHServerConnectVerifyFingerprint,
|
||||
buf[:d.w.HeaderSize()+fgpLen],
|
||||
)
|
||||
|
||||
if wErr != nil {
|
||||
return wErr
|
||||
}
|
||||
|
||||
confirmed, confirmOK := <-d.fingerprintVerifyResultReceive
|
||||
|
||||
if !confirmOK {
|
||||
return ErrSSHRemoteFingerprintVerificationCancelled
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
return ErrSSHRemoteFingerprintRefused
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *sshClient) dialRemote(
|
||||
network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
|
||||
conn, err := d.dial(network, addr, config.Timeout)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ssh.NewClient(c, chans, reqs), nil
|
||||
}
|
||||
|
||||
func (d *sshClient) remote(
|
||||
user string, address string, authMethodBuilder sshAuthMethodBuilder) {
|
||||
defer func() {
|
||||
d.w.Signal(command.HeaderClose)
|
||||
|
||||
close(d.remoteConnReceive)
|
||||
d.remoteCloseWait.Done()
|
||||
}()
|
||||
|
||||
buf := [4096]byte{}
|
||||
|
||||
conn, dErr := d.dialRemote("tcp", address, &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: authMethodBuilder(buf[:]),
|
||||
HostKeyCallback: func(h string, r net.Addr, k ssh.PublicKey) error {
|
||||
return d.comfirmRemoteFingerprint(h, r, k, buf[:])
|
||||
},
|
||||
Timeout: d.dialTimeout,
|
||||
})
|
||||
|
||||
if dErr != nil {
|
||||
errLen := copy(buf[d.w.HeaderSize():], dErr.Error()) + d.w.HeaderSize()
|
||||
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
|
||||
|
||||
d.l.Debug("Unable to connect to remote machine: %s", dErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
session, sErr := conn.NewSession()
|
||||
|
||||
if sErr != nil {
|
||||
errLen := copy(buf[d.w.HeaderSize():], sErr.Error()) + d.w.HeaderSize()
|
||||
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
|
||||
|
||||
d.l.Debug("Unable open new session on remote machine: %s", sErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer session.Close()
|
||||
|
||||
in, inErr := session.StdinPipe()
|
||||
|
||||
if inErr != nil {
|
||||
errLen := copy(buf[d.w.HeaderSize():], inErr.Error()) + d.w.HeaderSize()
|
||||
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
|
||||
|
||||
d.l.Debug("Unable export Stdin pipe: %s", inErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
out, outErr := session.StdoutPipe()
|
||||
|
||||
if outErr != nil {
|
||||
errLen := copy(buf[d.w.HeaderSize():], outErr.Error()) +
|
||||
d.w.HeaderSize()
|
||||
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
|
||||
|
||||
d.l.Debug("Unable export Stdout pipe: %s", outErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
errOut, outErrErr := session.StderrPipe()
|
||||
|
||||
if outErrErr != nil {
|
||||
errLen := copy(buf[d.w.HeaderSize():], outErrErr.Error()) +
|
||||
d.w.HeaderSize()
|
||||
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
|
||||
|
||||
d.l.Debug("Unable export Stderr pipe: %s", outErrErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sErr = session.RequestPty("xterm", 80, 40, ssh.TerminalModes{
|
||||
ssh.ECHO: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
})
|
||||
|
||||
if sErr != nil {
|
||||
errLen := copy(buf[d.w.HeaderSize():], sErr.Error()) + d.w.HeaderSize()
|
||||
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
|
||||
|
||||
d.l.Debug("Unable request PTY: %s", sErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sErr = session.Shell()
|
||||
|
||||
if sErr != nil {
|
||||
errLen := copy(buf[d.w.HeaderSize():], sErr.Error()) + d.w.HeaderSize()
|
||||
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
|
||||
|
||||
d.l.Debug("Unable to start Shell: %s", sErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer session.Wait()
|
||||
|
||||
d.remoteConnReceive <- sshRemoteConn{
|
||||
writer: in,
|
||||
closer: func() error {
|
||||
sErr := session.Close()
|
||||
|
||||
if sErr != nil {
|
||||
return sErr
|
||||
}
|
||||
|
||||
return conn.Close()
|
||||
},
|
||||
session: session,
|
||||
}
|
||||
|
||||
wErr := d.w.SendManual(
|
||||
SSHServerConnectSucceed, buf[:d.w.HeaderSize()])
|
||||
|
||||
if wErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
d.l.Debug("Serving")
|
||||
|
||||
d.remoteCloseWait.Add(1)
|
||||
|
||||
go func() {
|
||||
defer d.remoteCloseWait.Done()
|
||||
|
||||
errOutBuf := [4096]byte{}
|
||||
|
||||
for {
|
||||
rLen, rErr := errOut.Read(errOutBuf[d.w.HeaderSize():])
|
||||
|
||||
if rErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rErr = d.w.SendManual(
|
||||
SSHServerRemoteStdErr, errOutBuf[:d.w.HeaderSize()+rLen])
|
||||
|
||||
if rErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
rLen, rErr := out.Read(buf[d.w.HeaderSize():])
|
||||
|
||||
if rErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rErr = d.w.SendManual(
|
||||
SSHServerRemoteStdOut, buf[:d.w.HeaderSize()+rLen])
|
||||
|
||||
if rErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sshClient) getRemote() (sshRemoteConn, error) {
|
||||
if d.remoteConn.isValid() {
|
||||
return d.remoteConn, nil
|
||||
}
|
||||
|
||||
remoteConn, remoteConnFetched := <-d.remoteConnReceive
|
||||
|
||||
if !remoteConnFetched {
|
||||
return sshRemoteConn{}, ErrSSHRemoteConnUnavailable
|
||||
}
|
||||
|
||||
d.remoteConn = remoteConn
|
||||
|
||||
return d.remoteConn, nil
|
||||
}
|
||||
|
||||
func (d *sshClient) local(
|
||||
f *command.FSM,
|
||||
r *rw.LimitedReader,
|
||||
h command.StreamHeader,
|
||||
b []byte,
|
||||
) error {
|
||||
switch h.Marker() {
|
||||
case SSHClientStdIn:
|
||||
remote, remoteErr := d.getRemote()
|
||||
|
||||
if remoteErr != nil {
|
||||
return remoteErr
|
||||
}
|
||||
|
||||
for !r.Completed() {
|
||||
rData, rErr := r.Buffered()
|
||||
|
||||
if rErr != nil {
|
||||
return rErr
|
||||
}
|
||||
|
||||
_, wErr := remote.writer.Write(rData)
|
||||
|
||||
if wErr != nil {
|
||||
return wErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case SSHClientResize:
|
||||
remote, remoteErr := d.getRemote()
|
||||
|
||||
if remoteErr != nil {
|
||||
return remoteErr
|
||||
}
|
||||
|
||||
_, rErr := io.ReadFull(r, b[:4])
|
||||
|
||||
if rErr != nil {
|
||||
return rErr
|
||||
}
|
||||
|
||||
rows := int(b[0])
|
||||
rows <<= 8
|
||||
rows |= int(b[1])
|
||||
|
||||
cols := int(b[2])
|
||||
cols <<= 8
|
||||
cols |= int(b[3])
|
||||
|
||||
// It's ok for it to fail
|
||||
wcErr := remote.session.WindowChange(rows, cols)
|
||||
|
||||
if wcErr != nil {
|
||||
d.l.Debug("Failed to resize to %d, %d: %s", rows, cols, wcErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case SSHClientRespondFingerprint:
|
||||
if d.fingerprintProcessed {
|
||||
return ErrSSHUnexpectedFingerprintVerificationRespond
|
||||
}
|
||||
|
||||
d.fingerprintProcessed = true
|
||||
|
||||
rData, rErr := rw.FetchOneByte(r.Fetch)
|
||||
|
||||
if rErr != nil {
|
||||
return rErr
|
||||
}
|
||||
|
||||
comfirmed := rData[0] == 0
|
||||
|
||||
if !comfirmed {
|
||||
d.fingerprintVerifyResultReceive <- false
|
||||
|
||||
remote, remoteErr := d.getRemote()
|
||||
|
||||
if remoteErr == nil {
|
||||
remote.closer()
|
||||
}
|
||||
} else {
|
||||
d.fingerprintVerifyResultReceive <- true
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case SSHClientRespondCredential:
|
||||
if d.credentialProcessed {
|
||||
return ErrSSHUnexpectedCredentialDataRespond
|
||||
}
|
||||
|
||||
d.credentialProcessed = true
|
||||
|
||||
sshCredentialBufSize := 0
|
||||
|
||||
if r.Remains() > sshCredentialMaxSize {
|
||||
sshCredentialBufSize = sshCredentialMaxSize
|
||||
} else {
|
||||
sshCredentialBufSize = r.Remains()
|
||||
}
|
||||
|
||||
credentialDataBuf := make([]byte, 0, sshCredentialBufSize)
|
||||
totalCredentialRead := 0
|
||||
|
||||
for !r.Completed() {
|
||||
rData, rErr := r.Buffered()
|
||||
|
||||
if rErr != nil {
|
||||
return rErr
|
||||
}
|
||||
|
||||
totalCredentialRead += len(rData)
|
||||
|
||||
if totalCredentialRead > sshCredentialBufSize {
|
||||
return ErrSSHCredentialDataTooLarge
|
||||
}
|
||||
|
||||
credentialDataBuf = append(credentialDataBuf, rData...)
|
||||
}
|
||||
|
||||
d.credentialReceive <- credentialDataBuf
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
return ErrSSHUnknownClientSignal
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sshClient) Close() error {
|
||||
d.credentialProcessed = true
|
||||
d.fingerprintProcessed = true
|
||||
|
||||
if !d.credentialReceiveClosed {
|
||||
close(d.credentialReceive)
|
||||
|
||||
d.credentialReceiveClosed = true
|
||||
}
|
||||
|
||||
if !d.fingerprintVerifyResultReceiveClosed {
|
||||
close(d.fingerprintVerifyResultReceive)
|
||||
|
||||
d.fingerprintVerifyResultReceiveClosed = true
|
||||
}
|
||||
|
||||
remote, remoteErr := d.getRemote()
|
||||
|
||||
if remoteErr == nil {
|
||||
remote.closer()
|
||||
}
|
||||
|
||||
d.remoteCloseWait.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *sshClient) Release() error {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user