Initial commit

This commit is contained in:
NI
2019-08-07 15:56:51 +08:00
commit 02f14eb14f
206 changed files with 38863 additions and 0 deletions

View File

@@ -0,0 +1,276 @@
// 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"
"net"
"strconv"
"github.com/niruix/sshwifty/application/rw"
)
//Errors
var (
ErrAddressParseBufferTooSmallForHeader = errors.New(
"Buffer space was too small to parse the address header")
ErrAddressParseBufferTooSmallForIPv4 = errors.New(
"Buffer space was too small to parse the IPv4 address")
ErrAddressParseBufferTooSmallForIPv6 = errors.New(
"Buffer space was too small to parse the IPv6 address")
ErrAddressParseBufferTooSmallForHostName = errors.New(
"Buffer space was too small to parse the hostname address")
ErrAddressMarshalBufferTooSmall = errors.New(
"Buffer space was too small to marshal the address")
ErrAddressInvalidAddressType = errors.New(
"Invalid address type")
)
// AddressType Type of the address
type AddressType byte
// Address types
const (
LoopbackAddr AddressType = 0x00
IPv4Addr AddressType = 0x01
IPv6Addr AddressType = 0x02
HostNameAddr AddressType = 0x03
)
// Address data
type Address struct {
port uint16
kind AddressType
data []byte
}
// ParseAddress parses the reader and return an Address
//
// Address data format:
// +-------------+--------------+---------------+
// | 2 bytes | 1 byte | n bytes |
// +-------------+--------------+---------------+
// | Port number | Address type | Address data |
// +-------------+--------------+---------------+
//
// Address types:
// - LoopbackAddr: 00 Localhost, don't carry Address data
// - IPv4Addr: 01 IPv4 Address, carries 4 bytes of Address data
// - IPv6Addr: 10 IPv6 Address, carries 16 bytes Address data
// - HostnameAddr: 11 Host name string, length of Address data is indicated
// by the remainer of the byte (11-- ----). maxlen = 63
//
func ParseAddress(reader rw.ReaderFunc, buf []byte) (Address, error) {
if len(buf) < 3 {
return Address{}, ErrAddressParseBufferTooSmallForHeader
}
_, rErr := rw.ReadFull(reader, buf[:3])
if rErr != nil {
return Address{}, rErr
}
portNum := uint16(0)
portNum |= uint16(buf[0])
portNum <<= 8
portNum |= uint16(buf[1])
addrType := AddressType(buf[2] >> 6)
var addrData []byte
switch addrType {
case LoopbackAddr:
// Do nothing
case IPv4Addr:
if len(buf) < 4 {
return Address{}, ErrAddressParseBufferTooSmallForIPv4
}
_, rErr := rw.ReadFull(reader, buf[:4])
if rErr != nil {
return Address{}, rErr
}
addrData = buf[:4]
case IPv6Addr:
if len(buf) < 16 {
return Address{}, ErrAddressParseBufferTooSmallForIPv6
}
_, rErr := rw.ReadFull(reader, buf[:16])
if rErr != nil {
return Address{}, rErr
}
addrData = buf[:16]
case HostNameAddr:
addrDataLen := int(0x3f & buf[2])
if len(buf) < addrDataLen {
return Address{}, ErrAddressParseBufferTooSmallForHostName
}
_, rErr := rw.ReadFull(reader, buf[:addrDataLen])
if rErr != nil {
return Address{}, rErr
}
addrData = buf[:addrDataLen]
default:
return Address{}, ErrAddressInvalidAddressType
}
return Address{
port: portNum,
kind: addrType,
data: addrData,
}, nil
}
// NewAddress creates a new Address
func NewAddress(addrType AddressType, data []byte, port uint16) Address {
return Address{
port: port,
kind: addrType,
data: data,
}
}
// Type returns the type of the address
func (a Address) Type() AddressType {
return a.kind
}
// Data returns the address data
func (a Address) Data() []byte {
return a.data
}
// Port returns port number
func (a Address) Port() uint16 {
return a.port
}
// Marshal writes address data to the given b
func (a Address) Marshal(b []byte) (int, error) {
bLen := len(b)
switch a.Type() {
case LoopbackAddr:
if bLen < 3 {
return 0, ErrAddressMarshalBufferTooSmall
}
b[0] = byte(a.port >> 8)
b[1] = byte(a.port)
b[2] = byte(LoopbackAddr << 6)
return 3, nil
case IPv4Addr:
if bLen < 7 {
return 0, ErrAddressMarshalBufferTooSmall
}
b[0] = byte(a.port >> 8)
b[1] = byte(a.port)
b[2] = byte(IPv4Addr << 6)
copy(b[3:], a.data)
return 7, nil
case IPv6Addr:
if bLen < 19 {
return 0, ErrAddressMarshalBufferTooSmall
}
b[0] = byte(a.port >> 8)
b[1] = byte(a.port)
b[2] = byte(IPv6Addr << 6)
copy(b[3:], a.data)
return 19, nil
case HostNameAddr:
hLen := len(a.data)
if hLen > 0x3f {
panic("Host name cannot longer than 0x3f")
}
if bLen < hLen+3 {
return 0, ErrAddressMarshalBufferTooSmall
}
b[0] = byte(a.port >> 8)
b[1] = byte(a.port)
b[2] = byte(HostNameAddr << 6)
b[2] |= byte(hLen)
copy(b[3:], a.data)
return hLen + 3, nil
default:
return 0, ErrAddressInvalidAddressType
}
}
// String return the Address as string
func (a Address) String() string {
switch a.Type() {
case LoopbackAddr:
return net.JoinHostPort(
"localhost",
strconv.FormatUint(uint64(a.Port()), 10))
case IPv4Addr:
return net.JoinHostPort(
net.IPv4(a.data[0], a.data[1], a.data[2], a.data[3]).String(),
strconv.FormatUint(uint64(a.Port()), 10))
case IPv6Addr:
return net.JoinHostPort(
net.IP(a.data[:net.IPv6len]).String(),
strconv.FormatUint(uint64(a.Port()), 10))
case HostNameAddr:
return net.JoinHostPort(
string(a.data),
strconv.FormatUint(uint64(a.Port()), 10))
default:
panic("Unknown Address type")
}
}

