Initial commit

This commit is contained in:
NI
2019-08-07 15:56:51 +08:00
commit 02f14eb14f
206 changed files with 38863 additions and 0 deletions
+176
View 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 application
import (
"fmt"
"io"
goLog "log"
"os"
"os/signal"
"sync"
"syscall"
"github.com/niruix/sshwifty/application/configuration"
"github.com/niruix/sshwifty/application/log"
"github.com/niruix/sshwifty/application/server"
)
// ProccessSignaller send signal to the running application
type ProccessSignaller chan os.Signal
// ProccessSignallerBuilder builds a ProccessSignaler
type ProccessSignallerBuilder func() chan os.Signal
// DefaultProccessSignallerBuilder the default ProccessSignallerBuilder
func DefaultProccessSignallerBuilder() chan os.Signal {
return make(chan os.Signal, 1)
}
var (
screenLineWipper = []byte("\r")
)
// Application contains data required for the application, and yes I don't like
// to write comments
type Application struct {
screen io.Writer
logger log.Logger
}
// New creates a new Application
func New(screen io.Writer, logger log.Logger) Application {
return Application{
screen: screen,
logger: logger,
}
}
// Run execute the application. It will return when the application is finished
// running
func (a Application) run(
cLoader configuration.Loader,
closeSigBuilder ProccessSignallerBuilder,
handlerBuilder server.HandlerBuilder,
) (bool, error) {
var err error
loaderName, c, cErr := cLoader(a.logger.Context("Configuration"))
if cErr != nil {
a.logger.Error("\"%s\" loader cannot load configuration: %s",
loaderName, cErr)
return false, cErr
}
err = c.Verify()
if err != nil {
a.logger.Error("Configuration was invalid: %s", err)
return false, err
}
closeNotify := closeSigBuilder()
signal.Notify(closeNotify, os.Kill, os.Interrupt, syscall.SIGHUP)
defer signal.Stop(closeNotify)
servers := make([]*server.Serving, 0, len(c.Servers))
s := server.New(a.logger)
defer func() {
for i := len(servers); i > 0; i-- {
servers[i-1].Close()
}
s.Wait()
}()
closeNotifyDisableLock := sync.Mutex{}
for _, ss := range c.Servers {
newServer := s.Serve(c.Common(), ss, func(e error) {
closeNotifyDisableLock.Lock()
defer closeNotifyDisableLock.Unlock()
if closeNotify == nil {
return
}
err = e
close(closeNotify)
closeNotify = nil
}, handlerBuilder)
servers = append(servers, newServer)
}
switch <-closeNotify {
case syscall.SIGHUP:
return true, nil
case os.Kill:
fallthrough
case os.Interrupt:
a.screen.Write(screenLineWipper)
return false, nil
default:
closeNotifyDisableLock.Lock()
defer closeNotifyDisableLock.Unlock()
return false, err
}
}
// Run execute the application. It will return when the application is finished
// running
func (a Application) Run(
cLoader configuration.Loader,
closeSigBuilder ProccessSignallerBuilder,
handlerBuilder server.HandlerBuilder,
) error {
fmt.Fprintf(a.screen, banner, FullName, version, Author, URL)
goLog.SetOutput(a.logger)
defer goLog.SetOutput(os.Stderr)
a.logger.Info("Initializing")
defer a.logger.Info("Closed")
for {
restart, runErr := a.run(cLoader, closeSigBuilder, handlerBuilder)
if runErr != nil {
a.logger.Error("Unable to start due to error: %s", runErr)
return runErr
}
if restart {
a.logger.Info("Restarting")
continue
}
return nil
}
}
+62
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}
+276
View File
@@ -0,0 +1,276 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package commands
import (
"errors"
"net"
"strconv"
"github.com/niruix/sshwifty/application/rw"
)
//Errors
var (
ErrAddressParseBufferTooSmallForHeader = errors.New(
"Buffer space was too small to parse the address header")
ErrAddressParseBufferTooSmallForIPv4 = errors.New(
"Buffer space was too small to parse the IPv4 address")
ErrAddressParseBufferTooSmallForIPv6 = errors.New(
"Buffer space was too small to parse the IPv6 address")
ErrAddressParseBufferTooSmallForHostName = errors.New(
"Buffer space was too small to parse the hostname address")
ErrAddressMarshalBufferTooSmall = errors.New(
"Buffer space was too small to marshal the address")
ErrAddressInvalidAddressType = errors.New(
"Invalid address type")
)
// AddressType Type of the address
type AddressType byte
// Address types
const (
LoopbackAddr AddressType = 0x00
IPv4Addr AddressType = 0x01
IPv6Addr AddressType = 0x02
HostNameAddr AddressType = 0x03
)
// Address data
type Address struct {
port uint16
kind AddressType
data []byte
}
// ParseAddress parses the reader and return an Address
//
// Address data format:
// +-------------+--------------+---------------+
// | 2 bytes | 1 byte | n bytes |
// +-------------+--------------+---------------+
// | Port number | Address type | Address data |
// +-------------+--------------+---------------+
//
// Address types:
// - LoopbackAddr: 00 Localhost, don't carry Address data
// - IPv4Addr: 01 IPv4 Address, carries 4 bytes of Address data
// - IPv6Addr: 10 IPv6 Address, carries 16 bytes Address data
// - HostnameAddr: 11 Host name string, length of Address data is indicated
// by the remainer of the byte (11-- ----). maxlen = 63
//
func ParseAddress(reader rw.ReaderFunc, buf []byte) (Address, error) {
if len(buf) < 3 {
return Address{}, ErrAddressParseBufferTooSmallForHeader
}
_, rErr := rw.ReadFull(reader, buf[:3])
if rErr != nil {
return Address{}, rErr
}
portNum := uint16(0)
portNum |= uint16(buf[0])
portNum <<= 8
portNum |= uint16(buf[1])
addrType := AddressType(buf[2] >> 6)
var addrData []byte
switch addrType {
case LoopbackAddr:
// Do nothing
case IPv4Addr:
if len(buf) < 4 {
return Address{}, ErrAddressParseBufferTooSmallForIPv4
}
_, rErr := rw.ReadFull(reader, buf[:4])
if rErr != nil {
return Address{}, rErr
}
addrData = buf[:4]
case IPv6Addr:
if len(buf) < 16 {
return Address{}, ErrAddressParseBufferTooSmallForIPv6
}
_, rErr := rw.ReadFull(reader, buf[:16])
if rErr != nil {
return Address{}, rErr
}
addrData = buf[:16]
case HostNameAddr:
addrDataLen := int(0x3f & buf[2])
if len(buf) < addrDataLen {
return Address{}, ErrAddressParseBufferTooSmallForHostName
}
_, rErr := rw.ReadFull(reader, buf[:addrDataLen])
if rErr != nil {
return Address{}, rErr
}
addrData = buf[:addrDataLen]
default:
return Address{}, ErrAddressInvalidAddressType
}
return Address{
port: portNum,
kind: addrType,
data: addrData,
}, nil
}
// NewAddress creates a new Address
func NewAddress(addrType AddressType, data []byte, port uint16) Address {
return Address{
port: port,
kind: addrType,
data: data,
}
}
// Type returns the type of the address
func (a Address) Type() AddressType {
return a.kind
}
// Data returns the address data
func (a Address) Data() []byte {
return a.data
}
// Port returns port number
func (a Address) Port() uint16 {
return a.port
}
// Marshal writes address data to the given b
func (a Address) Marshal(b []byte) (int, error) {
bLen := len(b)
switch a.Type() {
case LoopbackAddr:
if bLen < 3 {
return 0, ErrAddressMarshalBufferTooSmall
}
b[0] = byte(a.port >> 8)
b[1] = byte(a.port)
b[2] = byte(LoopbackAddr << 6)
return 3, nil
case IPv4Addr:
if bLen < 7 {
return 0, ErrAddressMarshalBufferTooSmall
}
b[0] = byte(a.port >> 8)
b[1] = byte(a.port)
b[2] = byte(IPv4Addr << 6)
copy(b[3:], a.data)
return 7, nil
case IPv6Addr:
if bLen < 19 {
return 0, ErrAddressMarshalBufferTooSmall
}
b[0] = byte(a.port >> 8)
b[1] = byte(a.port)
b[2] = byte(IPv6Addr << 6)
copy(b[3:], a.data)
return 19, nil
case HostNameAddr:
hLen := len(a.data)
if hLen > 0x3f {
panic("Host name cannot longer than 0x3f")
}
if bLen < hLen+3 {
return 0, ErrAddressMarshalBufferTooSmall
}
b[0] = byte(a.port >> 8)
b[1] = byte(a.port)
b[2] = byte(HostNameAddr << 6)
b[2] |= byte(hLen)
copy(b[3:], a.data)
return hLen + 3, nil
default:
return 0, ErrAddressInvalidAddressType
}
}
// String return the Address as string
func (a Address) String() string {
switch a.Type() {
case LoopbackAddr:
return net.JoinHostPort(
"localhost",
strconv.FormatUint(uint64(a.Port()), 10))
case IPv4Addr:
return net.JoinHostPort(
net.IPv4(a.data[0], a.data[1], a.data[2], a.data[3]).String(),
strconv.FormatUint(uint64(a.Port()), 10))
case IPv6Addr:
return net.JoinHostPort(
net.IP(a.data[:net.IPv6len]).String(),
strconv.FormatUint(uint64(a.Port()), 10))
case HostNameAddr:
return net.JoinHostPort(
string(a.data),
strconv.FormatUint(uint64(a.Port()), 10))
default:
panic("Unknown Address type")
}
}
+138
View File
@@ -0,0 +1,138 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package commands
import (
"bytes"
"strings"
"testing"
)
func testParseAddress(
t *testing.T,
input []byte,
buf []byte,
expectedType AddressType,
expectedData []byte,
expectedPort uint16,
expectedHostPortString string,
) {
source := bytes.NewBuffer(input)
addr, addrErr := ParseAddress(source.Read, buf)
if addrErr != nil {
t.Error("Failed to parse due to error:", addrErr)
return
}
if addr.Type() != expectedType {
t.Errorf("Expecting the Type to be %d, got %d instead",
expectedType, addr.Type())
return
}
if !bytes.Equal(addr.Data(), expectedData) {
t.Errorf("Expecting the Data to be %d, got %d instead",
expectedData, addr.Data())
return
}
if addr.Port() != expectedPort {
t.Errorf("Expecting the Port to be %d, got %d instead",
expectedPort, addr.Port())
return
}
if addr.String() != expectedHostPortString {
t.Errorf("Expecting the Host Port string to be \"%s\", "+
"got \"%s\" instead",
expectedHostPortString, addr.String())
return
}
output := make([]byte, len(input))
mLen, mErr := addr.Marshal(output)
if mErr != nil {
t.Error("Failed to marshal due to error:", mErr)
return
}
if !bytes.Equal(output[:mLen], input) {
t.Errorf("Expecting marshaled result to be %d, got %d instead",
input, output[:mLen])
return
}
}
func TestParseAddress(t *testing.T) {
testParseAddress(
t, []byte{0x04, 0x1e, 0x00}, make([]byte, 3), LoopbackAddr, nil, 1054,
"localhost:1054")
testParseAddress(
t,
[]byte{
0x04, 0x1e, 0x40,
0x7f, 0x00, 0x00, 0x01,
},
make([]byte, 4), IPv4Addr, []byte{0x7f, 0x00, 0x00, 0x01}, 1054,
"127.0.0.1:1054")
testParseAddress(
t,
[]byte{
0x04, 0x1e, 0x80,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x01,
},
make([]byte, 16), IPv6Addr, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7f, 0x00, 0x00, 0x01}, 1054,
"[::7f00:1]:1054")
testParseAddress(
t,
[]byte{
0x04, 0x1e, 0xff,
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'1', '2', '3',
},
make([]byte, 63), HostNameAddr, []byte{
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'1', '2', '3',
}, 1054,
strings.Repeat("ABCDEFGHIJ", 6)+"123:1054")
}
+30
View File
@@ -0,0 +1,30 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package commands
import (
"github.com/niruix/sshwifty/application/command"
)
// New creates a new commands group
func New() command.Commands {
return command.Commands{
newTelnet,
newSSH,
}
}
+120
View File
@@ -0,0 +1,120 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package commands
import (
"errors"
"github.com/niruix/sshwifty/application/rw"
)
// Errors
var (
ErrIntegerMarshalNotEnoughBuffer = errors.New(
"Not enough buffer to marshal the integer")
ErrIntegerMarshalTooLarge = errors.New(
"Integer cannot be marshalled, because the vaule was too large")
)
// Integer is a 16bit unsigned integer data
//
// Format:
// +-------------------------------------+--------------+
// | 1 bit | 7 bits |
// +-------------------------------------+--------------+
// | 1 when current byte is the end byte | Integer data |
// +-------------------------------------+--------------+
//
// Example:
// - 00000000 00000000: 0
// - 01111111: 127
// - 11111111 01000000: 255
type Integer uint16
const (
integerHasNextBit = 0x80
integerValueCutter = 0x7f
)
// Consts
const (
MaxInteger = 0x3fff
MaxIntegerBytes = 2
)
// ByteSize returns how many bytes current integer will be encoded into
func (i *Integer) ByteSize() int {
if *i > integerValueCutter {
return 2
}
return 1
}
// Int returns a int of current Integer
func (i *Integer) Int() int {
return int(*i)
}
// Marshal build serialized data of the integer
func (i *Integer) Marshal(b []byte) (int, error) {
bLen := len(b)
if *i > MaxInteger {
return 0, ErrIntegerMarshalTooLarge
}
if bLen < i.ByteSize() {
return 0, ErrIntegerMarshalNotEnoughBuffer
}
if *i <= integerValueCutter {
b[0] = byte(*i & integerValueCutter)
return 1, nil
}
b[0] = byte((*i >> 7) | integerHasNextBit)
b[1] = byte(*i & integerValueCutter)
return 2, nil
}
// Unmarshal read data and parse the integer
func (i *Integer) Unmarshal(reader rw.ReaderFunc) error {
buf := [1]byte{}
for j := 0; j < MaxIntegerBytes; j++ {
_, rErr := rw.ReadFull(reader, buf[:])
if rErr != nil {
return rErr
}
*i |= Integer(buf[0] & integerValueCutter)
if integerHasNextBit&buf[0] == 0 {
return nil
}
*i <<= 7
}
return nil
}
+124
View File
@@ -0,0 +1,124 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package commands
import (
"bytes"
"testing"
)
func TestInteger(t *testing.T) {
ii := Integer(0x3fff)
result := Integer(0)
buf := make([]byte, 2)
mLen, mErr := ii.Marshal(buf)
if mErr != nil {
t.Error("Failed to marshal:", mErr)
return
}
mData := bytes.NewBuffer(buf[:mLen])
mErr = result.Unmarshal(mData.Read)
if mErr != nil {
t.Error("Failed to unmarshal:", mErr)
return
}
if result != ii {
t.Errorf("Expecting result to be %d, got %d instead", ii, result)
return
}
}
func TestIntegerSingleByte1(t *testing.T) {
ii := Integer(102)
result := Integer(0)
buf := make([]byte, 2)
mLen, mErr := ii.Marshal(buf)
if mErr != nil {
t.Error("Failed to marshal:", mErr)
return
}
if mLen != 1 {
t.Error("Expecting the Integer to be marshalled into %d bytes, got "+
"%d instead", 1, mLen)
return
}
mData := bytes.NewBuffer(buf[:mLen])
mErr = result.Unmarshal(mData.Read)
if mErr != nil {
t.Error("Failed to unmarshal:", mErr)
return
}
if result != ii {
t.Errorf("Expecting result to be %d, got %d instead", ii, result)
return
}
}
func TestIntegerSingleByte2(t *testing.T) {
ii := Integer(127)
result := Integer(0)
buf := make([]byte, 2)
mLen, mErr := ii.Marshal(buf)
if mErr != nil {
t.Error("Failed to marshal:", mErr)
return
}
if mLen != 1 {
t.Error("Expecting the Integer to be marshalled into %d bytes, got "+
"%d instead", 1, mLen)
return
}
mData := bytes.NewBuffer(buf[:mLen])
mErr = result.Unmarshal(mData.Read)
if mErr != nil {
t.Error("Failed to unmarshal:", mErr)
return
}
if result != ii {
t.Errorf("Expecting result to be %d, got %d instead", ii, result)
return
}
}
+664
View File
@@ -0,0 +1,664 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package commands
import (
"errors"
"io"
"net"
"sync"
"time"
"golang.org/x/crypto/ssh"
"github.com/niruix/sshwifty/application/command"
"github.com/niruix/sshwifty/application/log"
"github.com/niruix/sshwifty/application/network"
"github.com/niruix/sshwifty/application/rw"
)
// Server -> client signal Consts
const (
SSHServerRemoteStdOut = 0x00
SSHServerRemoteStdErr = 0x01
SSHServerConnectFailed = 0x02
SSHServerConnectSucceed = 0x03
SSHServerConnectVerifyFingerprint = 0x04
SSHServerConnectRequestCredential = 0x05
)
// Client -> server signal consts
const (
SSHClientStdIn = 0x00
SSHClientResize = 0x01
SSHClientRespondFingerprint = 0x02
SSHClientRespondCredential = 0x03
)
const (
sshCredentialMaxSize = 4096
)
// Error codes
const (
SSHRequestErrorBadUserName = command.StreamError(0x01)
SSHRequestErrorBadRemoteAddress = command.StreamError(0x02)
SSHRequestErrorBadAuthMethod = command.StreamError(0x03)
)
// Auth methods
const (
SSHAuthMethodNone byte = 0x00
SSHAuthMethodPasspharse byte = 0x01
SSHAuthMethodPrivateKey byte = 0x02
)
type sshAuthMethodBuilder func(b []byte) []ssh.AuthMethod
// Errors
var (
ErrSSHAuthCancelled = errors.New(
"Authenication has been cancelled")
ErrSSHInvalidAuthMethod = errors.New(
"Invalid auth method")
ErrSSHInvalidAddress = errors.New(
"Invalid address")
ErrSSHRemoteFingerprintVerificationCancelled = errors.New(
"Server Fingerprint verification process has been cancelled")
ErrSSHRemoteFingerprintRefused = errors.New(
"Server Fingerprint has been refused")
ErrSSHRemoteConnUnavailable = errors.New(
"Remote SSH connection is unavailable")
ErrSSHUnexpectedFingerprintVerificationRespond = errors.New(
"Unexpected fingerprint verification respond")
ErrSSHUnexpectedCredentialDataRespond = errors.New(
"Unexpected credential data respond")
ErrSSHCredentialDataTooLarge = errors.New(
"Credential was too large")
ErrSSHUnknownClientSignal = errors.New(
"Unknown client signal")
)
type sshRemoteConn struct {
writer io.Writer
closer func() error
session *ssh.Session
}
func (s sshRemoteConn) isValid() bool {
return s.writer != nil && s.closer != nil && s.session != nil
}
type sshClient struct {
w command.StreamResponder
l log.Logger
dial network.Dial
dialTimeout time.Duration
remoteCloseWait sync.WaitGroup
credentialReceive chan []byte
credentialProcessed bool
credentialReceiveClosed bool
fingerprintVerifyResultReceive chan bool
fingerprintProcessed bool
fingerprintVerifyResultReceiveClosed bool
remoteConnReceive chan sshRemoteConn
remoteConn sshRemoteConn
}
func newSSH(
l log.Logger,
w command.StreamResponder,
dial network.Dial,
) command.FSMMachine {
return &sshClient{
w: w,
l: l,
dial: dial,
dialTimeout: 10 * time.Second,
remoteCloseWait: sync.WaitGroup{},
credentialReceive: make(chan []byte, 1),
credentialProcessed: false,
credentialReceiveClosed: false,
fingerprintVerifyResultReceive: make(chan bool, 1),
fingerprintProcessed: false,
fingerprintVerifyResultReceiveClosed: false,
remoteConnReceive: make(chan sshRemoteConn, 1),
remoteConn: sshRemoteConn{},
}
}
func (d *sshClient) Bootup(
r *rw.LimitedReader,
b []byte,
) (command.FSMState, command.FSMError) {
// User name
userName, userNameErr := ParseString(r.Read, b)
if userNameErr != nil {
return nil, command.ToFSMError(
userNameErr, SSHRequestErrorBadUserName)
}
userNameStr := string(userName.Data())
// Address
addr, addrErr := ParseAddress(r.Read, b)
if addrErr != nil {
return nil, command.ToFSMError(
addrErr, SSHRequestErrorBadRemoteAddress)
}
addrStr := addr.String()
if len(addrStr) <= 0 {
return nil, command.ToFSMError(
ErrSSHInvalidAddress, SSHRequestErrorBadRemoteAddress)
}
// Auth method
rData, rErr := rw.FetchOneByte(r.Fetch)
if rErr != nil {
return nil, command.ToFSMError(
rErr, SSHRequestErrorBadAuthMethod)
}
authMethodBuilder, authMethodBuilderErr := d.buildAuthMethod(rData[0])
if authMethodBuilderErr != nil {
return nil, command.ToFSMError(
authMethodBuilderErr, SSHRequestErrorBadAuthMethod)
}
d.remoteCloseWait.Add(1)
go d.remote(userNameStr, addrStr, authMethodBuilder)
return d.local, command.NoFSMError()
}
func (d *sshClient) buildAuthMethod(
methodType byte) (sshAuthMethodBuilder, error) {
switch methodType {
case SSHAuthMethodNone:
return func(b []byte) []ssh.AuthMethod {
return nil
}, nil
case SSHAuthMethodPasspharse:
return func(b []byte) []ssh.AuthMethod {
return []ssh.AuthMethod{
ssh.PasswordCallback(func() (string, error) {
wErr := d.w.SendManual(
SSHServerConnectRequestCredential,
b[d.w.HeaderSize():],
)
if wErr != nil {
return "", wErr
}
passpharseBytes, passpharseReceived := <-d.credentialReceive
if !passpharseReceived {
return "", ErrSSHAuthCancelled
}
return string(passpharseBytes), nil
}),
}
}, nil
case SSHAuthMethodPrivateKey:
return func(b []byte) []ssh.AuthMethod {
return []ssh.AuthMethod{
ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
wErr := d.w.SendManual(
SSHServerConnectRequestCredential,
b[d.w.HeaderSize():],
)
if wErr != nil {
return nil, wErr
}
privateKeyBytes, privateKeyReceived := <-d.credentialReceive
if !privateKeyReceived {
return nil, ErrSSHAuthCancelled
}
signer, signerErr := ssh.ParsePrivateKey(privateKeyBytes)
if signerErr != nil {
return nil, signerErr
}
return []ssh.Signer{signer}, signerErr
}),
}
}, nil
}
return nil, ErrSSHInvalidAuthMethod
}
func (d *sshClient) comfirmRemoteFingerprint(
hostname string,
remote net.Addr,
key ssh.PublicKey,
buf []byte,
) error {
fgp := ssh.FingerprintSHA256(key)
fgpLen := copy(buf[d.w.HeaderSize():], fgp)
wErr := d.w.SendManual(
SSHServerConnectVerifyFingerprint,
buf[:d.w.HeaderSize()+fgpLen],
)
if wErr != nil {
return wErr
}
confirmed, confirmOK := <-d.fingerprintVerifyResultReceive
if !confirmOK {
return ErrSSHRemoteFingerprintVerificationCancelled
}
if !confirmed {
return ErrSSHRemoteFingerprintRefused
}
return nil
}
func (d *sshClient) dialRemote(
network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
conn, err := d.dial(network, addr, config.Timeout)
if err != nil {
return nil, err
}
c, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
if err != nil {
return nil, err
}
return ssh.NewClient(c, chans, reqs), nil
}
func (d *sshClient) remote(
user string, address string, authMethodBuilder sshAuthMethodBuilder) {
defer func() {
d.w.Signal(command.HeaderClose)
close(d.remoteConnReceive)
d.remoteCloseWait.Done()
}()
buf := [4096]byte{}
conn, dErr := d.dialRemote("tcp", address, &ssh.ClientConfig{
User: user,
Auth: authMethodBuilder(buf[:]),
HostKeyCallback: func(h string, r net.Addr, k ssh.PublicKey) error {
return d.comfirmRemoteFingerprint(h, r, k, buf[:])
},
Timeout: d.dialTimeout,
})
if dErr != nil {
errLen := copy(buf[d.w.HeaderSize():], dErr.Error()) + d.w.HeaderSize()
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
d.l.Debug("Unable to connect to remote machine: %s", dErr)
return
}
defer conn.Close()
session, sErr := conn.NewSession()
if sErr != nil {
errLen := copy(buf[d.w.HeaderSize():], sErr.Error()) + d.w.HeaderSize()
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
d.l.Debug("Unable open new session on remote machine: %s", sErr)
return
}
defer session.Close()
in, inErr := session.StdinPipe()
if inErr != nil {
errLen := copy(buf[d.w.HeaderSize():], inErr.Error()) + d.w.HeaderSize()
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
d.l.Debug("Unable export Stdin pipe: %s", inErr)
return
}
out, outErr := session.StdoutPipe()
if outErr != nil {
errLen := copy(buf[d.w.HeaderSize():], outErr.Error()) +
d.w.HeaderSize()
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
d.l.Debug("Unable export Stdout pipe: %s", outErr)
return
}
errOut, outErrErr := session.StderrPipe()
if outErrErr != nil {
errLen := copy(buf[d.w.HeaderSize():], outErrErr.Error()) +
d.w.HeaderSize()
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
d.l.Debug("Unable export Stderr pipe: %s", outErrErr)
return
}
sErr = session.RequestPty("xterm", 80, 40, ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
})
if sErr != nil {
errLen := copy(buf[d.w.HeaderSize():], sErr.Error()) + d.w.HeaderSize()
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
d.l.Debug("Unable request PTY: %s", sErr)
return
}
sErr = session.Shell()
if sErr != nil {
errLen := copy(buf[d.w.HeaderSize():], sErr.Error()) + d.w.HeaderSize()
d.w.SendManual(SSHServerConnectFailed, buf[:errLen])
d.l.Debug("Unable to start Shell: %s", sErr)
return
}
defer session.Wait()
d.remoteConnReceive <- sshRemoteConn{
writer: in,
closer: func() error {
sErr := session.Close()
if sErr != nil {
return sErr
}
return conn.Close()
},
session: session,
}
wErr := d.w.SendManual(
SSHServerConnectSucceed, buf[:d.w.HeaderSize()])
if wErr != nil {
return
}
d.l.Debug("Serving")
d.remoteCloseWait.Add(1)
go func() {
defer d.remoteCloseWait.Done()
errOutBuf := [4096]byte{}
for {
rLen, rErr := errOut.Read(errOutBuf[d.w.HeaderSize():])
if rErr != nil {
return
}
rErr = d.w.SendManual(
SSHServerRemoteStdErr, errOutBuf[:d.w.HeaderSize()+rLen])
if rErr != nil {
return
}
}
}()
for {
rLen, rErr := out.Read(buf[d.w.HeaderSize():])
if rErr != nil {
return
}
rErr = d.w.SendManual(
SSHServerRemoteStdOut, buf[:d.w.HeaderSize()+rLen])
if rErr != nil {
return
}
}
}
func (d *sshClient) getRemote() (sshRemoteConn, error) {
if d.remoteConn.isValid() {
return d.remoteConn, nil
}
remoteConn, remoteConnFetched := <-d.remoteConnReceive
if !remoteConnFetched {
return sshRemoteConn{}, ErrSSHRemoteConnUnavailable
}
d.remoteConn = remoteConn
return d.remoteConn, nil
}
func (d *sshClient) local(
f *command.FSM,
r *rw.LimitedReader,
h command.StreamHeader,
b []byte,
) error {
switch h.Marker() {
case SSHClientStdIn:
remote, remoteErr := d.getRemote()
if remoteErr != nil {
return remoteErr
}
for !r.Completed() {
rData, rErr := r.Buffered()
if rErr != nil {
return rErr
}
_, wErr := remote.writer.Write(rData)
if wErr != nil {
return wErr
}
}
return nil
case SSHClientResize:
remote, remoteErr := d.getRemote()
if remoteErr != nil {
return remoteErr
}
_, rErr := io.ReadFull(r, b[:4])
if rErr != nil {
return rErr
}
rows := int(b[0])
rows <<= 8
rows |= int(b[1])
cols := int(b[2])
cols <<= 8
cols |= int(b[3])
// It's ok for it to fail
wcErr := remote.session.WindowChange(rows, cols)
if wcErr != nil {
d.l.Debug("Failed to resize to %d, %d: %s", rows, cols, wcErr)
}
return nil
case SSHClientRespondFingerprint:
if d.fingerprintProcessed {
return ErrSSHUnexpectedFingerprintVerificationRespond
}
d.fingerprintProcessed = true
rData, rErr := rw.FetchOneByte(r.Fetch)
if rErr != nil {
return rErr
}
comfirmed := rData[0] == 0
if !comfirmed {
d.fingerprintVerifyResultReceive <- false
remote, remoteErr := d.getRemote()
if remoteErr == nil {
remote.closer()
}
} else {
d.fingerprintVerifyResultReceive <- true
}
return nil
case SSHClientRespondCredential:
if d.credentialProcessed {
return ErrSSHUnexpectedCredentialDataRespond
}
d.credentialProcessed = true
sshCredentialBufSize := 0
if r.Remains() > sshCredentialMaxSize {
sshCredentialBufSize = sshCredentialMaxSize
} else {
sshCredentialBufSize = r.Remains()
}
credentialDataBuf := make([]byte, 0, sshCredentialBufSize)
totalCredentialRead := 0
for !r.Completed() {
rData, rErr := r.Buffered()
if rErr != nil {
return rErr
}
totalCredentialRead += len(rData)
if totalCredentialRead > sshCredentialBufSize {
return ErrSSHCredentialDataTooLarge
}
credentialDataBuf = append(credentialDataBuf, rData...)
}
d.credentialReceive <- credentialDataBuf
return nil
default:
return ErrSSHUnknownClientSignal
}
}
func (d *sshClient) Close() error {
d.credentialProcessed = true
d.fingerprintProcessed = true
if !d.credentialReceiveClosed {
close(d.credentialReceive)
d.credentialReceiveClosed = true
}
if !d.fingerprintVerifyResultReceiveClosed {
close(d.fingerprintVerifyResultReceive)
d.fingerprintVerifyResultReceiveClosed = true
}
remote, remoteErr := d.getRemote()
if remoteErr == nil {
remote.closer()
}
d.remoteCloseWait.Wait()
return nil
}
func (d *sshClient) Release() error {
return nil
}
+103
View File
@@ -0,0 +1,103 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package commands
import (
"errors"
"github.com/niruix/sshwifty/application/rw"
)
// Errors
var (
ErrStringParseBufferTooSmall = errors.New(
"Not enough buffer space to parse given string")
ErrStringMarshalBufferTooSmall = errors.New(
"Not enough buffer space to marshal given string")
)
// String data
type String struct {
len Integer
data []byte
}
// ParseString build the String according to readed data
func ParseString(reader rw.ReaderFunc, b []byte) (String, error) {
lenData := Integer(0)
mErr := lenData.Unmarshal(reader)
if mErr != nil {
return String{}, mErr
}
bLen := len(b)
if bLen < lenData.Int() {
return String{}, ErrStringParseBufferTooSmall
}
_, rErr := rw.ReadFull(reader, b[:lenData])
if rErr != nil {
return String{}, rErr
}
return String{
len: lenData,
data: b[:lenData],
}, nil
}
// NewString create a new String
func NewString(d []byte) String {
dLen := len(d)
if dLen > MaxInteger {
panic("Data was too long for a String")
}
return String{
len: Integer(dLen),
data: d,
}
}
// Data returns the data of the string
func (s String) Data() []byte {
return s.data
}
// Marshal the string to give buffer
func (s String) Marshal(b []byte) (int, error) {
bLen := len(b)
if bLen < s.len.ByteSize()+len(s.data) {
return 0, ErrStringMarshalBufferTooSmall
}
mLen, mErr := s.len.Marshal(b)
if mErr != nil {
return 0, mErr
}
return copy(b[mLen:], s.data) + mLen, nil
}
+83
View File
@@ -0,0 +1,83 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package commands
import (
"bytes"
"testing"
)
func testString(t *testing.T, str []byte) {
ss := NewString(str)
mm := make([]byte, len(str)+2)
mLen, mErr := ss.Marshal(mm)
if mErr != nil {
t.Error("Failed to marshal:", mErr)
return
}
buf := make([]byte, mLen)
source := bytes.NewBuffer(mm[:mLen])
result, rErr := ParseString(source.Read, buf)
if rErr != nil {
t.Error("Failed to parse:", rErr)
return
}
if !bytes.Equal(result.Data(), ss.Data()) {
t.Errorf("Expecting the data to be %d, got %d instead",
ss.Data(), result.Data())
return
}
}
func TestString(t *testing.T) {
testString(t, []byte{
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
})
testString(t, []byte{
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i',
})
}
+204
View File
@@ -0,0 +1,204 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package commands
import (
"errors"
"io"
"sync"
"time"
"github.com/niruix/sshwifty/application/command"
"github.com/niruix/sshwifty/application/log"
"github.com/niruix/sshwifty/application/network"
"github.com/niruix/sshwifty/application/rw"
)
// Errors
var (
ErrTelnetUnableToReceiveRemoteConn = errors.New(
"Unable to acquire remote connection handle")
)
// Error codes
const (
TelnetRequestErrorBadRemoteAddress = command.StreamError(0x01)
)
// Server signal codes
const (
TelnetServerRemoteBand = 0x00
TelnetServerDialFailed = 0x01
TelnetServerDialConnected = 0x02
)
type telnetClient struct {
l log.Logger
w command.StreamResponder
dial network.Dial
remoteChan chan io.WriteCloser
remoteConn io.WriteCloser
closeWait sync.WaitGroup
dialTimeout time.Duration
}
func newTelnet(
l log.Logger,
w command.StreamResponder,
dial network.Dial,
) command.FSMMachine {
return &telnetClient{
l: l,
w: w,
dial: dial,
remoteChan: make(chan io.WriteCloser, 1),
remoteConn: nil,
closeWait: sync.WaitGroup{},
dialTimeout: 10 * time.Second,
}
}
func (d *telnetClient) Bootup(
r *rw.LimitedReader,
b []byte) (command.FSMState, command.FSMError) {
addr, addrErr := ParseAddress(r.Read, b)
if addrErr != nil {
return nil, command.ToFSMError(
addrErr, TelnetRequestErrorBadRemoteAddress)
}
// TODO: Test whether or not the address is allowed
d.closeWait.Add(1)
go d.remote(addr.String())
return d.client, command.NoFSMError()
}
func (d *telnetClient) remote(addr string) {
defer func() {
d.w.Signal(command.HeaderClose)
close(d.remoteChan)
d.closeWait.Done()
}()
buf := [4096]byte{}
clientConn, clientConnErr := d.dial("tcp", addr, d.dialTimeout)
if clientConnErr != nil {
errLen := copy(
buf[d.w.HeaderSize():], clientConnErr.Error()) + d.w.HeaderSize()
d.w.SendManual(TelnetServerDialFailed, buf[:errLen])
return
}
defer clientConn.Close()
clientConnErr = d.w.SendManual(
TelnetServerDialConnected,
buf[:d.w.HeaderSize()],
)
if clientConnErr != nil {
return
}
d.remoteChan <- clientConn
for {
rLen, rErr := clientConn.Read(buf[d.w.HeaderSize():])
if rErr != nil {
return
}
wErr := d.w.SendManual(
TelnetServerRemoteBand, buf[:rLen+d.w.HeaderSize()])
if wErr != nil {
return
}
}
}
func (d *telnetClient) getRemote() (io.WriteCloser, error) {
if d.remoteConn != nil {
return d.remoteConn, nil
}
remoteConn, ok := <-d.remoteChan
if !ok {
return nil, ErrTelnetUnableToReceiveRemoteConn
}
d.remoteConn = remoteConn
return d.remoteConn, nil
}
func (d *telnetClient) client(
f *command.FSM,
r *rw.LimitedReader,
h command.StreamHeader,
b []byte,
) error {
remoteConn, remoteConnErr := d.getRemote()
if remoteConnErr != nil {
return remoteConnErr
}
// All Telnet requests are in-band, so we just directly send them all
// to the server
for !r.Completed() {
rBuf, rErr := r.Buffered()
if rErr != nil {
return rErr
}
_, wErr := remoteConn.Write(rBuf)
if wErr != nil {
return wErr
}
}
return nil
}
func (d *telnetClient) Close() error {
remoteConn, remoteConnErr := d.getRemote()
if remoteConnErr == nil {
remoteConn.Close()
}
d.closeWait.Wait()
return nil
}
func (d *telnetClient) Release() error {
return nil
}
+164
View File
@@ -0,0 +1,164 @@
// 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 configuration
import (
"errors"
"fmt"
"net"
"time"
"github.com/niruix/sshwifty/application/network"
)
// Server contains configuration of a HTTP server
type Server struct {
ListenInterface string
ListenPort uint16
InitialTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
HeartbeatTimeout time.Duration
ReadDelay time.Duration
WriteDelay time.Duration
TLSCertificateFile string
TLSCertificateKeyFile string
}
func (s Server) defaultListenInterface() string {
if len(s.ListenInterface) > 0 {
return s.ListenInterface
}
return net.IPv4(127, 0, 0, 1).String()
}
func (s Server) defaultListenPort() uint16 {
if s.ListenPort > 0 {
return s.ListenPort
}
return 80
}
func (s Server) maxDur(cur, def time.Duration) time.Duration {
if cur > def {
return cur
}
return def
}
// WithDefault build the configuration and fill the blank with default values
func (s Server) WithDefault() Server {
initialTimeout := s.maxDur(s.InitialTimeout, 1*time.Second)
readTimeout := s.maxDur(initialTimeout, 3*time.Second)
readTimeout = s.maxDur(s.ReadTimeout, readTimeout)
return Server{
ListenInterface: s.defaultListenInterface(),
ListenPort: s.defaultListenPort(),
InitialTimeout: initialTimeout,
ReadTimeout: readTimeout,
WriteTimeout: s.maxDur(s.WriteTimeout, 3*time.Second),
HeartbeatTimeout: s.maxDur(s.ReadTimeout, readTimeout/2),
ReadDelay: 0,
WriteDelay: 0,
TLSCertificateFile: "",
TLSCertificateKeyFile: "",
}
}
// IsTLS returns whether or not TLS should be used
func (s Server) IsTLS() bool {
return len(s.TLSCertificateFile) > 0 && len(s.TLSCertificateKeyFile) > 0
}
// Verify verifies current configuration
func (s Server) Verify() error {
if net.ParseIP(s.ListenInterface) == nil {
return fmt.Errorf("Invalid IP address \"%s\"", s.ListenInterface)
}
if (len(s.TLSCertificateFile) > 0 && len(s.TLSCertificateKeyFile) <= 0) ||
(len(s.TLSCertificateFile) <= 0 && len(s.TLSCertificateKeyFile) > 0) {
return errors.New("TLSCertificateFile and TLSCertificateKeyFile must " +
"both be specified in order to enable TLS")
}
return nil
}
// Configuration contains configuration of the application
type Configuration struct {
HostName string
SharedKey string
Dialer network.Dial
Servers []Server
}
// Common settings shared by mulitple servers
type Common struct {
HostName string
SharedKey string
Dialer network.Dial
}
// Verify verifies current setting
func (c Configuration) Verify() error {
if len(c.Servers) <= 0 {
return errors.New("Must specify at least one server")
}
for i, c := range c.Servers {
vErr := c.Verify()
if vErr == nil {
continue
}
return fmt.Errorf("Invalid setting for server %d: %s", i, vErr)
}
return nil
}
// Common returns common settings
func (c Configuration) Common() Common {
return Common{
HostName: c.HostName,
SharedKey: c.SharedKey,
Dialer: c.Dialer,
}
}
// WithDefault build the configuration and fill the blank with default values
func (c Common) WithDefault() Common {
dialer := c.Dialer
if dialer == nil {
dialer = network.TCPDial()
}
return Common{
HostName: c.HostName,
SharedKey: c.SharedKey,
Dialer: dialer,
}
}
+25
View File
@@ -0,0 +1,25 @@
// 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 configuration
import (
"github.com/niruix/sshwifty/application/log"
)
// Loader Configuration loader
type Loader func(log log.Logger) (name string, cfg Configuration, err error)
+107
View File
@@ -0,0 +1,107 @@
// 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 configuration
import (
"fmt"
"os"
"strconv"
"github.com/niruix/sshwifty/application/log"
"github.com/niruix/sshwifty/application/network"
)
const (
enviroTypeName = "Environment Variable"
)
// Enviro creates an environment variable based configuration loader
func Enviro() Loader {
return func(log log.Logger) (string, Configuration, error) {
log.Info("Loading configuration from environment variables ...")
cfg := fileCfgCommon{
HostName: os.Getenv("SSHWIFTY_HOSTNAME"),
SharedKey: os.Getenv("SSHWIFTY_SHAREDKEY"),
Socks5: os.Getenv("SSHWIFTY_SOCKS5"),
Socks5User: os.Getenv("SSHWIFTY_SOCKS5_USER"),
Socks5Password: os.Getenv("SSHWIFTY_SOCKS5_PASSWORD"),
}
listenPort, listenPortErr := strconv.ParseUint(
os.Getenv("SSHWIFTY_LISTENPORT"), 10, 16)
if listenPortErr != nil {
return enviroTypeName, Configuration{}, fmt.Errorf(
"Invalid \"SSHWIFTY_LISTENPORT\": %s", listenPortErr)
}
initialTimeout, _ := strconv.ParseUint(
os.Getenv("SSHWIFTY_INITIALTIMEOUT"), 10, 32)
readTimeout, _ := strconv.ParseUint(
os.Getenv("SSHWIFTY_READTIMEOUT"), 10, 32)
writeTimeout, _ := strconv.ParseUint(
os.Getenv("SSHWIFTY_WRITETIMEOUT"), 10, 32)
heartbeatTimeout, _ := strconv.ParseUint(
os.Getenv("SSHWIFTY_HEARTBEATTIMEOUT"), 10, 32)
readDelay, _ := strconv.ParseUint(
os.Getenv("SSHWIFTY_READDELAY"), 10, 32)
writeDelay, _ := strconv.ParseUint(
os.Getenv("SSHWIFTY_WRITEELAY"), 10, 32)
cfgSer := fileCfgServer{
ListenInterface: os.Getenv("SSHWIFTY_LISTENINTERFACE"),
ListenPort: uint16(listenPort),
InitialTimeout: int(initialTimeout),
ReadTimeout: int(readTimeout),
WriteTimeout: int(writeTimeout),
HeartbeatTimeout: int(heartbeatTimeout),
ReadDelay: int(readDelay),
WriteDelay: int(writeDelay),
TLSCertificateFile: os.Getenv("SSHWIFTY_TLSCERTIFICATEFILE"),
TLSCertificateKeyFile: os.Getenv("SSHWIFTY_TLSCERTIFICATEKEYFILE"),
}
var dialer network.Dial
if len(cfg.Socks5) <= 0 {
dialer = network.TCPDial()
} else {
sDial, sDialErr := network.BuildSocks5Dial(
cfg.Socks5, cfg.Socks5User, cfg.Socks5Password)
if sDialErr != nil {
return enviroTypeName, Configuration{}, sDialErr
}
dialer = sDial
}
return enviroTypeName, Configuration{
HostName: cfg.HostName,
SharedKey: cfg.SharedKey,
Dialer: dialer,
Servers: []Server{cfgSer.build()},
}, nil
}
}
+190
View File
@@ -0,0 +1,190 @@
// 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 configuration
import (
"encoding/json"
"fmt"
"os"
"os/user"
"path"
"path/filepath"
"strings"
"time"
"github.com/niruix/sshwifty/application/network"
"github.com/niruix/sshwifty/application/log"
)
const (
fileTypeName = "File"
)
type fileCfgServer struct {
ListenInterface string // Interface to listen to
ListenPort uint16 // Port to listen
InitialTimeout int // Client initial request timeout, in second
ReadTimeout int // Read operation timeout, in second
WriteTimeout int // Write operation timeout, in second
HeartbeatTimeout int // Client heartbeat interval, in second
ReadDelay int // Read delay, in millisecond
WriteDelay int // Write delay, in millisecond
TLSCertificateFile string // Location of TLS certificate file
TLSCertificateKeyFile string // Location of TLS certificate key
}
func (f fileCfgServer) minDur(current, min int) int {
if current > min {
return current
}
return min
}
func (f *fileCfgServer) build() Server {
return Server{
ListenInterface: f.ListenInterface,
ListenPort: f.ListenPort,
InitialTimeout: time.Duration(
f.minDur(f.InitialTimeout, 5)) * time.Second,
ReadTimeout: time.Duration(
f.minDur(f.ReadTimeout, 30)) * time.Second,
WriteTimeout: time.Duration(
f.minDur(f.WriteTimeout, 30)) * time.Second,
HeartbeatTimeout: time.Duration(
f.minDur(f.HeartbeatTimeout, 10)) * time.Second,
ReadDelay: time.Duration(
f.minDur(f.ReadDelay, 0)) * time.Millisecond,
WriteDelay: time.Duration(
f.minDur(f.WriteDelay, 0)) * time.Millisecond,
TLSCertificateFile: f.TLSCertificateFile,
TLSCertificateKeyFile: f.TLSCertificateKeyFile,
}
}
type fileCfgCommon struct {
HostName string // Host name
SharedKey string // Shared key, empty to enable public access
Socks5 string // Socks5 server address, optional
Socks5User string // Login user for socks5 server, optional
Socks5Password string // Login pass for socks5 server, optional
Servers []*fileCfgServer // Servers
}
func loadFile(filePath string) (string, Configuration, error) {
f, fErr := os.Open(filePath)
if fErr != nil {
return fileTypeName, Configuration{}, fErr
}
defer f.Close()
cfg := fileCfgCommon{}
jDecoder := json.NewDecoder(f)
jDecodeErr := jDecoder.Decode(&cfg)
if jDecodeErr != nil {
return fileTypeName, Configuration{}, jDecodeErr
}
servers := make([]Server, len(cfg.Servers))
for i := range servers {
servers[i] = cfg.Servers[i].build()
}
var dialer network.Dial
if len(cfg.Socks5) <= 0 {
dialer = network.TCPDial()
} else {
sDial, sDialErr := network.BuildSocks5Dial(
cfg.Socks5, cfg.Socks5User, cfg.Socks5Password)
if sDialErr != nil {
return fileTypeName, Configuration{}, sDialErr
}
dialer = sDial
}
return fileTypeName, Configuration{
HostName: cfg.HostName,
SharedKey: cfg.SharedKey,
Dialer: dialer,
Servers: servers,
}, nil
}
// File creates a configuration file loader
func File(customPath string) Loader {
return func(log log.Logger) (string, Configuration, error) {
if len(customPath) > 0 {
log.Info("Loading configuration from: %s", customPath)
return loadFile(customPath)
}
fallbackFileSearchList := make([]string, 0, 3)
// ~/.config/sshwifty.conf.json
u, userErr := user.Current()
if userErr == nil {
fallbackFileSearchList = append(
fallbackFileSearchList,
path.Join(u.HomeDir, ".config", "sshwifty.conf.json"))
}
// /etc/sshwifty.conf.json
fallbackFileSearchList = append(
fallbackFileSearchList, "/etc/sshwifty.conf.json")
// sshwifty.conf.json located at the same directory as Sshwifty bin
ex, exErr := os.Executable()
if exErr == nil {
fallbackFileSearchList = append(
fallbackFileSearchList,
path.Join(filepath.Dir(ex), "sshwifty.conf.json"))
}
for f := range fallbackFileSearchList {
fInfo, fErr := os.Stat(fallbackFileSearchList[f])
if fErr != nil {
continue
}
if fInfo.IsDir() {
continue
}
log.Info("Loading configuration from: %s",
fallbackFileSearchList[f])
return loadFile(fallbackFileSearchList[f])
}
return "", Configuration{}, fmt.Errorf(
"Configuration file was not specified. Also tried fallback files "+
"\"%s\", but none of it was available",
strings.Join(fallbackFileSearchList, "\", \""))
}
}
@@ -0,0 +1,52 @@
// 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 configuration
import (
"fmt"
"github.com/niruix/sshwifty/application/log"
)
const (
redundantTypeName = "Redundant"
)
// Redundant creates a group of loaders. They will be executed one by one until
// one of it successfully returned a configuration
func Redundant(loaders ...Loader) Loader {
return func(log log.Logger) (string, Configuration, error) {
ll := log.Context("Redundant")
for i := range loaders {
lLoaderName, lCfg, lErr := loaders[i](ll)
if lErr != nil {
ll.Warning("Unable to load configuration from \"%s\": %s",
lLoaderName, lErr)
continue
}
return lLoaderName, lCfg, nil
}
return redundantTypeName, Configuration{}, fmt.Errorf(
"All existing redundant loader has failed")
}
}
+132
View File
@@ -0,0 +1,132 @@
// 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 controller
import (
"net/http"
"strings"
"github.com/niruix/sshwifty/application/log"
)
// Error
var (
ErrControllerNotImplemented = NewError(
http.StatusNotImplemented, "Server does not know how to handle the "+
"request")
)
type controller interface {
Get(w http.ResponseWriter, r *http.Request, l log.Logger) error
Head(w http.ResponseWriter, r *http.Request, l log.Logger) error
Post(w http.ResponseWriter, r *http.Request, l log.Logger) error
Put(w http.ResponseWriter, r *http.Request, l log.Logger) error
Delete(w http.ResponseWriter, r *http.Request, l log.Logger) error
Connect(w http.ResponseWriter, r *http.Request, l log.Logger) error
Options(w http.ResponseWriter, r *http.Request, l log.Logger) error
Trace(w http.ResponseWriter, r *http.Request, l log.Logger) error
Patch(w http.ResponseWriter, r *http.Request, l log.Logger) error
Other(
method string,
w http.ResponseWriter,
r *http.Request,
l log.Logger,
) error
}
type baseController struct{}
func (b baseController) Get(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func (b baseController) Head(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func (b baseController) Post(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func (b baseController) Put(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func (b baseController) Delete(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func (b baseController) Connect(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func (b baseController) Options(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func (b baseController) Trace(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func (b baseController) Patch(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func (b baseController) Other(
method string, w http.ResponseWriter, r *http.Request, l log.Logger) error {
return ErrControllerNotImplemented
}
func serveController(
c controller,
w http.ResponseWriter,
r *http.Request,
l log.Logger,
) error {
switch strings.ToUpper(r.Method) {
case "GET":
return c.Get(w, r, l)
case "HEAD":
return c.Head(w, r, l)
case "POST":
return c.Post(w, r, l)
case "PUT":
return c.Put(w, r, l)
case "DELETE":
return c.Delete(w, r, l)
case "CONNECT":
return c.Connect(w, r, l)
case "OPTIONS":
return c.Options(w, r, l)
case "TRACE":
return c.Trace(w, r, l)
case "PATCH":
return c.Patch(w, r, l)
default:
return c.Other(r.Method, w, r, l)
}
}
+75
View File
@@ -0,0 +1,75 @@
// 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 controller
import (
"net/http"
"strings"
"time"
)
func clientSupportGZIP(r *http.Request) bool {
// Should be good enough
return strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
}
func clientContentEtagIsValid(r *http.Request, eTag string) bool {
d := r.Header.Get("If-None-Match")
if len(d) < 0 {
return false
}
dStart := 0
qETag := "\"" + eTag + "\""
for {
dIdx := strings.Index(d[dStart:], ",")
if dIdx < 0 {
return strings.Contains(d[dStart:], qETag) ||
strings.Contains(d[dStart:], "*")
}
if strings.Contains(d[dStart:dStart+dIdx], qETag) {
return true
}
if strings.Contains(d[dStart:dStart+dIdx], "*") {
return true
}
dStart += dIdx + 1
}
}
func clientContentModifiedSince(r *http.Request, mod time.Time) bool {
d := r.Header.Get("If-Modified-Since")
if len(d) < 0 {
return false
}
dt, dtErr := time.Parse(time.RFC1123, d)
if dtErr != nil {
return false
}
return !mod.Before(dt)
}
+47
View File
@@ -0,0 +1,47 @@
// 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 controller
import (
"net/http"
"testing"
)
func TestClientContentEtagIsValid(t *testing.T) {
test := func(id int, hd []string, etag string, expected bool) {
r := http.Request{
Header: http.Header{
"If-None-Match": hd,
},
}
rr := clientContentEtagIsValid(&r, etag)
if rr != expected {
t.Errorf("Test: %d: Expecting the result to be %v, got %v instead",
id, expected, rr)
return
}
}
test(0, []string{""}, "test", false)
test(1, []string{"*"}, "test", true)
test(2, []string{"W/\"67ab43\", \"54ed21\", \"7892dd\""}, "54ed21", true)
test(3, []string{"\"bfc13a64729c4290ef5b2c2730249c88ca92d82d\""},
"bfc13a64729c4290ef5b2c2730249c88ca92d82d", true)
}
+134
View File
@@ -0,0 +1,134 @@
// 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 controller
import (
"net/http"
"strings"
"time"
"github.com/niruix/sshwifty/application/command"
"github.com/niruix/sshwifty/application/configuration"
"github.com/niruix/sshwifty/application/log"
"github.com/niruix/sshwifty/application/server"
)
// Errors
var (
ErrNotFound = NewError(
http.StatusNotFound, "Page not found")
)
// handler is the main service dispatcher
type handler struct {
hostNameChecker string
commonCfg configuration.Common
logger log.Logger
homeCtl home
socketCtl socket
}
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var err error
clientLogger := h.logger.Context("Client (%s)", r.RemoteAddr)
if len(h.commonCfg.HostName) > 0 {
hostPort := r.Host
if len(hostPort) <= 0 {
hostPort = r.URL.Host
}
if h.commonCfg.HostName != hostPort &&
!strings.HasPrefix(hostPort, h.hostNameChecker) {
clientLogger.Warning("Request invalid host \"%s\", deined",
r.Host)
serveFailure(
NewError(http.StatusForbidden, "Invalid host"), w, r, h.logger)
return
}
}
w.Header().Add("Date", time.Now().UTC().Format(time.RFC1123))
switch r.URL.Path {
case "/":
err = serveController(h.homeCtl, w, r, clientLogger)
case "/socket":
err = serveController(h.socketCtl, w, r, clientLogger)
case "/robots.txt":
fallthrough
case "/favicon.ico":
fallthrough
case "/README.md":
fallthrough
case "/LICENSE.md":
fallthrough
case "/DEPENDENCES.md":
err = serveStaticData(r.URL.Path[1:], w, r, clientLogger)
default:
if strings.HasPrefix(r.URL.Path, "/assets/") {
err = serveStaticData(r.URL.Path[8:], w, r, clientLogger)
} else {
err = ErrNotFound
}
}
if err == nil {
clientLogger.Info("Request completed: %s", r.URL.String())
return
}
clientLogger.Warning("Request ended with error: %s: %s",
r.URL.String(), err)
controllerErr, isControllerErr := err.(Error)
if isControllerErr {
serveFailure(controllerErr, w, r, h.logger)
return
}
serveFailure(
NewError(http.StatusInternalServerError, err.Error()), w, r, h.logger)
}
// Builder returns a http controller builder
func Builder(cmds command.Commands) server.HandlerBuilder {
return func(
commonCfg configuration.Common,
cfg configuration.Server,
logger log.Logger,
) http.Handler {
return handler{
hostNameChecker: commonCfg.HostName + ":",
commonCfg: commonCfg,
logger: logger,
homeCtl: home{},
socketCtl: newSocketCtl(commonCfg, cfg, cmds),
}
}
}
+44
View File
@@ -0,0 +1,44 @@
// 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 controller
import "fmt"
// Error Controller error
type Error struct {
code int
message string
}
// NewError creates a new Error
func NewError(code int, message string) Error {
return Error{
code: code,
message: message,
}
}
// Code return the error code
func (f Error) Code() int {
return f.code
}
// Error returns the error message
func (f Error) Error() string {
return fmt.Sprintf("HTTP Error (%d): %s", f.code, f.message)
}
+35
View File
@@ -0,0 +1,35 @@
// 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 controller
import (
"net/http"
"github.com/niruix/sshwifty/application/log"
)
func serveFailure(
err Error,
w http.ResponseWriter,
r *http.Request,
l log.Logger,
) error {
w.WriteHeader(err.Code())
return serveStaticPage("error.html", w, r, l)
}
+33
View File
@@ -0,0 +1,33 @@
// 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 controller
import (
"net/http"
"github.com/niruix/sshwifty/application/log"
)
// home controller
type home struct {
baseController
}
func (h home) Get(w http.ResponseWriter, r *http.Request, l log.Logger) error {
return serveStaticPage("index.html", w, r, l)
}
+444
View File
@@ -0,0 +1,444 @@
// 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 controller
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"fmt"
"io"
"net/http"
"strconv"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/niruix/sshwifty/application/command"
"github.com/niruix/sshwifty/application/configuration"
"github.com/niruix/sshwifty/application/log"
"github.com/niruix/sshwifty/application/rw"
)
// Errors
var (
ErrSocketAuthFailed = NewError(
http.StatusForbidden,
"To use Websocket interface, a valid Auth Key must be provided")
ErrSocketUnableToGenerateKey = NewError(
http.StatusInternalServerError,
"Unable to generate crypto key")
ErrSocketInvalidDataPackage = NewError(
http.StatusBadRequest, "Invalid data package")
)
const (
socketGCMStandardNonceSize = 12
)
type socket struct {
baseController
commonCfg configuration.Common
serverCfg configuration.Server
randomKey string
authKey []byte
upgrader websocket.Upgrader
commander command.Commander
}
func getNewSocketCtlRandomSharedKey() string {
b := [32]byte{}
io.ReadFull(rand.Reader, b[:])
return base64.StdEncoding.EncodeToString(b[:])
}
func getSocketAuthKey(randomKey string, sharedKey string) []byte {
var k []byte
if len(sharedKey) > 0 {
k = []byte(sharedKey)
} else {
k = []byte(randomKey)
}
h := hmac.New(sha512.New, k)
h.Write([]byte(randomKey))
return h.Sum(nil)
}
func newSocketCtl(
commonCfg configuration.Common,
cfg configuration.Server,
cmds command.Commands,
) socket {
randomKey := getNewSocketCtlRandomSharedKey()
return socket{
commonCfg: commonCfg,
serverCfg: cfg,
randomKey: randomKey,
authKey: getSocketAuthKey(randomKey, commonCfg.SharedKey)[:32],
upgrader: buildWebsocketUpgrader(cfg),
commander: command.New(cmds),
}
}
type websocketWriter struct {
*websocket.Conn
}
func (w websocketWriter) Write(b []byte) (int, error) {
wErr := w.WriteMessage(websocket.BinaryMessage, b)
if wErr != nil {
return 0, wErr
}
return len(b), nil
}
type socketPackageWriter struct {
w websocketWriter
packager func(w websocketWriter, b []byte) error
}
func (s socketPackageWriter) Write(b []byte) (int, error) {
packageWriteErr := s.packager(s.w, b)
if packageWriteErr != nil {
return 0, packageWriteErr
}
return len(b), nil
}
func buildWebsocketUpgrader(cfg configuration.Server) websocket.Upgrader {
return websocket.Upgrader{
HandshakeTimeout: cfg.InitialTimeout,
CheckOrigin: func(r *http.Request) bool {
return true
},
Error: func(
w http.ResponseWriter,
r *http.Request,
status int,
reason error,
) {
},
}
}
func (s socket) Options(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Headers", "X-Key")
return nil
}
func (s socket) setServerConfigHeader(hd *http.Header) {
hd.Add("X-Heartbeat",
strconv.FormatFloat(s.serverCfg.HeartbeatTimeout.Seconds(), 'g', 2, 64))
hd.Add("X-Timeout",
strconv.FormatFloat(s.serverCfg.ReadTimeout.Seconds(), 'g', 2, 64))
}
func (s socket) Head(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
key := r.Header.Get("X-Key")
hd := w.Header()
if len(key) <= 0 {
hd.Add("X-Key", s.randomKey)
if len(s.commonCfg.SharedKey) <= 0 {
s.setServerConfigHeader(&hd)
return nil
}
return ErrSocketAuthFailed
}
if len(key) > 64 {
return ErrSocketAuthFailed
}
decodedKey, decodedKeyErr := base64.StdEncoding.DecodeString(key)
if decodedKeyErr != nil {
return NewError(http.StatusBadRequest, decodedKeyErr.Error())
}
if !hmac.Equal(s.authKey, decodedKey) {
return ErrSocketAuthFailed
}
hd.Add("X-Key", s.randomKey)
s.setServerConfigHeader(&hd)
return nil
}
func (s socket) buildWSFetcher(c *websocket.Conn) rw.FetchReaderFetcher {
return func() ([]byte, error) {
for {
mt, message, err := c.ReadMessage()
if err != nil {
return nil, err
}
if mt != websocket.BinaryMessage {
return nil, NewError(
http.StatusBadRequest,
fmt.Sprintf("Received unknown type of data: %d", message))
}
return message, nil
}
}
}
func (s socket) generateNonce(nonce []byte) error {
_, rErr := io.ReadFull(rand.Reader, nonce[:socketGCMStandardNonceSize])
return rErr
}
func (s socket) increaseNonce(nonce []byte) {
for i := len(nonce); i > 0; i-- {
nonce[i-1]++
if nonce[i-1] <= 0 {
continue
}
break
}
}
func (s socket) createCipher(key []byte) (cipher.AEAD, cipher.AEAD, error) {
readCipher, readCipherErr := aes.NewCipher(key)
if readCipherErr != nil {
return nil, nil, readCipherErr
}
writeCipher, writeCipherErr := aes.NewCipher(key)
if writeCipherErr != nil {
return nil, nil, writeCipherErr
}
gcmRead, gcmReadErr := cipher.NewGCMWithNonceSize(
readCipher, socketGCMStandardNonceSize)
if gcmReadErr != nil {
return nil, nil, gcmReadErr
}
gcmWrite, gcmWriteErr := cipher.NewGCMWithNonceSize(
writeCipher, socketGCMStandardNonceSize)
if gcmWriteErr != nil {
return nil, nil, gcmWriteErr
}
return gcmRead, gcmWrite, nil
}
func (s socket) privateKey() string {
if len(s.commonCfg.SharedKey) > 0 {
return s.commonCfg.SharedKey
}
return s.randomKey
}
func (s socket) buildCipherKey() [24]byte {
key := [24]byte{}
now := strconv.FormatInt(time.Now().Unix()/100, 10)
copy(key[:], getSocketAuthKey(now, s.privateKey()))
return key
}
func (s socket) Get(
w http.ResponseWriter, r *http.Request, l log.Logger) error {
// Error will not be returned when Websocket already handled
// (i.e. returned the error to client). We just log the error and that's it
c, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
return NewError(http.StatusBadRequest, err.Error())
}
defer c.Close()
wsReader := rw.NewFetchReader(s.buildWSFetcher(c))
wsWriter := websocketWriter{Conn: c}
// Initialize ciphers
readNonce := [socketGCMStandardNonceSize]byte{}
_, nonceReadErr := io.ReadFull(&wsReader, readNonce[:])
if nonceReadErr != nil {
return NewError(http.StatusBadRequest, fmt.Sprintf(
"Unable to read initial client nonce: %s", nonceReadErr.Error()))
}
writeNonce := [socketGCMStandardNonceSize]byte{}
nonceReadErr = s.generateNonce(writeNonce[:])
if nonceReadErr != nil {
return NewError(http.StatusBadRequest, fmt.Sprintf(
"Unable to generate initial server nonce: %s",
nonceReadErr.Error()))
}
_, nonceSendErr := wsWriter.Write(writeNonce[:])
if nonceSendErr != nil {
return NewError(http.StatusBadRequest, fmt.Sprintf(
"Unable to send server nonce to client: %s", nonceSendErr.Error()))
}
cipherKey := s.buildCipherKey()
readCipher, writeCipher, cipherCreationErr := s.createCipher(cipherKey[:])
if cipherCreationErr != nil {
return NewError(http.StatusInternalServerError, fmt.Sprintf(
"Unable to create cipher: %s", cipherCreationErr.Error()))
}
// Start service
const cipherReadBufSize = 1024
cipherReadBuf := [cipherReadBufSize]byte{}
cipherWriteBuf := [cipherReadBufSize]byte{}
maxWriteLen := cipherReadBufSize - (writeCipher.Overhead() + 2)
senderLock := sync.Mutex{}
cmdExec, cmdExecErr := s.commander.New(
s.commonCfg.Dialer, rw.NewFetchReader(func() ([]byte, error) {
defer s.increaseNonce(readNonce[:])
// Size is unencrypted
_, rErr := io.ReadFull(&wsReader, cipherReadBuf[:2])
if rErr != nil {
return nil, rErr
}
// Read full size
packageSize := uint16(cipherReadBuf[0])
packageSize <<= 8
packageSize |= uint16(cipherReadBuf[1])
if packageSize <= 0 {
return nil, ErrSocketInvalidDataPackage
}
if int(packageSize) <= wsReader.Remain() {
rData, rErr := wsReader.Export(int(packageSize))
if rErr != nil {
return nil, rErr
}
return readCipher.Open(rData[:0], readNonce[:], rData, nil)
}
if packageSize > cipherReadBufSize {
return nil, ErrSocketInvalidDataPackage
}
_, rErr = io.ReadFull(&wsReader, cipherReadBuf[:packageSize])
if rErr != nil {
return nil, rErr
}
return readCipher.Open(
cipherReadBuf[:0],
readNonce[:],
cipherReadBuf[:packageSize],
nil)
}), socketPackageWriter{
w: wsWriter,
packager: func(w websocketWriter, b []byte) error {
start := 0
bLen := len(b)
readLen := bLen
for start < bLen {
if readLen > maxWriteLen {
readLen = maxWriteLen
}
encrypted := writeCipher.Seal(
cipherWriteBuf[2:2],
writeNonce[:],
b[start:start+readLen],
nil)
s.increaseNonce(writeNonce[:])
encryptedSize := uint16(len(encrypted))
if encryptedSize <= 0 {
return ErrSocketInvalidDataPackage
}
cipherWriteBuf[0] = byte(encryptedSize >> 8)
cipherWriteBuf[1] = byte(encryptedSize)
_, wErr := w.Write(cipherWriteBuf[:encryptedSize+2])
if wErr != nil {
return wErr
}
start += readLen
readLen = bLen - start
}
return nil
},
}, &senderLock, s.serverCfg.ReadDelay, s.serverCfg.WriteDelay, l)
if cmdExecErr != nil {
return NewError(http.StatusBadRequest, cmdExecErr.Error())
}
return cmdExec.Handle()
}
+154
View File
@@ -0,0 +1,154 @@
// 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 controller
import (
"mime"
"net/http"
"strings"
"time"
"github.com/niruix/sshwifty/application/log"
)
type staticData struct {
data []byte
dataHash string
compressd []byte
compressdHash string
created time.Time
}
func (s staticData) hasCompressed() bool {
return len(s.compressd) > 0
}
func staticFileExt(fileName string) string {
extIdx := strings.LastIndex(fileName, ".")
if extIdx < 0 {
return ""
}
return strings.ToLower(fileName[extIdx:])
}
func serveStaticPage(
dataName string,
w http.ResponseWriter,
r *http.Request,
l log.Logger,
) error {
d, dFound := staticPages[dataName]
if !dFound {
return ErrNotFound
}
selectedData := d.data
if clientSupportGZIP(r) && d.hasCompressed() {
selectedData = d.compressd
w.Header().Add("Vary", "Accept-Encoding")
w.Header().Add("Content-Encoding", "gzip")
}
mimeType := mime.TypeByExtension(staticFileExt(dataName))
if len(mimeType) > 0 {
w.Header().Add("Content-Type", mimeType)
} else {
w.Header().Add("Content-Type", "application/binary")
}
_, wErr := w.Write(selectedData)
return wErr
}
func serveStaticData(
dataName string,
w http.ResponseWriter,
r *http.Request,
l log.Logger,
) error {
if strings.ToUpper(r.Method) != "GET" {
return ErrControllerNotImplemented
}
fileExt := staticFileExt(dataName)
if fileExt == ".html" || fileExt == ".htm" {
return ErrNotFound
}
d, dFound := staticPages[dataName]
if !dFound {
return ErrNotFound
}
selectedData := d.data
selectedDataHash := d.dataHash
compressEnabled := false
if clientSupportGZIP(r) && d.hasCompressed() {
selectedData = d.compressd
selectedDataHash = d.compressdHash
compressEnabled = true
w.Header().Add("Vary", "Accept-Encoding")
}
canUseCache := true
if !clientContentEtagIsValid(r, selectedDataHash) {
canUseCache = false
}
if clientContentModifiedSince(r, d.created) {
canUseCache = false
}
if canUseCache {
w.WriteHeader(http.StatusNotModified)
return nil
}
w.Header().Add("Cache-Control", "public, max-age=31536000")
w.Header().Add("ETag", "\""+selectedDataHash+"\"")
mimeType := mime.TypeByExtension(fileExt)
if len(mimeType) > 0 {
w.Header().Add("Content-Type", mimeType)
} else {
w.Header().Add("Content-Type", "application/binary")
}
if compressEnabled {
w.Header().Add("Content-Encoding", "gzip")
}
_, wErr := w.Write([]byte(selectedData))
return wErr
}
@@ -0,0 +1,457 @@
// 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 main
import (
"bufio"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
"time"
)
const (
parentPackage = "github.com/niruix/sshwifty/application/controller"
)
const (
staticListTemplate = `import "time"
var (
staticPages = map[string]staticData{
{{ range . }}"{{ .Name }}":
parseStaticData({{ .GOPackage }}.{{ .GOVariableName }}()),
{{ end }}
}
)
// parseStaticData parses result from a static file returner and generate
// a new ` + "`" + `staticData` + "`" + ` item
func parseStaticData(
fileStart int,
fileEnd int,
compressedStart int,
compressedEnd int,
contentHash string,
compressedHash string,
creation time.Time,
data []byte,
) staticData {
return staticData{
data: data[fileStart:fileEnd],
dataHash: contentHash,
compressd: data[compressedStart:compressedEnd],
compressdHash: compressedHash,
created: creation,
}
}
`
staticListTemplateDev = `import "io/ioutil"
import "bytes"
import "fmt"
import "compress/gzip"
import "encoding/base64"
import "time"
import "crypto/sha256"
// WARNING: THIS GENERATION IS FOR DEBUG / DEVELOPMENT ONLY, DO NOT
// USE IT IN PRODUCTION!
func staticFileGen(filePath string) staticData {
content, readErr := ioutil.ReadFile(filePath)
if readErr != nil {
panic(fmt.Sprintln("Cannot read file:", readErr))
}
compressed := bytes.NewBuffer(make([]byte, 0, 1024))
compresser, compresserBuildErr := gzip.NewWriterLevel(
compressed, gzip.BestCompression)
if compresserBuildErr != nil {
panic(fmt.Sprintln("Cannot build data compresser:", compresserBuildErr))
}
contentLen := len(content)
_, compressErr := compresser.Write(content)
if compressErr != nil {
panic(fmt.Sprintln("Cannot write compressed data:", compressErr))
}
compressErr = compresser.Flush()
if compressErr != nil {
panic(fmt.Sprintln("Cannot write compressed data:", compressErr))
}
content = append(content, compressed.Bytes()...)
getHash := func(b []byte) []byte {
h := sha256.New()
h.Write(b)
return h.Sum(nil)
}
return staticData{
data: content[0:contentLen],
dataHash: base64.StdEncoding.EncodeToString(
getHash(content[0:contentLen])[:8]),
compressd: content[contentLen:],
compressdHash: base64.StdEncoding.EncodeToString(
getHash(content[contentLen:])[:8]),
created: time.Now(),
}
}
var (
staticPages = map[string]staticData{
{{ range . }}"{{ .Name }}": staticFileGen(
"{{ .Path }}",
),
{{ end }}
}
)`
staticPageTemplate = `package {{ .GOPackage }}
// This file is part of Sshwifty Project
//
// Copyright (C) {{ .Date.Year }} Rui NI (nirui@gmx.com)
//
// https://github.com/niruix/sshwifty
//
// This file is generated at {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 MST" }}
// by "go generate", DO NOT EDIT! Also, do not open this file, it maybe too large
// for your editor. You've been warned.
//
// This file may contain third-party binaries. See DEPENDENCES for detail.
import (
"time"
"encoding/hex"
)
var raw{{ .GOVariableName }}Data = ` + "`" + `{{ .Data }}` + "`" + `
// {{ .GOVariableName }} returns static file
func {{ .GOVariableName }}() (
int, // FileStart
int, // FileEnd
int, // CompressedStart
int, // CompressedEnd
string, // ContentHash
string, // CompressedHash
time.Time, // Time of creation
[]byte, // Data
) {
created, createErr := time.Parse(
time.RFC1123, "{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 MST" }}")
if createErr != nil {
panic(createErr)
}
data, dataErr := hex.DecodeString(raw{{ .GOVariableName }}Data)
raw{{ .GOVariableName }}Data = ""
if dataErr != nil {
panic(dataErr)
}
return {{ .FileStart }}, {{ .FileEnd }},
{{ .CompressedStart }}, {{ .CompressedEnd }},
"{{ .ContentHash }}", "{{ .CompressedHash }}", created, data
}
`
)
const (
templateStarts = "//go:generate"
)
type parsedFile struct {
Name string
GOVariableName string
GOFileName string
GOPackage string
Path string
Data string
FileStart int
FileEnd int
CompressedStart int
CompressedEnd int
ContentHash string
CompressedHash string
Date time.Time
}
func getHash(b []byte) []byte {
h := sha256.New()
h.Write(b)
return h.Sum(nil)
}
func buildListFile(w io.Writer, data interface{}) error {
tpl := template.Must(template.New(
"StaticPageList").Parse(staticListTemplate))
return tpl.Execute(w, data)
}
func buildListFileDev(w io.Writer, data interface{}) error {
tpl := template.Must(template.New(
"StaticPageList").Parse(staticListTemplateDev))
return tpl.Execute(w, data)
}
func buildDataFile(w io.Writer, data interface{}) error {
tpl := template.Must(template.New(
"StaticPageData").Parse(staticPageTemplate))
return tpl.Execute(w, data)
}
func byteToArrayStr(b []byte) string {
return hex.EncodeToString(b)
}
func parseFile(
id int, name string, filePath string, packageName string) parsedFile {
content, readErr := ioutil.ReadFile(filePath)
if readErr != nil {
panic(fmt.Sprintln("Cannot read file:", readErr))
}
compressed := bytes.NewBuffer(make([]byte, 0, 1024))
compresser, compresserBuildErr := gzip.NewWriterLevel(
compressed, gzip.BestCompression)
if compresserBuildErr != nil {
panic(fmt.Sprintln("Cannot build data compresser:", compresserBuildErr))
}
contentLen := len(content)
_, compressErr := compresser.Write(content)
if compressErr != nil {
panic(fmt.Sprintln("Cannot write compressed data:", compressErr))
}
compressErr = compresser.Flush()
if compressErr != nil {
panic(fmt.Sprintln("Cannot write compressed data:", compressErr))
}
content = append(content, compressed.Bytes()...)
goFileName := "Static" + strconv.FormatInt(int64(id), 10)
return parsedFile{
Name: name,
GOVariableName: strings.Title(goFileName),
GOFileName: strings.ToLower(goFileName) + "_generated.go",
GOPackage: packageName,
Path: filePath,
Data: byteToArrayStr(content),
FileStart: 0,
FileEnd: contentLen,
CompressedStart: contentLen,
CompressedEnd: len(content),
ContentHash: base64.StdEncoding.EncodeToString(
getHash(content[0:contentLen])[:8]),
CompressedHash: base64.StdEncoding.EncodeToString(
getHash(content[contentLen:len(content)])[:8]),
Date: time.Now(),
}
}
func main() {
if len(os.Args) < 3 {
panic("Usage: <Source Folder> <(Destination) List File>")
}
sourcePath, sourcePathErr := filepath.Abs(os.Args[1])
if sourcePathErr != nil {
panic(fmt.Sprintf("Invalid source folder path %s: %s",
os.Args[1], sourcePathErr))
}
listFilePath, listFilePathErr := filepath.Abs(os.Args[2])
if listFilePathErr != nil {
panic(fmt.Sprintf("Invalid destination list file path %s: %s",
os.Args[2], listFilePathErr))
}
listFileName := filepath.Base(listFilePath)
destFolderPackage := strings.TrimSuffix(
listFileName, filepath.Ext(listFileName))
destFolderPath := filepath.Join(
filepath.Dir(listFilePath), destFolderPackage)
destFolderPathErr := os.RemoveAll(destFolderPath)
if destFolderPathErr != nil {
panic(fmt.Sprintf("Unable to remove data destination folder %s: %s",
destFolderPath, destFolderPathErr))
}
destFolderPathErr = os.Mkdir(destFolderPath, 0777)
if destFolderPathErr != nil {
panic(fmt.Sprintf("Unable to build data destination folder %s: %s",
destFolderPath, destFolderPathErr))
}
listFile, listFileErr := os.OpenFile(listFilePath, os.O_RDWR, 0666)
if listFileErr != nil {
panic(fmt.Sprintf("Unable to open destination list file %s: %s",
listFilePath, listFileErr))
}
defer listFile.Close()
files, dirOpenErr := ioutil.ReadDir(sourcePath)
if dirOpenErr != nil {
panic(fmt.Sprintf("Unable to open dir: %s", dirOpenErr))
}
scanner := bufio.NewScanner(listFile)
destBytesByPass := int64(0)
foundLastLine := false
for scanner.Scan() {
text := scanner.Text()
if strings.Index(text, templateStarts) < 0 {
if foundLastLine {
break
}
destBytesByPass += int64(len(text) + 1)
continue
}
destBytesByPass += int64(len(text) + 1)
foundLastLine = true
}
listFile.Seek(destBytesByPass, 0)
listFile.Truncate(destBytesByPass)
listFile.WriteString("\n// This file is generated by `go generate` at " +
time.Now().Format(time.RFC1123) + "\n// DO NOT EDIT!\n\n")
switch os.Getenv("NODE_ENV") {
case "development":
type sourceFiles struct {
Name string
Path string
}
var sources []sourceFiles
for f := range files {
if !files[f].Mode().IsRegular() {
continue
}
sources = append(sources, sourceFiles{
Name: files[f].Name(),
Path: filepath.Join(sourcePath, files[f].Name()),
})
}
tempBuildErr := buildListFileDev(listFile, sources)
if tempBuildErr != nil {
panic(fmt.Sprintf(
"Unable to build destination file due to error: %s",
tempBuildErr))
}
default:
var parsedFiles []parsedFile
for f := range files {
if !files[f].Mode().IsRegular() {
continue
}
currentFilePath := filepath.Join(sourcePath, files[f].Name())
parsedFiles = append(parsedFiles, parseFile(
f, files[f].Name(), currentFilePath, destFolderPackage))
}
for f := range parsedFiles {
fn := filepath.Join(destFolderPath, parsedFiles[f].GOFileName)
ff, ffErr := os.Create(fn)
if ffErr != nil {
panic(fmt.Sprintf("Unable to create static page file %s: %s",
fn, ffErr))
}
bErr := buildDataFile(ff, parsedFiles[f])
if bErr != nil {
panic(fmt.Sprintf("Unable to build static page file %s: %s",
fn, bErr))
}
}
listFile.WriteString(
"\nimport \"" + parentPackage + "/" + destFolderPackage + "\"\n\n")
tempBuildErr := buildListFile(listFile, parsedFiles)
if tempBuildErr != nil {
panic(fmt.Sprintf(
"Unable to build destination file due to error: %s",
tempBuildErr))
}
}
}
+128
View File
@@ -0,0 +1,128 @@
// 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 controller
//go:generate go run ./static_page_generater ../../.tmp/dist ./static_pages.go
//go:generate go fmt ./static_pages.go
// This file is generated by `go generate` at Wed, 07 Aug 2019 15:54:10 CST
// DO NOT EDIT!
import "github.com/niruix/sshwifty/application/controller/static_pages"
import "time"
var (
staticPages = map[string]staticData{
"13ec0eb5bdb821ff4930237d7c9f943f.woff2": parseStaticData(static_pages.Static0()),
"13efe6cbc10b97144a28310ebdeda594.woff": parseStaticData(static_pages.Static1()),
"1d6594826615607f6dc860bb49258acb.woff": parseStaticData(static_pages.Static2()),
"313a65630d341645c13e4f2a0364381d.woff": parseStaticData(static_pages.Static3()),
"35b07eb2f8711ae08d1f58c043880930.woff": parseStaticData(static_pages.Static4()),
"4357beb823a5f8d65c260f045d9e019a.woff2": parseStaticData(static_pages.Static5()),
"4fe0f73cc919ba2b7a3c36e4540d725c.woff": parseStaticData(static_pages.Static6()),
"50d75e48e0a3ddab1dd15d6bfb9d3700.woff": parseStaticData(static_pages.Static7()),
"59eb3601394dd87f30f82433fb39dd94.woff2": parseStaticData(static_pages.Static8()),
"5b4a33e176ff736a74f0ca2dd9e6b396.woff2": parseStaticData(static_pages.Static9()),
"6972f63e5242168cc5637e2771292ec0-android-chrome-144x144.png": parseStaticData(static_pages.Static10()),
"6972f63e5242168cc5637e2771292ec0-android-chrome-192x192.png": parseStaticData(static_pages.Static11()),
"6972f63e5242168cc5637e2771292ec0-android-chrome-256x256.png": parseStaticData(static_pages.Static12()),
"6972f63e5242168cc5637e2771292ec0-android-chrome-36x36.png": parseStaticData(static_pages.Static13()),
"6972f63e5242168cc5637e2771292ec0-android-chrome-384x384.png": parseStaticData(static_pages.Static14()),
"6972f63e5242168cc5637e2771292ec0-android-chrome-48x48.png": parseStaticData(static_pages.Static15()),
"6972f63e5242168cc5637e2771292ec0-android-chrome-512x512.png": parseStaticData(static_pages.Static16()),
"6972f63e5242168cc5637e2771292ec0-android-chrome-72x72.png": parseStaticData(static_pages.Static17()),
"6972f63e5242168cc5637e2771292ec0-android-chrome-96x96.png": parseStaticData(static_pages.Static18()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-114x114.png": parseStaticData(static_pages.Static19()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-120x120.png": parseStaticData(static_pages.Static20()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-144x144.png": parseStaticData(static_pages.Static21()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-152x152.png": parseStaticData(static_pages.Static22()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-167x167.png": parseStaticData(static_pages.Static23()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-180x180.png": parseStaticData(static_pages.Static24()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-57x57.png": parseStaticData(static_pages.Static25()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-60x60.png": parseStaticData(static_pages.Static26()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-72x72.png": parseStaticData(static_pages.Static27()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-76x76.png": parseStaticData(static_pages.Static28()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon-precomposed.png": parseStaticData(static_pages.Static29()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-icon.png": parseStaticData(static_pages.Static30()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-1182x2208.png": parseStaticData(static_pages.Static31()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-1242x2148.png": parseStaticData(static_pages.Static32()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-1496x2048.png": parseStaticData(static_pages.Static33()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-1536x2008.png": parseStaticData(static_pages.Static34()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-320x460.png": parseStaticData(static_pages.Static35()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-640x1096.png": parseStaticData(static_pages.Static36()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-640x920.png": parseStaticData(static_pages.Static37()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-748x1024.png": parseStaticData(static_pages.Static38()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-750x1294.png": parseStaticData(static_pages.Static39()),
"6972f63e5242168cc5637e2771292ec0-apple-touch-startup-image-768x1004.png": parseStaticData(static_pages.Static40()),
"6972f63e5242168cc5637e2771292ec0-favicon-16x16.png": parseStaticData(static_pages.Static41()),
"6972f63e5242168cc5637e2771292ec0-favicon-32x32.png": parseStaticData(static_pages.Static42()),
"6972f63e5242168cc5637e2771292ec0-favicon.ico": parseStaticData(static_pages.Static43()),
"6972f63e5242168cc5637e2771292ec0-firefox_app_128x128.png": parseStaticData(static_pages.Static44()),
"6972f63e5242168cc5637e2771292ec0-firefox_app_512x512.png": parseStaticData(static_pages.Static45()),
"6972f63e5242168cc5637e2771292ec0-firefox_app_60x60.png": parseStaticData(static_pages.Static46()),
"6972f63e5242168cc5637e2771292ec0-manifest.json": parseStaticData(static_pages.Static47()),
"6972f63e5242168cc5637e2771292ec0-manifest.webapp": parseStaticData(static_pages.Static48()),
"7237b0744491024097d3d.css": parseStaticData(static_pages.Static49()),
"7237b0744491024097d3d.css.map": parseStaticData(static_pages.Static50()),
"7237b0744491024097d3d.js": parseStaticData(static_pages.Static51()),
"73f0a88bbca1bec19fb1303c689d04c6.woff2": parseStaticData(static_pages.Static52()),
"83e114c316fcc3f23f524ec3e1c65984.woff": parseStaticData(static_pages.Static53()),
"8a96edbbcd9a6991d79371aed0b0288e.woff": parseStaticData(static_pages.Static54()),
"90d1676003d9c28c04994c18bfd8b558.woff2": parseStaticData(static_pages.Static55()),
"94008e69aaf05da75c0bbf8f8bb0db41.woff2": parseStaticData(static_pages.Static56()),
"DEPENDENCES.md": parseStaticData(static_pages.Static57()),
"LICENSE.md": parseStaticData(static_pages.Static58()),
"README.md": parseStaticData(static_pages.Static59()),
"ad538a69b0e8615ed0419c4529344ffc.woff2": parseStaticData(static_pages.Static60()),
"b52fac2bb93c5858f3f2675e4b52e1de.woff2": parseStaticData(static_pages.Static61()),
"c599374e350aa7225ee3e8a114e8d8d7.svg": parseStaticData(static_pages.Static62()),
"c73eb1ceba3321a80a0aff13ad373cb4.woff": parseStaticData(static_pages.Static63()),
"cc2fadc3928f2f223418887111947b40.woff": parseStaticData(static_pages.Static64()),
"d26871e8149b5759f814fd3c7a4f784b.woff2": parseStaticData(static_pages.Static65()),
"d3b47375afd904983d9be8d6e239a949.woff": parseStaticData(static_pages.Static66()),
"e8eaae902c3a4dacb9a5062667e10576.woff2": parseStaticData(static_pages.Static67()),
"ea4853ff509fe5f353d52586fcb94e20.svg": parseStaticData(static_pages.Static68()),
"f5902d5ef961717ed263902fc429e6ae.woff": parseStaticData(static_pages.Static69()),
"f75569f8a5fab0893fa712d8c0d9c3fe.woff2": parseStaticData(static_pages.Static70()),
"index.html": parseStaticData(static_pages.Static71()),
"manifest.json": parseStaticData(static_pages.Static72()),
"robots.txt": parseStaticData(static_pages.Static73()),
}
)
// parseStaticData parses result from a static file returner and generate
// a new `staticData` item
func parseStaticData(
fileStart int,
fileEnd int,
compressedStart int,
compressedEnd int,
contentHash string,
compressedHash string,
creation time.Time,
data []byte,
) staticData {
return staticData{
data: data[fileStart:fileEnd],
dataHash: contentHash,
compressd: data[compressedStart:compressedEnd],
compressdHash: compressedHash,
created: creation,
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
package static_pages
// This file is part of Sshwifty Project
//
// Copyright (C) 2019 Rui NI (nirui@gmx.com)
//
// https://github.com/niruix/sshwifty
//
// This file is generated at Wed, 07 Aug 2019 15:54:10 CST
// by "go generate", DO NOT EDIT! Also, do not open this file, it maybe too large
// for your editor. You've been warned.
//
// This file may contain third-party binaries. See DEPENDENCES for detail.
import (
"time"
"encoding/hex"
)
var rawStatic41Data = `89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff6100000002494441547801ec1a7ed2000002f5494441542dc14d689c551480e1f7def9ee4c7e6a930c8d9a1447512aad92fa035584d68516110a5310eaa62bbb510b8ae822e84610c1c69592aec42e8a0b372ed2080aa250248aa042a5d22ada1012a9689be94ce3cc7ce7dc7bbe6b167d1ec72d6b2f2dceda787f1e68032d5cf6bea870c1f0a1c21756b960eb3ed8e7b59dfd53b79f387d956d8e6dabafbf7f30bbea1cd03432d7b3a22e8137f046f6891032adc9827a23e383ddf085b5779df870c5fdf9c6a959071781e6956ac84fb1cb3045524a98252c19658cf4ca9280d1ded7e4d0fd13b8609d5a11e70a07f340f30f1bb0a21d528c9825524a5832cc127d514484ada87cf45d0ff3333cbd7faa49c1bc07da89cc0f650751a5b2c49b47eee6f89333dc360a4311066589882022880867ceaf925cc4d5aded81d6bf55c94d2d115506226c5e13e6a677f0d6b1bd3c776837356f880a228aa870bd3be0f2df5d6ac15ade15952f9d22aa94220c4ae1dd2f2ff1f1d757e85d130e3f30c3e22b4ff0ece3bb1115440451a1371ce2ebe6bd0f46e512a28a444554198af0cde5bf78f5ecf77cf2d5eff821bc7c648e50cb880a224276091f8cc2070349882a3146628aa4188931d17030966a84be27fb4c590a228aaa927dc207a3f0c1c83ea1aa688cc418b194786afa4e8e3fb88789a9069bff0cf9e0b31fd9ec6d1135a251c92ee18251b8ba55a38dec45158d919c12efdd7580fb9a13c48171f6b78b9cf9e502ddfe008d4a8c11556572d2e3eb56153ed8fabdd38d7b8233fe538154b18b11beddd86071ed67d66e76508d685454231a951da370e0e19df8a25af7ae6ecb8d91ccb147ef4054d92a071cbdb0c4fca5f3ac763729451011440451415579e7b57d8c8d032e2ffb50ab167c489dc3fb9b9c7ca645a356514a8988202a8828a28288323e9239fdf643bcf07c0b1c1d7c5e706cbbf1e9c983ae96cfb95035638efcbad1a13b2cc92e814b646f4c4d7a1e7b6492b171c7b60e70d4ed5d5a71dcd25b7a71d61736ef83b55d51b57c30ef82e143852b0c1faacad56c1ddc32355d707bbeb8cab6ff01f23ad711d68e17be0000000049454e44ae4260821f8b08000000000002ff003c03c3fc89504e470d0a1a0a0000000d49484452000000100000001008060000001ff3ff6100000002494441547801ec1a7ed2000002f5494441542dc14d689c551480e1f7def9ee4c7e6a930c8d9a1447512aad92fa035584d68516110a5310eaa62bbb510b8ae822e84610c1c69592aec42e8a0b372ed2080aa250248aa042a5d22ada1012a9689be94ce3cc7ce7dc7bbe6b167d1ec72d6b2f2dceda787f1e68032d5cf6bea870c1f0a1c21756b960eb3ed8e7b59dfd53b79f387d956d8e6dabafbf7f30bbea1cd03432d7b3a22e8137f046f6891032adc9827a23e383ddf085b5779df870c5fdf9c6a959071781e6956ac84fb1cb3045524a98252c19658cf4ca9280d1ded7e4d0fd13b8609d5a11e70a07f340f30f1bb0a21d528c9825524a5832cc127d514484ada87cf45d0ff3333cbd7faa49c1bc07da89cc0f650751a5b2c49b47eee6f89333dc360a4311066589882022880867ceaf925cc4d5aded81d6bf55c94d2d115506226c5e13e6a677f0d6b1bd3c776837356f880a228aa870bd3be0f2df5d6ac15ade15952f9d22aa94220c4ae1dd2f2ff1f1d757e85d130e3f30c3e22b4ff0ece3bb1115440451a1371ce2ebe6bd0f46e512a28a444554198af0cde5bf78f5ecf77cf2d5eff821bc7c648e50cb880a224276091f8cc2070349882a3146628aa4188931d17030966a84be27fb4c590a228aaa927dc207a3f0c1c83ea1aa688cc418b194786afa4e8e3fb88789a9069bff0cf9e0b31fd9ec6d1135a251c92ee18251b8ba55a38dec45158d919c12efdd7580fb9a13c48171f6b78b9cf9e502ddfe008d4a8c11556572d2e3eb56153ed8fabdd38d7b8233fe538154b18b11beddd86071ed67d66e76508d685454231a951da370e0e19df8a25af7ae6ecb8d91ccb147ef4054d92a071cbdb0c4fca5f3ac763729451011440451415579e7b57d8c8d032e2ffb50ab167c489dc3fb9b9c7ca645a356514a8988202a8828a28288323e9239fdf643bcf07c0b1c1d7c5e706cbbf1e9c983ae96cfb95035638efcbad1a13b2cc92e814b646f4c4d7a1e7b6492b171c7b60e70d4ed5d5a71dcd25b7a71d61736ef83b55d51b57c30ef82e143852b0c1faacad56c1ddc32355d707bbeb8cab6ff01f23ad711d68e17be0000000049454e44ae426082000000ffff`
// Static41 returns static file
func Static41() (
int, // FileStart
int, // FileEnd
int, // CompressedStart
int, // CompressedEnd
string, // ContentHash
string, // CompressedHash
time.Time, // Time of creation
[]byte, // Data
) {
created, createErr := time.Parse(
time.RFC1123, "Wed, 07 Aug 2019 15:54:10 CST")
if createErr != nil {
panic(createErr)
}
data, dataErr := hex.DecodeString(rawStatic41Data)
rawStatic41Data = ""
if dataErr != nil {
panic(dataErr)
}
return 0, 828,
828, 1676,
"DPkhEZBiqsE=", "H8/B3536RVw=", created, data
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
package static_pages
// This file is part of Sshwifty Project
//
// Copyright (C) 2019 Rui NI (nirui@gmx.com)
//
// https://github.com/niruix/sshwifty
//
// This file is generated at Wed, 07 Aug 2019 15:54:10 CST
// by "go generate", DO NOT EDIT! Also, do not open this file, it maybe too large
// for your editor. You've been warned.
//
// This file may contain third-party binaries. See DEPENDENCES for detail.
import (
"time"
"encoding/hex"
)
var rawStatic47Data = `7b0a2020226e616d65223a202253737769667479222c0a20202273686f72745f6e616d65223a202253737769667479222c0a2020226465736372697074696f6e223a206e756c6c2c0a202022646972223a20226175746f222c0a2020226c616e67223a2022656e2d5553222c0a202022646973706c6179223a20227374616e64616c6f6e65222c0a2020226f7269656e746174696f6e223a2022616e79222c0a20202273746172745f75726c223a20222f3f686f6d6573637265656e3d31222c0a2020226261636b67726f756e645f636f6c6f72223a202223666666222c0a20202269636f6e73223a205b0a202020207b0a20202020202022737263223a2022616e64726f69642d6368726f6d652d33367833362e706e67222c0a2020202020202273697a6573223a20223336783336222c0a2020202020202274797065223a2022696d6167652f706e67220a202020207d2c0a202020207b0a20202020202022737263223a2022616e64726f69642d6368726f6d652d34387834382e706e67222c0a2020202020202273697a6573223a20223438783438222c0a2020202020202274797065223a2022696d6167652f706e67220a202020207d2c0a202020207b0a20202020202022737263223a2022616e64726f69642d6368726f6d652d37327837322e706e67222c0a2020202020202273697a6573223a20223732783732222c0a2020202020202274797065223a2022696d6167652f706e67220a202020207d2c0a202020207b0a20202020202022737263223a2022616e64726f69642d6368726f6d652d39367839362e706e67222c0a2020202020202273697a6573223a20223936783936222c0a2020202020202274797065223a2022696d6167652f706e67220a202020207d2c0a202020207b0a20202020202022737263223a2022616e64726f69642d6368726f6d652d313434783134342e706e67222c0a2020202020202273697a6573223a202231343478313434222c0a2020202020202274797065223a2022696d6167652f706e67220a202020207d2c0a202020207b0a20202020202022737263223a2022616e64726f69642d6368726f6d652d313932783139322e706e67222c0a2020202020202273697a6573223a202231393278313932222c0a2020202020202274797065223a2022696d6167652f706e67220a202020207d2c0a202020207b0a20202020202022737263223a2022616e64726f69642d6368726f6d652d323536783235362e706e67222c0a2020202020202273697a6573223a202232353678323536222c0a2020202020202274797065223a2022696d6167652f706e67220a202020207d2c0a202020207b0a20202020202022737263223a2022616e64726f69642d6368726f6d652d333834783338342e706e67222c0a2020202020202273697a6573223a202233383478333834222c0a2020202020202274797065223a2022696d6167652f706e67220a202020207d2c0a202020207b0a20202020202022737263223a2022616e64726f69642d6368726f6d652d353132783531322e706e67222c0a2020202020202273697a6573223a202235313278353132222c0a2020202020202274797065223a2022696d6167652f706e67220a202020207d0a20205d0a7d1f8b08000000000002ffac90cd6eab301085f73c85e5bb4d6e04180291aa3e44d45555452e1862d58c9131aa699477affc537553ab5d78e3c5f966fc8dce2d4308039d183e217c5edef9a037bcb3e172954a5f7e443d5b3ac567cd25e0138255081f736547e9aaa59f1314469b30d83f9dc32a5f6641379b2e9a424f8504e691549c81a6e1574ce1eb104d95beac4ad8f4f0789593d533060fb91f78a5dddba8e40afda59342ba23fe0dc3e029ef242cf8849e338410bab9d7feaa3a6fe995e4fdbebb2a39b17d599bb2fe3fc3e876fd20ff60761f3bf69deb6d76cdf0898eec60571cb9effee2218d214dc4e35822cfb130c722e2712c91a7ad4d1bebcdb1449e9c1093131231059acad516266f63ed059ac85554b529aa5883812672950d316513eb30d044ae2a2f4c95c73a0cf4775786d04b76ff040000ffff`
// Static47 returns static file
func Static47() (
int, // FileStart
int, // FileEnd
int, // CompressedStart
int, // CompressedEnd
string, // ContentHash
string, // CompressedHash
time.Time, // Time of creation
[]byte, // Data
) {
created, createErr := time.Parse(
time.RFC1123, "Wed, 07 Aug 2019 15:54:10 CST")
if createErr != nil {
panic(createErr)
}
data, dataErr := hex.DecodeString(rawStatic47Data)
rawStatic47Data = ""
if dataErr != nil {
panic(dataErr)
}
return 0, 1196,
1196, 1511,
"WdtgnBG8Sks=", "EKpkCbFqOGQ=", created, data
}
@@ -0,0 +1,51 @@
package static_pages
// This file is part of Sshwifty Project
//
// Copyright (C) 2019 Rui NI (nirui@gmx.com)
//
// https://github.com/niruix/sshwifty
//
// This file is generated at Wed, 07 Aug 2019 15:54:10 CST
// by "go generate", DO NOT EDIT! Also, do not open this file, it maybe too large
// for your editor. You've been warned.
//
// This file may contain third-party binaries. See DEPENDENCES for detail.
import (
"time"
"encoding/hex"
)
var rawStatic48Data = `7b0a20202276657273696f6e223a2022312e30222c0a2020226e616d65223a202253737769667479222c0a2020226465736372697074696f6e223a206e756c6c2c0a20202269636f6e73223a207b0a20202020223630223a202266697265666f785f6170705f36307836302e706e67222c0a2020202022313238223a202266697265666f785f6170705f313238783132382e706e67222c0a2020202022353132223a202266697265666f785f6170705f353132783531322e706e67220a20207d2c0a202022646576656c6f706572223a207b0a20202020226e616d65223a206e756c6c2c0a202020202275726c223a206e756c6c0a20207d0a7d1f8b08000000000002ff5c8e410a83301444f79e62f8eb22f90145bc460f20626309a44948d4a614ef5ea29616b78f37c37b17002d2a44ed2cb5202e055d32b3fd4365708d4f3d4eaf1dde541c82f6d32edbd9980debc1d9482df21940b5c8c3510735bad4f5de77b548b528bdbd6f3700b16cce0ecb26b16cfead8ae5d9aa58a68ae56615c07a642dca38afc2afe1c8ff260234077380bc2bd60f000000ffff`
// Static48 returns static file
func Static48() (
int, // FileStart
int, // FileEnd
int, // CompressedStart
int, // CompressedEnd
string, // ContentHash
string, // CompressedHash
time.Time, // Time of creation
[]byte, // Data
) {
created, createErr := time.Parse(
time.RFC1123, "Wed, 07 Aug 2019 15:54:10 CST")
if createErr != nil {
panic(createErr)
}
data, dataErr := hex.DecodeString(rawStatic48Data)
rawStatic48Data = ""
if dataErr != nil {
panic(dataErr)
}
return 0, 250,
250, 410,
"1dt/vqnONiQ=", "S7HxwGVBXUY=", created, data
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
package static_pages
// This file is part of Sshwifty Project
//
// Copyright (C) 2019 Rui NI (nirui@gmx.com)
//
// https://github.com/niruix/sshwifty
//
// This file is generated at Wed, 07 Aug 2019 15:54:10 CST
// by "go generate", DO NOT EDIT! Also, do not open this file, it maybe too large
// for your editor. You've been warned.
//
// This file may contain third-party binaries. See DEPENDENCES for detail.
import (
"time"
"encoding/hex"
)
var rawStatic50Data = `7b2276657273696f6e223a332c22736f7572636573223a5b5d2c226e616d6573223a5b5d2c226d617070696e6773223a22222c2266696c65223a223732333762303734343439313032343039376433642e637373222c22736f75726365526f6f74223a22227d1f8b08000000000002ffaa562a4b2d2acecccf53b232d6512ace2f2d4a4e2d56b28a8ed551ca4bcc853173130b0a32f3d28b95ac94947494d232735295ac94cc8d8ccd930ccc4d4c4c2c0d0d8c4c0c2ccd538c53f4928b8b95600605e5e79780b4d402000000ffff`
// Static50 returns static file
func Static50() (
int, // FileStart
int, // FileEnd
int, // CompressedStart
int, // CompressedEnd
string, // ContentHash
string, // CompressedHash
time.Time, // Time of creation
[]byte, // Data
) {
created, createErr := time.Parse(
time.RFC1123, "Wed, 07 Aug 2019 15:54:10 CST")
if createErr != nil {
panic(createErr)
}
data, dataErr := hex.DecodeString(rawStatic50Data)
rawStatic50Data = ""
if dataErr != nil {
panic(dataErr)
}
return 0, 102,
102, 206,
"Pax14oQxroQ=", "aCysfJVxov4=", created, data
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
package static_pages
// This file is part of Sshwifty Project
//
// Copyright (C) 2019 Rui NI (nirui@gmx.com)
//
// https://github.com/niruix/sshwifty
//
// This file is generated at Wed, 07 Aug 2019 15:54:10 CST
// by "go generate", DO NOT EDIT! Also, do not open this file, it maybe too large
// for your editor. You've been warned.
//
// This file may contain third-party binaries. See DEPENDENCES for detail.
import (
"time"
"encoding/hex"
)
var rawStatic57Data = `2320446570656e64656e636573204f662053736877696674790a0a53736877696674792075736573206d616e792074686972642d706172747920636f6d706f6e656e74732e2054686f736520636f6d706f6e656e747320697320726571756972656420696e6f726465720a666f7220537368776966747920746f2066756e6374696f6e2e0a0a41206c697374206f66207573656420636f6d706f6e656e74732063616e20626520666f756e6420696e7369646520607061636b6167652e6a736f6e6020616e642060676f2e6d6f64602066696c652e0a0a4d616a6f7220646570656e64656e63657320696e636c756465733a0a0a232320466f722066726f6e742d656e64206170706c69636174696f6e0a0a2d205b5675655d2868747470733a2f2f7675656a732e6f7267292c204c6963656e73656420756e646572204d4954206c6963656e73650a2d205b426162656c5d2868747470733a2f2f626162656c6a732e696f2f292c204c6963656e73656420756e646572204d4954206c6963656e73650a2d205b585465726d2e6a735d2868747470733a2f2f787465726d6a732e6f72672f292c204c6963656e73656420756e646572204d4954206c6963656e73650a2d205b6e6f726d616c697a652e6373735d2868747470733a2f2f6769746875622e636f6d2f6e65636f6c61732f6e6f726d616c697a652e637373292c204c6963656e73656420756e646572204d4954206c6963656e73650a2d205b526f626f746f20666f6e745d2868747470733a2f2f656e2e77696b6970656469612e6f72672f77696b692f526f626f746f292c204c6963656e73656420756e64657220417061636865206c6963656e73652e0a20205061636b61676564206279205b43687269737469616e20486f66666d6569737465725d2868747470733a2f2f6769746875622e636f6d2f63686f66666d6569737465722f726f626f746f2d666f6e74666163652d626f776572292c204c6963656e73656420756e6465722041706163686520322e300a0a232320466f72206261636b2d656e64206170706c69636174696f6e0a0a2d20606769746875622e636f6d2f676f72696c6c612f776562736f636b6574602c204c6963656e73656420756e646572204253442d322d4361757365206c6963656e73650a2d2060676f6c616e672e6f72672f782f6e65742f70726f787960205b56696577206c6963656e73655d2868747470733a2f2f6769746875622e636f6d2f676f6c616e672f6e65742f626c6f622f6d61737465722f4c4943454e5345290a2d2060676f6c616e672e6f72672f782f63727970746f602c205b56696577206c6963656e73655d2868747470733a2f2f6769746875622e636f6d2f676f6c616e672f63727970746f2f626c6f622f6d61737465722f4c4943454e5345291f8b08000000000002ff8c914f6bdc3c1087effa1403b9bc81587ec931b7fc2b0d246d699650580a96a5b13dbbf28c2bc9dd753f7df19acdbaec96ed4d12f33cbf19cd053c6087ec902d46f85cc16b6c3654a541a9fd09fa88115ac303a48682cb3a13d20056da4e1839450d8b4622ce5e802204fcd1534007c4121c06554978b74312a87ab68984b552b7e02926906acc727391350c2542253d8fa6480ea1e88c5d9b1af52a0a1760d841518b6ec515509147add48b594900371b8dd8fade61bc51eae2023e48802a08a70cd981e93a4fd68ccd2895c1f2adc7efff352975f126cf7ff6b88a5a427d7905cf6491c70e7b7618e0e569017e7a1ab13b53a23f80e5785d454d929f45bf2d30b47a150ff4366168a7e0f3384b688da75fa86d9c396a4a4d5f6a2b6dce68c59b98ff5179d6fb554a1937259c0e5664bda13575e8c8ecda1b6ff9547a6cbced8c6d702fd50ae0cbb43d07e500cbfb26504c64183e4a55b54831613839816d0e0579d8a565636395b19895b2c1f0d7f06bfdfffbd64b63d7a7965ecca26a09e4bdc9375846b16b4cc591f9eef521bbceee4d1f71f661452dde70bdfb956dce98f22ec8762860f946b8d9179e1c6f22774ce9a5cc5bb31bf4f9e9fef1d3ebe3e591dc86a14b525cfdbb7a224eda7f030000ffff`
// Static57 returns static file
func Static57() (
int, // FileStart
int, // FileEnd
int, // CompressedStart
int, // CompressedEnd
string, // ContentHash
string, // CompressedHash
time.Time, // Time of creation
[]byte, // Data
) {
created, createErr := time.Parse(
time.RFC1123, "Wed, 07 Aug 2019 15:54:10 CST")
if createErr != nil {
panic(createErr)
}
data, dataErr := hex.DecodeString(rawStatic57Data)
rawStatic57Data = ""
if dataErr != nil {
panic(dataErr)
}
return 0, 1030,
1030, 1507,
"ZkFdpk2P7P0=", "tqmXI4zYPC8=", created, data
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More