Initial commit
This commit is contained in:
276
application/commands/address.go
Normal file
276
application/commands/address.go
Normal 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")
|
||||
}
|
||||
}
|
||||
138
application/commands/address_test.go
Normal file
138
application/commands/address_test.go
Normal 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")
|
||||
}
|
||||
30
application/commands/commands.go
Normal file
30
application/commands/commands.go
Normal 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,
|
||||
}
|
||||
}
|
||||
120
application/commands/integer.go
Normal file
120
application/commands/integer.go
Normal 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
|
||||
}
|
||||
124
application/commands/integer_test.go
Normal file
124
application/commands/integer_test.go
Normal 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
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
|
||||
}
|
||||
103
application/commands/string.go
Normal file
103
application/commands/string.go
Normal 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
|
||||
}
|
||||
83
application/commands/string_test.go
Normal file
83
application/commands/string_test.go
Normal 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',
|
||||
})
|
||||
}
|
||||
204
application/commands/telnet.go
Normal file
204
application/commands/telnet.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user