View File

@@ -0,0 +1,138 @@
// 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 (
"bytes"
"strings"
"testing"
)
func testParseAddress(
t *testing.T,
input []byte,
buf []byte,
expectedType AddressType,
expectedData []byte,
expectedPort uint16,
expectedHostPortString string,
) {
source := bytes.NewBuffer(input)
addr, addrErr := ParseAddress(source.Read, buf)
if addrErr != nil {
t.Error("Failed to parse due to error:", addrErr)
return
}
if addr.Type() != expectedType {
t.Errorf("Expecting the Type to be %d, got %d instead",
expectedType, addr.Type())
return
}
if !bytes.Equal(addr.Data(), expectedData) {
t.Errorf("Expecting the Data to be %d, got %d instead",
expectedData, addr.Data())
return
}
if addr.Port() != expectedPort {
t.Errorf("Expecting the Port to be %d, got %d instead",
expectedPort, addr.Port())
return
}
if addr.String() != expectedHostPortString {
t.Errorf("Expecting the Host Port string to be \"%s\", "+
"got \"%s\" instead",
expectedHostPortString, addr.String())
return
}
output := make([]byte, len(input))
mLen, mErr := addr.Marshal(output)
if mErr != nil {
t.Error("Failed to marshal due to error:", mErr)
return
}
if !bytes.Equal(output[:mLen], input) {
t.Errorf("Expecting marshaled result to be %d, got %d instead",
input, output[:mLen])
return
}
}
func TestParseAddress(t *testing.T) {
testParseAddress(
t, []byte{0x04, 0x1e, 0x00}, make([]byte, 3), LoopbackAddr, nil, 1054,
"localhost:1054")
testParseAddress(
t,
[]byte{
0x04, 0x1e, 0x40,
0x7f, 0x00, 0x00, 0x01,
},
make([]byte, 4), IPv4Addr, []byte{0x7f, 0x00, 0x00, 0x01}, 1054,
"127.0.0.1:1054")
testParseAddress(
t,
[]byte{
0x04, 0x1e, 0x80,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x01,
},
make([]byte, 16), IPv6Addr, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x01}, 1054,
"[::7f00:1]:1054")
testParseAddress(
t,
[]byte{
0x04, 0x1e, 0xff,
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'1', '2', '3',
},
make([]byte, 63), HostNameAddr, []byte{
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'1', '2', '3',
}, 1054,
strings.Repeat("ABCDEFGHIJ", 6)+"123:1054")
}

