mirror of
https://github.com/XZB-1248/Spark
synced 2025-09-27 04:26:20 +08:00
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:
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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
53
LICENSE
@@ -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.
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
data, err := hex.DecodeString(val.(string))
|
||||
if err != nil {
|
||||
return errDataInvalid
|
||||
}
|
||||
func InputRawTerminal(input []byte, uuid string) {
|
||||
var session *terminal
|
||||
|
||||
val, ok = pack.GetData(`terminal`, reflect.String)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
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)
|
||||
terminal.pty.Write(data)
|
||||
terminal.lastPack = utils.Unix
|
||||
return nil
|
||||
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
|
||||
}
|
||||
width := val.(float64)
|
||||
val, ok = pack.GetData(`height`, reflect.Float64)
|
||||
if !ok {
|
||||
return errDataInvalid
|
||||
}
|
||||
height := val.(float64)
|
||||
func InputTerminal(pack modules.Packet) {
|
||||
var err error
|
||||
var uuid string
|
||||
var input []byte
|
||||
var session *terminal
|
||||
|
||||
val, ok = pack.GetData(`terminal`, reflect.String)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
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)
|
||||
pty.Setsize(terminal.pty, &pty.Winsize{
|
||||
Rows: uint16(height),
|
||||
Cols: uint16(width),
|
||||
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) {
|
||||
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))
|
||||
}
|
||||
if val, ok := pack.GetData(`rows`, reflect.Float64); !ok {
|
||||
return
|
||||
} else {
|
||||
rows = uint16(val.(float64))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
@@ -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
|
||||
}
|
||||
data, err := hex.DecodeString(val.(string))
|
||||
if err != nil {
|
||||
return errDataInvalid
|
||||
}
|
||||
func InputRawTerminal(input []byte, uuid string) {
|
||||
var session *terminal
|
||||
|
||||
val, ok = pack.GetData(`terminal`, reflect.String)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
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
|
||||
(*session.stdin).Write(input)
|
||||
session.lastPack = utils.Unix
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
22
licenses/concurrent-map.LICENSE
Normal file
22
licenses/concurrent-map.LICENSE
Normal 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
22
licenses/melody.LICENSE
Normal 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
201
licenses/zmodemjs.LICENSE
Normal 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
27
scripts/release.js
Normal 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.");
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
2813
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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}
|
||||
|
@@ -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 [
|
||||
|
@@ -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()
|
||||
|
@@ -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;
|
||||
@@ -199,14 +261,14 @@ function TerminalModal(props) {
|
||||
break;
|
||||
case '\x1B\x5B\x44': // left arrow.
|
||||
if (cursor > 0) {
|
||||
term.write('\x1B\x5B\x44'.repeat(wcwidth(cmd[cursor-1])));
|
||||
term.write('\x1B\x5B\x44'.repeat(wcwidth(cmd[cursor - 1])));
|
||||
cursor--;
|
||||
}
|
||||
break;
|
||||
case '\r':
|
||||
case '\n':
|
||||
if (cmd === 'clear' || cmd === 'cls') {
|
||||
clearTerm.call(this);
|
||||
clearTerm();
|
||||
term.clear();
|
||||
} else {
|
||||
term.write('\n');
|
||||
@@ -228,7 +290,7 @@ function TerminalModal(props) {
|
||||
cursor--;
|
||||
let charWidth = wcwidth(cmd[cursor]);
|
||||
let before = cmd.substring(0, cursor);
|
||||
let after = cmd.substring(cursor+1);
|
||||
let after = cmd.substring(cursor + 1);
|
||||
cmd = before + after;
|
||||
term.write('\b'.repeat(charWidth));
|
||||
term.write(after + ' '.repeat(charWidth));
|
||||
@@ -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,18 +648,26 @@ 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}}>
|
||||
<>
|
||||
<Button
|
||||
type={this.state.ctrl?'primary':'default'}
|
||||
type={this.state.ctrl ? 'primary' : 'default'}
|
||||
onClick={this.onCtrl.bind(this)}
|
||||
>
|
||||
CTRL
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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;
|
4
web/src/config/version.json
Normal file
4
web/src/config/version.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": "$VERSION",
|
||||
"commit": "$COMMIT"
|
||||
}
|
@@ -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",
|
||||
|
@@ -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;
|
@@ -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": "桌面会话创建失败",
|
||||
|
@@ -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)}
|
||||
/>
|
||||
|
@@ -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);
|
||||
}
|
||||
return result;
|
||||
function str2ua(str) {
|
||||
return new TextEncoder().encode(str);
|
||||
}
|
||||
|
||||
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
124
web/src/vendors/zmodem.js/encode.js
vendored
Normal 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) {
|
||||
//Can’t bit-shift because that runs into JS’s 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
33
web/src/vendors/zmodem.js/text.js
vendored
Normal 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
143
web/src/vendors/zmodem.js/zcrc.js
vendored
Normal 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
240
web/src/vendors/zmodem.js/zdle.js
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
"use strict";
|
||||
|
||||
var Zmodem = module.exports;
|
||||
|
||||
Object.assign(
|
||||
Zmodem,
|
||||
require("./zmlib")
|
||||
);
|
||||
|
||||
//encode() variables - declare them here so we don’t
|
||||
//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 don’t 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 isn’t 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 isn’t 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) {
|
||||
|
||||
//Don’t 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 doesn’t 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
47
web/src/vendors/zmodem.js/zerror.js
vendored
Normal 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
763
web/src/vendors/zmodem.js/zheader.js
vendored
Normal 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 {
|
||||
|
||||
//lrzsz’s “sz” command sends a random (?) CR/0x0d byte
|
||||
//after ZEOF. Let’s accommodate 0x0a, 0x0d, 0x8a, and 0x8d.
|
||||
//
|
||||
//Also, when you skip a file, sz outputs a message about it.
|
||||
//
|
||||
//It appears that we’re supposed to ignore anything until
|
||||
//[ ZPAD, ZDLE ] when we’re looking for a header.
|
||||
|
||||
/**
|
||||
* Weed out the leading bytes that aren’t 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 there’s no escaping of the output it’s possible
|
||||
//that the garbage could trip us up, e.g., by having a filename
|
||||
//be a legit ZMODEM header. But that’s 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 it’s 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.
|
||||
|
||||
//We’re assuming the length of the header is 4 in
|
||||
//this logic … but ZMODEM isn’t 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 don’t trim any more.
|
||||
break TRIM_LOOP;
|
||||
}
|
||||
|
||||
//Otherwise, we’ll 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 we’ll throw away the parser.
|
||||
//It’s 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 aren’t 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 Mattheij’s 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 don’t 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 what’s 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 don’t see it used in lrzsz or syncterm
|
||||
escape_8th_bit() {
|
||||
return !!( this._bytes4[3] & ZSINIT_FLAG.ESC8 );
|
||||
}
|
||||
}
|
||||
|
||||
//Thus far it doesn’t seem we really need this header except to respond
|
||||
//to ZSINIT, which doesn’t 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”. It’s 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 shouldn’t 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 doesn’t 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
102
web/src/vendors/zmodem.js/zmlib.js
vendored
Normal 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 - ZMODEM’s 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” doesn’t 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
5
web/src/vendors/zmodem.js/zmodem.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
Object.assign(
|
||||
module.exports,
|
||||
require("./zsentry"),
|
||||
require("./zmodem_browser"),
|
||||
);
|
180
web/src/vendors/zmodem.js/zmodem_browser.js
vendored
Normal file
180
web/src/vendors/zmodem.js/zmodem_browser.js
vendored
Normal 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 it’s 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 shouldn’t happen … so let’s
|
||||
//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) don’t.
|
||||
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 we’re done, or with another file-send promise
|
||||
//if there’s 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 element’s `click()`
|
||||
* method. The element is removed immediately after.
|
||||
*
|
||||
* @param {Array} packets - Same as the first argument to [Blob’s 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;
|
||||
//I’d 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
394
web/src/vendors/zmodem.js/zsentry.js
vendored
Normal 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'
|
||||
//ZRQINIT’s next byte will be '0'; ZRINIT’s 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 Sentry’s 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 Sentry’s `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 doesn’t end in error. That’s
|
||||
//possible if we’re the sender, we accept the session,
|
||||
//then we just send a close(), but it doesn’t seem to be
|
||||
//possible for a receiver. Thus, let’s just leave it so
|
||||
//it’s 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 session’s 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
|
||||
* it’s 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 wouldn’t 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. That’ll be one less step
|
||||
* for the user, but an unaccustomed user might find that a bit
|
||||
* confusing. It’s also then possible to have a “false positive”:
|
||||
* a text stream that contains a ZMODEM initialization string but
|
||||
* isn’t, 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 user’s chance to say, “yeah, I know those
|
||||
* bytes look like ZMODEM, but they’re not. So back off!”
|
||||
*
|
||||
* If you .confirm(), the Session object is returned, and
|
||||
* further input that goes to the Sentry’s .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 it’s 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 we’ve 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 won’t 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 sentry’s 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. They’re 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;
|
||||
|
||||
//Don’t 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 don’t 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
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
241
web/src/vendors/zmodem.js/zsubpacket.js
vendored
Normal 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 payload’s 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 won’t need the Subpacket anymore, but
|
||||
* just be careful.
|
||||
*
|
||||
* @returns {number[]} The subpacket’s 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, we’ll 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 they’re 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 shouldn’t need to do this, but just for good measure.
|
||||
//I suppose it’s 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
130
web/src/vendors/zmodem.js/zvalidation.js
vendored
Normal 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 it’s
|
||||
* 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 doesn’t 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;
|
||||
},
|
||||
};
|
Reference in New Issue
Block a user