Initial commit
This commit is contained in:
62
application/command/commander.go
Normal file
62
application/command/commander.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/niruix/sshwifty/application/log"
|
||||
"github.com/niruix/sshwifty/application/network"
|
||||
"github.com/niruix/sshwifty/application/rw"
|
||||
)
|
||||
|
||||
// Commander command control
|
||||
type Commander struct {
|
||||
commands Commands
|
||||
}
|
||||
|
||||
// New creates a new Commander
|
||||
func New(cs Commands) Commander {
|
||||
return Commander{
|
||||
commands: cs,
|
||||
}
|
||||
}
|
||||
|
||||
// New Adds a new client
|
||||
func (c Commander) New(
|
||||
dialer network.Dial,
|
||||
receiver rw.FetchReader,
|
||||
sender io.Writer,
|
||||
senderLock *sync.Mutex,
|
||||
receiveDelay time.Duration,
|
||||
sendDelay time.Duration,
|
||||
l log.Logger,
|
||||
) (Handler, error) {
|
||||
return newHandler(
|
||||
dialer,
|
||||
&c.commands,
|
||||
receiver,
|
||||
sender,
|
||||
senderLock,
|
||||
receiveDelay,
|
||||
sendDelay,
|
||||
l,
|
||||
), nil
|
||||
}
|
||||
72
application/command/commands.go
Normal file
72
application/command/commands.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/niruix/sshwifty/application/log"
|
||||
"github.com/niruix/sshwifty/application/network"
|
||||
)
|
||||
|
||||
// Consts
|
||||
const (
|
||||
MaxCommandID = 0x0f
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrCommandRunUndefinedCommand = errors.New(
|
||||
"Undefined Command")
|
||||
)
|
||||
|
||||
// Command represents a command handler machine builder
|
||||
type Command func(l log.Logger, w StreamResponder, d network.Dial) FSMMachine
|
||||
|
||||
// Commands contains data of all commands
|
||||
type Commands [MaxCommandID + 1]Command
|
||||
|
||||
// Register registers a new command
|
||||
func (c *Commands) Register(id byte, cb Command) {
|
||||
if id > MaxCommandID {
|
||||
panic("Command ID must be not greater than MaxCommandID")
|
||||
}
|
||||
|
||||
if (*c)[id] != nil {
|
||||
panic(fmt.Sprintf("Command %d already been registered", id))
|
||||
}
|
||||
|
||||
(*c)[id] = cb
|
||||
}
|
||||
|
||||
// Run creates command executer
|
||||
func (c Commands) Run(
|
||||
id byte, l log.Logger, w StreamResponder, dial network.Dial) (FSM, error) {
|
||||
if id > MaxCommandID {
|
||||
return FSM{}, ErrCommandRunUndefinedCommand
|
||||
}
|
||||
|
||||
cc := c[id]
|
||||
|
||||
if cc == nil {
|
||||
return FSM{}, ErrCommandRunUndefinedCommand
|
||||
}
|
||||
|
||||
return newFSM(cc(l, w, dial)), nil
|
||||
}
|
||||
176
application/command/fsm.go
Normal file
176
application/command/fsm.go
Normal file
@@ -0,0 +1,176 @@
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/niruix/sshwifty/application/rw"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrFSMMachineClosed = errors.New(
|
||||
"FSM Machine is already closed, it cannot do anything but be released")
|
||||
)
|
||||
|
||||
// FSMError Represents an error from FSM
|
||||
type FSMError struct {
|
||||
code StreamError
|
||||
message string
|
||||
succeed bool
|
||||
}
|
||||
|
||||
// ToFSMError converts error to FSMError
|
||||
func ToFSMError(e error, c StreamError) FSMError {
|
||||
return FSMError{
|
||||
code: c,
|
||||
message: e.Error(),
|
||||
succeed: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NoFSMError return a FSMError that represents a success operation
|
||||
func NoFSMError() FSMError {
|
||||
return FSMError{
|
||||
code: 0,
|
||||
message: "No error",
|
||||
succeed: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Error return the error message
|
||||
func (e FSMError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// Code return the error code
|
||||
func (e FSMError) Code() StreamError {
|
||||
return e.code
|
||||
}
|
||||
|
||||
// Succeed returns whether or not current error represents a succeed operation
|
||||
func (e FSMError) Succeed() bool {
|
||||
return e.succeed
|
||||
}
|
||||
|
||||
// FSMState represents a state of a machine
|
||||
type FSMState func(f *FSM, r *rw.LimitedReader, h StreamHeader, b []byte) error
|
||||
|
||||
// FSMMachine State machine
|
||||
type FSMMachine interface {
|
||||
// Bootup boots up the machine
|
||||
Bootup(r *rw.LimitedReader, b []byte) (FSMState, FSMError)
|
||||
|
||||
// Close stops the machine and get it ready for release.
|
||||
//
|
||||
// NOTE: Close function is responsible in making sure the HeaderClose signal
|
||||
// is sent before it returns.
|
||||
// (It may not need to send the header by itself, but it have to
|
||||
// make sure the header is sent)
|
||||
Close() error
|
||||
|
||||
// Release shuts the machine down completely and release it's resources
|
||||
Release() error
|
||||
}
|
||||
|
||||
// FSM state machine control
|
||||
type FSM struct {
|
||||
m FSMMachine
|
||||
s FSMState
|
||||
closed bool
|
||||
}
|
||||
|
||||
// newFSM creates a new FSM
|
||||
func newFSM(m FSMMachine) FSM {
|
||||
return FSM{
|
||||
m: m,
|
||||
s: nil,
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
// emptyFSM creates a empty FSM
|
||||
func emptyFSM() FSM {
|
||||
return FSM{
|
||||
m: nil,
|
||||
s: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// bootup initialize the machine
|
||||
func (f *FSM) bootup(r *rw.LimitedReader, b []byte) FSMError {
|
||||
s, err := f.m.Bootup(r, b)
|
||||
|
||||
if s == nil {
|
||||
panic("FSMState must not be nil")
|
||||
}
|
||||
|
||||
if !err.Succeed() {
|
||||
return err
|
||||
}
|
||||
|
||||
f.s = s
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// running returns whether or not current FSM is running
|
||||
func (f *FSM) running() bool {
|
||||
return f.s != nil
|
||||
}
|
||||
|
||||
// tick ticks current machine
|
||||
func (f *FSM) tick(r *rw.LimitedReader, h StreamHeader, b []byte) error {
|
||||
if f.closed {
|
||||
return ErrFSMMachineClosed
|
||||
}
|
||||
|
||||
return f.s(f, r, h, b)
|
||||
}
|
||||
|
||||
// Release shuts down current machine and release it's resource
|
||||
func (f *FSM) release() error {
|
||||
f.s = nil
|
||||
|
||||
if !f.closed {
|
||||
f.close()
|
||||
}
|
||||
|
||||
rErr := f.m.Release()
|
||||
|
||||
f.m = nil
|
||||
|
||||
if rErr != nil {
|
||||
return rErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close stops the machine and get it ready to release
|
||||
func (f *FSM) close() error {
|
||||
f.closed = true
|
||||
|
||||
return f.m.Close()
|
||||
}
|
||||
|
||||
// Switch switch to specificied State for the next tick
|
||||
func (f *FSM) Switch(s FSMState) {
|
||||
f.s = s
|
||||
}
|
||||
334
application/command/handler.go
Normal file
334
application/command/handler.go
Normal file
@@ -0,0 +1,334 @@
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/niruix/sshwifty/application/log"
|
||||
"github.com/niruix/sshwifty/application/network"
|
||||
"github.com/niruix/sshwifty/application/rw"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrHandlerUnknownHeaderType = errors.New(
|
||||
"Unknown command header type")
|
||||
|
||||
ErrHandlerControlMessageTooLong = errors.New(
|
||||
"Control message was too long")
|
||||
|
||||
ErrHandlerInvalidControlMessage = errors.New(
|
||||
"Invalid control message")
|
||||
)
|
||||
|
||||
// HandlerCancelSignal signals the cancel of the entire handling proccess
|
||||
type HandlerCancelSignal chan struct{}
|
||||
|
||||
const (
|
||||
handlerReadBufLen = HeaderMaxData + 3 // (3 = 1 Header, 2 Etc)
|
||||
)
|
||||
|
||||
type handlerBuf [handlerReadBufLen]byte
|
||||
|
||||
// handlerSender writes handler signal
|
||||
type handlerSender struct {
|
||||
writer io.Writer
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
// signal sends handler signal
|
||||
func (h handlerSender) signal(hd Header, d []byte, buf []byte) error {
|
||||
bufLen := len(buf)
|
||||
dLen := len(d)
|
||||
|
||||
if bufLen < dLen+1 {
|
||||
panic(fmt.Sprintln("Sending signal %s:%d requires %d bytes of buffer, "+
|
||||
"but only %d bytes is available", hd, d, dLen+1, bufLen))
|
||||
}
|
||||
|
||||
buf[0] = byte(hd)
|
||||
|
||||
wLen := copy(buf[1:], d) + 1
|
||||
|
||||
_, wErr := h.Write(buf[:wLen])
|
||||
|
||||
return wErr
|
||||
}
|
||||
|
||||
// Write sends data
|
||||
func (h handlerSender) Write(b []byte) (int, error) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
return h.writer.Write(b)
|
||||
}
|
||||
|
||||
// streamHandlerSender includes all receiver as handlerSender, but it been
|
||||
// designed to be use in streams
|
||||
type streamHandlerSender struct {
|
||||
*handlerSender
|
||||
|
||||
sendDelay time.Duration
|
||||
}
|
||||
|
||||
// signal sends handler signal
|
||||
func (h streamHandlerSender) signal(hd Header, d []byte, buf []byte) error {
|
||||
return h.handlerSender.signal(hd, d, buf)
|
||||
}
|
||||
|
||||
// Write sends data
|
||||
func (h streamHandlerSender) Write(b []byte) (int, error) {
|
||||
defer time.Sleep(h.sendDelay)
|
||||
|
||||
return h.handlerSender.Write(b)
|
||||
}
|
||||
|
||||
// Handler client stream control
|
||||
type Handler struct {
|
||||
dialer network.Dial
|
||||
commands *Commands
|
||||
receiver rw.FetchReader
|
||||
sender handlerSender
|
||||
senderPaused bool
|
||||
receiveDelay time.Duration
|
||||
sendDelay time.Duration
|
||||
log log.Logger
|
||||
rBuf handlerBuf
|
||||
streams streams
|
||||
}
|
||||
|
||||
func newHandler(
|
||||
dialer network.Dial,
|
||||
commands *Commands,
|
||||
receiver rw.FetchReader,
|
||||
sender io.Writer,
|
||||
senderLock *sync.Mutex,
|
||||
receiveDelay time.Duration,
|
||||
sendDelay time.Duration,
|
||||
l log.Logger,
|
||||
) Handler {
|
||||
return Handler{
|
||||
dialer: dialer,
|
||||
commands: commands,
|
||||
receiver: receiver,
|
||||
sender: handlerSender{writer: sender, lock: senderLock},
|
||||
senderPaused: false,
|
||||
receiveDelay: receiveDelay,
|
||||
sendDelay: sendDelay,
|
||||
log: l,
|
||||
rBuf: handlerBuf{},
|
||||
streams: newStreams(),
|
||||
}
|
||||
}
|
||||
|
||||
// handleControl handles Control request
|
||||
//
|
||||
// Params:
|
||||
// - d: length of the control message
|
||||
//
|
||||
// Returns:
|
||||
// - error
|
||||
func (e *Handler) handleControl(d byte, l log.Logger) error {
|
||||
buf := e.rBuf[1:]
|
||||
|
||||
if len(buf) < int(d) {
|
||||
return ErrHandlerControlMessageTooLong
|
||||
}
|
||||
|
||||
rLen, rErr := io.ReadFull(&e.receiver, buf[:d])
|
||||
|
||||
if rErr != nil {
|
||||
return rErr
|
||||
}
|
||||
|
||||
if rLen <= 0 {
|
||||
return ErrHandlerInvalidControlMessage
|
||||
}
|
||||
|
||||
switch buf[0] {
|
||||
case HeaderControlEcho:
|
||||
hd := HeaderControl
|
||||
hd.Set(d)
|
||||
|
||||
e.rBuf[0] = byte(hd)
|
||||
e.rBuf[1] = HeaderControlEcho
|
||||
|
||||
var wErr error
|
||||
|
||||
if !e.senderPaused {
|
||||
_, wErr = e.sender.Write(e.rBuf[:rLen+1])
|
||||
} else {
|
||||
_, wErr = e.sender.writer.Write(e.rBuf[:rLen+1])
|
||||
}
|
||||
|
||||
return wErr
|
||||
|
||||
case HeaderControlPauseStream:
|
||||
if !e.senderPaused {
|
||||
e.sender.lock.Lock()
|
||||
e.senderPaused = true
|
||||
}
|
||||
|
||||
case HeaderControlResumeStream:
|
||||
if e.senderPaused {
|
||||
e.sender.lock.Unlock()
|
||||
e.senderPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleStream handles streams
|
||||
//
|
||||
// Params:
|
||||
// - d: Stream ID
|
||||
//
|
||||
// Returns:
|
||||
// - error
|
||||
func (e *Handler) handleStream(h Header, d byte, l log.Logger) error {
|
||||
st, stErr := e.streams.get(d)
|
||||
|
||||
if stErr != nil {
|
||||
return stErr
|
||||
}
|
||||
|
||||
// WARNING: stream.Tick and it's underlaying commands MUST NOT write to
|
||||
// client. This is because the client data writer maybe locked
|
||||
// and only current routine (the same routine will be used to
|
||||
// tick the stream) can unlock it.
|
||||
// Calling write may dead lock the routine, with there is no way
|
||||
// of recover.
|
||||
if st.running() {
|
||||
l.Debug("Ticking stream")
|
||||
|
||||
return st.tick(h, &e.receiver, e.rBuf[:])
|
||||
}
|
||||
|
||||
l.Debug("Start stream %d", h.Data())
|
||||
|
||||
if e.senderPaused {
|
||||
e.sender.lock.Unlock()
|
||||
defer e.sender.lock.Lock()
|
||||
}
|
||||
|
||||
return st.reinit(h, &e.receiver, streamHandlerSender{
|
||||
handlerSender: &e.sender,
|
||||
sendDelay: e.sendDelay,
|
||||
}, l, e.commands, e.dialer, e.rBuf[:])
|
||||
}
|
||||
|
||||
func (e *Handler) handleClose(h Header, d byte, l log.Logger) error {
|
||||
st, stErr := e.streams.get(d)
|
||||
|
||||
if stErr != nil {
|
||||
return stErr
|
||||
}
|
||||
|
||||
if e.senderPaused {
|
||||
e.sender.lock.Unlock()
|
||||
defer e.sender.lock.Lock()
|
||||
}
|
||||
|
||||
cErr := st.close()
|
||||
|
||||
if cErr != nil {
|
||||
return cErr
|
||||
}
|
||||
|
||||
hhd := HeaderCompleted
|
||||
hhd.Set(h.Data())
|
||||
|
||||
return e.sender.signal(hhd, nil, e.rBuf[:])
|
||||
}
|
||||
|
||||
func (e *Handler) handleCompleted(d byte, l log.Logger) error {
|
||||
st, stErr := e.streams.get(d)
|
||||
|
||||
if stErr != nil {
|
||||
return stErr
|
||||
}
|
||||
|
||||
if e.senderPaused {
|
||||
e.sender.lock.Unlock()
|
||||
defer e.sender.lock.Lock()
|
||||
}
|
||||
|
||||
return st.release()
|
||||
}
|
||||
|
||||
// Handle starts handling
|
||||
func (e *Handler) Handle() error {
|
||||
defer func() {
|
||||
if e.senderPaused {
|
||||
e.sender.lock.Unlock()
|
||||
e.senderPaused = false
|
||||
}
|
||||
|
||||
e.streams.shutdown()
|
||||
}()
|
||||
|
||||
requests := 0
|
||||
|
||||
for {
|
||||
time.Sleep(e.receiveDelay)
|
||||
|
||||
requests++
|
||||
|
||||
d, dErr := rw.FetchOneByte(e.receiver.Fetch)
|
||||
|
||||
if dErr != nil {
|
||||
return dErr
|
||||
}
|
||||
|
||||
h := Header(d[0])
|
||||
l := e.log.Context("Request (%d)", requests).Context(h.String())
|
||||
|
||||
l.Debug("Received")
|
||||
|
||||
switch h.Type() {
|
||||
case HeaderControl:
|
||||
dErr = e.handleControl(h.Data(), l)
|
||||
|
||||
case HeaderStream:
|
||||
dErr = e.handleStream(h, h.Data(), l)
|
||||
|
||||
case HeaderClose:
|
||||
dErr = e.handleClose(h, h.Data(), l)
|
||||
|
||||
case HeaderCompleted:
|
||||
dErr = e.handleCompleted(h.Data(), l)
|
||||
|
||||
default:
|
||||
return ErrHandlerUnknownHeaderType
|
||||
}
|
||||
|
||||
if dErr != nil {
|
||||
l.Debug("Request failed: %s", dErr)
|
||||
|
||||
return dErr
|
||||
}
|
||||
|
||||
l.Debug("Request successful")
|
||||
}
|
||||
}
|
||||
104
application/command/handler_echo_test.go
Normal file
104
application/command/handler_echo_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/niruix/sshwifty/application/log"
|
||||
"github.com/niruix/sshwifty/application/rw"
|
||||
)
|
||||
|
||||
func testDummyFetchGen(data []byte) rw.FetchReaderFetcher {
|
||||
current := 0
|
||||
|
||||
return func() ([]byte, error) {
|
||||
if current >= len(data) {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
oldCurrent := current
|
||||
current++
|
||||
|
||||
return data[oldCurrent:current], nil
|
||||
}
|
||||
}
|
||||
|
||||
type dummyWriter struct {
|
||||
written []byte
|
||||
}
|
||||
|
||||
func (d *dummyWriter) Write(b []byte) (int, error) {
|
||||
d.written = append(d.written, b...)
|
||||
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func TestHandlerHandleEcho(t *testing.T) {
|
||||
w := dummyWriter{
|
||||
written: make([]byte, 0, 64),
|
||||
}
|
||||
s := []byte{
|
||||
byte(HeaderControl | 13),
|
||||
HeaderControlEcho,
|
||||
'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '1',
|
||||
byte(HeaderControl | 13),
|
||||
HeaderControlEcho,
|
||||
'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '2',
|
||||
byte(HeaderControl | HeaderMaxData),
|
||||
HeaderControlEcho,
|
||||
'1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
|
||||
'1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
|
||||
'1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
|
||||
'1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
|
||||
'1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
|
||||
'1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
|
||||
'2', '2',
|
||||
byte(HeaderControl | 13),
|
||||
HeaderControlEcho,
|
||||
'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '3',
|
||||
}
|
||||
lock := sync.Mutex{}
|
||||
handler := newHandler(
|
||||
nil,
|
||||
nil,
|
||||
rw.NewFetchReader(testDummyFetchGen(s)),
|
||||
&w,
|
||||
&lock,
|
||||
0,
|
||||
0,
|
||||
log.NewDitch(),
|
||||
)
|
||||
|
||||
hErr := handler.Handle()
|
||||
|
||||
if hErr != nil && hErr != io.EOF {
|
||||
t.Error("Failed to write due to error:", hErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(w.written, s) {
|
||||
t.Errorf("Expecting the data to be %d, got %d instead", s, w.written)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
249
application/command/handler_stream_test.go
Normal file
249
application/command/handler_stream_test.go
Normal file
@@ -0,0 +1,249 @@
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/niruix/sshwifty/application/log"
|
||||
"github.com/niruix/sshwifty/application/network"
|
||||
"github.com/niruix/sshwifty/application/rw"
|
||||
)
|
||||
|
||||
func testDummyFetchChainGen(dd <-chan []byte) rw.FetchReaderFetcher {
|
||||
var data []byte
|
||||
var ok bool
|
||||
|
||||
current := 0
|
||||
|
||||
return func() ([]byte, error) {
|
||||
for {
|
||||
if current >= len(data) {
|
||||
data, ok = <-dd
|
||||
|
||||
if !ok {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
current = 0
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
oldCurrent := current
|
||||
current++
|
||||
|
||||
return data[oldCurrent:current], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type dummyStreamCommand struct {
|
||||
l log.Logger
|
||||
w StreamResponder
|
||||
downWait sync.WaitGroup
|
||||
echoData []byte
|
||||
echoTrans chan []byte
|
||||
}
|
||||
|
||||
func newDummyStreamCommand(
|
||||
l log.Logger, w StreamResponder, d network.Dial) FSMMachine {
|
||||
return &dummyStreamCommand{
|
||||
l: l,
|
||||
w: w,
|
||||
downWait: sync.WaitGroup{},
|
||||
echoData: []byte{},
|
||||
echoTrans: make(chan []byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dummyStreamCommand) Bootup(
|
||||
r *rw.LimitedReader,
|
||||
b []byte,
|
||||
) (FSMState, FSMError) {
|
||||
d.downWait.Add(1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
d.w.Signal(HeaderClose)
|
||||
|
||||
d.downWait.Done()
|
||||
}()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
|
||||
for {
|
||||
dt, dtOK := <-d.echoTrans
|
||||
|
||||
if !dtOK {
|
||||
return
|
||||
}
|
||||
|
||||
wErr := d.w.Send(0, []byte{dt[0], dt[1], dt[2], dt[3]}, buf)
|
||||
|
||||
if wErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
commandDataBuf := [5]byte{}
|
||||
|
||||
_, rErr := io.ReadFull(r, commandDataBuf[:])
|
||||
|
||||
if rErr != nil {
|
||||
return nil, ToFSMError(rErr, 11)
|
||||
}
|
||||
|
||||
if !bytes.Equal(commandDataBuf[:], []byte("HELLO")) {
|
||||
panic(fmt.Sprintf("Expecting handsake data to be %s, got %s instead",
|
||||
[]byte("HELLO"), commandDataBuf[:]))
|
||||
}
|
||||
|
||||
if !r.Completed() {
|
||||
panic("R must be Completed")
|
||||
}
|
||||
|
||||
return d.run, NoFSMError()
|
||||
}
|
||||
|
||||
func (d *dummyStreamCommand) run(
|
||||
f *FSM, r *rw.LimitedReader, h StreamHeader, b []byte) error {
|
||||
rLen, rErr := rw.ReadUntilCompleted(r, b[:])
|
||||
|
||||
if rErr != nil {
|
||||
return rErr
|
||||
}
|
||||
|
||||
d.echoData = make([]byte, rLen)
|
||||
copy(d.echoData, b)
|
||||
|
||||
if d.echoTrans != nil {
|
||||
d.echoTrans <- d.echoData
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dummyStreamCommand) Close() error {
|
||||
close(d.echoTrans)
|
||||
d.echoTrans = nil
|
||||
d.downWait.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dummyStreamCommand) Release() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHandlerHandleStream(t *testing.T) {
|
||||
cmds := Commands{}
|
||||
cmds.Register(0, newDummyStreamCommand)
|
||||
|
||||
readerDataInput := make(chan []byte)
|
||||
|
||||
readerSource := testDummyFetchChainGen(readerDataInput)
|
||||
wBuffer := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
|
||||
lock := sync.Mutex{}
|
||||
hhd := newHandler(
|
||||
nil,
|
||||
&cmds,
|
||||
rw.NewFetchReader(readerSource),
|
||||
wBuffer,
|
||||
&lock,
|
||||
0,
|
||||
0,
|
||||
log.NewDitch())
|
||||
|
||||
go func() {
|
||||
stInitialHeader := streamInitialHeader{}
|
||||
|
||||
stInitialHeader.set(0, 5, true)
|
||||
|
||||
readerDataInput <- []byte{
|
||||
byte(HeaderStream | 63), stInitialHeader[0], stInitialHeader[1],
|
||||
'H', 'E', 'L', 'L', 'O',
|
||||
}
|
||||
|
||||
stHeader := StreamHeader{}
|
||||
stHeader.Set(0, 5)
|
||||
|
||||
readerDataInput <- []byte{
|
||||
byte(HeaderStream | 63), stHeader[0], stHeader[1],
|
||||
'W', 'O', 'R', 'L', 'D',
|
||||
}
|
||||
|
||||
readerDataInput <- []byte{
|
||||
byte(HeaderStream | 63), stHeader[0], stHeader[1],
|
||||
'0', '1', '2', '3', '4',
|
||||
}
|
||||
|
||||
readerDataInput <- []byte{
|
||||
byte(HeaderClose | 63),
|
||||
}
|
||||
|
||||
close(readerDataInput)
|
||||
}()
|
||||
|
||||
hErr := hhd.Handle()
|
||||
|
||||
if hErr != nil && hErr != io.EOF {
|
||||
t.Error("Failed to handle due to error:", hErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Build the expected header:
|
||||
|
||||
// HeaderStream(63): Success
|
||||
stInitialHeader := streamInitialHeader{}
|
||||
stInitialHeader.set(0, 0, true)
|
||||
|
||||
stHeaders := StreamHeader{}
|
||||
stHeaders.Set(0, 4)
|
||||
|
||||
expected := []byte{
|
||||
// HeaderStream(63): Success
|
||||
byte(HeaderStream | 63), stInitialHeader[0], stInitialHeader[1],
|
||||
|
||||
// HeaderStream(63): Echo 'W', 'O', 'R', 'L' (First 4 bytes of data)
|
||||
byte(HeaderStream | 63), stHeaders[0], stHeaders[1], 'W', 'O', 'R', 'L',
|
||||
|
||||
// HeaderStream(63): Echo '0', '1', '2', '3',
|
||||
byte(HeaderStream | 63), stHeaders[0], stHeaders[1], '0', '1', '2', '3',
|
||||
|
||||
// HeaderClose(63)
|
||||
byte(HeaderClose | 63),
|
||||
|
||||
// HeaderCompleted(63)
|
||||
byte(HeaderCompleted | 63),
|
||||
}
|
||||
|
||||
if !bytes.Equal(wBuffer.Bytes(), expected) {
|
||||
t.Errorf("Expecting received data to be %d, got %d instead",
|
||||
expected, wBuffer.Bytes())
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
18
application/command/handler_test.go
Normal file
18
application/command/handler_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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 command
|
||||
143
application/command/header.go
Normal file
143
application/command/header.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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 command
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Header Packet Type
|
||||
type Header byte
|
||||
|
||||
// Packet Types
|
||||
const (
|
||||
// 00------: Control signals
|
||||
// Remaing bits: Data length
|
||||
//
|
||||
// Format:
|
||||
// 0011111 [63 bytes long data] - 63 bytes of control data
|
||||
//
|
||||
HeaderControl Header = 0x00
|
||||
|
||||
// 01------: Bidirectional stream data
|
||||
// Remaining bits: Stream ID
|
||||
// Followed by: Parameter or data
|
||||
//
|
||||
// Format:
|
||||
// 0111111 [Command parameters / data] - Open/use stream 63 to execute
|
||||
// command or transmit data
|
||||
HeaderStream Header = 0x40
|
||||
|
||||
// 10------: Close stream
|
||||
// Remaining bits: Stream ID
|
||||
//
|
||||
// Format:
|
||||
// 1011111 - Close stream 63
|
||||
//
|
||||
// WARNING: The requester MUST NOT send any data to this stream once this
|
||||
// header is sent.
|
||||
//
|
||||
// WARNING: The receiver MUST reply with a Completed header to indicate
|
||||
// the success of the Close action. Until a Completed header is
|
||||
// replied, all data from the sender must be proccessed as normal.
|
||||
HeaderClose Header = 0x80
|
||||
|
||||
// 11------: Stream has been closed/completed in respond to client request
|
||||
// Remaining bits: Stream ID
|
||||
//
|
||||
// Format:
|
||||
// 1111111 - Stream 63 is completed
|
||||
//
|
||||
// WARNING: This header can ONLY be send in respond to a Close header
|
||||
//
|
||||
// WARNING: The sender of this header MUST NOT send any data to the stream
|
||||
// once this header is sent until this stream been re-opened by a
|
||||
// Data header
|
||||
HeaderCompleted Header = 0xc0
|
||||
)
|
||||
|
||||
// Control signal types
|
||||
const (
|
||||
HeaderControlEcho = 0x00
|
||||
HeaderControlPauseStream = 0x01
|
||||
HeaderControlResumeStream = 0x02
|
||||
)
|
||||
|
||||
// Consts
|
||||
const (
|
||||
HeaderMaxData = 0x3f
|
||||
)
|
||||
|
||||
// Cutters
|
||||
const (
|
||||
headerHeaderCutter = 0xc0
|
||||
headerDataCutter = 0x3f
|
||||
)
|
||||
|
||||
// Type get packet type
|
||||
func (p Header) Type() Header {
|
||||
return (p & headerHeaderCutter)
|
||||
}
|
||||
|
||||
// Data returns the data of current Packet header
|
||||
func (p Header) Data() byte {
|
||||
return byte(p & headerDataCutter)
|
||||
}
|
||||
|
||||
// Set set a new value of the Header
|
||||
func (p *Header) Set(data byte) {
|
||||
if data > headerDataCutter {
|
||||
panic("data must not be greater than 0x3f")
|
||||
}
|
||||
|
||||
*p |= (headerDataCutter & Header(data))
|
||||
}
|
||||
|
||||
// Set set a new value of the Header
|
||||
func (p Header) String() string {
|
||||
switch p.Type() {
|
||||
case HeaderControl:
|
||||
return fmt.Sprintf("Control (%d bytes)", p.Data())
|
||||
|
||||
case HeaderStream:
|
||||
return fmt.Sprintf("Stream (%d)", p.Data())
|
||||
|
||||
case HeaderClose:
|
||||
return fmt.Sprintf("Close (Stream %d)", p.Data())
|
||||
|
||||
case HeaderCompleted:
|
||||
return fmt.Sprintf("Completed (Stream %d)", p.Data())
|
||||
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// IsStreamControl returns true when the header is for stream control, false
|
||||
// when otherwise
|
||||
func (p Header) IsStreamControl() bool {
|
||||
switch p {
|
||||
case HeaderStream:
|
||||
fallthrough
|
||||
case HeaderClose:
|
||||
fallthrough
|
||||
case HeaderCompleted:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
439
application/command/streams.go
Normal file
439
application/command/streams.go
Normal file
@@ -0,0 +1,439 @@
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/niruix/sshwifty/application/log"
|
||||
"github.com/niruix/sshwifty/application/network"
|
||||
"github.com/niruix/sshwifty/application/rw"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrStreamsInvalidStreamID = errors.New(
|
||||
"Stream ID is invalid")
|
||||
|
||||
ErrStreamsStreamOperateInactiveStream = errors.New(
|
||||
"Specified stream was inactive for operation")
|
||||
|
||||
ErrStreamsStreamClosingInactiveStream = errors.New(
|
||||
"Closing an inactive stream is not allowed")
|
||||
|
||||
ErrStreamsStreamReleasingInactiveStream = errors.New(
|
||||
"Releasing an inactive stream is not allowed")
|
||||
)
|
||||
|
||||
// StreamError Stream Error signal
|
||||
type StreamError uint16
|
||||
|
||||
// Error signals
|
||||
const (
|
||||
StreamErrorCommandUndefined StreamError = 0x01
|
||||
StreamErrorCommandFailedToBootup StreamError = 0x02
|
||||
)
|
||||
|
||||
// StreamHeader contains data of the stream header
|
||||
type StreamHeader [2]byte
|
||||
|
||||
// Stream header consts
|
||||
const (
|
||||
StreamHeaderMaxLength = 0x1fff
|
||||
StreamHeaderMaxMarker = 0x07
|
||||
|
||||
streamHeaderLengthFirstByteCutter = 0x1f
|
||||
)
|
||||
|
||||
// Marker returns the header marker data
|
||||
func (s StreamHeader) Marker() byte {
|
||||
return s[0] >> 5
|
||||
}
|
||||
|
||||
// Length returns the data length of the stream
|
||||
func (s StreamHeader) Length() uint16 {
|
||||
r := uint16(0)
|
||||
|
||||
r |= uint16(s[0] & streamHeaderLengthFirstByteCutter)
|
||||
r <<= 8
|
||||
r |= uint16(s[1])
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Set sets the stream header
|
||||
func (s *StreamHeader) Set(marker byte, n uint16) {
|
||||
if marker > StreamHeaderMaxMarker {
|
||||
panic("marker must not be greater than 0x07")
|
||||
}
|
||||
|
||||
if n > StreamHeaderMaxLength {
|
||||
panic("n must not be greater than 0x1fff")
|
||||
}
|
||||
|
||||
s[0] = (marker << 5) | byte((n>>8)&streamHeaderLengthFirstByteCutter)
|
||||
s[1] = byte(n)
|
||||
}
|
||||
|
||||
// streamInitialHeader contains header data of the first stream after stream
|
||||
// reset.
|
||||
// Unlike StreamHeader, streamInitialHeader carries no extra data
|
||||
type streamInitialHeader StreamHeader
|
||||
|
||||
// command returns command ID of the stream
|
||||
func (s streamInitialHeader) command() byte {
|
||||
return s[0] >> 4
|
||||
}
|
||||
|
||||
// length returns the data of the stream header
|
||||
func (s streamInitialHeader) data() uint16 {
|
||||
r := uint16(0)
|
||||
|
||||
r |= uint16(s[0] & 0x07) // 0000 0111
|
||||
r <<= 8
|
||||
r |= uint16(s[1])
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// success returns whether or not the command is representing a success
|
||||
func (s streamInitialHeader) success() bool {
|
||||
return (s[0] & 0x08) != 0
|
||||
}
|
||||
|
||||
// set sets header values
|
||||
func (s *streamInitialHeader) set(commandID byte, data uint16, success bool) {
|
||||
if commandID > 0x0f {
|
||||
panic("Command ID must not greater than 0x0f")
|
||||
}
|
||||
|
||||
if data > 0x07ff {
|
||||
panic("Data must not greater than 0x07ff")
|
||||
}
|
||||
|
||||
dd := data & 0x07ff
|
||||
|
||||
if success {
|
||||
dd |= 0x0800
|
||||
}
|
||||
|
||||
(*s)[0] = 0
|
||||
(*s)[0] |= commandID << 4
|
||||
(*s)[0] |= byte(dd >> 8)
|
||||
(*s)[1] = 0
|
||||
(*s)[1] |= byte(dd)
|
||||
}
|
||||
|
||||
// send sends current stream header as signal
|
||||
func (s *streamInitialHeader) signal(
|
||||
w *handlerSender,
|
||||
hd Header,
|
||||
buf []byte,
|
||||
) error {
|
||||
return w.signal(hd, (*s)[:], buf)
|
||||
}
|
||||
|
||||
// StreamInitialSignalSender sends stream initial signal
|
||||
type StreamInitialSignalSender struct {
|
||||
w *handlerSender
|
||||
hd Header
|
||||
cmdID byte
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// Signal send signal
|
||||
func (s *StreamInitialSignalSender) Signal(
|
||||
errno StreamError, success bool) error {
|
||||
shd := streamInitialHeader{}
|
||||
shd.set(s.cmdID, uint16(errno), success)
|
||||
|
||||
return shd.signal(s.w, s.hd, s.buf)
|
||||
}
|
||||
|
||||
// StreamResponder sends data through stream
|
||||
type StreamResponder struct {
|
||||
w streamHandlerSender
|
||||
h Header
|
||||
}
|
||||
|
||||
// newStreamResponder creates a new StreamResponder
|
||||
func newStreamResponder(w streamHandlerSender, h Header) StreamResponder {
|
||||
return StreamResponder{
|
||||
w: w,
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
func (w StreamResponder) write(mk byte, b []byte, buf []byte) (int, error) {
|
||||
bufLen := len(buf)
|
||||
bLen := len(b)
|
||||
|
||||
if bLen > bufLen {
|
||||
bLen = bufLen
|
||||
}
|
||||
|
||||
if bLen > StreamHeaderMaxLength {
|
||||
bLen = StreamHeaderMaxLength
|
||||
}
|
||||
|
||||
sHeaderStream := StreamHeader{}
|
||||
sHeaderStream.Set(mk, uint16(bLen))
|
||||
|
||||
toWrite := copy(buf[3:], b)
|
||||
buf[0] = byte(w.h)
|
||||
buf[1] = sHeaderStream[0]
|
||||
buf[2] = sHeaderStream[1]
|
||||
|
||||
_, wErr := w.w.Write(buf[:toWrite+3])
|
||||
|
||||
if wErr != nil {
|
||||
return 0, wErr
|
||||
}
|
||||
|
||||
return len(b), wErr
|
||||
}
|
||||
|
||||
// HeaderSize returns the size of header
|
||||
func (w StreamResponder) HeaderSize() int {
|
||||
return 3
|
||||
}
|
||||
|
||||
// Send sends data. Data will be automatically segmentated if it's too long to
|
||||
// fit into one data package or buffer space
|
||||
func (w StreamResponder) Send(marker byte, data []byte, buf []byte) error {
|
||||
if len(buf) <= w.HeaderSize() {
|
||||
panic("The length of data buffer must be greater than 3")
|
||||
}
|
||||
|
||||
dataLen := len(data)
|
||||
start := 0
|
||||
|
||||
for {
|
||||
wLen, wErr := w.write(marker, data[start:], buf)
|
||||
|
||||
start += wLen
|
||||
|
||||
if wErr != nil {
|
||||
return wErr
|
||||
}
|
||||
|
||||
if start < dataLen {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SendManual sends the data without automatical segmentation. It will construct
|
||||
// the data package directly using the given `data` buffer, that is, the first
|
||||
// n bytes of the given `data` will be used to setup headers. It is the caller's
|
||||
// responsibility to leave n bytes of space so no meaningful data will be over
|
||||
// written. The number n can be acquired by calling .HeaderSize() method.
|
||||
func (w StreamResponder) SendManual(marker byte, data []byte) error {
|
||||
dataLen := len(data)
|
||||
|
||||
if dataLen < w.HeaderSize() {
|
||||
panic("The length of data buffer must be greater than the " +
|
||||
"w.HeaderSize()")
|
||||
}
|
||||
|
||||
if dataLen > StreamHeaderMaxLength {
|
||||
panic("Data length must not greater than StreamHeaderMaxLength")
|
||||
}
|
||||
|
||||
sHeaderStream := StreamHeader{}
|
||||
sHeaderStream.Set(marker, uint16(dataLen-w.HeaderSize()))
|
||||
|
||||
data[0] = byte(w.h)
|
||||
data[1] = sHeaderStream[0]
|
||||
data[2] = sHeaderStream[1]
|
||||
|
||||
_, wErr := w.w.Write(data)
|
||||
|
||||
return wErr
|
||||
}
|
||||
|
||||
// Signal sends a signal
|
||||
func (w StreamResponder) Signal(signal Header) error {
|
||||
if !signal.IsStreamControl() {
|
||||
panic("Only stream control signal is allowed")
|
||||
}
|
||||
|
||||
sHeader := signal
|
||||
sHeader.Set(w.h.Data())
|
||||
|
||||
_, wErr := w.w.Write([]byte{byte(sHeader)})
|
||||
|
||||
return wErr
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
f FSM
|
||||
closed bool
|
||||
}
|
||||
|
||||
type streams [HeaderMaxData + 1]stream
|
||||
|
||||
func newStream() stream {
|
||||
return stream{
|
||||
f: emptyFSM(),
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
func newStreams() streams {
|
||||
s := streams{}
|
||||
|
||||
for i := range s {
|
||||
s[i] = newStream()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *streams) get(id byte) (*stream, error) {
|
||||
if id > HeaderMaxData {
|
||||
return nil, ErrStreamsInvalidStreamID
|
||||
}
|
||||
|
||||
return &(*c)[id], nil
|
||||
}
|
||||
|
||||
func (c *streams) shutdown() {
|
||||
cc := *c
|
||||
|
||||
for i := range cc {
|
||||
if !cc[i].running() {
|
||||
continue
|
||||
}
|
||||
|
||||
if !cc[i].closed {
|
||||
cc[i].close()
|
||||
}
|
||||
|
||||
cc[i].release()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *stream) running() bool {
|
||||
return c.f.running()
|
||||
}
|
||||
|
||||
func (c *stream) reinit(
|
||||
h Header,
|
||||
r *rw.FetchReader,
|
||||
w streamHandlerSender,
|
||||
l log.Logger,
|
||||
cc *Commands,
|
||||
dialer network.Dial,
|
||||
b []byte,
|
||||
) error {
|
||||
hd := streamInitialHeader{}
|
||||
|
||||
_, rErr := io.ReadFull(r, hd[:])
|
||||
|
||||
if rErr != nil {
|
||||
return rErr
|
||||
}
|
||||
|
||||
l = l.Context("Command (%d)", hd.command())
|
||||
|
||||
ccc, cccErr := cc.Run(hd.command(), l, newStreamResponder(w, h), dialer)
|
||||
|
||||
if cccErr != nil {
|
||||
hd.set(0, uint16(StreamErrorCommandUndefined), false)
|
||||
hd.signal(w.handlerSender, h, b)
|
||||
|
||||
l.Warning("Trying to execute an unknown command %d", hd.command())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
signaller := StreamInitialSignalSender{
|
||||
w: w.handlerSender,
|
||||
hd: h,
|
||||
cmdID: hd.command(),
|
||||
buf: b,
|
||||
}
|
||||
|
||||
rr := rw.NewLimitedReader(r, int(hd.data()))
|
||||
defer rr.Ditch(b)
|
||||
|
||||
bootErr := ccc.bootup(&rr, b)
|
||||
|
||||
if !bootErr.Succeed() {
|
||||
l.Warning("Unable to start command %d due to error: %s",
|
||||
hd.command(), bootErr.Error())
|
||||
|
||||
signaller.Signal(bootErr.code, false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
c.f = ccc
|
||||
c.closed = false
|
||||
|
||||
signaller.Signal(bootErr.code, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *stream) tick(
|
||||
h Header,
|
||||
r *rw.FetchReader,
|
||||
b []byte,
|
||||
) error {
|
||||
if !c.f.running() {
|
||||
return ErrStreamsStreamOperateInactiveStream
|
||||
}
|
||||
|
||||
hd := StreamHeader{}
|
||||
|
||||
_, rErr := io.ReadFull(r, hd[:])
|
||||
|
||||
if rErr != nil {
|
||||
return rErr
|
||||
}
|
||||
|
||||
rr := rw.NewLimitedReader(r, int(hd.Length()))
|
||||
defer rr.Ditch(b)
|
||||
|
||||
return c.f.tick(&rr, hd, b)
|
||||
}
|
||||
|
||||
func (c *stream) close() error {
|
||||
if !c.f.running() {
|
||||
return ErrStreamsStreamClosingInactiveStream
|
||||
}
|
||||
|
||||
// Set a marker so streams.shutdown won't call it. Stream can call it
|
||||
// however they want, though that may cause error that disconnects.
|
||||
c.closed = true
|
||||
|
||||
return c.f.close()
|
||||
}
|
||||
|
||||
func (c *stream) release() error {
|
||||
if !c.f.running() {
|
||||
return ErrStreamsStreamReleasingInactiveStream
|
||||
}
|
||||
|
||||
return c.f.release()
|
||||
}
|
||||
97
application/command/streams_test.go
Normal file
97
application/command/streams_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStreamInitialHeader(t *testing.T) {
|
||||
hd := streamInitialHeader{}
|
||||
|
||||
hd.set(15, 128, true)
|
||||
|
||||
if hd.command() != 15 {
|
||||
t.Errorf("Expecting command to be %d, got %d instead",
|
||||
15, hd.command())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if hd.data() != 128 {
|
||||
t.Errorf("Expecting data to be %d, got %d instead", 128, hd.data())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if hd.success() != true {
|
||||
t.Errorf("Expecting success to be %v, got %v instead",
|
||||
true, hd.success())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hd.set(0, 2047, false)
|
||||
|
||||
if hd.command() != 0 {
|
||||
t.Errorf("Expecting command to be %d, got %d instead",
|
||||
0, hd.command())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if hd.data() != 2047 {
|
||||
t.Errorf("Expecting data to be %d, got %d instead", 2047, hd.data())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if hd.success() != false {
|
||||
t.Errorf("Expecting success to be %v, got %v instead",
|
||||
false, hd.success())
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamHeader(t *testing.T) {
|
||||
s := StreamHeader{}
|
||||
|
||||
s.Set(StreamHeaderMaxMarker, StreamHeaderMaxLength)
|
||||
|
||||
if s.Marker() != StreamHeaderMaxMarker {
|
||||
t.Errorf("Expecting the marker to be %d, got %d instead",
|
||||
StreamHeaderMaxMarker, s.Marker())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if s.Length() != StreamHeaderMaxLength {
|
||||
t.Errorf("Expecting the length to be %d, got %d instead",
|
||||
StreamHeaderMaxLength, s.Length())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if s[0] != s[1] || s[0] != 0xff {
|
||||
t.Errorf("Expecting the header to be 255, 255, got %d, %d instead",
|
||||
s[0], s[1])
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user