release: v0.2.0

add: zmodem(lrzsz) support for terminal
add: update notification (won't auto update)
optimize: protocol of terminal and desktop
optimize: experience of explorer
optimize: github workflow
remove: CryptoJS
This commit is contained in:
XZB-1248
2022-11-01 08:51:47 +08:00
parent 451bff43d6
commit 6920f726d7
49 changed files with 7771 additions and 1069 deletions

View File

@@ -91,6 +91,11 @@ jobs:
- name: Build and pack static resources
run: |
export COMMIT=`git rev-parse HEAD`
export VERSION=`git describe --tags | sed 's/^v//'`
sed -i "s/\$COMMIT/$COMMIT/g" ./web/src/config/version.json
sed -i "s/\$VERSION/$VERSION/g" ./web/src/config/version.json
cd ./web
npm install
npm run build-prod
@@ -115,20 +120,20 @@ jobs:
- name: Prepare release note
run: |
wget -q https://1248.ink/Tools/release.js
node release.js ${{ github.ref_name }}
export REF_NAME=`git describe --tags`
node ./scripts/release.js $REF_NAME
- name: Pack releases
run: |
mv ./built ./releases/built
cd ./releases
sudo apt install zip tar -y
tar -zcf server_darwin_arm64.tar.gz server_darwin_arm64 ./built
tar -zcf server_darwin_amd64.tar.gz server_darwin_amd64 ./built
tar -zcf server_linux_arm.tar.gz server_linux_arm ./built
tar -zcf server_linux_i386.tar.gz server_linux_i386 ./built
tar -zcf server_linux_arm64.tar.gz server_linux_arm64 ./built
tar -zcf server_linux_amd64.tar.gz server_linux_amd64 ./built
sudo apt install zip tar pigz -y
tar -cpf server_darwin_arm64.tar.gz server_darwin_arm64 ./built
tar -cpf server_darwin_amd64.tar.gz server_darwin_amd64 ./built
tar -cpf server_linux_arm.tar.gz server_linux_arm ./built
tar -cpf server_linux_i386.tar.gz server_linux_i386 ./built
tar -cpf server_linux_arm64.tar.gz server_linux_arm64 ./built
tar -cpf server_linux_amd64.tar.gz server_linux_amd64 ./built
zip -r -9 -q server_windows_arm.zip server_windows_arm.exe ./built
zip -r -9 -q server_windows_i386.zip server_windows_i386.exe ./built
zip -r -9 -q server_windows_arm64.zip server_windows_arm64.exe ./built
@@ -158,3 +163,9 @@ jobs:
name: |
darwin_arm64
darwin_amd64
- name: Update version info
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
curl -X POST -H "Authorization: $RELEASE_TOKEN" --retry 10 -m 60 -o /dev/null https://1248.ink/spark/release > /dev/null 2>&1

View File

@@ -1,3 +1,19 @@
## v0.2.0
* Add: zmodem(lrzsz) support for terminal.
* Add: update notification (won't auto update).
* Optimize: protocol of terminal and desktop.
* Optimize: experience of explorer.
* Remove: CryptoJS.
* 新增终端支持zmodem协议lrzsz
* 新增:版本更新通知(不会自动更新)。
* 优化:终端和桌面端的通信协议。
* 优化:文件管理器的使用体验。
* 移除CryptoJS。
## v0.1.9
* Add: special keys for terminal.

53
LICENSE
View File

@@ -23,56 +23,3 @@ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------
utils/melody are copied and modified from olahol/melody.
Copyright (c) 2015 Ola Holmström. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------
utils/cmap are copied and modified from orcaman/concurrent-map.
The MIT License (MIT)
Copyright (c) 2014 streamrail
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -4,6 +4,7 @@ import (
"Spark/client/config"
"Spark/modules"
"Spark/utils"
"encoding/binary"
"encoding/hex"
"errors"
ws "github.com/gorilla/websocket"
@@ -73,6 +74,25 @@ func (wsConn *Conn) SendPack(pack any) error {
return wsConn.WriteMessage(ws.BinaryMessage, data)
}
func (wsConn *Conn) SendRawData(event, data []byte, service byte, op byte) error {
Mutex.Lock()
defer Mutex.Unlock()
if WSConn == nil {
return errors.New(`${i18n|COMMON.DISCONNECTED}`)
}
buffer := make([]byte, 24)
copy(buffer[6:22], event)
copy(buffer[:4], []byte{34, 22, 19, 17})
buffer[4] = service
buffer[5] = op
binary.BigEndian.PutUint16(buffer[22:24], uint16(len(data)))
buffer = append(buffer, data...)
wsConn.SetWriteDeadline(utils.Now.Add(5 * time.Second))
defer wsConn.SetWriteDeadline(time.Time{})
return wsConn.WriteMessage(ws.BinaryMessage, buffer)
}
func (wsConn *Conn) SendCallback(pack, prev modules.Packet) error {
if len(prev.Event) > 0 {
pack.Event = prev.Event

View File

@@ -161,6 +161,18 @@ func handleWS(wsConn *common.Conn) error {
golog.Error(err)
return nil
}
if service, op, isBinary := utils.CheckBinaryPack(data); isBinary && len(data) > 24 {
event := hex.EncodeToString(data[6:22])
switch service {
case 20:
case 21:
switch op {
case 0:
inputRawTerminal(data[24:], event)
}
}
continue
}
data, err = utils.Decrypt(data, wsConn.GetSecret())
if err != nil {
golog.Error(err)

View File

@@ -179,16 +179,16 @@ func GetDevice() (*modules.Device, error) {
}
localIP, err := GetLocalIP()
if err != nil {
localIP = `unknown`
localIP = `<unknown>`
}
macAddr, err := GetMacAddress()
if err != nil {
macAddr = `unknown`
macAddr = `<unknown>`
}
cpuInfo, err := GetCPUInfo()
if err != nil {
cpuInfo = modules.CPU{
Model: `unknown`,
Model: `<unknown>`,
Usage: 0,
}
}
@@ -221,11 +221,11 @@ func GetDevice() (*modules.Device, error) {
}
hostname, err := os.Hostname()
if err != nil {
hostname = `unknown`
hostname = `<unknown>`
}
username, err := user.Current()
if err != nil {
username = &user.User{Username: `unknown`}
username = &user.User{Username: `<unknown>`}
} else {
slashIndex := strings.Index(username.Username, `\`)
if slashIndex > -1 && slashIndex+1 < len(username.Username) {
@@ -252,7 +252,7 @@ func GetPartialInfo() (*modules.Device, error) {
cpuInfo, err := GetCPUInfo()
if err != nil {
cpuInfo = modules.CPU{
Model: `unknown`,
Model: `<unknown>`,
Usage: 0,
}
}

View File

@@ -370,3 +370,7 @@ func execCommand(pack modules.Packet, wsConn *common.Conn) {
proc.Process.Release()
}
}
func inputRawTerminal(pack []byte, event string) {
terminal.InputRawTerminal(pack, event)
}

View File

@@ -33,20 +33,22 @@ type message struct {
frame *[]*[]byte
}
// +---------+---------+----------+----------+------------+---------+---------+---------+---------+-------+
// | magic | OP code | event id | img type | img length | x | y | width | height | image |
// +---------+---------+----------+----------+------------+---------+---------+---------+---------+-------+
// packet explanation:
// +---------+---------+----------+-------------+----------+---------+---------+---------+---------+-------+
// | magic | op code | event id | body length | img type | x | y | width | height | image |
// +---------+---------+----------+-------------+----------+---------+---------+---------+---------+-------+
// | 5 bytes | 1 byte | 16 bytes | 2 bytes | 2 bytes | 2 bytes | 2 bytes | 2 bytes | 2 bytes | - |
// +---------+---------+----------+----------+------------+---------+---------+---------+---------+-------+
// +---------+---------+----------+-------------+----------+---------+---------+---------+---------+-------+
// magic:
// []byte{34, 22, 19, 17, 20}
// OP code:
// 00: first part of a frame
// 01: rest parts of a frame
// 02: set resolution of every frame
// 03: JSON string (only for server)
// op code:
// 00: first part of a frame, device -> browser
// 01: rest parts of a frame, device -> browser
// 02: set resolution of every frame, device -> browser
// 03: JSON string, server -> browser
// img type:
// 0: raw image
@@ -167,12 +169,12 @@ func imageCompare(img, prev *image.RGBA, compress bool) []*[]byte {
for _, rect := range diff {
block := getImageBlock(img, rect, compress)
buf := make([]byte, 12)
binary.BigEndian.PutUint16(buf[0:2], uint16(len(block)+10))
if compress {
binary.BigEndian.PutUint16(buf[0:2], uint16(1))
binary.BigEndian.PutUint16(buf[2:4], uint16(1))
} else {
binary.BigEndian.PutUint16(buf[0:2], uint16(0))
binary.BigEndian.PutUint16(buf[2:4], uint16(0))
}
binary.BigEndian.PutUint16(buf[2:4], uint16(len(block)))
binary.BigEndian.PutUint16(buf[4:6], uint16(rect.Min.X))
binary.BigEndian.PutUint16(buf[6:8], uint16(rect.Min.Y))
binary.BigEndian.PutUint16(buf[8:10], uint16(rect.Size().X))
@@ -197,12 +199,12 @@ func splitFullImage(img *image.RGBA, compress bool) []*[]byte {
width := utils.If(x+blockSize > imgWidth, imgWidth-x, blockSize)
block := getImageBlock(img, image.Rect(x, y, x+width, y+height), compress)
buf := make([]byte, 12)
binary.BigEndian.PutUint16(buf[0:2], uint16(len(block)+10))
if compress {
binary.BigEndian.PutUint16(buf[0:2], uint16(1))
binary.BigEndian.PutUint16(buf[2:4], uint16(1))
} else {
binary.BigEndian.PutUint16(buf[0:2], uint16(0))
binary.BigEndian.PutUint16(buf[2:4], uint16(0))
}
binary.BigEndian.PutUint16(buf[2:4], uint16(len(block)))
binary.BigEndian.PutUint16(buf[4:6], uint16(x))
binary.BigEndian.PutUint16(buf[6:8], uint16(y))
binary.BigEndian.PutUint16(buf[8:10], uint16(width))
@@ -318,7 +320,9 @@ func InitDesktop(pack modules.Packet) error {
if screenshot.NumActiveDisplays() == 0 {
if displayBounds.Dx() == 0 || displayBounds.Dy() == 0 {
close(desktop.channel)
common.WSConn.SendCallback(modules.Packet{Act: `DESKTOP_QUIT`, Msg: `${i18n|DESKTOP.NO_DISPLAY_FOUND}`}, pack)
data, _ := utils.JSON.Marshal(modules.Packet{Act: `DESKTOP_QUIT`, Msg: `${i18n|DESKTOP.NO_DISPLAY_FOUND}`})
data = utils.XOR(data, common.WSConn.GetSecret())
common.WSConn.SendRawData(desktop.rawEvent, data, 20, 03)
return errors.New(`${i18n|DESKTOP.NO_DISPLAY_FOUND}`)
}
}
@@ -368,11 +372,13 @@ func KillDesktop(pack modules.Packet) {
desktop = val.(*session)
}
sessions.Remove(uuid)
data, _ := utils.JSON.Marshal(modules.Packet{Act: `DESKTOP_QUIT`, Msg: `${i18n|DESKTOP.SESSION_CLOSED}`})
data = utils.XOR(data, common.WSConn.GetSecret())
common.WSConn.SendRawData(desktop.rawEvent, data, 20, 03)
desktop.lock.Lock()
desktop.escape = true
desktop.rawEvent = nil
desktop.lock.Unlock()
common.WSConn.SendCallback(modules.Packet{Act: `DESKTOP_QUIT`, Msg: `${i18n|DESKTOP.SESSION_CLOSED}`}, pack)
}
func GetDesktop(pack modules.Packet) {
@@ -404,7 +410,9 @@ func handleDesktop(pack modules.Packet, uuid string, desktop *session) {
case msg, ok := <-desktop.channel:
// send error info
if msg.t == 1 || !ok {
common.WSConn.SendCallback(modules.Packet{Act: `DESKTOP_QUIT`, Msg: msg.info}, pack)
data, _ := utils.JSON.Marshal(modules.Packet{Act: `DESKTOP_QUIT`, Msg: msg.info})
data = utils.XOR(data, common.WSConn.GetSecret())
common.WSConn.SendRawData(desktop.rawEvent, data, 20, 03)
desktop.escape = true
sessions.Remove(uuid)
break
@@ -428,9 +436,10 @@ func handleDesktop(pack modules.Packet, uuid string, desktop *session) {
// set resolution
if msg.t == 2 {
buf := append([]byte{34, 22, 19, 17, 20, 02}, desktop.rawEvent...)
data := make([]byte, 4)
binary.BigEndian.PutUint16(data[:2], uint16(displayBounds.Dx()))
binary.BigEndian.PutUint16(data[2:], uint16(displayBounds.Dy()))
data := make([]byte, 6)
binary.BigEndian.PutUint16(data[:2], 4)
binary.BigEndian.PutUint16(data[2:4], uint16(displayBounds.Dx()))
binary.BigEndian.PutUint16(data[4:6], uint16(displayBounds.Dy()))
buf = append(buf, data...)
common.WSConn.SendData(buf)
break

View File

@@ -11,3 +11,18 @@ var (
errDataInvalid = errors.New(`can not parse data in packet`)
errUUIDNotFound = errors.New(`can not find terminal identifier`)
)
// packet explanation:
// +---------+---------+----------+-------------+------+
// | magic | op code | event id | data length | data |
// +---------+---------+----------+-------------+------+
// | 5 bytes | 1 byte | 16 bytes | 2 bytes | - |
// +---------+---------+----------+-------------+------+
// magic:
// []byte{34, 22, 19, 17, 21}
// op code:
// 00: binary packet
// 01: JSON packet

View File

@@ -17,6 +17,7 @@ import (
type terminal struct {
escape bool
lastPack int64
rawEvent []byte
event string
pty *os.File
cmd *exec.Cmd
@@ -37,29 +38,47 @@ func InitTerminal(pack modules.Packet) error {
defaultShell = getTerminal(true)
return err
}
termSession := &terminal{
rawEvent, _ := hex.DecodeString(pack.Event)
session := &terminal{
cmd: cmd,
pty: ptySession,
event: pack.Event,
lastPack: utils.Unix,
rawEvent: rawEvent,
escape: false,
}
terminals.Set(pack.Data[`terminal`].(string), termSession)
terminals.Set(pack.Data[`terminal`].(string), session)
go func() {
for !termSession.escape {
buffer := make([]byte, 512)
bufSize := 1024
for !session.escape {
buffer := make([]byte, bufSize)
n, err := ptySession.Read(buffer)
buffer = buffer[:n]
common.WSConn.SendCallback(modules.Packet{Act: `TERMINAL_OUTPUT`, Data: map[string]any{
`output`: hex.EncodeToString(buffer),
}}, pack)
termSession.lastPack = utils.Unix
if err != nil {
if !termSession.escape {
termSession.escape = true
doKillTerminal(termSession)
// if output is larger than 1KB, then send binary data
if n > 1024 {
if bufSize < 32768 {
bufSize *= 2
}
common.WSConn.SendCallback(modules.Packet{Act: `TERMINAL_QUIT`}, pack)
common.WSConn.SendRawData(session.rawEvent, buffer, 21, 00)
} else {
bufSize = 1024
buffer, _ = utils.JSON.Marshal(modules.Packet{Act: `TERMINAL_OUTPUT`, Data: map[string]any{
`output`: hex.EncodeToString(buffer),
}})
buffer = utils.XOR(buffer, common.WSConn.GetSecret())
common.WSConn.SendRawData(session.rawEvent, buffer, 21, 01)
}
session.lastPack = utils.Unix
if err != nil {
if !session.escape {
session.escape = true
doKillTerminal(session)
}
data, _ := utils.JSON.Marshal(modules.Packet{Act: `TERMINAL_QUIT`})
data = utils.XOR(data, common.WSConn.GetSecret())
common.WSConn.SendRawData(session.rawEvent, data, 21, 01)
break
}
}
@@ -68,77 +87,95 @@ func InitTerminal(pack modules.Packet) error {
return nil
}
func InputTerminal(pack modules.Packet) error {
val, ok := pack.GetData(`input`, reflect.String)
if !ok {
return errDataNotFound
func InputRawTerminal(input []byte, uuid string) {
var session *terminal
if val, ok := terminals.Get(uuid); ok {
session = val.(*terminal)
} else {
return
}
data, err := hex.DecodeString(val.(string))
if err != nil {
return errDataInvalid
session.pty.Write(input)
session.lastPack = utils.Unix
}
val, ok = pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
func InputTerminal(pack modules.Packet) {
var err error
var uuid string
var input []byte
var session *terminal
if val, ok := pack.GetData(`input`, reflect.String); !ok {
return
} else {
if input, err = hex.DecodeString(val.(string)); err != nil {
return
}
termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.WSConn.SendCallback(modules.Packet{Act: `TERMINAL_QUIT`, Msg: `${i18n|TERMINAL.SESSION_CLOSED}`}, pack)
return nil
}
terminal := val.(*terminal)
terminal.pty.Write(data)
terminal.lastPack = utils.Unix
return nil
if val, ok := pack.GetData(`terminal`, reflect.String); !ok {
return
} else {
uuid = val.(string)
if val, ok = terminals.Get(uuid); ok {
session = val.(*terminal)
} else {
return
}
}
session.pty.Write(input)
session.lastPack = utils.Unix
}
func ResizeTerminal(pack modules.Packet) error {
val, ok := pack.GetData(`width`, reflect.Float64)
if !ok {
return errDataInvalid
func ResizeTerminal(pack modules.Packet) {
var uuid string
var cols, rows uint16
var session *terminal
if val, ok := pack.GetData(`cols`, reflect.Float64); !ok {
return
} else {
cols = uint16(val.(float64))
}
width := val.(float64)
val, ok = pack.GetData(`height`, reflect.Float64)
if !ok {
return errDataInvalid
if val, ok := pack.GetData(`rows`, reflect.Float64); !ok {
return
} else {
rows = uint16(val.(float64))
}
height := val.(float64)
val, ok = pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
if val, ok := pack.GetData(`terminal`, reflect.String); !ok {
return
} else {
uuid = val.(string)
if val, ok = terminals.Get(uuid); ok {
session = val.(*terminal)
} else {
return
}
termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.WSConn.SendCallback(modules.Packet{Act: `TERMINAL_QUIT`, Msg: `${i18n|TERMINAL.SESSION_CLOSED}`}, pack)
return nil
}
terminal := val.(*terminal)
pty.Setsize(terminal.pty, &pty.Winsize{
Rows: uint16(height),
Cols: uint16(width),
pty.Setsize(session.pty, &pty.Winsize{
Cols: cols,
Rows: rows,
})
return nil
}
func KillTerminal(pack modules.Packet) error {
val, ok := pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
func KillTerminal(pack modules.Packet) {
var uuid string
var session *terminal
if val, ok := pack.GetData(`terminal`, reflect.String); !ok {
return
} else {
uuid = val.(string)
}
termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.WSConn.SendCallback(modules.Packet{Act: `TERMINAL_QUIT`, Msg: `${i18n|TERMINAL.SESSION_CLOSED}`}, pack)
return nil
if val, ok := terminals.Get(uuid); !ok {
return
} else {
session = val.(*terminal)
}
terminal := val.(*terminal)
terminals.Remove(termUUID)
doKillTerminal(terminal)
return nil
terminals.Remove(uuid)
data, _ := utils.JSON.Marshal(modules.Packet{Act: `TERMINAL_QUIT`, Msg: `${i18n|TERMINAL.SESSION_CLOSED}`})
data = utils.XOR(data, common.WSConn.GetSecret())
common.WSConn.SendRawData(session.rawEvent, data, 21, 01)
session.escape = true
session.rawEvent = nil
}
func PingTerminal(pack modules.Packet) {

View File

@@ -14,8 +14,9 @@ import (
type terminal struct {
lastPack int64
rawEvent []byte
escape bool
event string
stop bool
cmd *exec.Cmd
stdout *io.ReadCloser
stderr *io.ReadCloser
@@ -50,29 +51,46 @@ func InitTerminal(pack modules.Packet) error {
if err != nil {
return err
}
termSession := &terminal{
rawEvent, _ := hex.DecodeString(pack.Event)
session := &terminal{
cmd: cmd,
stop: false,
event: pack.Event,
escape: false,
stdout: &stdout,
stderr: &stderr,
stdin: &stdin,
rawEvent: rawEvent,
lastPack: utils.Unix,
}
readSender := func(rc io.ReadCloser) {
for !termSession.stop {
buffer := make([]byte, 512)
bufSize := 1024
for !session.escape {
buffer := make([]byte, 1024)
n, err := rc.Read(buffer)
buffer = buffer[:n]
common.WSConn.SendCallback(modules.Packet{Act: `TERMINAL_OUTPUT`, Data: map[string]any{
// if output is larger than 1KB, then send binary data
if n > 1024 {
if bufSize < 32768 {
bufSize *= 2
}
common.WSConn.SendRawData(session.rawEvent, buffer, 21, 00)
} else {
bufSize = 1024
buffer, _ = utils.JSON.Marshal(modules.Packet{Act: `TERMINAL_OUTPUT`, Data: map[string]any{
`output`: hex.EncodeToString(buffer),
}}, pack)
termSession.lastPack = utils.Unix
}})
buffer = utils.XOR(buffer, common.WSConn.GetSecret())
common.WSConn.SendRawData(session.rawEvent, buffer, 21, 01)
}
session.lastPack = utils.Unix
if err != nil {
termSession.stop = true
common.WSConn.SendCallback(modules.Packet{Act: `TERMINAL_QUIT`}, pack)
session.escape = true
data, _ := utils.JSON.Marshal(modules.Packet{Act: `TERMINAL_QUIT`})
data = utils.XOR(data, common.WSConn.GetSecret())
common.WSConn.SendRawData(session.rawEvent, data, 21, 01)
break
}
}
@@ -82,73 +100,90 @@ func InitTerminal(pack modules.Packet) error {
err = cmd.Start()
if err != nil {
termSession.stop = true
session.escape = true
return err
}
terminals.Set(pack.Data[`terminal`].(string), termSession)
terminals.Set(pack.Data[`terminal`].(string), session)
return nil
}
func InputTerminal(pack modules.Packet) error {
val, ok := pack.GetData(`input`, reflect.String)
if !ok {
return errDataNotFound
func InputRawTerminal(input []byte, uuid string) {
var session *terminal
if val, ok := terminals.Get(uuid); ok {
session = val.(*terminal)
} else {
return
}
data, err := hex.DecodeString(val.(string))
if err != nil {
return errDataInvalid
(*session.stdin).Write(input)
session.lastPack = utils.Unix
}
val, ok = pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
func InputTerminal(pack modules.Packet) {
var err error
var uuid string
var input []byte
var session *terminal
if val, ok := pack.GetData(`input`, reflect.String); !ok {
return
} else {
if input, err = hex.DecodeString(val.(string)); err != nil {
return
}
termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.WSConn.SendCallback(modules.Packet{Act: `TERMINAL_QUIT`, Msg: `${i18n|TERMINAL.SESSION_CLOSED}`}, pack)
return nil
}
terminal := val.(*terminal)
(*terminal.stdin).Write(data)
terminal.lastPack = utils.Unix
return nil
if val, ok := pack.GetData(`terminal`, reflect.String); !ok {
return
} else {
uuid = val.(string)
if val, ok = terminals.Get(uuid); ok {
session = val.(*terminal)
} else {
return
}
}
(*session.stdin).Write(input)
session.lastPack = utils.Unix
}
func ResizeTerminal(pack modules.Packet) error {
return nil
}
func KillTerminal(pack modules.Packet) error {
val, ok := pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
}
termUUID := val.(string)
val, ok = terminals.Get(termUUID)
if !ok {
common.WSConn.SendCallback(modules.Packet{Act: `TERMINAL_QUIT`, Msg: `${i18n|TERMINAL.SESSION_CLOSED}`}, pack)
return nil
}
terminal := val.(*terminal)
terminals.Remove(termUUID)
doKillTerminal(terminal)
return nil
}
func PingTerminal(pack modules.Packet) {
var termUUID string
var termSession *terminal
func KillTerminal(pack modules.Packet) {
var uuid string
var session *terminal
if val, ok := pack.GetData(`terminal`, reflect.String); !ok {
return
} else {
termUUID = val.(string)
uuid = val.(string)
}
if val, ok := terminals.Get(termUUID); !ok {
if val, ok := terminals.Get(uuid); !ok {
return
} else {
termSession = val.(*terminal)
termSession.lastPack = utils.Unix
session = val.(*terminal)
}
terminals.Remove(uuid)
data, _ := utils.JSON.Marshal(modules.Packet{Act: `TERMINAL_QUIT`, Msg: `${i18n|TERMINAL.SESSION_CLOSED}`})
data = utils.XOR(data, common.WSConn.GetSecret())
common.WSConn.SendRawData(session.rawEvent, data, 21, 01)
session.escape = true
session.rawEvent = nil
}
func PingTerminal(pack modules.Packet) {
var uuid string
var session *terminal
if val, ok := pack.GetData(`terminal`, reflect.String); !ok {
return
} else {
uuid = val.(string)
}
if val, ok := terminals.Get(uuid); !ok {
return
} else {
session = val.(*terminal)
session.lastPack = utils.Unix
}
}

View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 streamrail
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

22
licenses/melody.LICENSE Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2015 Ola Holmström. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

201
licenses/zmodemjs.LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

27
scripts/release.js Normal file
View File

@@ -0,0 +1,27 @@
const process = require("process");
const fs = require("fs");
let changelogs = fs.readFileSync("CHANGELOG.md", "utf-8").toString().split("\n\n\n\n");
if (changelogs.length === 0) {
console.error("Failed to read CHANGELOG.md.");
process.exit(1);
}
let generated = false;
for (let i = 0; i < changelogs.length; i++) {
let thisNotes = changelogs[i].split("\n");
if (thisNotes.length === 0) {
continue;
}
if (!thisNotes.shift().endsWith(process.argv[2])) {
continue
}
thisNotes.shift();
fs.writeFileSync("CHANGELOG.md", thisNotes.join("\n"));
generated = true;
break
}
if (!generated) {
console.log(`Failed to find version ${process.argv[2]} in current changelog.`);
process.exit(1);
}
console.log("New CHANGELOG.md generated.");

View File

@@ -62,20 +62,29 @@ func InitDesktop(ctx *gin.Context) {
})
}
// desktopEventWrapper returns a eventCb function that will be called when
// device need to send a packet to browser
// desktopEventWrapper returns a eventCallback function that will
// be called when device need to send a packet to browser terminal
func desktopEventWrapper(desktop *desktop) common.EventCallback {
return func(pack modules.Packet, device *melody.Session) {
if len(pack.Act) == 0 {
if pack.Data == nil {
if pack.Act == `RAW_DATA_ARRIVE` && pack.Data != nil {
data := *pack.Data[`data`].(*[]byte)
if data[5] == 00 || data[5] == 01 || data[5] == 02 {
desktop.srcConn.WriteBinary(data)
return
}
if data, ok := pack.Data[`data`]; ok {
desktop.srcConn.WriteBinary(*data.(*[]byte))
}
if data[5] != 03 {
return
}
if pack.Act == `DESKTOP_INIT` {
data = data[8:]
data = utility.SimpleDecrypt(data, device)
if utils.JSON.Unmarshal(data, &pack) != nil {
return
}
}
switch pack.Act {
case `DESKTOP_INIT`:
if pack.Code != 0 {
msg := `${i18n|DESKTOP.CREATE_SESSION_FAILED}`
if len(pack.Msg) > 0 {
@@ -94,9 +103,7 @@ func desktopEventWrapper(desktop *desktop) common.EventCallback {
`deviceConn`: desktop.deviceConn,
})
}
return
}
if pack.Act == `DESKTOP_QUIT` {
case `DESKTOP_QUIT`:
msg := `${i18n|DESKTOP.SESSION_CLOSED}`
if len(pack.Msg) > 0 {
msg = pack.Msg
@@ -107,7 +114,6 @@ func desktopEventWrapper(desktop *desktop) common.EventCallback {
common.Info(desktop.srcConn, `DESKTOP_QUIT`, `success`, ``, map[string]any{
`deviceConn`: desktop.deviceConn,
})
return
}
}
}
@@ -150,31 +156,39 @@ func onDesktopConnect(session *melody.Session) {
func onDesktopMessage(session *melody.Session, data []byte) {
var pack modules.Packet
data, ok := utility.SimpleDecrypt(data, session)
if !(ok && utils.JSON.Unmarshal(data, &pack) == nil) {
if val, ok := session.Get(`Desktop`); !ok {
desktop := val.(*desktop)
common.SendPack(modules.Packet{Act: `DESKTOP_KILL`, Data: gin.H{
`desktop`: desktop.uuid,
}, Event: desktop.uuid}, desktop.deviceConn)
}
sendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
val, ok := session.Get(`Desktop`)
if !ok {
return
}
desktop := val.(*desktop)
service, op, isBinary := utils.CheckBinaryPack(data)
if !isBinary || service != 20 {
sendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
if op != 03 {
sendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
data = utility.SimpleDecrypt(data[8:], session)
if utils.JSON.Unmarshal(data, &pack) != nil {
sendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
session.Set(`LastPack`, utils.Unix)
if pack.Act == `DESKTOP_PING` {
switch pack.Act {
case `DESKTOP_PING`:
common.SendPack(modules.Packet{Act: `DESKTOP_PING`, Data: gin.H{
`desktop`: desktop.uuid,
}, Event: desktop.uuid}, desktop.deviceConn)
return
}
if pack.Act == `DESKTOP_KILL` {
case `DESKTOP_KILL`:
common.Info(desktop.srcConn, `DESKTOP_KILL`, `success`, ``, map[string]any{
`deviceConn`: desktop.deviceConn,
})
@@ -182,8 +196,7 @@ func onDesktopMessage(session *melody.Session, data []byte) {
`desktop`: desktop.uuid,
}, Event: desktop.uuid}, desktop.deviceConn)
return
}
if pack.Act == `DESKTOP_SHOT` {
case `DESKTOP_SHOT`:
common.SendPack(modules.Packet{Act: `DESKTOP_SHOT`, Data: gin.H{
`desktop`: desktop.uuid,
}, Event: desktop.uuid}, desktop.deviceConn)
@@ -218,11 +231,8 @@ func sendPack(pack modules.Packet, session *melody.Session) bool {
if err != nil {
return false
}
data, ok := utility.SimpleEncrypt(data, session)
if !ok {
return false
}
err = session.WriteBinary(append([]byte{00, 22, 34, 19, 20, 03}, data...))
data = utility.SimpleEncrypt(data, session)
err = session.WriteBinary(append([]byte{34, 22, 19, 17, 20, 03}, data...))
return err == nil
}

View File

@@ -22,6 +22,7 @@ type terminal struct {
var terminalSessions = melody.New()
func init() {
terminalSessions.Config.MaxMessageSize = common.MaxMessageSize
terminalSessions.HandleConnect(onTerminalConnect)
terminalSessions.HandleMessage(onTerminalMessage)
terminalSessions.HandleMessageBinary(onTerminalMessage)
@@ -62,11 +63,29 @@ func InitTerminal(ctx *gin.Context) {
})
}
// terminalEventWrapper returns a eventCb function that will be called when
// device need to send a packet to browser terminal
// terminalEventWrapper returns a eventCallback function that will
// be called when device need to send a packet to browser terminal
func terminalEventWrapper(terminal *terminal) common.EventCallback {
return func(pack modules.Packet, device *melody.Session) {
if pack.Act == `TERMINAL_INIT` {
if pack.Act == `RAW_DATA_ARRIVE` && pack.Data != nil {
data := *pack.Data[`data`].(*[]byte)
if data[5] == 00 {
terminal.session.WriteBinary(data)
return
}
if data[5] != 01 {
return
}
data = data[8:]
data = utility.SimpleDecrypt(data, device)
if utils.JSON.Unmarshal(data, &pack) != nil {
return
}
}
switch pack.Act {
case `TERMINAL_INIT`:
if pack.Code != 0 {
msg := `${i18n|TERMINAL.CREATE_SESSION_FAILED}`
if len(pack.Msg) > 0 {
@@ -74,7 +93,7 @@ func terminalEventWrapper(terminal *terminal) common.EventCallback {
} else {
msg += `${i18n|COMMON.UNKNOWN_ERROR}`
}
sendPack(modules.Packet{Act: `WARN`, Msg: msg}, terminal.session)
sendPack(modules.Packet{Act: `QUIT`, Msg: msg}, terminal.session)
common.RemoveEvent(terminal.uuid)
terminal.session.Close()
common.Warn(terminal.session, `TERMINAL_INIT`, `fail`, msg, map[string]any{
@@ -85,22 +104,18 @@ func terminalEventWrapper(terminal *terminal) common.EventCallback {
`deviceConn`: terminal.deviceConn,
})
}
return
}
if pack.Act == `TERMINAL_QUIT` {
case `TERMINAL_QUIT`:
msg := `${i18n|TERMINAL.SESSION_CLOSED}`
if len(pack.Msg) > 0 {
msg = pack.Msg
}
sendPack(modules.Packet{Act: `WARN`, Msg: msg}, terminal.session)
sendPack(modules.Packet{Act: `QUIT`, Msg: msg}, terminal.session)
common.RemoveEvent(terminal.uuid)
terminal.session.Close()
common.Info(terminal.session, `TERMINAL_QUIT`, ``, msg, map[string]any{
`deviceConn`: terminal.deviceConn,
})
return
}
if pack.Act == `TERMINAL_OUTPUT` {
case `TERMINAL_OUTPUT`:
if pack.Data == nil {
return
}
@@ -132,18 +147,18 @@ func onTerminalConnect(session *melody.Session) {
session.Close()
return
}
termUUID := utils.GetStrUUID()
uuid := utils.GetStrUUID()
terminal := &terminal{
uuid: termUUID,
uuid: uuid,
device: device.(string),
session: session,
deviceConn: deviceConn,
}
session.Set(`Terminal`, terminal)
common.AddEvent(terminalEventWrapper(terminal), connUUID, termUUID)
common.AddEvent(terminalEventWrapper(terminal), connUUID, uuid)
common.SendPack(modules.Packet{Act: `TERMINAL_INIT`, Data: gin.H{
`terminal`: termUUID,
}, Event: termUUID}, deviceConn)
`terminal`: uuid,
}, Event: uuid}, deviceConn)
common.Info(terminal.session, `TERMINAL_CONN`, `success`, ``, map[string]any{
`deviceConn`: terminal.deviceConn,
})
@@ -151,19 +166,43 @@ func onTerminalConnect(session *melody.Session) {
func onTerminalMessage(session *melody.Session, data []byte) {
var pack modules.Packet
data, ok := utility.SimpleDecrypt(data, session)
if !(ok && utils.JSON.Unmarshal(data, &pack) == nil) {
sendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
val, ok := session.Get(`Terminal`)
if !ok {
return
}
terminal := val.(*terminal)
service, op, isBinary := utils.CheckBinaryPack(data)
if !isBinary || service != 21 {
sendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
if op == 00 {
session.Set(`LastPack`, utils.Unix)
if pack.Act == `TERMINAL_INPUT` {
rawEvent, _ := hex.DecodeString(terminal.uuid)
data = append(data, rawEvent...)
copy(data[22:], data[6:])
copy(data[6:], rawEvent)
terminal.deviceConn.WriteBinary(data)
return
}
if op != 01 {
sendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
data = utility.SimpleDecrypt(data[8:], session)
if utils.JSON.Unmarshal(data, &pack) != nil {
sendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
session.Set(`LastPack`, utils.Unix)
switch pack.Act {
case `TERMINAL_INPUT`:
if pack.Data == nil {
return
}
@@ -179,26 +218,21 @@ func onTerminalMessage(session *melody.Session, data []byte) {
}, Event: terminal.uuid}, terminal.deviceConn)
}
return
}
if pack.Act == `TERMINAL_RESIZE` {
case `TERMINAL_RESIZE`:
if pack.Data == nil {
return
}
if width, ok := pack.Data[`width`]; ok {
if height, ok := pack.Data[`height`]; ok {
if cols, ok := pack.Data[`cols`]; ok {
if rows, ok := pack.Data[`rows`]; ok {
common.SendPack(modules.Packet{Act: `TERMINAL_RESIZE`, Data: gin.H{
`width`: width,
`height`: height,
`cols`: cols,
`rows`: rows,
`terminal`: terminal.uuid,
}, Event: terminal.uuid}, terminal.deviceConn)
}
}
return
}
if pack.Act == `TERMINAL_KILL` {
if pack.Data == nil {
return
}
case `TERMINAL_KILL`:
common.Info(terminal.session, `TERMINAL_KILL`, `success`, ``, map[string]any{
`deviceConn`: terminal.deviceConn,
})
@@ -206,11 +240,7 @@ func onTerminalMessage(session *melody.Session, data []byte) {
`terminal`: terminal.uuid,
}, Event: terminal.uuid}, terminal.deviceConn)
return
}
if pack.Act == `PING` {
if pack.Data == nil {
return
}
case `PING`:
common.SendPack(modules.Packet{Act: `TERMINAL_PING`, Data: gin.H{
`terminal`: terminal.uuid,
}, Event: terminal.uuid}, terminal.deviceConn)
@@ -245,10 +275,7 @@ func sendPack(pack modules.Packet, session *melody.Session) bool {
if err != nil {
return false
}
data, ok := utility.SimpleEncrypt(data, session)
if !ok {
return false
}
data = utility.SimpleEncrypt(data, session)
err = session.WriteBinary(data)
return err == nil
}

View File

@@ -8,8 +8,6 @@ import (
"Spark/utils/melody"
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
@@ -331,36 +329,22 @@ func CallDevice(ctx *gin.Context) {
}
}
func SimpleEncrypt(data []byte, session *melody.Session) ([]byte, bool) {
func SimpleEncrypt(data []byte, session *melody.Session) []byte {
temp, ok := session.Get(`Secret`)
if !ok {
return nil, false
return nil
}
secret := temp.([]byte)
block, err := aes.NewCipher(secret)
if err != nil {
return nil, false
}
stream := cipher.NewCTR(block, secret)
encBuffer := make([]byte, len(data))
stream.XORKeyStream(encBuffer, data)
return encBuffer, true
return utils.XOR(data, secret)
}
func SimpleDecrypt(data []byte, session *melody.Session) ([]byte, bool) {
func SimpleDecrypt(data []byte, session *melody.Session) []byte {
temp, ok := session.Get(`Secret`)
if !ok {
return nil, false
return nil
}
secret := temp.([]byte)
block, err := aes.NewCipher(secret)
if err != nil {
return nil, false
}
stream := cipher.NewCTR(block, secret)
decBuffer := make([]byte, len(data))
stream.XORKeyStream(decBuffer, data)
return decBuffer, true
return utils.XOR(data, secret)
}
func WSHealthCheck(container *melody.Melody, sender Sender) {

View File

@@ -158,20 +158,38 @@ func wsOnMessage(session *melody.Session, _ []byte) {
func wsOnMessageBinary(session *melody.Session, data []byte) {
var pack modules.Packet
{
dataLen := len(data)
if dataLen >= 22 {
if bytes.Equal(data[:5], []byte{34, 22, 19, 17, 20}) {
if dataLen > 24 {
if service, op, isBinary := utils.CheckBinaryPack(data); isBinary {
switch service {
case 20:
switch op {
case 00, 01, 02, 03:
event := hex.EncodeToString(data[6:22])
copy(data[6:], data[22:])
common.CallEvent(modules.Packet{
Act: `RAW_DATA_ARRIVE`,
Event: event,
Data: gin.H{
`data`: utils.GetSlicePrefix(&data, dataLen-16),
},
}, session)
return
}
case 21:
switch op {
case 00, 01:
event := hex.EncodeToString(data[6:22])
copy(data[6:], data[22:])
common.CallEvent(modules.Packet{
Act: `RAW_DATA_ARRIVE`,
Event: event,
Data: gin.H{
`data`: utils.GetSlicePrefix(&data, dataLen-16),
},
}, session)
}
}
return
}
}

View File

@@ -41,6 +41,16 @@ func Max[T int | int32 | int64 | uint | uint32 | uint64 | float32 | float64](a,
return b
}
func XOR(data []byte, key []byte) []byte {
if len(key) == 0 {
return data
}
for i := 0; i < len(data); i++ {
data[i] = data[i] ^ key[i%len(key)]
}
return data
}
func GenRandByte(n int) []byte {
secBuffer := make([]byte, n)
rand.Reader.Read(secBuffer)
@@ -185,3 +195,14 @@ func GetSliceChunk[T any](data *[]T, start, end int) *[]T {
Cap: end - start,
}))
}
func CheckBinaryPack(data []byte) (byte, byte, bool) {
if len(data) >= 8 {
if bytes.Equal(data[:4], []byte{34, 22, 19, 17}) {
if data[4] == 20 || data[4] == 21 {
return data[4], data[5], true
}
}
}
return 0, 0, false
}

2813
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,9 +14,9 @@
"@ant-design/pro-layout": "^6.23.0",
"@ant-design/pro-table": "^2.45.0",
"ace-builds": "^1.5.3",
"antd": "^4.16.8",
"antd": "^4.23.6",
"axios": "^0.26.1",
"crypto-js": "^4.1.1",
"crc-32": "^1.2.2",
"dayjs": "^1.10.6",
"i18next": "^21.6.15",
"lodash": "^4.17.21",
@@ -25,13 +25,14 @@
"react-ace": "^10.1.0",
"react-dom": "^17.0.2",
"react-draggable": "^4.4.5",
"react-markdown": "^8.0.3",
"react-router": "^6.2.2",
"react-router-dom": "^6.2.2",
"virtuallist-antd": "^0.7.4-beta.0",
"wcwidth": "^1.0.1",
"xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-web-links": "^0.5.1"
"xterm": "^5.0.0",
"xterm-addon-fit": "^0.6.0",
"xterm-addon-web-links": "^0.7.0"
},
"devDependencies": {
"@babel/core": "^7.17.9",

View File

@@ -1,8 +1,7 @@
import React, {useCallback, useEffect, useState} from 'react';
import {encrypt, decrypt, formatSize, genRandHex, getBaseURL, translate} from "../utils/utils";
import {encrypt, decrypt, formatSize, genRandHex, getBaseURL, translate, hex2ua, ua2hex} from "../utils/utils";
import i18n from "../locale/locale";
import DraggableModal from "./modal";
import CryptoJS from "crypto-js";
import {Button, message} from "antd";
import {FullscreenOutlined, ReloadOutlined} from "@ant-design/icons";
@@ -20,15 +19,15 @@ function ScreenModal(props) {
const [bandwidth, setBandwidth] = useState(0);
const [fps, setFps] = useState(0);
const canvasRef = useCallback((e) => {
if (e && props.visible && !conn) {
if (e && props.open && !conn) {
canvas = e;
initCanvas(canvas);
construct(canvas);
}
}, [props]);
useEffect(() => {
if (props.visible) {
secret = CryptoJS.enc.Hex.parse(genRandHex(32));
if (props.open) {
secret = hex2ua(genRandHex(32));
} else {
if (ws && conn) {
clearInterval(ticker);
@@ -36,7 +35,7 @@ function ScreenModal(props) {
conn = false;
}
}
}, [props.visible, props.device]);
}, [props.open, props.device]);
function initCanvas() {
if (!canvas) return;
@@ -47,7 +46,7 @@ function ScreenModal(props) {
if (ws !== null && conn) {
ws.close();
}
ws = new WebSocket(getBaseURL(true, `api/device/desktop?device=${props.device.id}&secret=${secret}`));
ws = new WebSocket(getBaseURL(true, `api/device/desktop?device=${props.device.id}&secret=${ua2hex(secret)}`));
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
conn = true;
@@ -79,9 +78,9 @@ function ScreenModal(props) {
ticks++;
if (ticks > 10 && conn) {
ticks = 0;
ws.send(encrypt({
sendData({
act: 'DESKTOP_PING'
}, secret));
});
}
}, 1000);
}
@@ -101,16 +100,16 @@ function ScreenModal(props) {
} catch {}
}
function refresh() {
if (canvas && props.visible) {
if (canvas && props.open) {
if (!conn) {
canvas.width = 1920;
canvas.height = 1080;
initCanvas(canvas);
construct(canvas);
} else {
ws.send(encrypt({
sendData({
act: 'DESKTOP_SHOT'
}, secret));
});
}
}
}
@@ -124,8 +123,8 @@ function ScreenModal(props) {
return;
}
if (op === 2) {
let width = dv.getUint16(1, false);
let height = dv.getUint16(3, false);
let width = dv.getUint16(3, false);
let height = dv.getUint16(5, false);
if (width === 0 || height === 0) return;
canvas.width = width;
canvas.height = height;
@@ -135,12 +134,13 @@ function ScreenModal(props) {
bytes += ab.byteLength;
let offset = 1;
while (offset < ab.byteLength) {
let it = dv.getUint16(offset + 0, false); // image type
let il = dv.getUint16(offset + 2, false); // image length
let bl = dv.getUint16(offset + 0, false); // body length
let it = dv.getUint16(offset + 2, false); // image type
let dx = dv.getUint16(offset + 4, false); // image block x
let dy = dv.getUint16(offset + 6, false); // image block y
let bw = dv.getUint16(offset + 8, false); // image block width
let bh = dv.getUint16(offset + 10, false); // image block height
let il = bl - 10; // image length
offset += 12;
updateImage(ab.slice(offset, offset + il), it, dx, dy, bw, bh, canvasCtx);
offset += il;
@@ -173,6 +173,17 @@ function ScreenModal(props) {
}
}
function sendData(data) {
if (conn) {
let body = encrypt(data, secret);
let buffer = new Uint8Array(body.length + 8);
buffer.set(new Uint8Array([34, 22, 19, 17, 20, 3]), 0);
buffer.set(new Uint8Array([body.length >> 8, body.length & 0xFF]), 6);
buffer.set(body, 8);
ws.send(buffer);
}
}
return (
<DraggableModal
draggable={true}

View File

@@ -2,11 +2,6 @@ a {
user-select: none;
}
.file-row {
user-select: none;
cursor: pointer;
}
.ant-table-body {
max-height: 300px;
min-height: 300px;
@@ -27,9 +22,7 @@ a {
display: none;
}
.upload-progress-square > .ant-progress-outer > .ant-progress-inner {
border-radius: 0 !important;
}
.editor-modal, .editor-modal > .ant-modal-content {
top: 0;
@@ -43,3 +36,8 @@ a {
.editor-modal > .ant-modal-content > .ant-modal-body {
height: calc(100% - 110px);
}
.file-row {
user-select: none;
cursor: pointer;
}

View File

@@ -1,30 +1,23 @@
import React, {useEffect, useMemo, useRef, useState} from "react";
import {
Alert,
Breadcrumb,
Button,
Dropdown,
Image,
Menu,
message,
Modal,
Popconfirm,
Progress,
Space,
Spin
} from "antd";
import ProTable, {TableDropdown} from "@ant-design/pro-table";
import {catchBlobReq, formatSize, orderCompare, post, preventClose, request, waitTime} from "../utils/utils";
import {
Alert, Breadcrumb, Button,
Dropdown, Image, Menu,
message, Modal, Popconfirm,
Progress, Space, Spin, Typography
} from "antd";
import {
catchBlobReq, formatSize,
orderCompare, post, request,
preventClose, waitTime
} from "../utils/utils";
import dayjs from "dayjs";
import i18n from "../locale/locale";
import {VList} from "virtuallist-antd";
import {
CloseOutlined, FullscreenOutlined,
HomeOutlined,
LoadingOutlined,
QuestionCircleOutlined,
ReloadOutlined,
UploadOutlined
CloseOutlined, HomeOutlined,
LoadingOutlined, QuestionCircleOutlined,
ReloadOutlined, UploadOutlined
} from "@ant-design/icons";
import axios from "axios";
import Qs from "qs";
@@ -68,7 +61,7 @@ function FileBrowser(props) {
},
{
key: 'Time',
title: i18n.t('EXPLORER.MODIFY_TIME'),
title: i18n.t('EXPLORER.DATE_MODIFIED'),
dataIndex: 'time',
ellipsis: true,
width: 100,
@@ -94,7 +87,8 @@ function FileBrowser(props) {
const tableRef = useRef();
const virtualTable = useMemo(() => {
return VList({
height: 300
height: 300,
vid: 'file-table',
})
}, []);
const alertOptionRenderer = () => (<Space size={16}>
@@ -116,11 +110,11 @@ function FileBrowser(props) {
position = '/';
setPath(`/`);
}
if (props.visible) {
if (props.open) {
fileList = [];
setLoading(false);
}
}, [props.device, props.visible]);
}, [props.device, props.open]);
function renderOperation(file) {
let menus = [
@@ -269,7 +263,7 @@ function FileBrowser(props) {
return;
}
}
document.getElementById('uploader').click();
document.getElementById('file-uploader').click();
}
function onFileChange(e) {
let file = e.target.files[0];
@@ -472,10 +466,10 @@ function FileBrowser(props) {
onClick={uploadFile}
/>
<input
id='uploader'
id='file-uploader'
type='file'
style={{display: 'none'}}
onChange={onFileChange}
style={{display: 'none'}}
/>
<TextEditor
path={path}
@@ -490,11 +484,12 @@ function FileBrowser(props) {
}}
/>
<FileUploader
open={uploading}
path={path}
file={uploading}
device={props.device}
onSuccess={onUploadSuccess}
onCanel={onUploadCancel}
onCancel={onUploadCancel}
/>
<Image
preview={{
@@ -587,7 +582,7 @@ function TextEditor(props) {
const [editorTheme, setEditorTheme] = useState(editorConfig.theme);
const [editorMode, setEditorMode] = useState('text');
const [loading, setLoading] = useState(false);
const [visible, setVisible] = useState(props.file);
const [open, setOpen] = useState(props.file);
const editorRef = useRef();
const fontMenu = (
<Menu onClick={onFontMenuClick}>
@@ -626,7 +621,7 @@ function TextEditor(props) {
} catch (e) {
require('ace-builds/src-min-noconflict/mode-text');
}
setVisible(true);
setOpen(true);
setFileContent(props.content);
setEditorMode(fileMode);
}
@@ -662,7 +657,7 @@ function TextEditor(props) {
function onForceCancel(reload) {
setCancelConfirm(false);
setTimeout(() => {
setVisible(false);
setOpen(false);
setFileContent('');
window.onbeforeunload = null;
props.onCancel(reload);
@@ -676,7 +671,7 @@ function TextEditor(props) {
if (fileStatus === 1) {
setCancelConfirm(true);
} else {
setVisible(false);
setOpen(false);
setFileContent('');
window.onbeforeunload = null;
props.onCancel(fileStatus === 2);
@@ -718,7 +713,7 @@ function TextEditor(props) {
title={props.file}
mask={false}
keyboard={false}
visible={visible}
open={open}
maskClosable={false}
className='editor-modal'
closeIcon={loading ? <Spin indicator={<LoadingOutlined />} /> : <CloseOutlined />}
@@ -771,7 +766,7 @@ function TextEditor(props) {
}]}
value={fileContent}
onChange={val => {
if (!visible) return;
if (!open) return;
if (val.length === fileContent.length) {
if (val === fileContent) return;
}
@@ -788,7 +783,7 @@ function TextEditor(props) {
/>
<Modal
closable={true}
visible={cancelConfirm}
open={cancelConfirm}
onCancel={onExitCancel}
footer={[
<Button
@@ -841,7 +836,7 @@ function setEditorConfig(config) {
let abortController = null;
function FileUploader(props) {
const [visible, setVisible] = useState(!!props.file);
const [open, setOpen] = useState(!!props.file);
const [percent, setPercent] = useState(0);
const [status, setStatus] = useState(0);
// 0: ready, 1: uploading, 2: success, 3: fail, 4: cancel
@@ -849,7 +844,7 @@ function FileUploader(props) {
useEffect(() => {
setStatus(0);
if (props.file) {
setVisible(true);
setOpen(true);
setPercent(0);
}
}, [props.file]);
@@ -906,19 +901,19 @@ function FileUploader(props) {
abortController = null;
window.onbeforeunload = null;
setTimeout(() => {
setVisible(false);
setOpen(false);
if (uploadStatus === 2) {
props.onSuccess();
} else {
props.onCanel();
props.onCancel();
}
}, 1500);
});
}
function onCancel() {
if (status === 0) {
setVisible(false);
setTimeout(props.onCanel, 300);
setOpen(false);
setTimeout(props.onCancel, 300);
return;
}
if (status === 1) {
@@ -935,8 +930,8 @@ function FileUploader(props) {
return;
}
setTimeout(() => {
setVisible(false);
setTimeout(props.onCanel, 300);
setOpen(false);
setTimeout(props.onCancel, 300);
}, 1500);
}
@@ -953,41 +948,50 @@ function FileUploader(props) {
default:
return i18n.t('EXPLORER.UPLOAD');
}
}
return (
<DraggableModal
centered
draggable
visible={visible}
open={open}
closable={false}
keyboard={false}
maskClosable={false}
destroyOnClose={true}
confirmLoading={status === 1}
okText={i18n.t(status === 1 ? 'EXPLORER.UPLOADING' : 'EXPLORER.UPLOAD')}
onOk={onConfirm}
onCancel={onCancel}
modalTitle={i18n.t(status === 1 ? 'EXPLORER.UPLOADING' : 'EXPLORER.UPLOAD')}
okButtonProps={{disabled: status !== 0}}
cancelButtonProps={{disabled: status > 1}}
onCancel={onCancel}
onOk={onConfirm}
width={550}
>
<>
<div>
<span
style={{
whiteSpace: 'nowrap',
fontSize: '20px',
marginRight: '10px',
}}
>
{getDescription()}
</span>
{props.file.name + ` (${formatSize(props.file.size)})`}
</>
<Typography.Text
ellipsis={{rows: 1}}
style={{maxWidth: 'calc(100% - 140px)'}}
>
{props.file.name}
</Typography.Text>
<span
style={{whiteSpace: 'nowrap'}}
>
{''+formatSize(props.file.size)+''}
</span>
</div>
<Progress
className='upload-progress-square'
strokeLinecap='square'
strokeLinecap='butt'
percent={percent}
showInfo={false}
/>

View File

@@ -10,11 +10,11 @@ function DraggableModal(props) {
bottom: 0,
right: 0,
});
const draggleRef = useRef(null);
const draggableRef = useRef(null);
const onStart = (_event, uiData) => {
const { clientWidth, clientHeight } = window.document.documentElement;
const targetRect = draggleRef.current?.getBoundingClientRect();
const targetRect = draggableRef.current?.getBoundingClientRect();
if (!targetRect || disabled) {
return;
}
@@ -55,7 +55,7 @@ function DraggableModal(props) {
bounds={bounds}
onStart={(event, uiData) => onStart(event, uiData)}
>
<div ref={draggleRef}>{modal}</div>
<div ref={draggableRef}>{modal}</div>
</Draggable>
)}
{...props}

View File

@@ -47,10 +47,10 @@ function ProcessMgr(props) {
})
}, []);
useEffect(() => {
if (props.visible) {
if (props.open) {
setLoading(false);
}
}, [props.device, props.visible]);
}, [props.device, props.open]);
function renderOperation(proc) {
return [

View File

@@ -24,8 +24,8 @@ function Runner(props) {
title={i18n.t('RUNNER.TITLE')}
width={380}
onFinish={onFinish}
onVisibleChange={visible => {
if (!visible) props.onCancel();
onVisibleChange={open => {
if (!open) props.onCancel();
}}
submitter={{
render: (_, elems) => elems.pop()

View File

@@ -1,48 +1,62 @@
import React, {createRef, useCallback} from "react";
import React, {createRef, useCallback, useState} from "react";
import {Button, Dropdown, Menu, message, Space} from "antd";
import {Terminal} from "xterm";
import {WebLinksAddon} from "xterm-addon-web-links";
import {FitAddon} from "xterm-addon-fit";
import debounce from 'lodash/debounce';
import CryptoJS from 'crypto-js';
import wcwidth from 'wcwidth';
import "xterm/css/xterm.css";
import i18n from "../locale/locale";
import {encrypt, decrypt, ab2str, genRandHex, getBaseURL, hex2buf, translate} from "../utils/utils";
import {
decrypt, encrypt, genRandHex, getBaseURL,
hex2ua, str2hex, str2ua, translate,
ua2hex, ua2str
} from "../utils/utils";
import DraggableModal from "./modal";
const Zmodem = require("../vendors/zmodem.js/zmodem");
let ws = null;
let zsentry = null;
let zsession = null;
let webLinks = null;
let fit = null;
let term = null;
let termEv = null;
let secret = null;
let ws = null;
let ctrl = false;
let conn = false;
let ticker = 0;
let buffer = {content: '', output: ''};
function TerminalModal(props) {
let os = props.device.os;
let extKeyRef = createRef();
let secret = CryptoJS.enc.Hex.parse(genRandHex(32));
let termRef = useCallback(e => {
if (e !== null) {
termRef.current = e;
if (props.visible) {
ctrl = false;
if (props.open) {
secret = hex2ua(genRandHex(32));
fit = new FitAddon();
webLinks = new WebLinksAddon();
term = new Terminal({
convertEol: true,
allowProposedApi: true,
allowTransparency: false,
cursorBlink: true,
cursorStyle: "block",
fontFamily: "Hack, monospace",
fontSize: 16,
logLevel: "off",
})
fit = new FitAddon();
});
termEv = initialize(null);
term.loadAddon(fit);
term.loadAddon(new WebLinksAddon());
term.open(termRef.current);
fit.fit();
term.clear();
term.loadAddon(webLinks);
window.onresize = onResize;
ticker = setInterval(() => {
if (conn) sendData({act: 'PING'});
@@ -51,10 +65,15 @@ function TerminalModal(props) {
doResize();
}
}
}, [props.visible]);
}, [props.open]);
function afterClose() {
clearInterval(ticker);
if (zsession) {
zsession._last_header_name = 'ZRINIT';
zsession.close();
zsession = null;
}
if (conn) {
sendData({act: 'TERMINAL_KILL'});
ws.onclose = null;
@@ -64,65 +83,47 @@ function TerminalModal(props) {
termEv = null;
fit?.dispose();
fit = null;
webLinks?.dispose();
webLinks = null;
zsentry = null;
term?.dispose();
term = null;
ws = null;
conn = false;
ctrl = false;
}
function initialize(ev) {
ev?.dispose();
let buffer = { content: '', output: '' };
buffer = {content: '', output: ''};
let termEv = null;
// Windows doesn't support pty, so we still use traditional way.
// And we need to handle arrow events manually.
if (os === 'windows') {
termEv = term.onData(onWindowsInput.call(this, buffer));
termEv = term.onData(onWindowsInput(buffer));
} else {
termEv = term.onData(onUnixOSInput.call(this, buffer));
initZmodem();
termEv = term.onData(onUnixOSInput(buffer));
}
ws = new WebSocket(getBaseURL(true, `api/device/terminal?device=${props.device.id}&secret=${secret}`));
ws = new WebSocket(getBaseURL(true, `api/device/terminal?device=${props.device.id}&secret=${ua2hex(secret)}`));
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
conn = true;
}
ws.onmessage = (e) => {
let data = decrypt(e.data, secret);
try {
data = JSON.parse(data);
} catch (_) {}
if (conn) {
if (data?.act === 'TERMINAL_OUTPUT') {
data = ab2str(hex2buf(data?.data?.output));
if (buffer.output.length > 0) {
data = buffer.output + data;
buffer.output = '';
}
if (buffer.content.length > 0) {
if (data.length > buffer.content.length) {
if (data.startsWith(buffer.content)) {
data = data.substring(buffer.content.length);
buffer.content = '';
}
} else {
buffer.output = data;
return;
}
}
term.write(data);
return;
}
if (data?.act === 'WARN') {
message.warn(data.msg ? translate(data.msg) : i18n.t('COMMON.UNKNOWN_ERROR'));
}
}
onWsMessage(e.data, buffer);
}
ws.onclose = (e) => {
if (conn) {
conn = false;
term.write(`\n${i18n.t('COMMON.DISCONNECTED')}\n`);
secret = CryptoJS.enc.Hex.parse(genRandHex(32));
secret = hex2ua(genRandHex(32));
if (zsession !== null) {
zsession._last_header_name = 'ZRINIT';
zsession.close();
zsession = null;
}
}
}
ws.onerror = (e) => {
@@ -130,13 +131,81 @@ function TerminalModal(props) {
if (conn) {
conn = false;
term.write(`\n${i18n.t('COMMON.DISCONNECTED')}\n`);
secret = CryptoJS.enc.Hex.parse(genRandHex(32));
secret = hex2ua(genRandHex(32));
if (zsession !== null) {
zsession._last_header_name = 'ZRINIT';
zsession.close();
zsession = null;
}
} else {
term.write(`\n${i18n.t('COMMON.CONNECTION_FAILED')}\n`);
}
}
return termEv;
}
function onWsMessage(data) {
data = new Uint8Array(data);
if (data[0] === 34 && data[1] === 22 && data[2] === 19 && data[3] === 17 && data[4] === 21 && data[5] === 0) {
data = data.slice(8);
if (zsentry === null) {
onOutput(ua2str(data));
} else {
try {
zsentry.consume(data);
} catch (e) {
console.error(e);
}
}
} else {
data = decrypt(data, secret);
try {
data = JSON.parse(data);
} catch (_) {}
if (conn) {
if (data?.act === 'TERMINAL_OUTPUT') {
data = hex2ua(data?.data?.output);
if (zsentry === null) {
onOutput(ua2str(data));
} else {
try {
zsentry.consume(data);
} catch (e) {
console.error(e);
}
}
return;
}
if (data?.act === 'WARN') {
message.warn(data.msg ? translate(data.msg) : i18n.t('COMMON.UNKNOWN_ERROR'));
return;
}
if (data?.act === 'QUIT') {
message.warn(data.msg ? translate(data.msg) : i18n.t('COMMON.UNKNOWN_ERROR'));
ws.close();
return;
}
}
}
}
function onOutput(data) {
if (buffer.output.length > 0) {
data = buffer.output + data;
buffer.output = '';
}
if (buffer.content.length > 0) {
if (data.length >= buffer.content.length) {
if (data.startsWith(buffer.content)) {
data = data.substring(buffer.content.length);
buffer.content = '';
}
} else {
buffer.output = data;
return
}
}
term.write(data);
}
function onWindowsInput(buffer) {
let cmd = '';
let index = 0;
@@ -144,13 +213,6 @@ function TerminalModal(props) {
let history = [];
let tempCmd = '';
let tempCursor = 0;
function clearTerm() {
let before = cmd.substring(0, cursor);
let after = cmd.substring(cursor);
term.write('\b'.repeat(wcwidth(before)));
term.write(' '.repeat(wcwidth(cmd)));
term.write('\b'.repeat(wcwidth(cmd)));
}
return function (e) {
if (!conn) {
if (e === '\r' || e === '\n' || e === ' ') {
@@ -167,7 +229,7 @@ function TerminalModal(props) {
tempCursor = cursor;
}
index--;
clearTerm.call(this);
clearTerm();
cmd = history[index];
cursor = cmd.length;
term.write(cmd);
@@ -176,12 +238,12 @@ function TerminalModal(props) {
case '\x1B\x5B\x42': // down arrow.
if (index + 1 < history.length) {
index++;
clearTerm.call(this);
clearTerm();
cmd = history[index];
cursor = cmd.length;
term.write(cmd);
} else if (index + 1 <= history.length) {
clearTerm.call(this);
clearTerm();
index++;
cmd = tempCmd;
cursor = tempCursor;
@@ -206,7 +268,7 @@ function TerminalModal(props) {
case '\r':
case '\n':
if (cmd === 'clear' || cmd === 'cls') {
clearTerm.call(this);
clearTerm();
term.clear();
} else {
term.write('\n');
@@ -251,6 +313,14 @@ function TerminalModal(props) {
}
}
};
function clearTerm() {
let before = cmd.substring(0, cursor);
let after = cmd.substring(cursor);
term.write('\b'.repeat(wcwidth(before)));
term.write(' '.repeat(wcwidth(cmd)));
term.write('\b'.repeat(wcwidth(cmd)));
}
}
function onUnixOSInput(_) {
return function (e) {
@@ -264,13 +334,134 @@ function TerminalModal(props) {
sendUnixOSInput(e);
};
}
function initZmodem() {
const clear = () => {
extKeyRef.current.setFileSelect(false);
zsession._last_header_name = 'ZRINIT';
zsession.close();
zsession = null;
};
zsentry = new Zmodem.Sentry({
on_retract: () => {},
on_detect: detection => {
if (zsession !== null) {
clear();
}
zsession = detection.confirm();
if (zsession.type === 'send') {
uploadFile(zsession);
} else {
downloadFile(zsession);
}
},
to_terminal: data => {
onOutput(ua2str(new Uint8Array(data)));
},
sender: data => {
sendData(new Uint8Array(data), true);
}
});
function uploadFile() {
return new Promise((resolve, reject) => {
let uploader = document.getElementById('file-uploader');
let hasFile = false;
uploader.onchange = e => {
extKeyRef.current.setFileSelect(false);
if (zsession === null) {
e.target.value = null;
message.warn(i18n.t('TERMINAL.ZMODEM_UPLOADER_CALL_TIMEOUT'));
return;
}
let file = e.target.files[0];
if (file === undefined) {
term.write("\n" + i18n.t('TERMINAL.ZMODEM_UPLOADER_NO_FILE') + "\n");
clear();
reject('NO_FILE_SELECTED');
return;
}
hasFile = true;
e.target.value = null;
term.write("\n" + file.name + "\t" + i18n.t('TERMINAL.ZMODEM_TRANSFER_START') + "\n");
Zmodem.Browser.send_files(zsession, [file], {
on_offer_response: (file, xfer) => {
if (!xfer) {
term.write(file.name + "\t" + i18n.t('TERMINAL.ZMODEM_TRANSFER_REJECTED') + "\n");
reject('TRANSFER_REJECTED');
}
},
on_file_complete: () => {
term.write(file.name + "\t" + i18n.t('TERMINAL.ZMODEM_TRANSFER_SUCCESS') + "\n");
resolve();
}
}).catch(e => {
console.error(e);
term.write(file.name + "\t" + i18n.t('TERMINAL.ZMODEM_TRANSFER_FAILED') + "\n");
reject(e);
}).finally(() => {
clear();
});
};
term.write("\n" + i18n.t('TERMINAL.ZMODEM_UPLOADER_TIP'));
term.write("\n" + i18n.t('TERMINAL.ZMODEM_UPLOADER_WARNING') + "\n");
extKeyRef.current.setFileSelect(() => {
uploader.click();
});
uploader.click();
setTimeout(() => {
if (!hasFile) {
term.write("\n" + i18n.t('TERMINAL.ZMODEM_UPLOADER_CALL_TIMEOUT') + "\n");
clear();
reject('UPLOADER_CALL_TIMEOUT');
}
}, 10000);
});
}
function downloadFile() {
return new Promise((resolve, reject) => {
let resolved = false;
let rejected = false;
zsession.on('offer', xfer => {
let detail = xfer.get_details();
if (detail.size > 16 * 1024 * 1024) {
xfer.skip();
term.write("\n" + detail.name + "\t" + i18n.t('TERMINAL.ZMODEM_FILE_TOO_LARGE') + "\n");
} else {
let filename = detail.name;
let content = [];
xfer.on('input', data => {
content.push(new Uint8Array(data));
});
xfer.accept().then(() => {
Zmodem.Browser.save_to_disk(content, filename);
term.write("\n" + detail.name + "\t" + i18n.t('TERMINAL.ZMODEM_TRANSFER_SUCCESS') + "\n");
resolved = true;
resolve();
}).catch(e => {
console.error(e);
term.write("\n" + detail.name + "\t" + i18n.t('TERMINAL.ZMODEM_TRANSFER_FAILED') + "\n");
rejected = true;
reject();
});
}
});
zsession.on('session_end', () => {
zsession = null;
if (!resolved && !rejected) {
reject();
}
});
zsession.start();
});
}
}
function sendWindowsInput(input) {
if (conn) {
sendData({
act: 'TERMINAL_INPUT',
data: {
input: CryptoJS.enc.Hex.stringify(CryptoJS.enc.Utf8.parse(input))
input: str2hex(input)
}
});
}
@@ -293,14 +484,33 @@ function TerminalModal(props) {
sendData({
act: 'TERMINAL_INPUT',
data: {
input: CryptoJS.enc.Hex.stringify(CryptoJS.enc.Utf8.parse(input))
input: str2hex(input)
}
});
}
}
function sendData(data) {
function sendData(data, raw) {
if (conn) {
ws.send(encrypt(data, secret));
let body = [];
if (raw) {
if (data.length > 65536) {
let offset = 0;
while (offset < data.length) {
let chunk = data.slice(offset, offset + 65536);
sendData(chunk, true);
offset += chunk.length;
}
} else {
body = data;
}
} else {
body = encrypt(str2ua(JSON.stringify(data)), secret);
}
let buffer = new Uint8Array(body.length + 8);
buffer.set(new Uint8Array([34, 22, 19, 17, 21, raw ? 0 : 1]), 0);
buffer.set(new Uint8Array([body.length >> 8, body.length & 0xFF]), 6);
buffer.set(body, 8);
ws.send(buffer);
}
}
@@ -316,8 +526,8 @@ function TerminalModal(props) {
sendData({
act: 'TERMINAL_RESIZE',
data: {
width: cols,
height: rows
cols: cols,
rows: rows
}
});
}
@@ -346,20 +556,20 @@ function TerminalModal(props) {
draggable={true}
maskClosable={false}
modalTitle={i18n.t('TERMINAL.TITLE')}
visible={props.visible}
open={props.open}
onCancel={props.onCancel}
bodyStyle={{padding: 12}}
afterClose={afterClose}
destroyOnClose={true}
footer={null}
height={200}
height={250}
width={900}
>
<ExtKeyboard
ref={extKeyRef}
onCtrl={onCtrl}
onExtKey={onExtKey}
visible={os!=='windows'}
open={os !== 'windows'}
/>
<div
style={{
@@ -368,6 +578,11 @@ function TerminalModal(props) {
}}
ref={termRef}
/>
<input
id='file-uploader'
type='file'
style={{display: 'none'}}
/>
</DraggableModal>
)
}
@@ -375,8 +590,8 @@ function TerminalModal(props) {
class ExtKeyboard extends React.Component {
constructor(props) {
super(props);
this.visible = props.visible;
if (!this.visible) return;
this.open = props.open;
if (!this.open) return;
this.funcKeys = [
{key: '\x1B\x4F\x50', label: 'F1'},
{key: '\x1B\x4F\x51', label: 'F2'},
@@ -417,7 +632,10 @@ class ExtKeyboard extends React.Component {
)}
</Menu>
);
this.state = {ctrl: false};
this.state = {
ctrl: false,
fileSelect: false,
};
}
onCtrl() {
@@ -430,13 +648,21 @@ class ExtKeyboard extends React.Component {
onExtKey(key) {
this.props.onExtKey(key, true);
}
onFileSelect() {
if (typeof this.state.fileSelect === 'function') {
this.state.fileSelect();
}
}
setCtrl(val) {
this.setState({ctrl: val});
}
setFileSelect(cb) {
this.setState({fileSelect: cb});
}
render() {
if (!this.visible) return null;
if (!this.open) return null;
return (
<Space style={{paddingBottom: 12}}>
<>
@@ -489,6 +715,13 @@ class ExtKeyboard extends React.Component {
>
{i18n.t('TERMINAL.FUNCTION_KEYS')}
</Dropdown.Button>
{
this.state.fileSelect?(
<Button onClick={this.onFileSelect.bind(this)}>
选择文件
</Button>
):null
}
</Space>
);
}

View File

@@ -2,10 +2,15 @@ import React from 'react';
import ProLayout, {PageContainer} from '@ant-design/pro-layout';
import zhCN from 'antd/lib/locale/zh_CN';
import en from 'antd/lib/locale/en_US';
import {getLang} from "../locale/locale";
import {ConfigProvider} from "antd";
import {getLang, getLocale} from "../locale/locale";
import {Button, ConfigProvider, notification} from "antd";
import axios from "axios";
import './wrapper.css';
import version from "../config/version.json";
import ReactMarkdown from "react-markdown";
import i18n from "i18next";
promptUpdate();
function wrapper(props) {
return (
<ProLayout
@@ -27,6 +32,7 @@ function wrapper(props) {
</ProLayout>
);
}
function Title() {
return (
<div
@@ -39,4 +45,105 @@ function Title() {
</div>
)
}
function promptUpdate() {
let latest = '';
axios('https://1248.ink/spark/update', {
method: 'POST',
data: version
}).then(res => {
const data = res.data;
const locale = getLocale();
latest = data?.latest;
// if is the latest version, don't show update notification
if (!checkVersion(version.version, latest)) return;
let localCache = getLocalCache();
if (!shouldPrompt(localCache, latest)) return;
if (!data.versions[latest] || !data.versions[latest].message) return;
let message = data.versions[latest].message[locale];
if (!message.content) return;
notification.open({
key: 'update',
message: message.title ? <b>{message.title}</b> : undefined,
description: <UpdateNotice url={message.url} content={message.content}/>,
onClose: dismissUpdate,
duration: 0
});
}).catch(e => {
console.error(e);
});
function getLocalCache() {
let localCache = {};
let localRawCache = localStorage.getItem('updateCache');
if (localRawCache) {
try {
localCache = JSON.parse(localRawCache);
} catch (e) {
localCache = {};
}
}
localCache = Object.assign({
lastCheck: 0,
latestVersion: '0.0.0',
hasDismissed: false
}, localCache);
return localCache;
}
function checkVersion(current, latest) {
let latestVersion = parseInt(String(latest).replaceAll('.', ''));
let currentVersion = parseInt(String(current).replaceAll('.', ''));
return currentVersion < latestVersion;
}
function shouldPrompt(cache, latest) {
let should = true;
let now = Math.floor(Date.now() / 1000);
if (!checkVersion(cache.latestVersion, latest)) {
if (now - cache?.lastCheck < 7 * 86400) {
should = !cache.hasDismissed;
}
}
return should;
}
function dismissUpdate() {
notification.close('update');
let now = Math.floor(Date.now() / 1000);
localStorage.setItem('updateCache', JSON.stringify({
lastCheck: now,
latestVersion: latest,
dismissUpdate: true
}));
}
function UpdateNotice(props) {
return (
<>
<ReactMarkdown>
{props.content}
</ReactMarkdown>
<div style={{marginTop: '10px'}}>
<Button
type='primary'
onClick={() => {
window.open(props.url, '_blank');
notification.close('update');
}}
>
{i18n.t('COMMON.UPDATE_DETAILS')}
</Button>
<Button
style={{marginLeft: '10px'}}
onClick={dismissUpdate}
>
{i18n.t('COMMON.UPDATE_DISMISS')}
</Button>
</div>
</>
);
}
}
export default wrapper;

View File

@@ -0,0 +1,4 @@
{
"version": "$VERSION",
"commit": "$COMMIT"
}

View File

@@ -11,6 +11,9 @@
"COMMON.DISCONNECTED": "Session disconnected",
"COMMON.CONNECTION_FAILED": "Connection failed",
"COMMON.UPDATE_DETAILS": "View Details",
"COMMON.UPDATE_DISMISS": "Dismiss",
"COMMON.HOURS": "h",
"COMMON.MINUTES": "m",
"COMMON.COLON": ": ",
@@ -51,7 +54,7 @@
"EXPLORER.TITLE": "File Explorer",
"EXPLORER.FILE_NAME": "Name",
"EXPLORER.FILE_SIZE": "Size",
"EXPLORER.MODIFY_TIME": "Modify Time",
"EXPLORER.DATE_MODIFIED": "Date Modified",
"EXPLORER.FILE": "file",
"EXPLORER.FOLDER": "folder",
"EXPLORER.RELOAD": "Reload",
@@ -111,6 +114,16 @@
"TERMINAL.SESSION_CLOSED": "Terminal session closed",
"TERMINAL.SPECIAL_KEYS": "Special Keys",
"TERMINAL.FUNCTION_KEYS": "Function Keys",
"TERMINAL.ZMODEM_FILE_TOO_LARGE": "File exceeds the size limit (16MB)",
"TERMINAL.ZMODEM_TRANSFER_START": "File transfer started, please wait...",
"TERMINAL.ZMODEM_TRANSFER_FAILED": "File transfer failed",
"TERMINAL.ZMODEM_TRANSFER_SUCCESS": "File is transferred successfully",
"TERMINAL.ZMODEM_TRANSFER_REJECTED": "Transfer request has been rejected",
"TERMINAL.ZMODEM_UPLOADER_NO_FILE": "No file selected",
"TERMINAL.ZMODEM_UPLOADER_CALL_FAILED": "Failed to pull up file uploader",
"TERMINAL.ZMODEM_UPLOADER_CALL_TIMEOUT": "File selection timeout, please try again",
"TERMINAL.ZMODEM_UPLOADER_TIP": "File selector will open, if not, please click 'Select File' button",
"TERMINAL.ZMODEM_UPLOADER_WARNING": "If no file selected, please wait for 10 seconds to make session timeout",
"DESKTOP.TITLE": "Desktop",
"DESKTOP.CREATE_SESSION_FAILED": "Failed to create desktop session",

View File

@@ -6,6 +6,7 @@ const locales = {
'zh-CN': 'zh-CN',
};
const lang = navigator.language && navigator.language.length ? navigator.language : 'en';
const locale = locales[lang] || 'en';
let resources = {};
for (const locale in locales) {
@@ -24,6 +25,9 @@ i18n.init({
function getLang() {
return lang;
}
function getLocale() {
return locale;
}
export { getLang };
export { getLang, getLocale };
export default i18n;

View File

@@ -11,6 +11,9 @@
"COMMON.DISCONNECTED": "连接已断开",
"COMMON.CONNECTION_FAILED": "连接失败",
"COMMON.UPDATE_DETAILS": "详情",
"COMMON.UPDATE_DISMISS": "忽略",
"COMMON.HOURS": "小时",
"COMMON.MINUTES": "分钟",
"COMMON.COLON": "",
@@ -51,7 +54,7 @@
"EXPLORER.TITLE": "文件管理器",
"EXPLORER.FILE_NAME": "文件名",
"EXPLORER.FILE_SIZE": "大小",
"EXPLORER.MODIFY_TIME": "修改时间",
"EXPLORER.DATE_MODIFIED": "修改时间",
"EXPLORER.FILE": "文件",
"EXPLORER.FOLDER": "文件夹",
"EXPLORER.RELOAD": "刷新",
@@ -111,6 +114,16 @@
"TERMINAL.SESSION_CLOSED": "终端会话已关闭",
"TERMINAL.SPECIAL_KEYS": "特殊键",
"TERMINAL.FUNCTION_KEYS": "功能键",
"TERMINAL.ZMODEM_FILE_TOO_LARGE": "文件大小超出限制16MB",
"TERMINAL.ZMODEM_TRANSFER_START": "文件传输已开始,请稍等...",
"TERMINAL.ZMODEM_TRANSFER_FAILED": "文件传输失败",
"TERMINAL.ZMODEM_TRANSFER_SUCCESS": "文件传输完成",
"TERMINAL.ZMODEM_TRANSFER_REJECTED": "传输请求已被拒绝",
"TERMINAL.ZMODEM_UPLOADER_NO_FILE": "未选择文件",
"TERMINAL.ZMODEM_UPLOADER_CALL_FAILED": "文件上传组件调用失败",
"TERMINAL.ZMODEM_UPLOADER_CALL_TIMEOUT": "文件选择超时,请重试",
"TERMINAL.ZMODEM_UPLOADER_TIP": "文件选择器将会打开,如果没有,请手动点击 '选择文件' 按钮",
"TERMINAL.ZMODEM_UPLOADER_WARNING": "如果未选择文件请等待10秒直至会话超时",
"DESKTOP.TITLE": "桌面",
"DESKTOP.CREATE_SESSION_FAILED": "桌面会话创建失败",

View File

@@ -171,7 +171,7 @@ function overview(props) {
const tableRef = useRef();
useEffect(() => {
// Auto update is only available when all modal are closed.
// auto update is only available when all modal are closed.
if (!runner && !desktop && !procMgr && !explorer && !generate && !terminal) {
let id = setInterval(getData, 3000);
return () => {
@@ -405,12 +405,12 @@ function overview(props) {
/>
<Explorer
isWindows={isWindows}
visible={explorer}
open={explorer}
device={explorer}
onCancel={setExplorer.bind(null, false)}
/>
<ProcMgr
visible={procMgr}
open={procMgr}
device={procMgr}
onCancel={setProcMgr.bind(null, false)}
/>
@@ -420,12 +420,12 @@ function overview(props) {
onCancel={setRunner.bind(null, false)}
/>
<Desktop
visible={desktop}
open={desktop}
device={desktop}
onCancel={setDesktop.bind(null, false)}
/>
<Terminal
visible={terminal}
open={terminal}
device={terminal}
onCancel={setTerminal.bind(null, false)}
/>

View File

@@ -2,7 +2,6 @@ import axios from "axios";
import Qs from "qs";
import i18n, {getLang} from "../locale/locale";
import {message} from "antd";
import CryptoJS from "crypto-js";
let orderCompare;
try {
@@ -119,7 +118,7 @@ function catchBlobReq(err) {
}
}
function hex2buf(hex) {
function hex2ua(hex) {
if (typeof hex !== 'string') {
return new Uint8Array([]);
}
@@ -130,86 +129,43 @@ function hex2buf(hex) {
return new Uint8Array(list.map(byte => parseInt(byte, 16)));
}
function ab2str(buffer) {
const array = new Uint8Array(buffer);
let out, i, len, c;
let char2, char3;
out = "";
len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
out += String.fromCharCode(c);
break;
case 12:
case 13:
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}
return out;
function ua2hex(buf) {
let hexArr = Array.prototype.map.call(buf, bit => {
return ('00' + bit.toString(16)).slice(-2);
});
return hexArr.join('');
}
function ws2ua(wordArray) {
const l = wordArray.sigBytes;
const words = wordArray.words;
const result = new Uint8Array(l);
let i = 0, j = 0;
while (true) {
if (i === l)
break;
const w = words[j++];
result[i++] = (w & 0xff000000) >>> 24;
if (i === l)
break;
result[i++] = (w & 0x00ff0000) >>> 16;
if (i === l)
break;
result[i++] = (w & 0x0000ff00) >>> 8;
if (i === l)
break;
result[i++] = (w & 0x000000ff);
function str2ua(str) {
return new TextEncoder().encode(str);
}
return result;
function ua2str(buf) {
return new TextDecoder().decode(buf);
}
function hex2str(hex) {
return new TextDecoder().decode(hex2ua(hex));
}
function str2hex(str) {
return ua2hex(new TextEncoder().encode(str));
}
function encrypt(data, secret) {
let json = JSON.stringify(data);
json = CryptoJS.enc.Utf8.parse(json);
let encrypted = CryptoJS.AES.encrypt(json, secret, {
mode: CryptoJS.mode.CTR,
iv: secret,
padding: CryptoJS.pad.NoPadding
});
return ws2ua(encrypted.ciphertext);
let buf = data;
for (let i = 0; i < buf.length; i++) {
buf[i] ^= secret[i % secret.length];
}
return buf;
}
function decrypt(data, secret) {
data = CryptoJS.lib.WordArray.create(data);
let decrypted = CryptoJS.AES.encrypt(data, secret, {
mode: CryptoJS.mode.CTR,
iv: secret,
padding: CryptoJS.pad.NoPadding
});
return ab2str(ws2ua(decrypted.ciphertext).buffer);
data = new Uint8Array(data);
for (let i = 0; i < data.length; i++) {
data[i] ^= secret[i % secret.length];
}
return ua2str(data);
}
export {post, request, waitTime, formatSize, tsToTime, getBaseURL, genRandHex, translate, preventClose, catchBlobReq, hex2buf, ab2str, ws2ua, encrypt, decrypt, orderCompare};
export {post, request, waitTime, formatSize, tsToTime, getBaseURL, genRandHex, translate, preventClose, catchBlobReq, hex2ua, ua2hex, str2ua, ua2str, hex2str, str2hex, encrypt, decrypt, orderCompare};

124
web/src/vendors/zmodem.js/encode.js vendored Normal file
View File

@@ -0,0 +1,124 @@
"use strict";
var Zmodem = module.exports;
const HEX_DIGITS = [ 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102 ];
const HEX_OCTET_VALUE = {};
for (var hd=0; hd<HEX_DIGITS.length; hd++) {
HEX_OCTET_VALUE[ HEX_DIGITS[hd] ] = hd;
}
/**
* General, non-ZMODEM-specific encoding logic.
*
* @exports ENCODELIB
*/
Zmodem.ENCODELIB = {
/**
* Return an array with the given number as 2 big-endian bytes.
*
* @param {number} number - The number to encode.
*
* @returns {number[]} The octet values.
*/
pack_u16_be: function pack_u16_be(number) {
if (number > 0xffff) throw( "Number cannot exceed 16 bits: " + number )
return [ number >> 8, number & 0xff ];
},
/**
* Return an array with the given number as 4 little-endian bytes.
*
* @param {number} number - The number to encode.
*
* @returns {number[]} The octet values.
*/
pack_u32_le: function pack_u32_le(number) {
//Cant bit-shift because that runs into JSs bit-shift problem.
//(See _updcrc32() for an example.)
var high_bytes = number / 65536; //fraction is ok
//a little-endian 4-byte sequence
return [
number & 0xff,
(number & 65535) >> 8,
high_bytes & 0xff,
high_bytes >> 8,
];
},
/**
* The inverse of pack_u16_be() - i.e., take in 2 octet values
* and parse them as an unsigned, 2-byte big-endian number.
*
* @param {number[]} octets - The octet values (2 of them).
*
* @returns {number} The decoded number.
*/
unpack_u16_be: function unpack_u16_be(bytes_arr) {
return (bytes_arr[0] << 8) + bytes_arr[1];
},
/**
* The inverse of pack_u32_le() - i.e., take in a 4-byte sequence
* and parse it as an unsigned, 4-byte little-endian number.
*
* @param {number[]} octets - The octet values (4 of them).
*
* @returns {number} The decoded number.
*/
unpack_u32_le: function unpack_u32_le(octets) {
//<sigh> … (254 << 24 is -33554432, according to JavaScript)
return octets[0] + (octets[1] << 8) + (octets[2] << 16) + (octets[3] * 16777216);
},
/**
* Encode a series of octet values to be the octet values that
* correspond to the ASCII hex characters for each octet. The
* returned array is suitable for use as binary data.
*
* For example:
*
* Original Hex Returned
* 254 fe 102, 101
* 12 0c 48, 99
* 129 81 56, 49
*
* @param {number[]} octets - The original octet values.
*
* @returns {number[]} The octet values that correspond to an ASCII
* representation of the given octets.
*/
octets_to_hex: function octets_to_hex(octets) {
var hex = [];
for (var o=0; o<octets.length; o++) {
hex.push(
HEX_DIGITS[ octets[o] >> 4 ],
HEX_DIGITS[ octets[o] & 0x0f ]
);
}
return hex;
},
/**
* The inverse of octets_to_hex(): takes an array
* of hex octet pairs and returns their octet values.
*
* @param {number[]} hex_octets - The hex octet values.
*
* @returns {number[]} The parsed octet values.
*/
parse_hex_octets: function parse_hex_octets(hex_octets) {
var octets = new Array(hex_octets.length / 2);
for (var i=0; i<octets.length; i++) {
octets[i] = (HEX_OCTET_VALUE[ hex_octets[2 * i] ] << 4) + HEX_OCTET_VALUE[ hex_octets[1 + 2 * i] ];
}
return octets;
},
};

33
web/src/vendors/zmodem.js/text.js vendored Normal file
View File

@@ -0,0 +1,33 @@
class _my_TextEncoder {
encode(text) {
text = unescape(encodeURIComponent(text));
var bytes = new Array( text.length );
for (var b = 0; b < text.length; b++) {
bytes[b] = text.charCodeAt(b);
}
return new Uint8Array(bytes);
}
}
class _my_TextDecoder {
decode(bytes) {
return decodeURIComponent( escape( String.fromCharCode.apply(String, bytes) ) );
}
}
var Zmodem = module.exports;
/**
* A limited-use compatibility shim for TextEncoder and TextDecoder.
* Useful because both Edge and node.js still lack support for these
* as of October 2017.
*
* @exports Text
*/
Zmodem.Text = {
Encoder: (typeof TextEncoder !== "undefined") ? TextEncoder : _my_TextEncoder,
Decoder: (typeof TextDecoder !== "undefined") ? TextDecoder : _my_TextDecoder,
};

143
web/src/vendors/zmodem.js/zcrc.js vendored Normal file
View File

@@ -0,0 +1,143 @@
"use strict";
const CRC32_MOD = require('crc-32');
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zerror"),
require("./encode")
);
//----------------------------------------------------------------------
// BEGIN adapted from crc-js by Johannes Rudolph
var _crctab;
const
crc_width = 16,
crc_polynomial = 0x1021,
crc_castmask = 0xffff,
crc_msbmask = 1 << (crc_width - 1)
;
function _compute_crctab() {
_crctab = new Array(256);
var divident_shift = crc_width - 8;
for (var divident = 0; divident < 256; divident++) {
var currByte = (divident << divident_shift) & crc_castmask;
for (var bit = 0; bit < 8; bit++) {
if ((currByte & crc_msbmask) !== 0) {
currByte <<= 1;
currByte ^= crc_polynomial;
}
else {
currByte <<= 1;
}
}
_crctab[divident] = (currByte & crc_castmask);
}
}
// END adapted from crc-js by Johannes Rudolph
//----------------------------------------------------------------------
function _updcrc(cp, crc) {
if (!_crctab) _compute_crctab();
return(
_crctab[((crc >> 8) & 255)]
^ ((255 & crc) << 8)
^ cp
);
}
function __verify(expect, got) {
var err;
if ( expect.join() !== got.join() ) {
throw new Zmodem.Error("crc", got, expect);
}
}
//TODO: use external implementation(s)
Zmodem.CRC = {
//https://www.lammertbies.nl/comm/info/crc-calculation.html
//CRC-CCITT (XModem)
/**
* Deduce a given set of octet values CRC16, as per the CRC16
* variant that ZMODEM uses (CRC-CCITT/XModem).
*
* @param {Array} octets - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @returns {Array} crc - The CRC, expressed as an array of octet values.
*/
crc16: function crc16(octet_nums) {
var crc = octet_nums[0];
for (var b=1; b<octet_nums.length; b++) {
crc = _updcrc( octet_nums[b], crc );
}
crc = _updcrc( 0, _updcrc(0, crc) );
//a big-endian 2-byte sequence
return Zmodem.ENCODELIB.pack_u16_be(crc);
},
/**
* Deduce a given set of octet values CRC32.
*
* @param {Array} octets - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @returns {Array} crc - The CRC, expressed as an array of octet values.
*/
crc32: function crc32(octet_nums) {
return Zmodem.ENCODELIB.pack_u32_le(
CRC32_MOD.buf(octet_nums) >>> 0 //bit-shift to get unsigned
);
},
/**
* Verify a given set of octet values CRC16.
* An exception is thrown on failure.
*
* @param {Array} bytes_arr - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @param {Array} crc - The CRC to check against, expressed as
* an array of octet values.
*/
verify16: function verify16(bytes_arr, got) {
return __verify( this.crc16(bytes_arr), got );
},
/**
* Verify a given set of octet values CRC32.
* An exception is thrown on failure.
*
* @param {Array} bytes_arr - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @param {Array} crc - The CRC to check against, expressed as
* an array of octet values.
*/
verify32: function verify32(bytes_arr, crc) {
try {
__verify( this.crc32(bytes_arr), crc );
}
catch(err) {
err.input = bytes_arr.slice(0);
throw err;
}
},
};

240
web/src/vendors/zmodem.js/zdle.js vendored Normal file
View File

@@ -0,0 +1,240 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zmlib")
);
//encode() variables - declare them here so we dont
//create them in the function.
var encode_cur, encode_todo;
const ZDLE = Zmodem.ZMLIB.ZDLE;
/**
* Class that handles ZDLE encoding and decoding.
* Encoding is subject to a given configuration--specifically, whether
* we want to escape all control characters. Decoding is static; however
* a given string is encoded we can always decode it.
*/
Zmodem.ZDLE = class ZmodemZDLE {
/**
* Create a ZDLE encoder.
*
* @param {object} [config] - The initial configuration.
* @param {object} config.escape_ctrl_chars - Whether the ZDLE encoder
* should escape control characters.
*/
constructor(config) {
this._config = {};
if (config) {
this.set_escape_ctrl_chars(!!config.escape_ctrl_chars);
}
}
/**
* Enable or disable control-character escaping.
* You should probably enable this for sender sessions.
*
* @param {boolean} value - Whether to enable (true) or disable (false).
*/
set_escape_ctrl_chars(value) {
if (typeof value !== "boolean") throw "need boolean!";
if (value !== this._config.escape_ctrl_chars) {
this._config.escape_ctrl_chars = value;
this._setup_zdle_table();
}
}
/**
* Whether or not control-character escaping is enabled.
*
* @return {boolean} Whether the escaping is on (true) or off (false).
*/
escapes_ctrl_chars() {
return !!this._config.escape_ctrl_chars;
}
//I dont know of any Zmodem implementations that use ZESC8
//(“escape_8th_bit”)??
/*
ZMODEM software escapes ZDLE, 020, 0220, 021, 0221, 023, and 0223. If
preceded by 0100 or 0300 (@), 015 and 0215 are also escaped to protect the
Telenet command escape CR-@-CR.
*/
/**
* Encode an array of octet values and return it.
* This will mutate the given array.
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The passed-in array, transformed. This is the
* same object that is passed in.
*/
encode(octets) {
//NB: Performance matters here!
if (!this._zdle_table) throw "No ZDLE encode table configured!";
var zdle_table = this._zdle_table;
var last_code = this._lastcode;
var arrbuf = new ArrayBuffer( 2 * octets.length );
var arrbuf_uint8 = new Uint8Array(arrbuf);
var escctl_yn = this._config.escape_ctrl_chars;
var arrbuf_i = 0;
for (encode_cur=0; encode_cur<octets.length; encode_cur++) {
encode_todo = zdle_table[octets[encode_cur]];
if (!encode_todo) {
console.trace();
console.error("bad encode() call:", JSON.stringify(octets));
this._lastcode = last_code;
throw( "Invalid octet: " + octets[encode_cur] );
}
last_code = octets[encode_cur];
if (encode_todo === 1) {
//Do nothing; we append last_code below.
}
//0x40 = '@'; i.e., only escape if the last
//octet was '@'.
else if (escctl_yn || (encode_todo === 2) || ((last_code & 0x7f) === 0x40)) {
arrbuf_uint8[arrbuf_i] = ZDLE;
arrbuf_i++;
last_code ^= 0x40; //0100
}
arrbuf_uint8[arrbuf_i] = last_code;
arrbuf_i++;
}
this._lastcode = last_code;
octets.splice(0);
octets.push.apply(octets, new Uint8Array( arrbuf, 0, arrbuf_i ));
return octets;
}
/**
* Decode an array of octet values and return it.
* This will mutate the given array.
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The passed-in array.
* This is the same object that is passed in.
*/
static decode(octets) {
for (var o=octets.length-1; o>=0; o--) {
if (octets[o] === ZDLE) {
octets.splice( o, 2, octets[o+1] - 64 );
}
}
return octets;
}
/**
* Remove, ZDLE-decode, and return bytes from the passed-in array.
* If the requested number of ZDLE-encoded bytes isnt available,
* then the passed-in array is unmodified (and the return is undefined).
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @param {number} offset - The number of (undecoded) bytes to skip
* at the beginning of the “octets” array.
*
* @param {number} count - The number of bytes (octet values) to return.
*
* @returns {number[]|undefined} An array with the requested number of
* decoded octet values, or undefined if that number of decoded
* octets isnt available (given the passed-in offset).
*/
static splice(octets, offset, count) {
var so_far = 0;
if (!offset) offset = 0;
for (var i = offset; i<octets.length && so_far<count; i++) {
so_far++;
if (octets[i] === ZDLE) i++;
}
if (so_far === count) {
//Dont accept trailing ZDLE. This check works
//because of the i++ logic above.
if (octets.length === (i - 1)) return;
octets.splice(0, offset);
return ZmodemZDLE.decode( octets.splice(0, i - offset) );
}
return;
}
_setup_zdle_table() {
var zsendline_tab = new Array(256);
for (var i=0; i<zsendline_tab.length; i++) {
//1 = never escape
//2 = always escape
//3 = escape only if the previous byte was '@'
//Never escape characters from 0x20 (32) to 0x7f (127).
//This is the range of printable characters, plus DEL.
//I guess ZMODEM doesnt consider DEL to be a control character?
if ( i & 0x60 ) {
zsendline_tab[i] = 1;
}
else {
switch(i) {
case ZDLE: //NB: no (ZDLE | 0x80)
case Zmodem.ZMLIB.XOFF:
case Zmodem.ZMLIB.XON:
case (Zmodem.ZMLIB.XOFF | 0x80):
case (Zmodem.ZMLIB.XON | 0x80):
zsendline_tab[i] = 2;
break;
case 0x10: // 020
case 0x90: // 0220
zsendline_tab[i] = this._config.turbo_escape ? 1 : 2;
break;
case 0x0d: // 015
case 0x8d: // 0215
zsendline_tab[i] = this._config.escape_ctrl_chars ? 2 : !this._config.turbo_escape ? 3 : 1;
break;
default:
zsendline_tab[i] = this._config.escape_ctrl_chars ? 2 : 1;
}
}
}
this._zdle_table = zsendline_tab;
}
}

47
web/src/vendors/zmodem.js/zerror.js vendored Normal file
View File

@@ -0,0 +1,47 @@
"use strict";
var Zmodem = module.exports;
function _crc_message(got, expected) {
this.got = got.slice(0);
this.expected = expected.slice(0);
return "CRC check failed! (got: " + got.join() + "; expected: " + expected.join() + ")";
}
function _pass(val) { return val }
const TYPE_MESSAGE = {
aborted: "Session aborted",
peer_aborted: "Peer aborted session",
already_aborted: "Session already aborted",
crc: _crc_message,
validation: _pass,
};
function _generate_message(type) {
const msg = TYPE_MESSAGE[type];
switch (typeof msg) {
case "string":
return msg;
case "function":
var args_after_type = [].slice.call(arguments).slice(1);
return msg.apply(this, args_after_type);
}
return null;
}
Zmodem.Error = class ZmodemError extends Error {
constructor(msg_or_type) {
super();
var generated = _generate_message.apply(this, arguments);
if (generated) {
this.type = msg_or_type;
this.message = generated;
}
else {
this.message = msg_or_type;
}
}
};

763
web/src/vendors/zmodem.js/zheader.js vendored Normal file
View File

@@ -0,0 +1,763 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./encode"),
require("./zdle"),
require("./zmlib"),
require("./zcrc"),
require("./zerror")
);
const ZPAD = '*'.charCodeAt(0),
ZBIN = 'A'.charCodeAt(0),
ZHEX = 'B'.charCodeAt(0),
ZBIN32 = 'C'.charCodeAt(0)
;
//NB: lrzsz uses \x8a rather than \x0a where the specs
//say to use LF. For simplicity, we avoid that and just use
//the 7-bit LF character.
const HEX_HEADER_CRLF = [ 0x0d, 0x0a ];
const HEX_HEADER_CRLF_XON = HEX_HEADER_CRLF.slice(0).concat( [Zmodem.ZMLIB.XON] );
//These are more or less duplicated by the logic in trim_leading_garbage().
//
//"**" + ZDLE_CHAR + "B"
const HEX_HEADER_PREFIX = [ ZPAD, ZPAD, Zmodem.ZMLIB.ZDLE, ZHEX ];
const BINARY16_HEADER_PREFIX = [ ZPAD, Zmodem.ZMLIB.ZDLE, ZBIN ];
const BINARY32_HEADER_PREFIX = [ ZPAD, Zmodem.ZMLIB.ZDLE, ZBIN32 ];
/** Class that represents a ZMODEM header. */
Zmodem.Header = class ZmodemHeader {
//lrzszs “sz” command sends a random (?) CR/0x0d byte
//after ZEOF. Lets accommodate 0x0a, 0x0d, 0x8a, and 0x8d.
//
//Also, when you skip a file, sz outputs a message about it.
//
//It appears that were supposed to ignore anything until
//[ ZPAD, ZDLE ] when were looking for a header.
/**
* Weed out the leading bytes that arent valid to start a ZMODEM header.
*
* @param {number[]} ibuffer - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The octet values that were removed from the start
* of “ibuffer”. Order is preserved.
*/
static trim_leading_garbage(ibuffer) {
//Since theres no escaping of the output its possible
//that the garbage could trip us up, e.g., by having a filename
//be a legit ZMODEM header. But thats pretty unlikely.
//Everything up to the first ZPAD: garbage
//If first ZPAD has asterisk + ZDLE
var garbage = [];
var discard_all, parser, next_ZPAD_at_least = 0;
TRIM_LOOP:
while (ibuffer.length && !parser) {
var first_ZPAD = ibuffer.indexOf(ZPAD);
//No ZPAD? Then we purge the input buffer cuz its all garbage.
if (first_ZPAD === -1) {
discard_all = true;
break TRIM_LOOP;
}
else {
garbage.push.apply( garbage, ibuffer.splice(0, first_ZPAD) );
//buffer has only an asterisk … gotta see about more
if (ibuffer.length < 2) {
break TRIM_LOOP;
}
else if (ibuffer[1] === ZPAD) {
//Two leading ZPADs should be a hex header.
//Were assuming the length of the header is 4 in
//this logic … but ZMODEM isnt likely to change, so.
if (ibuffer.length < HEX_HEADER_PREFIX.length) {
if (ibuffer.join() === HEX_HEADER_PREFIX.slice(0, ibuffer.length).join()) {
//We have an incomplete fragment that matches
//HEX_HEADER_PREFIX. So dont trim any more.
break TRIM_LOOP;
}
//Otherwise, well discard one.
}
else if ((ibuffer[2] === HEX_HEADER_PREFIX[2]) && (ibuffer[3] === HEX_HEADER_PREFIX[3])) {
parser = _parse_hex;
}
}
else if (ibuffer[1] === Zmodem.ZMLIB.ZDLE) {
//ZPAD + ZDLE should be a binary header.
if (ibuffer.length < BINARY16_HEADER_PREFIX.length) {
break TRIM_LOOP;
}
if (ibuffer[2] === BINARY16_HEADER_PREFIX[2]) {
parser = _parse_binary16;
}
else if (ibuffer[2] === BINARY32_HEADER_PREFIX[2]) {
parser = _parse_binary32;
}
}
if (!parser) {
garbage.push( ibuffer.shift() );
}
}
}
if (discard_all) {
garbage.push.apply( garbage, ibuffer.splice(0) );
}
//For now well throw away the parser.
//Its not hard for parse() to discern anyway.
return garbage;
}
/**
* Parse out a Header object from a given array of octet values.
*
* An exception is thrown if the given bytes are definitively invalid
* as header values.
*
* @param {number[]} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {Header|undefined} An instance of the appropriate Header
* subclass, or undefined if not enough octet values are given
* to determine whether there is a valid header here or not.
*/
static parse(octets) {
var hdr;
if (octets[1] === ZPAD) {
hdr = _parse_hex(octets);
return hdr && [ hdr, 16 ];
}
else if (octets[2] === ZBIN) {
hdr = _parse_binary16(octets, 3);
return hdr && [ hdr, 16 ];
}
else if (octets[2] === ZBIN32) {
hdr = _parse_binary32(octets);
return hdr && [ hdr, 32 ];
}
if (octets.length < 3) return;
throw( "Unrecognized/unsupported octets: " + octets.join() );
}
/**
* Build a Header subclass given a name and arguments.
*
* @param {string} name - The header type name, e.g., “ZRINIT”.
*
* @param {...*} args - The arguments to pass to the appropriate
* subclass constructor. These arent documented currently
* but are pretty easy to glean from the code.
*
* @returns {Header} An instance of the appropriate Header subclass.
*/
static build(name /*, args */) {
var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));
//TODO: make this better
var Ctr = FRAME_NAME_CREATOR[name];
if (!Ctr) throw("No frame class “" + name + "” is defined!");
args.shift();
//Plegh!
//https://stackoverflow.com/questions/33193310/constr-applythis-args-in-es6-classes
var hdr = new (Ctr.bind.apply(Ctr, [null].concat(args)));
return hdr;
}
/**
* Return the octet values array that represents the object
* in ZMODEM hex encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
to_hex() {
var to_crc = this._crc_bytes();
return HEX_HEADER_PREFIX.concat(
Zmodem.ENCODELIB.octets_to_hex( to_crc.concat( Zmodem.CRC.crc16(to_crc) ) ),
this._hex_header_ending
);
}
/**
* Return the octet values array that represents the object
* in ZMODEM binary encoding with a 16-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for
* ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
to_binary16(zencoder) {
return this._to_binary(zencoder, BINARY16_HEADER_PREFIX, Zmodem.CRC.crc16);
}
/**
* Return the octet values array that represents the object
* in ZMODEM binary encoding with a 32-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for
* ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
to_binary32(zencoder) {
return this._to_binary(zencoder, BINARY32_HEADER_PREFIX, Zmodem.CRC.crc32);
}
//This is never called directly, but only as super().
constructor() {
if (!this._bytes4) {
this._bytes4 = [0, 0, 0, 0];
}
}
_to_binary(zencoder, prefix, crc_func) {
var to_crc = this._crc_bytes();
//Both the 4-byte payload and the CRC bytes are ZDLE-encoded.
var octets = prefix.concat(
zencoder.encode( to_crc.concat( crc_func(to_crc) ) )
);
return octets;
}
_crc_bytes() {
return [ this.TYPENUM ].concat(this._bytes4);
}
}
Zmodem.Header.prototype._hex_header_ending = HEX_HEADER_CRLF_XON;
class ZRQINIT_HEADER extends Zmodem.Header {};
//----------------------------------------------------------------------
const ZRINIT_FLAG = {
//----------------------------------------------------------------------
// Bit Masks for ZRINIT flags byte ZF0
//----------------------------------------------------------------------
CANFDX: 0x01, // Rx can send and receive true FDX
CANOVIO: 0x02, // Rx can receive data during disk I/O
CANBRK: 0x04, // Rx can send a break signal
CANCRY: 0x08, // Receiver can decrypt -- nothing does this
CANLZW: 0x10, // Receiver can uncompress -- nothing does this
CANFC32: 0x20, // Receiver can use 32 bit Frame Check
ESCCTL: 0x40, // Receiver expects ctl chars to be escaped
ESC8: 0x80, // Receiver expects 8th bit to be escaped
};
function _get_ZRINIT_flag_num(fl) {
if (!ZRINIT_FLAG[fl]) {
throw new Zmodem.Error("Invalid ZRINIT flag: " + fl);
}
return ZRINIT_FLAG[fl];
}
class ZRINIT_HEADER extends Zmodem.Header {
constructor(flags_arr, bufsize) {
super();
var flags_num = 0;
if (!bufsize) bufsize = 0;
flags_arr.forEach( function(fl) {
flags_num |= _get_ZRINIT_flag_num(fl);
} );
this._bytes4 = [
bufsize & 0xff,
bufsize >> 8,
0,
flags_num,
];
}
//undefined if nonstop I/O is allowed
get_buffer_size() {
return Zmodem.ENCODELIB.unpack_u16_be( this._bytes4.slice(0, 2) ) || undefined;
}
//Unimplemented:
// can_decrypt
// can_decompress
//----------------------------------------------------------------------
//function names taken from Jacques Mattheijs implementation,
//as used in syncterm.
can_full_duplex() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANFDX );
}
can_overlap_io() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANOVIO );
}
can_break() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANBRK );
}
can_fcs_32() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANFC32 );
}
escape_ctrl_chars() {
return !!( this._bytes4[3] & ZRINIT_FLAG.ESCCTL );
}
//Is this used? I dont see it used in lrzsz or syncterm
//Looks like it was a “foreseen” feature that Forsberg
//never implemented. (The need for it went away, maybe?)
escape_8th_bit() {
return !!( this._bytes4[3] & ZRINIT_FLAG.ESC8 );
}
};
//----------------------------------------------------------------------
//Since context makes clear whats going on, we use these
//rather than the T-prefixed constants in the specification.
const ZSINIT_FLAG = {
ESCCTL: 0x40, // Transmitter will escape ctl chars
ESC8: 0x80, // Transmitter will escape 8th bit
};
function _get_ZSINIT_flag_num(fl) {
if (!ZSINIT_FLAG[fl]) {
throw("Invalid ZSINIT flag: " + fl);
}
return ZSINIT_FLAG[fl];
}
class ZSINIT_HEADER extends Zmodem.Header {
constructor( flags_arr, attn_seq_arr ) {
super();
var flags_num = 0;
flags_arr.forEach( function(fl) {
flags_num |= _get_ZSINIT_flag_num(fl);
} );
this._bytes4 = [ 0, 0, 0, flags_num ];
if (attn_seq_arr) {
if (attn_seq_arr.length > 31) {
throw("Attn sequence must be <= 31 bytes");
}
if (attn_seq_arr.some( function(num) { return num > 255 } )) {
throw("Attn sequence (" + attn_seq_arr + ") must be <256");
}
this._data = attn_seq_arr.concat([0]);
}
}
escape_ctrl_chars() {
return !!( this._bytes4[3] & ZSINIT_FLAG.ESCCTL );
}
//Is this used? I dont see it used in lrzsz or syncterm
escape_8th_bit() {
return !!( this._bytes4[3] & ZSINIT_FLAG.ESC8 );
}
}
//Thus far it doesnt seem we really need this header except to respond
//to ZSINIT, which doesnt require a payload.
class ZACK_HEADER extends Zmodem.Header {
constructor(payload4) {
super();
if (payload4) {
this._bytes4 = payload4.slice();
}
}
}
ZACK_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;
//----------------------------------------------------------------------
const ZFILE_VALUES = {
//ZF3 (i.e., first byte)
extended: {
sparse: 0x40, //ZXSPARS
},
//ZF2
transport: [
undefined,
"compress", //ZTLZW
"encrypt", //ZTCRYPT
"rle", //ZTRLE
],
//ZF1
management: [
undefined,
"newer_or_longer", //ZF1_ZMNEWL
"crc", //ZF1_ZMCRC
"append", //ZF1_ZMAPND
"clobber", //ZF1_ZMCLOB
"newer", //ZF1_ZMNEW
"mtime_or_length", //ZF1_ZMNEW
"protect", //ZF1_ZMPROT
"rename", //ZF1_ZMPROT
],
//ZF0 (i.e., last byte)
conversion: [
undefined,
"binary", //ZCBIN
"text", //ZCNL
"resume", //ZCRESUM
],
};
const ZFILE_ORDER = ["extended", "transport", "management", "conversion"];
const ZMSKNOLOC = 0x80,
MANAGEMENT_MASK = 0x1f,
ZXSPARS = 0x40
;
class ZFILE_HEADER extends Zmodem.Header {
//TODO: allow options on instantiation
get_options() {
var opts = {
sparse: !!(this._bytes4[0] & ZXSPARS),
};
var bytes_copy = this._bytes4.slice(0);
ZFILE_ORDER.forEach( function(key, i) {
if (ZFILE_VALUES[key] instanceof Array) {
if (key === "management") {
opts.skip_if_absent = !!(bytes_copy[i] & ZMSKNOLOC);
bytes_copy[i] &= MANAGEMENT_MASK;
}
opts[key] = ZFILE_VALUES[key][ bytes_copy[i] ];
}
else {
for (var extkey in ZFILE_VALUES[key]) {
opts[extkey] = !!(bytes_copy[i] & ZFILE_VALUES[key][extkey]);
if (opts[extkey]) {
bytes_copy[i] ^= ZFILE_VALUES[key][extkey]
}
}
}
if (!opts[key] && bytes_copy[i]) {
opts[key] = "unknown:" + bytes_copy[i];
}
} );
return opts;
}
}
//----------------------------------------------------------------------
//Empty headers - in addition to ZRQINIT
class ZSKIP_HEADER extends Zmodem.Header {}
//No need for ZNAK
class ZABORT_HEADER extends Zmodem.Header {}
class ZFIN_HEADER extends Zmodem.Header {}
class ZFERR_HEADER extends Zmodem.Header {}
ZFIN_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;
class ZOffsetHeader extends Zmodem.Header {
constructor(offset) {
super();
this._bytes4 = Zmodem.ENCODELIB.pack_u32_le(offset);
}
get_offset() {
return Zmodem.ENCODELIB.unpack_u32_le(this._bytes4);
}
}
class ZRPOS_HEADER extends ZOffsetHeader {};
class ZDATA_HEADER extends ZOffsetHeader {};
class ZEOF_HEADER extends ZOffsetHeader {};
//As request, receiver creates.
/* UNIMPLEMENTED FOR NOW
class ZCRC_HEADER extends ZHeader {
constructor(crc_le_bytes) {
super();
if (crc_le_bytes) { //response, sender creates
this._bytes4 = crc_le_bytes;
}
}
}
*/
//No ZCHALLENGE implementation
//class ZCOMPL_HEADER extends ZHeader {}
//class ZCAN_HEADER extends Zmodem.Header {}
//As described, this header represents an information disclosure.
//It could be interpreted, I suppose, merely as “this is how much space
//I have FOR YOU.”
//TODO: implement if needed/requested
//class ZFREECNT_HEADER extends ZmodemHeader {}
//----------------------------------------------------------------------
const FRAME_CLASS_TYPES = [
[ ZRQINIT_HEADER, "ZRQINIT" ],
[ ZRINIT_HEADER, "ZRINIT" ],
[ ZSINIT_HEADER, "ZSINIT" ],
[ ZACK_HEADER, "ZACK" ],
[ ZFILE_HEADER, "ZFILE" ],
[ ZSKIP_HEADER, "ZSKIP" ],
undefined, // [ ZNAK_HEADER, "ZNAK" ],
[ ZABORT_HEADER, "ZABORT" ],
[ ZFIN_HEADER, "ZFIN" ],
[ ZRPOS_HEADER, "ZRPOS" ],
[ ZDATA_HEADER, "ZDATA" ],
[ ZEOF_HEADER, "ZEOF" ],
[ ZFERR_HEADER, "ZFERR" ], //see note
undefined, //[ ZCRC_HEADER, "ZCRC" ],
undefined, //[ ZCHALLENGE_HEADER, "ZCHALLENGE" ],
undefined, //[ ZCOMPL_HEADER, "ZCOMPL" ],
undefined, //[ ZCAN_HEADER, "ZCAN" ],
undefined, //[ ZFREECNT_HEADER, "ZFREECNT" ],
undefined, //[ ZCOMMAND_HEADER, "ZCOMMAND" ],
undefined, //[ ZSTDERR_HEADER, "ZSTDERR" ],
];
/*
ZFERR is described as “error in reading or writing file”. Its really
not a good idea from a security angle for the endpoint to expose this
information. We should parse this and handle it as ZABORT but never send it.
Likewise with ZFREECNT: the sender shouldnt ask how much space is left
on the other box; rather, the receiver should decide what to do with the
file size as the sender reports it.
*/
var FRAME_NAME_CREATOR = {};
for (var fc=0; fc<FRAME_CLASS_TYPES.length; fc++) {
if (!FRAME_CLASS_TYPES[fc]) continue;
FRAME_NAME_CREATOR[ FRAME_CLASS_TYPES[fc][1] ] = FRAME_CLASS_TYPES[fc][0];
Object.assign(
FRAME_CLASS_TYPES[fc][0].prototype,
{
TYPENUM: fc,
NAME: FRAME_CLASS_TYPES[fc][1],
}
);
}
//----------------------------------------------------------------------
const CREATORS = [
ZRQINIT_HEADER,
ZRINIT_HEADER,
ZSINIT_HEADER,
ZACK_HEADER,
ZFILE_HEADER,
ZSKIP_HEADER,
'ZNAK',
ZABORT_HEADER,
ZFIN_HEADER,
ZRPOS_HEADER,
ZDATA_HEADER,
ZEOF_HEADER,
ZFERR_HEADER,
'ZCRC', //ZCRC_HEADER, -- leaving unimplemented?
'ZCHALLENGE',
'ZCOMPL',
'ZCAN',
'ZFREECNT', // ZFREECNT_HEADER,
'ZCOMMAND',
'ZSTDERR',
];
function _get_blank_header(typenum) {
var creator = CREATORS[typenum];
if (typeof(creator) === "string") {
throw( "Received unsupported header: " + creator );
}
/*
if (creator === ZCRC_HEADER) {
return new creator([0, 0, 0, 0]);
}
*/
return _get_blank_header_from_constructor(creator);
}
//referenced outside TODO
function _get_blank_header_from_constructor(creator) {
if (creator.prototype instanceof ZOffsetHeader) {
return new creator(0);
}
return new creator([]);
}
function _parse_binary16(bytes_arr) {
//The max length of a ZDLE-encoded binary header w/ 16-bit CRC is:
// 3 initial bytes, NOT ZDLE-encoded
// 2 typenum bytes (1 decoded)
// 8 data bytes (4 decoded)
// 4 CRC bytes (2 decoded)
//A 16-bit payload has 7 ZDLE-encoded octets.
//The ZDLE-encoded octets follow the initial prefix.
var zdle_decoded = Zmodem.ZDLE.splice( bytes_arr, BINARY16_HEADER_PREFIX.length, 7 );
return zdle_decoded && _parse_non_zdle_binary16(zdle_decoded);
}
function _parse_non_zdle_binary16(decoded) {
Zmodem.CRC.verify16(
decoded.slice(0, 5),
decoded.slice(5)
);
var typenum = decoded[0];
var hdr = _get_blank_header(typenum);
hdr._bytes4 = decoded.slice( 1, 5 );
return hdr;
}
function _parse_binary32(bytes_arr) {
//Same deal as with 16-bit CRC except there are two more
//potentially ZDLE-encoded bytes, for a total of 9.
var zdle_decoded = Zmodem.ZDLE.splice(
bytes_arr, //omit the leading "*", ZDLE, and "C"
BINARY32_HEADER_PREFIX.length,
9
);
if (!zdle_decoded) return;
Zmodem.CRC.verify32(
zdle_decoded.slice(0, 5),
zdle_decoded.slice(5)
);
var typenum = zdle_decoded[0];
var hdr = _get_blank_header(typenum);
hdr._bytes4 = zdle_decoded.slice( 1, 5 );
return hdr;
}
function _parse_hex(bytes_arr) {
//A hex header always has:
// 4 bytes for the ** . ZDLE . 'B'
// 2 hex bytes for the header type
// 8 hex bytes for the header content
// 4 hex bytes for the CRC
// 1-2 bytes for (CR/)LF
// (...and at this point the trailing XON is already stripped)
//
//----------------------------------------------------------------------
//A carriage return and line feed are sent with HEX headers. The
//receive routine expects to see at least one of these characters, two
//if the first is CR.
//----------------------------------------------------------------------
//
//^^ I guess it can be either CR/LF or just LF … though those two
//sentences appear to be saying contradictory things.
var lf_pos = bytes_arr.indexOf( 0x8a ); //lrzsz sends this
if (-1 === lf_pos) {
lf_pos = bytes_arr.indexOf( 0x0a );
}
var hdr_err, hex_bytes;
if (-1 === lf_pos) {
if (bytes_arr.length > 11) {
hdr_err = "Invalid hex header - no LF detected within 12 bytes!";
}
//incomplete header
return;
}
else {
hex_bytes = bytes_arr.splice( 0, lf_pos );
//Trim off the LF
bytes_arr.shift();
if ( hex_bytes.length === 19 ) {
//NB: The spec says CR but seems to treat high-bit variants
//of control characters the same as the regulars; should we
//also allow 0x8d?
var preceding = hex_bytes.pop();
if ( preceding !== 0x0d && preceding !== 0x8d ) {
hdr_err = "Invalid hex header: (CR/)LF doesnt have CR!";
}
}
else if ( hex_bytes.length !== 18 ) {
hdr_err = "Invalid hex header: invalid number of bytes before LF!";
}
}
if (hdr_err) {
hdr_err += " (" + hex_bytes.length + " bytes: " + hex_bytes.join() + ")";
throw hdr_err;
}
hex_bytes.splice(0, 4);
//Should be 7 bytes ultimately:
// 1 for typenum
// 4 for header data
// 2 for CRC
var octets = Zmodem.ENCODELIB.parse_hex_octets(hex_bytes);
return _parse_non_zdle_binary16(octets);
}
Zmodem.Header.parse_hex = _parse_hex;

102
web/src/vendors/zmodem.js/zmlib.js vendored Normal file
View File

@@ -0,0 +1,102 @@
"use strict";
var Zmodem = module.exports;
const
ZDLE = 0x18,
XON = 0x11,
XOFF = 0x13,
XON_HIGH = 0x80 | XON,
XOFF_HIGH = 0x80 | XOFF,
CAN = 0x18 //NB: same character as ZDLE
;
/**
* Tools and constants that are useful for ZMODEM.
*
* @exports ZMLIB
*/
Zmodem.ZMLIB = {
/**
* @property {number} The ZDLE constant, which ZMODEM uses for escaping
*/
ZDLE: ZDLE,
/**
* @property {number} XON - ASCII XON
*/
XON: XON,
/**
* @property {number} XOFF - ASCII XOFF
*/
XOFF: XOFF,
/**
* @property {number[]} ABORT_SEQUENCE - ZMODEMs abort sequence
*/
ABORT_SEQUENCE: [ CAN, CAN, CAN, CAN, CAN ],
/**
* Remove octet values from the given array that ZMODEM always ignores.
* This will mutate the given array.
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The passed-in array. This is the same object that is
* passed in.
*/
strip_ignored_bytes: function strip_ignored_bytes(octets) {
for (var o=octets.length-1; o>=0; o--) {
switch (octets[o]) {
case XON:
case XON_HIGH:
case XOFF:
case XOFF_HIGH:
octets.splice(o, 1);
continue;
}
}
return octets;
},
/**
* Like Array.prototype.indexOf, but searches for a subarray
* rather than just a particular value.
*
* @param {Array} haystack - The array to search, i.e., the bigger.
*
* @param {Array} needle - The array whose values to find,
* i.e., the smaller.
*
* @returns {number} The position in “haystack” where “needle”
* first appears—or, -1 if “needle” doesnt appear anywhere
* in “haystack”.
*/
find_subarray: function find_subarray(haystack, needle) {
var h=0, n;
var start = Date.now();
HAYSTACK:
while (h !== -1) {
h = haystack.indexOf( needle[0], h );
if (h === -1) break HAYSTACK;
for (n=1; n<needle.length; n++) {
if (haystack[h + n] !== needle[n]) {
h++;
continue HAYSTACK;
}
}
return h;
}
return -1;
},
};

5
web/src/vendors/zmodem.js/zmodem.js vendored Normal file
View File

@@ -0,0 +1,5 @@
Object.assign(
module.exports,
require("./zsentry"),
require("./zmodem_browser"),
);

View File

@@ -0,0 +1,180 @@
"use strict";
var Zmodem = module.exports;
//TODO: Make this usable without require.js or what not.
window.Zmodem = Zmodem;
Object.assign(
Zmodem,
require("./zmodem")
);
function _check_aborted(session) {
if (session.aborted()) {
throw new Zmodem.Error("aborted");
}
}
/** Browser-specific tools
*
* @exports Browser
*/
Zmodem.Browser = {
/**
* Send a batch of files in sequence. The session is left open
* afterward, which allows for more files to be sent if desired.
*
* @param {Zmodem.Session} session - The send session
*
* @param {FileList|Array} files - A list of File objects
*
* @param {Object} [options]
* @param {Function} [options.on_offer_response] - Called when an
* offer response arrives. Arguments are:
*
* - (File) - The File object that corresponds to the offer.
* - (Transfer|undefined) - If the receiver accepts the offer, then
* this is a Transfer object; otherwise its undefined.
*
* @param {Function} [options.on_progress] - Called immediately
* after a chunk of a file is sent. Arguments are:
*
* - (File) - The File object that corresponds to the file.
* - (Transfer) - The Transfer object for the current transfer.
* - (Uint8Array) - The chunk of data that was just loaded from disk
* and sent to the receiver.
*
* @param {Function} [options.on_file_complete] - Called immediately
* after the last file packet is sent. Arguments are:
*
* - (File) - The File object that corresponds to the file.
* - (Transfer) - The Transfer object for the now-completed transfer.
*
* @return {Promise} A Promise that fulfills when the batch is done.
* Note that skipped files are not considered an error condition.
*/
send_files: function send_files(session, files, options) {
if (!options) options = {};
//Populate the batch in reverse order to simplify sending
//the remaining files/bytes components.
var batch = [];
var total_size = 0;
for (var f=files.length - 1; f>=0; f--) {
var fobj = files[f];
total_size += fobj.size;
batch[f] = {
obj: fobj,
name: fobj.name,
size: fobj.size,
mtime: new Date(fobj.lastModified),
files_remaining: files.length - f,
bytes_remaining: total_size,
};
}
var file_idx = 0;
function promise_callback() {
var cur_b = batch[file_idx];
if (!cur_b) {
return Promise.resolve(); //batch done!
}
file_idx++;
return session.send_offer(cur_b).then( function after_send_offer(xfer) {
if (options.on_offer_response) {
options.on_offer_response(cur_b.obj, xfer);
}
if (xfer === undefined) {
return promise_callback(); //skipped
}
return new Promise( function(res) {
var reader = new FileReader();
//This really shouldnt happen … so lets
//blow up if it does.
reader.onerror = function reader_onerror(e) {
console.error("file read error", e);
throw("File read error: " + e);
};
var piece;
reader.onprogress = function reader_onprogress(e) {
//Some browsers (e.g., Chrome) give partial returns,
//while others (e.g., Firefox) dont.
if (e.target.result) {
piece = new Uint8Array(e.target.result, xfer.get_offset())
_check_aborted(session);
xfer.send(piece);
if (options.on_progress) {
options.on_progress(cur_b.obj, xfer, piece);
}
}
};
reader.onload = function reader_onload(e) {
piece = new Uint8Array(e.target.result, xfer, piece)
_check_aborted(session);
xfer.end(piece).then( function() {
if (options.on_progress && piece.length) {
options.on_progress(cur_b.obj, xfer, piece);
}
if (options.on_file_complete) {
options.on_file_complete(cur_b.obj, xfer);
}
//Resolve the current file-send promise with
//another promise. That promise resolves immediately
//if were done, or with another file-send promise
//if theres more to send.
res( promise_callback() );
} );
};
reader.readAsArrayBuffer(cur_b.obj);
} );
} );
}
return promise_callback();
},
/**
* Prompt a user to save the given packets as a file by injecting an
* `<a>` element (with `display: none` styling) into the page and
* calling the elements `click()`
* method. The element is removed immediately after.
*
* @param {Array} packets - Same as the first argument to [Blobs constructor](https://developer.mozilla.org/en-US/docs/Web/API/Blob).
* @param {string} name - The name to give the file.
*/
save_to_disk: function save_to_disk(packets, name) {
var blob = new Blob(packets);
var url = URL.createObjectURL(blob);
var el = document.createElement("a");
el.style.display = "none";
el.href = url;
el.download = name;
//It seems like a security problem that this actually works;
//Id think there would need to be some confirmation before
//a browser could save arbitrarily many bytes onto the disk.
//But, hey.
el.click();
setTimeout(() => URL.revokeObjectURL(url), 10000);
},
};

394
web/src/vendors/zmodem.js/zsentry.js vendored Normal file
View File

@@ -0,0 +1,394 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zmlib"),
require("./zsession")
);
const
MIN_ZM_HEX_START_LENGTH = 20,
MAX_ZM_HEX_START_LENGTH = 21,
// **, ZDLE, 'B0'
//ZRQINITs next byte will be '0'; ZRINITs will be '1'.
COMMON_ZM_HEX_START = [ 42, 42, 24, 66, 48 ],
SENTRY_CONSTRUCTOR_REQUIRED_ARGS = [
"to_terminal",
"on_detect",
"on_retract",
"sender",
],
ASTERISK = 42
;
/**
* An instance of this object is passed to the Sentrys on_detect
* callback each time the Sentry object sees what looks like the
* start of a ZMODEM session.
*
* Note that it is possible for a detection to be “retracted”
* if the Sentry consumes bytes afterward that are not ZMODEM.
* When this happens, the Sentrys `retract` event will fire,
* after which the Detection object is no longer usable.
*/
class Detection {
/**
* Not called directly.
*/
constructor(session_type, accepter, denier, checker) {
//confirm() - user confirms that ZMODEM is desired
this._confirmer = accepter;
//deny() - user declines ZMODEM; send abort sequence
//
//TODO: It might be ideal to forgo the session “peaceably”,
//i.e., such that the peer doesnt end in error. Thats
//possible if were the sender, we accept the session,
//then we just send a close(), but it doesnt seem to be
//possible for a receiver. Thus, lets just leave it so
//its at least consistent (and simpler, too).
this._denier = denier;
this._is_valid = checker;
this._session_type = session_type;
}
/**
* Confirm that the detected ZMODEM sequence indicates the
* start of a ZMODEM session.
*
* @return {Session} The ZMODEM Session object (i.e., either a
* Send or Receive instance).
*/
confirm() {
return this._confirmer.apply(this, arguments);
}
/**
* Tell the Sentry that the detected bytes sequence is
* **NOT** intended to be the start of a ZMODEM session.
*/
deny() {
return this._denier.apply(this, arguments);
}
/**
* Tells whether the Detection is still valid; i.e., whether
* the Sentry has `consume()`d bytes that invalidate the
* Detection.
*
* @returns {boolean} Whether the Detection is valid.
*/
is_valid() {
return this._is_valid.apply(this, arguments);
}
/**
* Gives the sessions role.
*
* @returns {string} One of:
* - `receive`
* - `send`
*/
get_session_role() { return this._session_type }
}
/**
* Class that parses an input stream for the beginning of a
* ZMODEM session. We look for the tell-tale signs
* of a ZMODEM transfer and allow the client to determine whether
* its really ZMODEM or not.
*
* This is the “mother” class for zmodem.js;
* all other class instances are created, directly or indirectly,
* by an instance of this class.
*
* This logic is not unlikely to need tweaking, and it can never
* be fully bulletproof; if it could be bulletproof it would be
* simpler since there wouldnt need to be the .confirm()/.deny()
* step.
*
* One thing you could do to make things a bit simpler *is* just
* to make that assumption for your users--i.e., to .confirm()
* Detection objects automatically. Thatll be one less step
* for the user, but an unaccustomed user might find that a bit
* confusing. Its also then possible to have a “false positive”:
* a text stream that contains a ZMODEM initialization string but
* isnt, in fact, meant to start a ZMODEM session.
*
* Workflow:
* - parse all input with .consume(). As long as nothing looks
* like ZMODEM, all the traffic will go to to_terminal().
*
* - when a “tell-tale” sequence of bytes arrives, we create a
* Detection object and pass it to the “on_detect” handler.
*
* - Either .confirm() or .deny() with the Detection object.
* This is the users chance to say, “yeah, I know those
* bytes look like ZMODEM, but theyre not. So back off!”
*
* If you .confirm(), the Session object is returned, and
* further input that goes to the Sentrys .consume() will
* go to the (now-active) Session object.
*
* - Sometimes additional traffic arrives that makes it apparent
* that no ZMODEM session is intended to start; in this case,
* the Sentry marks the Detection as “stale” and calls the
* `on_retract` handler. Any attempt from here to .confirm()
* on the Detection object will prompt an exception.
*
* (This “retraction” behavior will only happen prior to
* .confirm() or .deny() being called on the Detection object.
* Beyond that point, either the Session has to deal with the
* “garbage”, or its back to the terminal anyway.
*
* - Once the Session object is done, the Sentry will again send
* all traffic to to_terminal().
*/
Zmodem.Sentry = class ZmodemSentry {
/**
* Invoked directly. Creates a new Sentry that inspects all
* traffic before it goes to the terminal.
*
* @param {Object} options - The Sentry parameters
*
* @param {Function} options.to_terminal - Handler that sends
* traffic to the terminal object. Receives an iterable object
* (e.g., an Array) that contains octet numbers.
*
* @param {Function} options.on_detect - Handler for new
* detection events. Receives a new Detection object.
*
* @param {Function} options.on_retract - Handler for retraction
* events. Receives no input.
*
* @param {Function} options.sender - Handler that sends traffic to
* the peer. If, for example, your application uses WebSocket to talk
* to the peer, use this to send data to the WebSocket instance.
*/
constructor(options) {
if (!options) throw "Need options!";
var sentry = this;
SENTRY_CONSTRUCTOR_REQUIRED_ARGS.forEach( function(arg) {
if (!options[arg]) {
throw "Need “" + arg + "”!";
}
sentry["_" + arg] = options[arg];
} );
this._cache = [];
}
_after_session_end() {
this._zsession = null;
}
/**
* “Consumes” a piece of input:
*
* - If there is no active or pending ZMODEM session, the text is
* all output. (This is regardless of whether weve got a new
* Detection.)
*
* - If there is no active ZMODEM session and the input **ends** with
* a ZRINIT or ZRQINIT, then a new Detection object is created,
* and it is passed to the “on_detect” function.
* If there was another pending Detection object, it is retracted.
*
* - If there is no active ZMODEM session and the input does NOT end
* with a ZRINIT or ZRQINIT, then any pending Detection object is
* retracted.
*
* - If there is an active ZMODEM session, the input is passed to it.
* Any non-ZMODEM data (i.e., “garbage”) parsed from the input
* is sent to output.
* If the ZMODEM session ends, any post-ZMODEM part of the input
* is sent to output.
*
* @param {number[] | ArrayBuffer} input - Octets to parse as input.
*/
consume(input) {
if (!(input instanceof Array)) {
input = Array.prototype.slice.call( new Uint8Array(input) );
}
if (this._zsession) {
var session_before_consume = this._zsession;
session_before_consume.consume(input);
if (session_before_consume.has_ended()) {
if (session_before_consume.type === "receive") {
input = session_before_consume.get_trailing_bytes();
}
else {
input = [];
}
}
else return;
}
var new_session = this._parse(input);
var to_terminal = input;
if (new_session) {
let replacement_detect = !!this._parsed_session;
if (replacement_detect) {
//no terminal output if the new session is of the
//same type as the old
if (this._parsed_session.type === new_session.type) {
to_terminal = [];
}
this._on_retract();
}
this._parsed_session = new_session;
var sentry = this;
function checker() {
return sentry._parsed_session === new_session;
}
//This runs with the Sentry object as the context.
function accepter() {
if (!this.is_valid()) {
throw "Stale ZMODEM session!";
}
new_session.on("garbage", sentry._to_terminal);
new_session.on(
"session_end",
sentry._after_session_end.bind(sentry)
);
new_session.set_sender(sentry._sender);
delete sentry._parsed_session;
return sentry._zsession = new_session;
};
function denier() {
if (!this.is_valid()) return;
};
this._on_detect( new Detection(
new_session.type,
accepter,
this._send_abort.bind(this),
checker
) );
}
else {
/*
if (this._parsed_session) {
this._session_stale_because = 'Non-ZMODEM output received after ZMODEM initialization.';
}
*/
var expired_session = this._parsed_session;
this._parsed_session = null;
if (expired_session) {
//If we got a single “C” after parsing a session,
//that means our peer is trying to downgrade to YMODEM.
//That wont work, so we just send the ABORT_SEQUENCE
//right away.
if (to_terminal.length === 1 && to_terminal[0] === 67) {
this._send_abort();
}
this._on_retract();
}
}
this._to_terminal(to_terminal);
}
/**
* @return {Session|null} The sentrys current Session object, or
* null if there is none.
*/
get_confirmed_session() {
return this._zsession || null;
}
_send_abort() {
this._sender( Zmodem.ZMLIB.ABORT_SEQUENCE );
}
/**
* Parse an input stream and decide how much of it goes to the
* terminal or to a new Session object.
*
* This will accommodate input strings that are fragmented
* across calls to this function; e.g., if you send the first
* two bytes at the end of one parse() call then send the rest
* at the beginning of the next, parse() will recognize it as
* the beginning of a ZMODEM session.
*
* In order to keep from blocking any actual useful data to the
* terminal in real-time, this will send on the initial
* ZRINIT/ZRQINIT bytes to the terminal. Theyre meant to go to the
* terminal anyway, so that should be fine.
*
* @private
*
* @param {Array|Uint8Array} array_like - The input bytes.
* Each member should be a number between 0 and 255 (inclusive).
*
* @return {Array} A two-member list:
* 0) the bytes that should be printed on the terminal
* 1) the created Session object (if any)
*/
_parse(array_like) {
var cache = this._cache;
cache.push.apply( cache, array_like );
while (true) {
let common_hex_at = Zmodem.ZMLIB.find_subarray( cache, COMMON_ZM_HEX_START );
if (-1 === common_hex_at) break;
let before_common_hex = cache.splice(0, common_hex_at);
let zsession;
try {
zsession = Zmodem.Session.parse(cache);
} catch(err) { //ignore errors
//console.log(err);
}
if (!zsession) break;
//Dont need to parse the trailing XON.
if ((cache.length === 1) && (cache[0] === Zmodem.ZMLIB.XON)) {
cache.shift();
}
//If there are still bytes in the cache,
//then we dont have a ZMODEM session. This logic depends
//on the sender only sending one initial header.
return cache.length ? null : zsession;
}
cache.splice( MAX_ZM_HEX_START_LENGTH );
return null;
}
}

1683
web/src/vendors/zmodem.js/zsession.js vendored Normal file

File diff suppressed because it is too large Load Diff

241
web/src/vendors/zmodem.js/zsubpacket.js vendored Normal file
View File

@@ -0,0 +1,241 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zcrc"),
require("./zdle"),
require("./zmlib"),
require("./zerror")
);
const
ZCRCE = 0x68, // 'h', 104, frame ends, header packet follows
ZCRCG = 0x69, // 'i', 105, frame continues nonstop
ZCRCQ = 0x6a, // 'j', 106, frame continues, ZACK expected
ZCRCW = 0x6b // 'k', 107, frame ends, ZACK expected
;
var SUBPACKET_BUILDER;
/** Class that represents a ZMODEM data subpacket. */
Zmodem.Subpacket = class ZmodemSubpacket {
/**
* Build a Subpacket subclass given a payload and frame end string.
*
* @param {Array} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @param {string} frameend - One of:
* - `no_end_no_ack`
* - `end_no_ack`
* - `no_end_ack` (unused currently)
* - `end_ack`
*
* @returns {Subpacket} An instance of the appropriate Subpacket subclass.
*/
static build(octets, frameend) {
//TODO: make this better
var Ctr = SUBPACKET_BUILDER[frameend];
if (!Ctr) {
throw("No subpacket type “" + frameend + "” is defined! Try one of: " + Object.keys(SUBPACKET_BUILDER).join(", "));
}
return new Ctr(octets);
}
/**
* Return the octet values array that represents the object
* encoded with a 16-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
encode16(zencoder) {
return this._encode( zencoder, Zmodem.CRC.crc16 );
}
/**
* Return the octet values array that represents the object
* encoded with a 32-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
encode32(zencoder) {
return this._encode( zencoder, Zmodem.CRC.crc32 );
}
/**
* Return the subpacket payloads octet values.
*
* NOTE: For speed, this returns the actual data in the subpacket;
* if you mutate this return value, you alter the Subpacket object
* internals. This is OK if you wont need the Subpacket anymore, but
* just be careful.
*
* @returns {number[]} The subpackets payload, represented as an
* array of octet values. **DO NOT ALTER THIS ARRAY** unless you
* no longer need the Subpacket.
*/
get_payload() { return this._payload }
/**
* Parse out a Subpacket object from a given array of octet values,
* assuming a 16-bit CRC.
*
* An exception is thrown if the given bytes are definitively invalid
* as subpacket values with 16-bit CRC.
*
* @param {number[]} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {Subpacket|undefined} An instance of the appropriate Subpacket
* subclass, or undefined if not enough octet values are given
* to determine whether there is a valid subpacket here or not.
*/
static parse16(octets) {
return ZmodemSubpacket._parse(octets, 2);
}
//parse32 test:
//[102, 105, 108, 101, 110, 97, 109, 101, 119, 105, 116, 104, 115, 112, 97, 99, 101, 115, 0, 49, 55, 49, 51, 49, 52, 50, 52, 51, 50, 49, 55, 50, 49, 48, 48, 54, 52, 52, 48, 49, 49, 55, 0, 43, 8, 63, 115, 23, 17]
/**
* Same as parse16(), but assuming a 32-bit CRC.
*
* @param {number[]} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {Subpacket|undefined} An instance of the appropriate Subpacket
* subclass, or undefined if not enough octet values are given
* to determine whether there is a valid subpacket here or not.
*/
static parse32(octets) {
return ZmodemSubpacket._parse(octets, 4);
}
/**
* Not used directly.
*/
constructor(payload) {
this._payload = payload;
}
_encode(zencoder, crc_func) {
return zencoder.encode( this._payload.slice(0) ).concat(
[ Zmodem.ZMLIB.ZDLE, this._frameend_num ],
zencoder.encode( crc_func( this._payload.concat(this._frameend_num) ) )
);
}
//Because of ZDLE encoding, well never see any of the frame-end octets
//in a stream except as the ends of data payloads.
static _parse(bytes_arr, crc_len) {
var end_at;
var creator;
//These have to be written in decimal since theyre lookup keys.
var _frame_ends_lookup = {
104: ZEndNoAckSubpacket,
105: ZNoEndNoAckSubpacket,
106: ZNoEndAckSubpacket,
107: ZEndAckSubpacket,
};
var zdle_at = 0;
while (zdle_at < bytes_arr.length) {
zdle_at = bytes_arr.indexOf( Zmodem.ZMLIB.ZDLE, zdle_at );
if (zdle_at === -1) return;
var after_zdle = bytes_arr[ zdle_at + 1 ];
creator = _frame_ends_lookup[ after_zdle ];
if (creator) {
end_at = zdle_at + 1;
break;
}
zdle_at++;
}
if (!creator) return;
var frameend_num = bytes_arr[end_at];
//sanity check
if (bytes_arr[end_at - 1] !== Zmodem.ZMLIB.ZDLE) {
throw( "Byte before frame end should be ZDLE, not " + bytes_arr[end_at - 1] );
}
var zdle_encoded_payload = bytes_arr.splice( 0, end_at - 1 );
var got_crc = Zmodem.ZDLE.splice( bytes_arr, 2, crc_len );
if (!got_crc) {
//got payload but no CRC yet .. should be rare!
//We have to put the ZDLE-encoded payload back before returning.
bytes_arr.unshift.apply(bytes_arr, zdle_encoded_payload);
return;
}
var payload = Zmodem.ZDLE.decode(zdle_encoded_payload);
//We really shouldnt need to do this, but just for good measure.
//I suppose its conceivable this may run over UDP or something?
Zmodem.CRC[ (crc_len === 2) ? "verify16" : "verify32" ](
payload.concat( [frameend_num] ),
got_crc
);
return new creator(payload, got_crc);
}
}
class ZEndSubpacketBase extends Zmodem.Subpacket {
frame_end() { return true }
}
class ZNoEndSubpacketBase extends Zmodem.Subpacket {
frame_end() { return false }
}
//Used for end-of-file.
class ZEndNoAckSubpacket extends ZEndSubpacketBase {
ack_expected() { return false }
}
ZEndNoAckSubpacket.prototype._frameend_num = ZCRCE;
//Used for ZFILE and ZSINIT payloads.
class ZEndAckSubpacket extends ZEndSubpacketBase {
ack_expected() { return true }
}
ZEndAckSubpacket.prototype._frameend_num = ZCRCW;
//Used for ZDATA, prior to end-of-file.
class ZNoEndNoAckSubpacket extends ZNoEndSubpacketBase {
ack_expected() { return false }
}
ZNoEndNoAckSubpacket.prototype._frameend_num = ZCRCG;
//only used if receiver can full-duplex
class ZNoEndAckSubpacket extends ZNoEndSubpacketBase {
ack_expected() { return true }
}
ZNoEndAckSubpacket.prototype._frameend_num = ZCRCQ;
SUBPACKET_BUILDER = {
end_no_ack: ZEndNoAckSubpacket,
end_ack: ZEndAckSubpacket,
no_end_no_ack: ZNoEndNoAckSubpacket,
no_end_ack: ZNoEndAckSubpacket,
};

130
web/src/vendors/zmodem.js/zvalidation.js vendored Normal file
View File

@@ -0,0 +1,130 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zerror")
);
const LOOKS_LIKE_ZMODEM_HEADER = /\*\x18[AC]|\*\*\x18B/;
function _validate_number(key, value) {
if (value < 0) {
throw new Zmodem.Error("validation", "“" + key + "” (" + value + ") must be nonnegative.");
}
if (value !== Math.floor(value)) {
throw new Zmodem.Error("validation", "“" + key + "” (" + value + ") must be an integer.");
}
}
/** Validation logic for zmodem.js
*
* @exports Validation
*/
Zmodem.Validation = {
/**
* Validates and normalizes a set of parameters for an offer to send.
* NOTE: This returns “mtime” as epoch seconds, not a Date. This is
* inconsistent with the get_details() method in Session, but its
* more useful for sending over the wire.
*
* @param {FileDetails} params - The file details. Some fairly trivial
* variances from the specification are allowed.
*
* @return {FileDetails} The parameters that should be sent. `mtime`
* will be a Date rather than a number.
*/
offer_parameters: function offer_parameters(params) {
if (!params.name) {
throw new Zmodem.Error("validation", "Need “name”!");
}
if (typeof params.name !== "string") {
throw new Zmodem.Error("validation", "“name” (" + params.name + ") must be a string!");
}
//So that we can override values as is useful
//without affecting the passed-in object.
params = Object.assign({}, params);
if (LOOKS_LIKE_ZMODEM_HEADER.test(params.name)) {
console.warn("The filename " + JSON.stringify(name) + " contains characters that look like a ZMODEM header. This could corrupt the ZMODEM session; consider renaming it so that the filename doesnt contain control characters.");
}
if (params.serial !== null && params.serial !== undefined) {
throw new Zmodem.Error("validation", "“serial” is meaningless.");
}
params.serial = null;
["size", "mode", "files_remaining", "bytes_remaining"].forEach(
function(k) {
var ok;
switch (typeof params[k]) {
case "object":
ok = (params[k] === null);
break;
case "undefined":
params[k] = null;
ok = true;
break;
case "number":
_validate_number(k, params[k]);
ok = true;
break;
}
if (!ok) {
throw new Zmodem.Error("validation", "“" + k + "” (" + params[k] + ") must be null, undefined, or a number.");
}
}
);
if (typeof params.mode === "number") {
params.mode |= 0x8000;
}
if (params.files_remaining === 0) {
throw new Zmodem.Error("validation", "“files_remaining”, if given, must be positive.");
}
var mtime_ok;
switch (typeof params.mtime) {
case "object":
mtime_ok = true;
if (params.mtime instanceof Date) {
var date_obj = params.mtime;
params.mtime = Math.floor( date_obj.getTime() / 1000 );
if (params.mtime < 0) {
throw new Zmodem.Error("validation", "“mtime” (" + date_obj + ") must not be earlier than 1970.");
}
}
else if (params.mtime !== null) {
mtime_ok = false;
}
break;
case "undefined":
params.mtime = null;
mtime_ok = true;
break;
case "number":
_validate_number("mtime", params.mtime);
mtime_ok = true;
break;
}
if (!mtime_ok) {
throw new Zmodem.Error("validation", "“mtime” (" + params.mtime + ") must be null, undefined, a Date, or a number.");
}
return params;
},
};