View File

@@ -0,0 +1,30 @@
// 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 (
"github.com/niruix/sshwifty/application/command"
)
// New creates a new commands group
func New() command.Commands {
return command.Commands{
newTelnet,
newSSH,
}
}

View File

@@ -0,0 +1,120 @@
// 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"
"github.com/niruix/sshwifty/application/rw"
)
// Errors
var (
ErrIntegerMarshalNotEnoughBuffer = errors.New(
"Not enough buffer to marshal the integer")
ErrIntegerMarshalTooLarge = errors.New(
"Integer cannot be marshalled, because the vaule was too large")
)
// Integer is a 16bit unsigned integer data
//
// Format:
// +-------------------------------------+--------------+
// | 1 bit | 7 bits |
// +-------------------------------------+--------------+
// | 1 when current byte is the end byte | Integer data |
// +-------------------------------------+--------------+
//
// Example:
// - 00000000 00000000: 0
// - 01111111: 127
// - 11111111 01000000: 255
type Integer uint16
const (
integerHasNextBit = 0x80
integerValueCutter = 0x7f
)
// Consts
const (
MaxInteger = 0x3fff
MaxIntegerBytes = 2
)
// ByteSize returns how many bytes current integer will be encoded into
func (i *Integer) ByteSize() int {
if *i > integerValueCutter {
return 2
}
return 1
}
// Int returns a int of current Integer
func (i *Integer) Int() int {
return int(*i)
}
// Marshal build serialized data of the integer
func (i *Integer) Marshal(b []byte) (int, error) {
bLen := len(b)
if *i > MaxInteger {
return 0, ErrIntegerMarshalTooLarge
}
if bLen < i.ByteSize() {
return 0, ErrIntegerMarshalNotEnoughBuffer
}
if *i <= integerValueCutter {
b[0] = byte(*i & integerValueCutter)
return 1, nil
}
b[0] = byte((*i >> 7) | integerHasNextBit)
b[1] = byte(*i & integerValueCutter)
return 2, nil
}
// Unmarshal read data and parse the integer
func (i *Integer) Unmarshal(reader rw.ReaderFunc) error {
buf := [1]byte{}
for j := 0; j < MaxIntegerBytes; j++ {
_, rErr := rw.ReadFull(reader, buf[:])
if rErr != nil {
return rErr
}
*i |= Integer(buf[0] & integerValueCutter)
if integerHasNextBit&buf[0] == 0 {
return nil
}
*i <<= 7
}
return nil
}

View File

@@ -0,0 +1,124 @@
// 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 (
"bytes"
"testing"
)
func TestInteger(t *testing.T) {
ii := Integer(0x3fff)
result := Integer(0)
buf := make([]byte, 2)
mLen, mErr := ii.Marshal(buf)
if mErr != nil {
t.Error("Failed to marshal:", mErr)
return
}
mData := bytes.NewBuffer(buf[:mLen])
mErr = result.Unmarshal(mData.Read)
if mErr != nil {
t.Error("Failed to unmarshal:", mErr)
return
}
if result != ii {
t.Errorf("Expecting result to be %d, got %d instead", ii, result)
return
}
}
func TestIntegerSingleByte1(t *testing.T) {
ii := Integer(102)
result := Integer(0)
buf := make([]byte, 2)
mLen, mErr := ii.Marshal(buf)
if mErr != nil {
t.Error("Failed to marshal:", mErr)
return
}
if mLen != 1 {
t.Error("Expecting the Integer to be marshalled into %d bytes, got "+
"%d instead", 1, mLen)
return
}
mData := bytes.NewBuffer(buf[:mLen])
mErr = result.Unmarshal(mData.Read)
if mErr != nil {
t.Error("Failed to unmarshal:", mErr)
return
}
if result != ii {
t.Errorf("Expecting result to be %d, got %d instead", ii, result)
return
}
}
func TestIntegerSingleByte2(t *testing.T) {
ii := Integer(127)
result := Integer(0)
buf := make([]byte, 2)
mLen, mErr := ii.Marshal(buf)
if mErr != nil {
t.Error("Failed to marshal:", mErr)
return
}
if mLen != 1 {
t.Error("Expecting the Integer to be marshalled into %d bytes, got "+
"%d instead", 1, mLen)
return
}
mData := bytes.NewBuffer(buf[:mLen])
mErr = result.Unmarshal(mData.Read)
if mErr != nil {
t.Error("Failed to unmarshal:", mErr)
return
}
if result != ii {
t.Errorf("Expecting result to be %d, got %d instead", ii, result)
return
}
}

664
application/commands/ssh.go Normal file
View 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
}

View File

@@ -0,0 +1,103 @@
// 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"
"github.com/niruix/sshwifty/application/rw"
)
// Errors
var (
ErrStringParseBufferTooSmall = errors.New(
"Not enough buffer space to parse given string")
ErrStringMarshalBufferTooSmall = errors.New(
"Not enough buffer space to marshal given string")
)
// String data
type String struct {
len Integer
data []byte
}
// ParseString build the String according to readed data
func ParseString(reader rw.ReaderFunc, b []byte) (String, error) {
lenData := Integer(0)
mErr := lenData.Unmarshal(reader)
if mErr != nil {
return String{}, mErr
}
bLen := len(b)
if bLen < lenData.Int() {
return String{}, ErrStringParseBufferTooSmall
}
_, rErr := rw.ReadFull(reader, b[:lenData])
if rErr != nil {
return String{}, rErr
}
return String{
len: lenData,
data: b[:lenData],
}, nil
}
// NewString create a new String
func NewString(d []byte) String {
dLen := len(d)
if dLen > MaxInteger {
panic("Data was too long for a String")
}
return String{
len: Integer(dLen),
data: d,
}
}
// Data returns the data of the string
func (s String) Data() []byte {
return s.data
}
// Marshal the string to give buffer
func (s String) Marshal(b []byte) (int, error) {
bLen := len(b)
if bLen < s.len.ByteSize()+len(s.data) {
return 0, ErrStringMarshalBufferTooSmall
}
mLen, mErr := s.len.Marshal(b)
if mErr != nil {
return 0, mErr
}
return copy(b[mLen:], s.data) + mLen, nil
}

View File

@@ -0,0 +1,83 @@
// 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 (
"bytes"
"testing"
)
func testString(t *testing.T, str []byte) {
ss := NewString(str)
mm := make([]byte, len(str)+2)
mLen, mErr := ss.Marshal(mm)
if mErr != nil {
t.Error("Failed to marshal:", mErr)
return
}
buf := make([]byte, mLen)
source := bytes.NewBuffer(mm[:mLen])
result, rErr := ParseString(source.Read, buf)
if rErr != nil {
t.Error("Failed to parse:", rErr)
return
}
if !bytes.Equal(result.Data(), ss.Data()) {
t.Errorf("Expecting the data to be %d, got %d instead",
ss.Data(), result.Data())
return
}
}
func TestString(t *testing.T) {
testString(t, []byte{
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
})
testString(t, []byte{
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
})
}

View File

@@ -0,0 +1,204 @@
// 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"
"sync"
"time"
"github.com/niruix/sshwifty/application/command"
"github.com/niruix/sshwifty/application/log"
"github.com/niruix/sshwifty/application/network"
"github.com/niruix/sshwifty/application/rw"
)
// Errors
var (
ErrTelnetUnableToReceiveRemoteConn = errors.New(
"Unable to acquire remote connection handle")
)
// Error codes
const (
TelnetRequestErrorBadRemoteAddress = command.StreamError(0x01)
)
// Server signal codes
const (
TelnetServerRemoteBand = 0x00
TelnetServerDialFailed = 0x01
TelnetServerDialConnected = 0x02
)
type telnetClient struct {
l log.Logger
w command.StreamResponder
dial network.Dial
remoteChan chan io.WriteCloser
remoteConn io.WriteCloser
closeWait sync.WaitGroup
dialTimeout time.Duration
}
func newTelnet(
l log.Logger,
w command.StreamResponder,
dial network.Dial,
) command.FSMMachine {
return &telnetClient{
l: l,
w: w,
dial: dial,
remoteChan: make(chan io.WriteCloser, 1),
remoteConn: nil,
closeWait: sync.WaitGroup{},
dialTimeout: 10 * time.Second,
}
}
func (d *telnetClient) Bootup(
r *rw.LimitedReader,
b []byte) (command.FSMState, command.FSMError) {
addr, addrErr := ParseAddress(r.Read, b)
if addrErr != nil {
return nil, command.ToFSMError(
addrErr, TelnetRequestErrorBadRemoteAddress)
}
// TODO: Test whether or not the address is allowed
d.closeWait.Add(1)
go d.remote(addr.String())
return d.client, command.NoFSMError()
}
func (d *telnetClient) remote(addr string) {
defer func() {
d.w.Signal(command.HeaderClose)
close(d.remoteChan)
d.closeWait.Done()
}()
buf := [4096]byte{}
clientConn, clientConnErr := d.dial("tcp", addr, d.dialTimeout)
if clientConnErr != nil {
errLen := copy(
buf[d.w.HeaderSize():], clientConnErr.Error()) + d.w.HeaderSize()
d.w.SendManual(TelnetServerDialFailed, buf[:errLen])
return
}
defer clientConn.Close()
clientConnErr = d.w.SendManual(
TelnetServerDialConnected,
buf[:d.w.HeaderSize()],
)
if clientConnErr != nil {
return
}
d.remoteChan <- clientConn
for {
rLen, rErr := clientConn.Read(buf[d.w.HeaderSize():])
if rErr != nil {
return
}
wErr := d.w.SendManual(
TelnetServerRemoteBand, buf[:rLen+d.w.HeaderSize()])
if wErr != nil {
return
}
}
}
func (d *telnetClient) getRemote() (io.WriteCloser, error) {
if d.remoteConn != nil {
return d.remoteConn, nil
}
remoteConn, ok := <-d.remoteChan
if !ok {
return nil, ErrTelnetUnableToReceiveRemoteConn
}
d.remoteConn = remoteConn
return d.remoteConn, nil
}
func (d *telnetClient) client(
f *command.FSM,
r *rw.LimitedReader,
h command.StreamHeader,
b []byte,
) error {
remoteConn, remoteConnErr := d.getRemote()
if remoteConnErr != nil {
return remoteConnErr
}
// All Telnet requests are in-band, so we just directly send them all
// to the server
for !r.Completed() {
rBuf, rErr := r.Buffered()
if rErr != nil {
return rErr
}
_, wErr := remoteConn.Write(rBuf)
if wErr != nil {
return wErr
}
}
return nil
}
func (d *telnetClient) Close() error {
remoteConn, remoteConnErr := d.getRemote()
if remoteConnErr == nil {
remoteConn.Close()
}
d.closeWait.Wait()
return nil
}
func (d *telnetClient) Release() error {
return nil
}