optimize: performance of front-end and back-end

optimize: security vulnerability
This commit is contained in:
XZB
2022-05-22 23:41:23 +08:00
parent dae496d934
commit b9a551114a
45 changed files with 902 additions and 540 deletions

View File

@@ -123,16 +123,16 @@ jobs:
run: |
cd ./releases
sudo apt install zip tar -y
tar -zcvf server_darwin_arm64.tar.gz server_darwin_arm64
tar -zcvf server_darwin_amd64.tar.gz server_darwin_amd64
tar -zcvf server_linux_arm.tar.gz server_linux_arm
tar -zcvf server_linux_arm64.tar.gz server_linux_arm64
tar -zcvf server_linux_i386.tar.gz server_linux_i386
tar -zcvf server_linux_amd64.tar.gz server_linux_amd64
zip -r server_windows_arm.zip server_windows_arm.exe
zip -r server_windows_arm64.zip server_windows_arm64.exe
zip -r server_windows_i386.zip server_windows_i386.exe
zip -r server_windows_amd64.zip server_windows_amd64.exe
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
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
zip -r -9 -q server_windows_amd64.zip server_windows_amd64.exe ../built
- name: Release
uses: softprops/action-gh-release@v1
@@ -142,12 +142,12 @@ jobs:
releases/server_darwin_arm64.tar.gz
releases/server_darwin_amd64.tar.gz
releases/server_linux_arm.tar.gz
releases/server_linux_arm64.tar.gz
releases/server_linux_i386.tar.gz
releases/server_linux_arm64.tar.gz
releases/server_linux_amd64.tar.gz
releases/server_windows_arm.zip
releases/server_windows_arm64.zip
releases/server_windows_i386.zip
releases/server_windows_arm64.zip
releases/server_windows_amd64.zip
- name: Clean up

View File

@@ -5,7 +5,9 @@
## 通用
所有请求均为`POST`
<br />
### 鉴权
每次请求都必须在Header中带上`Authorization`
<br />
`Authorization`请求头格式:`Basic <token>`basic auth
@@ -18,6 +20,10 @@ Authorization: Basic <base64('username:password')>
Authorization: Basic WFpCOjEyNDg=
```
在最初的Basic Authentication之后服务端会分配一个`Authorization`的Cookie。
<br />
该Cookie可用于请求的后续鉴权可以不再附带Authorization头。
---
## 响应

8
API.md
View File

@@ -5,7 +5,9 @@
## Common
Only `POST` requests are allowed.
<br />
### Authenticate
For every request, you should have `Authorization` on its header.
<br />
Authorization header is a string like `Basic <token>`(basic auth).
@@ -18,6 +20,10 @@ Example:
Authorization: Basic WFpCOjEyNDg=
```
After basic authentication, server will assign you a `Authorization` cookie.
<br />
You can use this token cookie to authenticate rest of your requests.
---
## Response

View File

@@ -1,3 +1,13 @@
## v0.0.9
* Optimize: performance of front-end and back-end.
* Optimize: security vulnerability.
* 优化:前后端性能。
* 优化:安全问题。
## v0.0.8
* Add: file upload.

View File

@@ -8,6 +8,18 @@
---
<div align="center">
|![GitHub repo size](https://img.shields.io/github/repo-size/DGP-Studio/Snap.Genshin?style=flat-square)|![GitHub issues](https://img.shields.io/github/issues/XZB-1248/Spark?style=flat-square)|![GitHub closed issues](https://img.shields.io/github/issues-closed/XZB-1248/Spark?style=flat-square)|
|-|-|-|
|[![GitHub downloads](https://img.shields.io/github/downloads/XZB-1248/Spark/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases)|[![GitHub release (latest by date)](https://img.shields.io/github/downloads/XZB-1248/Spark/latest/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases/latest)|
|-|-|
</div>
---
### **免责声明**
**本项目及其源代码和发行版,旨在用于学习和交流。使用本项目所带来的风险由使用者本人承担。作者和开发者不会对你的错误使用而造成的损害承担任何责任。**
@@ -123,12 +135,12 @@ $ statik -m -src="./web/dist" -f -dest="./server/embed" -p web -ns web
# 在使用类Unix系统时运行以下命令。
$ go mod tidy
$ go mod download
$ ./build.client.sh
$ ./scripts/build.client.sh
$ statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
# 最终开始编译服务端。
$ ./build.server.sh
$ ./scripts/build.server.sh
```
然后打开`releases`目录,放入上文提到的配置文件,选择对应平台的服务端运行即可。

View File

@@ -3,15 +3,30 @@
**Spark** is a free, safe, open-source, web-based, cross-platform and full-featured RAT (Remote Administration Tool)
that allow you to control all your devices via browser anywhere.
We **won't** collect any data, thus the server will never self-upgrade. Your clients will only communicate with your server forever.
We **won't** collect any data, thus the server will never self-upgrade. Your clients will only communicate with your
server forever.
### [English] [[中文]](./README.ZH.md) [[API Document]](./API.md) [[API文档]](./API.ZH.md)
---
<div align="center">
|![GitHub repo size](https://img.shields.io/github/repo-size/DGP-Studio/Snap.Genshin?style=flat-square)|![GitHub issues](https://img.shields.io/github/issues/XZB-1248/Spark?style=flat-square)|![GitHub closed issues](https://img.shields.io/github/issues-closed/XZB-1248/Spark?style=flat-square)|
|-|-|-|
|[![GitHub downloads](https://img.shields.io/github/downloads/XZB-1248/Spark/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases)|[![GitHub release (latest by date)](https://img.shields.io/github/downloads/XZB-1248/Spark/latest/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases/latest)|
|-|-|
</div>
---
## **Disclaimer**
**THIS PROJECT, ITS SOURCE CODE, AND ITS RELEASES SHOULD ONLY BE USED FOR EDUCATIONAL PURPOSES.YOU SHALL USE THIS PROJECT AT YOUR OWN RISK.THE AUTHORS AND DEVELOPERS ARE NOT RESPONSIBLE FOR ANY DAMAGE CAUSED BY YOUR MISUSE OF THIS PROJECT.**
**THIS PROJECT, ITS SOURCE CODE, AND ITS RELEASES SHOULD ONLY BE USED FOR EDUCATIONAL PURPOSES.YOU SHALL USE THIS
PROJECT AT YOUR OWN RISK.THE AUTHORS AND DEVELOPERS ARE NOT RESPONSIBLE FOR ANY DAMAGE CAUSED BY YOUR MISUSE OF THIS
PROJECT.**
**YOUR DATA IS PRICELESS. THINK TWICE BEFORE YOU CLICK ANY BUTTON OR ENTER ANY COMMAND.**
@@ -123,12 +138,12 @@ $ statik -m -src="./web/dist" -f -dest="./server/embed" -p web -ns web
# When you're using unix-like OS, you can use this.
$ go mod tidy
$ go mod download
$ ./build.client.sh
$ ./scripts/build.client.sh
$ statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
# Finally we're compiling the server side.
$ ./build.server.sh
$ ./scripts/build.server.sh
```
Then you can find executable files in `releases` directory.

View File

@@ -16,7 +16,7 @@ type Cfg struct {
// Localhost for my development only.
// Shall be commented out when development is done.
//var CfgBuffer = "\x00\xcd\x90\x50\x43\xfc\x3d\x36\x56\x6d\xf6\x01\xd1\xcd\x81\xc3\x1b\x80\xc9\x61\xd8\xdf\x5b\x76\x48\x88\xc5\xb1\x74\x22\x23\xab\x3b\xfc\x8b\xbe\x98\x27\xed\x05\xec\xbb\x40\x4f\xe9\xe7\xe5\xe0\x84\xaa\xb7\xfd\x4a\x30\x71\x08\x6c\x02\x50\xe9\xc5\x22\xcf\xcb\x89\x16\x0a\x89\x08\xd4\x26\xdc\x5c\xc1\xc9\xbf\xc4\xac\x0d\x92\x2f\x34\x7f\x45\xeb\x55\xa0\x6d\xf6\x64\xbc\xd5\x15\x40\x96\x43\x64\xe0\x24\x51\xfb\xe8\xc9\x7f\x48\x60\xcd\x30\x5e\x5e\x78\xba\xb6\x6f\x07\x64\xe8\x59\x81\x0b\x91\x13\x92\x1a\xdd\x49\x8f\x28\xe7\x74\xea\xff\x5b\x45\x0e\x4a\x2d\x60\x4e\xc9\xde\x9c\xbe\x50\xc6\x12\xc7\x45\xa2\x15\xa0\x58\x62\x45\x86\x74\x9f\xa5\x14\x5c\x17\x8a\xcc\x56\x73\xa7\x75\xb7\xf6\x6d\x52\x0f\xb8\xc1\xff\x9c\x39\x39\x00\x74\xe1\x4d\x65\x73\x9c\x02\x57\x8b\xcf\xdf\x0a\x20\x4c\xed\xe2\x25\xea\x01\x36\x12\x37\x12\x2e\x1a\x03\x41\x19\x2e\xc9\xdd\x71\xac\x73\x90\xfa\x5e\x60\x08\x43\x35\xef\x61\x45\xf9\xe3\xba\xcb\xb1\xc5\x7c\xf0\x11\xcd\x47\x57\x53\xdc\x35\x6b\x9f\xac\xad\x43\x4a\xc7\x54\x20\xb8\xd0\xf8\xb5\x0c\x45\x76\x57\xb9\xee\x4a\x3f\xd2\xda\xf7\x94\x54\x74\xf3\x91\xf3\x4d\x49\x98\xc6\xf8\x60\x80\xad\x84\x04\xef\x35\xca\x3a\xcf\xd3\x7e\x74\xc2\x4b\xb8\xb3\x9f\xb2\x83\xb8\xbd\x29\x13\x9f\x2b\xaa\x60\x47\x24\x7e\x20\xb2\x85\xdc\x47\xfe\x8f\x68\xb6\xc3\x43\xad\x61\x3d\x9b\x35\x60\x2e\x6c\x44\xf0\xaf\xb2\xf3\xdb\xe2\x1b\x8a\xec\x0a\x48\x5e\x43\xa9\xb3\x3a\x5e\xb6\x90\xa9\x3d\xee\x4f\xa1\x57\x7c\x94\xf4\xb1\x36\xda\x04\xa8\x5e\x48\x2a\xc3\xa1\xf0\x97\xf0\xe0\x10\x46\x32\x10\xe5\xd8\x36\x5a\x56\xa5\xbb\x37\x3c\x9f\xbd\xef\xf5\x2f"
//var CfgBuffer = "\x00\xcd\xc6\x68\x5d\xf5\x83\x53\x1c\x49\xa2\x35\x7b\x5b\xaf\xf2\x9e\x6d\x74\x00\x95\x23\x73\x00\x77\xa0\xe1\x46\x64\xd2\x33\x2b\x04\xb2\xca\x70\xda\x4b\xed\xec\x43\x6b\xeb\x6e\x10\x53\x6e\x62\x13\x3c\xb1\x0a\xdd\xc0\x48\x2d\x77\xfa\x4a\x9b\x26\xb5\x1b\x50\x62\x05\xcc\xc9\x3b\x22\xf5\x19\x5b\xac\x41\x74\xc9\x9e\x02\x9f\xe8\x75\xce\x3a\xe0\x50\x67\x0f\x81\x01\xca\x47\x0d\xb2\x09\x8b\x74\x6c\xfd\xc5\x73\xf9\x2a\xf0\x13\x52\xb7\x79\xff\xeb\xab\xcd\x9f\xe8\xb7\xae\xff\xa9\x50\xb2\x90\x11\x35\x4d\x94\x6e\x67\x55\x37\x66\x58\x21\xc0\x0d\xab\x3b\x6f\xc4\x00\x56\xd6\x06\xa0\x7e\x73\xdf\x46\x76\xe0\xb3\x89\x0d\xa2\x33\x07\x39\x81\x2b\x59\x30\x24\xc7\x4f\xe9\xb9\xf6\x3c\xb6\x24\xc5\x44\xde\xe6\x66\x66\x92\x49\xe1\x38\x50\xff\xb5\xf3\x20\xb9\x15\x60\x4a\xdf\xba\xd5\xae\x85\x7e\x3f\x8a\xf0\xb8\xf5\x23\x39\xf0\x46\x11\x64\x42\x04\x8c\xf0\x8a\x5e\xc7\x43\xd2\x0c\x89\xd1\xc4\x14\x26\xb1\x67\x64\x28\x77\xf4\xc8\xf3\x51\x69\xba\xf2\xca\xfa\x2f\x11\xe0\x8d\x6c\x4e\x8c\xb7\x28\xf5\x2a\x67\xe3\x8f\xf0\x7f\x79\xc5\xa5\x1a\xb5\xa1\x22\xe9\x55\x61\xdd\xce\x39\x13\x4b\xdd\x19\xf1\x5c\x86\x9b\x16\x89\x45\xba\x16\x68\xfc\x88\x4b\xd5\x13\xa4\x7e\x26\xce\x35\x2d\x42\x4d\x21\xf1\xc3\x6d\xf5\x64\x16\xc9\x05\xed\x9b\x6c\xbf\x26\xe3\xad\x40\x1d\xc6\x64\x03\xb9\xcb\xca\x3c\x62\x5d\x07\x6b\x07\x8b\xa9\x86\x60\x27\x28\xe7\xa3\xc2\x8d\x6f\xc0\x3d\x8e\x14\xa6\xcc\xe0\x50\x51\x22\x20\x6b\x16\x10\xe9\xe0\x4a\xd2\x4e\x77\xc8\xd1\xf7\x60\x4c\xed\xca\x3f\x1e\x13\x0a\x2e\x84\x15\xd3\xf6\x3e\x13\x4e\x68\xaf\xfd\x7a\xd7\x5b\xaa\x5b\x28\x7c\x3f\xb3\xd0\xd0"
// None
var CfgBuffer = "\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19"

View File

@@ -27,6 +27,7 @@ var (
errNoSecretHeader = errors.New(`can not find secret header`)
)
var handlers = map[string]func(pack modules.Packet, wsConn *common.Conn){
`ping`: ping,
`offline`: offline,
`lock`: lock,
`logoff`: logoff,
@@ -69,8 +70,6 @@ func Start() {
checkUpdate(common.WSConn)
go heartbeat(common.WSConn)
err = handleWS(common.WSConn)
if err != nil && !stop {
golog.Error(`Execution error: `, err)
@@ -212,25 +211,3 @@ func handleAct(pack modules.Packet, wsConn *common.Conn) {
act(pack, wsConn)
}
}
func heartbeat(wsConn *common.Conn) error {
t := 0
for range time.NewTicker(2 * time.Second).C {
t++
// GetPartialInfo always costs more than 1 second.
// So it is actually get disk info every 20*3 seconds (1 minute).
device, err := GetPartialInfo(t >= 20)
if err != nil {
golog.Error(err)
continue
}
if t >= 20 {
t = 0
}
err = common.SendPack(modules.CommonPack{Act: `setDevice`, Data: *device}, wsConn)
if err != nil {
return err
}
}
return nil
}

View File

@@ -245,7 +245,7 @@ func GetDevice() (*modules.Device, error) {
}, nil
}
func GetPartialInfo(getDisk bool) (*modules.Device, error) {
func GetPartialInfo() (*modules.Device, error) {
cpuInfo, err := GetCPUInfo()
if err != nil {
cpuInfo = modules.CPU{

View File

@@ -8,11 +8,22 @@ import (
Screenshot "Spark/client/service/screenshot"
"Spark/client/service/terminal"
"Spark/modules"
"github.com/kataras/golog"
"os"
"reflect"
"strconv"
)
func ping(pack modules.Packet, wsConn *common.Conn) {
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
device, err := GetPartialInfo()
if err != nil {
golog.Error(err)
return
}
common.SendPack(modules.CommonPack{Act: `setDevice`, Data: *device}, wsConn)
}
func offline(pack modules.Packet, wsConn *common.Conn) {
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
stop = true

View File

@@ -3,13 +3,13 @@ package file
import (
"Spark/client/config"
"errors"
"github.com/imroc/req/v3"
"io"
"io/ioutil"
"os"
"path"
"strconv"
"github.com/imroc/req/v3"
"unicode/utf8"
)
type File struct {
@@ -41,6 +41,53 @@ func listFiles(path string) ([]File, error) {
return result, nil
}
func ReadText(path, bridge string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
uploadReq := req.R()
stat, err := file.Stat()
if err != nil {
return err
}
size := stat.Size()
// Check if size larger than 2MB.
if size > 2<<20 {
return errors.New(`${i18n|fileTooLarge}`)
}
headers := map[string]string{
`FileName`: stat.Name(),
`FileSize`: strconv.FormatInt(size, 10),
}
uploadReq.RawRequest.ContentLength = size
// Check file if is a text file.
// UTF-8 and GBK are only supported yet.
buf := make([]byte, size)
_, err = file.Read(buf)
if err != nil {
return err
}
if utf8.Valid(buf) {
headers[`FileEncoding`] = `utf-8`
} else if gbkValidate(buf) {
headers[`FileEncoding`] = `gbk`
} else {
return errors.New(`${i18n|fileEncodingUnsupported}`)
}
file.Seek(0, 0)
url := config.GetBaseURL(false) + `/api/bridge/push`
_, err = uploadReq.
SetBody(file).
SetHeaders(headers).
SetQueryParam(`bridge`, bridge).
Send(`PUT`, url)
return err
}
// FetchFile saves file from bridge to local.
// Save body as temp file and when done, rename it to file.
func FetchFile(dir, file, bridge string) error {
@@ -162,6 +209,26 @@ func UploadFile(path, bridge string, start, end int64) error {
return err
}
func gbkValidate(b []byte) bool {
length := len(b)
var i int = 0
for i < length {
if b[i] <= 0x7f {
i++
continue
} else {
if i+1 < length {
if b[i] >= 0x81 && b[i] <= 0xfe && b[i+1] >= 0x40 && b[i+1] <= 0xfe && b[i+1] != 0xf7 {
i += 2
continue
}
}
return false
}
}
return true
}
func getTempFileName(dir, file string) string {
exists := true
tempFile := ``

View File

@@ -2,6 +2,8 @@
package screenshot
import "errors"
func GetScreenshot(bridge string) error {
return utils.ErrUnsupported
return errors.New(`${i18n|operationNotSupported}`)
}

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.17
require (
github.com/creack/pty v1.1.18
github.com/denisbrodbeck/machineid v1.0.1
github.com/gin-contrib/pprof v1.3.0
github.com/gin-gonic/gin v1.7.7
github.com/gorilla/websocket v1.5.0
github.com/imroc/req/v3 v3.8.2

4
go.sum
View File

@@ -7,8 +7,11 @@ github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMS
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY=
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@@ -19,6 +22,7 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=

View File

@@ -7,10 +7,10 @@ set GOOS=linux
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_arm Spark/client
set GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_arm64 Spark/client
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_i386 Spark/client
set GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_arm64 Spark/client
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_amd64 Spark/client
@@ -20,10 +20,10 @@ set GOOS=windows
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_arm Spark/client
set GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_arm64 Spark/client
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_i386 Spark/client
set GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_arm64 Spark/client
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_amd64 Spark/client
@@ -37,16 +37,16 @@ go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/wi
@REM set CXX=armv7a-linux-androideabi21-clang++
@REM go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/android_arm Spark/client
@REM set GOARCH=arm64
@REM set CC=aarch64-linux-android21-clang
@REM set CXX=aarch64-linux-android21-clang++
@REM go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/android_arm64 Spark/client
@REM set GOARCH=386
@REM set CC=i686-linux-android21-clang
@REM set CXX=i686-linux-android21-clang++
@REM go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/android_i386 Spark/client
@REM set GOARCH=arm64
@REM set CC=aarch64-linux-android21-clang
@REM set CXX=aarch64-linux-android21-clang++
@REM go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/android_arm64 Spark/client
@REM set GOARCH=amd64
@REM set CC=x86_64-linux-android21-clang
@REM set CXX=x86_64-linux-android21-clang++

View File

@@ -7,10 +7,10 @@ export GOOS=linux
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_arm Spark/client
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_arm64 Spark/client
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_i386 Spark/client
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_arm64 Spark/client
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_amd64 Spark/client
@@ -20,10 +20,10 @@ export GOOS=windows
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_arm Spark/client
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_arm64 Spark/client
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_i386 Spark/client
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_arm64 Spark/client
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_amd64 Spark/client
@@ -37,16 +37,16 @@ go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/win
# export CXX=armv7a-linux-androideabi21-clang++
# go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/android_arm Spark/client
# export GOARCH=arm64
# export CC=aarch64-linux-android21-clang
# export CXX=aarch64-linux-android21-clang++
# go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/android_arm64 Spark/client
# export GOARCH=386
# export CC=i686-linux-android21-clang
# export CXX=i686-linux-android21-clang++
# go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/android_i386 Spark/client
# export GOARCH=arm64
# export CC=aarch64-linux-android21-clang
# export CXX=aarch64-linux-android21-clang++
# go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/android_arm64 Spark/client
# export GOARCH=amd64
# export CC=x86_64-linux-android21-clang
# export CXX=x86_64-linux-android21-clang++

View File

@@ -1,38 +1,37 @@
cd ..
mkdir .\releases
set GO111MODULE=auto
for /F %%i in ('git rev-parse HEAD') do ( set COMMIT=%%i)
set GOOS=linux
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_arm Spark/Server
set GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_arm64 Spark/Server
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_i386 Spark/Server
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_amd64 Spark/Server
set GOOS=windows
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_arm.exe Spark/Server
set GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_arm64.exe Spark/Server
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/Server
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/Server
set GOOS=darwin
set GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
set GOOS=linux
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_arm Spark/Server
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_i386 Spark/Server
set GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_arm64 Spark/Server
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_amd64 Spark/Server
set GOOS=windows
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_arm.exe Spark/Server
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/Server
set GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_arm64.exe Spark/Server
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/Server

View File

@@ -3,35 +3,35 @@ export COMMIT=`git rev-parse HEAD`
export GOOS=linux
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_arm Spark/server
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_arm64 Spark/server
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_i386 Spark/server
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_amd64 Spark/server
export GOOS=windows
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_arm.exe Spark/server
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_arm64.exe Spark/server
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/server
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/server
export GOOS=darwin
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
export GOOS=linux
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_arm Spark/server
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_i386 Spark/server
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_arm64 Spark/server
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_amd64 Spark/server
export GOOS=windows
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_arm.exe Spark/server
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/server
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_arm64.exe Spark/server
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/server

View File

@@ -11,14 +11,11 @@ import (
"encoding/hex"
"github.com/gin-gonic/gin"
"net"
"net/http"
"strings"
"time"
)
var Melody = melody.New()
var Devices = cmap.New()
var BuiltFS http.FileSystem
func SendPackByUUID(pack modules.Packet, uuid string) bool {
session, ok := Melody.GetSessionByUUID(uuid)
@@ -70,54 +67,6 @@ func Decrypt(data []byte, session *melody.Session) ([]byte, bool) {
return dec, true
}
func HealthCheckWS(maxIdleSeconds int64, container *melody.Melody) {
go func() {
// ping client and update latency every 3 seconds
ping := func(uuid string, s *melody.Session) {
t := time.Now().UnixMilli()
trigger := utils.GetStrUUID()
SendPack(modules.Packet{Act: `ping`, Event: trigger}, s)
AddEventOnce(func(packet modules.Packet, session *melody.Session) {
val, ok := Devices.Get(uuid)
if ok {
deviceInfo := val.(*modules.Device)
deviceInfo.Latency = uint(time.Now().UnixMilli()-t) / 2
}
}, uuid, trigger, 3*time.Second)
}
for range time.NewTicker(3 * time.Second).C {
container.IterSessions(func(uuid string, s *melody.Session) bool {
go ping(uuid, s)
return true
})
}
}()
for now := range time.NewTicker(30 * time.Second).C {
timestamp := now.Unix()
// stores sessions to be disconnected
queue := make([]*melody.Session, 0)
container.IterSessions(func(uuid string, s *melody.Session) bool {
val, ok := s.Get(`LastPack`)
if !ok {
queue = append(queue, s)
return true
}
lastPack, ok := val.(int64)
if !ok {
queue = append(queue, s)
return true
}
if timestamp-lastPack > maxIdleSeconds {
queue = append(queue, s)
}
return true
})
for i := 0; i < len(queue); i++ {
queue[i].Close()
}
}
}
func GetRemoteAddr(ctx *gin.Context) string {
if remote, ok := ctx.RemoteIP(); ok {
if remote.IsLoopback() {

View File

@@ -48,9 +48,9 @@ func AddEventOnce(fn EventCallback, connUUID, trigger string, timeout time.Durat
remove: make(chan bool),
}
events.Set(trigger, ev)
defer events.Remove(trigger)
defer close(ev.finish)
defer close(ev.remove)
defer close(ev.finish)
defer events.Remove(trigger)
select {
case ok := <-ev.finish:
return ok
@@ -79,11 +79,16 @@ func RemoveEvent(trigger string, ok ...bool) {
return
}
events.Remove(trigger)
if ev := v.(*event); ev.remove != nil {
ev := v.(*event)
if ev.remove != nil {
if len(ok) > 0 {
ev.remove <- ok[0]
} else {
ev.remove <- false
}
}
v = nil
ev = nil
}
// HasEvent returns if the event exists.

14
server/common/time.go Normal file
View File

@@ -0,0 +1,14 @@
package common
import "time"
var Unix int64 = time.Now().Unix()
// To prevent call time.Now().Unix() too often.
func init() {
go func() {
for now := range time.NewTicker(time.Second).C {
Unix = now.Unix()
}
}()
}

View File

@@ -1,6 +1,10 @@
package config
type Cfg struct {
Debug struct {
Pprof bool `json:"pprof"`
Gin bool `json:"gin"`
} `json:"debug,omitempty"`
Listen string `json:"listen"`
Salt string `json:"salt"`
Auth map[string]string `json:"auth"`
@@ -8,6 +12,7 @@ type Cfg struct {
}
var Config Cfg
var BuiltPath = `./built/%v_%v`
// COMMIT means this commit hash, for auto upgrade.
var COMMIT = ``

View File

@@ -1,16 +0,0 @@
// Code generated by statik. DO NOT EDIT.
package built
import (
"github.com/rakyll/statik/fs"
)
const Built = "built" // static asset namespace
func init() {
data := "PK\x05\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
fs.RegisterWithNamespace("built", data)
}

View File

@@ -2,6 +2,7 @@ package handler
import (
"Spark/modules"
"Spark/server/common"
"Spark/utils/cmap"
"github.com/gin-gonic/gin"
"github.com/kataras/golog"
@@ -31,17 +32,12 @@ var bridges = cmap.New()
func init() {
go func() {
for now := range time.NewTicker(10 * time.Second).C {
var queue []*bridge
for now := range time.NewTicker(15 * time.Second).C {
var queue []string
timestamp := now.Unix()
bridges.IterCb(func(k string, v interface{}) bool {
b := v.(*bridge)
if b.creation < now.Unix()-60 && !b.using {
queue = append(queue, b)
}
return true
})
for _, b := range queue {
bridges.Remove(b.uuid)
if timestamp-b.creation > 60 && !b.using {
b.lock.Lock()
if b.src != nil && b.src.Request.Body != nil {
b.src.Request.Body.Close()
@@ -50,7 +46,11 @@ func init() {
b.dest = nil
b.lock.Unlock()
b = nil
queue = append(queue, b.uuid)
}
return true
})
bridges.Remove(queue...)
}
}()
}
@@ -61,12 +61,12 @@ func checkBridge(ctx *gin.Context) *bridge {
}
if err := ctx.ShouldBind(&form); err != nil {
golog.Error(err)
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return nil
}
val, ok := bridges.Get(form.Bridge)
if !ok {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidBridgeID}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidBridgeID}`})
return nil
}
return val.(*bridge)
@@ -80,7 +80,7 @@ func bridgePush(ctx *gin.Context) {
bridge.lock.Lock()
if bridge.using || (bridge.src != nil && bridge.dest != nil) {
bridge.lock.Unlock()
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
return
}
bridge.src = ctx
@@ -108,7 +108,7 @@ func bridgePull(ctx *gin.Context) {
bridge.lock.Lock()
if bridge.using || (bridge.src != nil && bridge.dest != nil) {
bridge.lock.Unlock()
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
return
}
bridge.dest = ctx
@@ -130,7 +130,7 @@ func bridgePull(ctx *gin.Context) {
func addBridge(ext interface{}, uuid string) *bridge {
bridge := &bridge{
creation: time.Now().Unix(),
creation: common.Unix,
uuid: uuid,
using: false,
lock: &sync.Mutex{},
@@ -142,7 +142,7 @@ func addBridge(ext interface{}, uuid string) *bridge {
func addBridgeWithSrc(ext interface{}, uuid string, src *gin.Context) *bridge {
bridge := &bridge{
creation: time.Now().Unix(),
creation: common.Unix,
uuid: uuid,
using: false,
lock: &sync.Mutex{},
@@ -155,7 +155,7 @@ func addBridgeWithSrc(ext interface{}, uuid string, src *gin.Context) *bridge {
func addBridgeWithDest(ext interface{}, uuid string, dest *gin.Context) *bridge {
bridge := &bridge{
creation: time.Now().Unix(),
creation: common.Unix,
uuid: uuid,
using: false,
lock: &sync.Mutex{},

View File

@@ -29,13 +29,13 @@ func removeDeviceFile(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Code: 0, Act: `removeFile`, Data: gin.H{`file`: form.File}, Event: trigger}, target)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}, target, trigger, 5*time.Second)
if !ok {
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
}
}
@@ -52,13 +52,13 @@ func listDeviceFiles(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Act: `listFiles`, Data: gin.H{`path`: form.Path}, Event: trigger}, target)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: p.Data})
}
}, target, trigger, 5*time.Second)
if !ok {
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
}
}
@@ -67,6 +67,7 @@ func listDeviceFiles(ctx *gin.Context) {
func getDeviceFile(ctx *gin.Context) {
var form struct {
File string `json:"file" yaml:"file" form:"file" binding:"required"`
Preview bool `json:"preview" yaml:"preview" form:"preview"`
}
target, ok := checkForm(ctx, &form)
if !ok {
@@ -82,29 +83,29 @@ func getDeviceFile(ctx *gin.Context) {
rangeHeader := ctx.GetHeader(`Range`)
if len(rangeHeader) > 6 {
if rangeHeader[:6] != `bytes=` {
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
rangeHeader = strings.TrimSpace(rangeHeader[6:])
rangesList := strings.Split(rangeHeader, `,`)
if len(rangesList) > 1 {
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
r := strings.Split(rangesList[0], `-`)
rangeStart, err = strconv.ParseInt(r[0], 10, 64)
if err != nil {
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
if len(r[1]) > 0 {
rangeEnd, err = strconv.ParseInt(r[1], 10, 64)
if err != nil {
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
if rangeEnd < rangeStart {
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
command[`end`] = rangeEnd
@@ -121,16 +122,22 @@ func getDeviceFile(ctx *gin.Context) {
called = true
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
}, target, trigger)
instance := addBridgeWithDest(nil, bridgeID, ctx)
instance.OnPush = func(bridge *bridge) {
called = true
common.RemoveEvent(trigger)
src := bridge.src
for k, v := range src.Request.Header {
if strings.HasPrefix(k, `File`) {
ctx.Header(k, v[0])
}
}
if src.Request.ContentLength > 0 {
ctx.Header(`Content-Length`, strconv.FormatInt(src.Request.ContentLength, 10))
}
if !form.Preview {
ctx.Header(`Accept-Ranges`, `bytes`)
ctx.Header(`Content-Transfer-Encoding`, `binary`)
ctx.Header(`Content-Type`, `application/octet-stream`)
@@ -140,6 +147,7 @@ func getDeviceFile(ctx *gin.Context) {
}
filename = url.PathEscape(filename)
ctx.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
}
if partial {
if rangeEnd == 0 {
@@ -164,11 +172,12 @@ func getDeviceFile(ctx *gin.Context) {
if !called {
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else {
<-wait
}
}
close(wait)
}
// uploadToDevice handles file from browser
@@ -191,7 +200,7 @@ func uploadToDevice(ctx *gin.Context) {
called = true
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
}, target, trigger)
instance := addBridgeWithSrc(nil, bridgeID, ctx)
instance.OnPull = func(bridge *bridge) {
@@ -223,10 +232,11 @@ func uploadToDevice(ctx *gin.Context) {
if !called {
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else {
<-wait
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}
close(wait)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"math/big"
"net/http"
"os"
"strconv"
"strings"
)
@@ -29,25 +30,6 @@ var (
errTooLargeEntity = errors.New(`length of data can not excess buffer size`)
)
//func init() {
// clientUUID := utils.GetUUID()
// clientKey, _ := common.EncAES(clientUUID, append([]byte("XZB_Spark"), bytes.Repeat([]byte{25}, 24-9)...))
// cfg, _ := genConfig(clientCfg{
// Secure: false,
// Host: "47.102.136.182",
// Port: 1025,
// Path: "/",
// UUID: hex.EncodeToString(clientUUID),
// Key: hex.EncodeToString(clientKey),
// })
// output := ``
// temp := hex.EncodeToString(cfg)
// for i := 0; i < len(temp); i += 2 {
// output += `\x` + temp[i:i+2]
// }
// ioutil.WriteFile(`./Client.cfg`, []byte(output), 0755)
//}
func checkClient(ctx *gin.Context) {
var form struct {
OS string `json:"os" yaml:"os" form:"os" binding:"required"`
@@ -58,12 +40,12 @@ func checkClient(ctx *gin.Context) {
Secure string `json:"secure" yaml:"secure" form:"secure"`
}
if err := ctx.ShouldBind(&form); err != nil {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
_, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
_, err := os.Open(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch))
if err != nil {
ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
ctx.AbortWithStatusJSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
return
}
_, err = genConfig(clientCfg{
@@ -76,10 +58,10 @@ func checkClient(ctx *gin.Context) {
})
if err != nil {
if err == errTooLargeEntity {
ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `${i18n|tooLargeConfig}`})
ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `${i18n|tooLargeConfig}`})
return
}
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
return
}
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
@@ -95,18 +77,18 @@ func generateClient(ctx *gin.Context) {
Secure string `json:"secure" yaml:"secure" form:"secure"`
}
if err := ctx.ShouldBind(&form); err != nil {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
tpl, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
tpl, err := os.Open(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch))
if err != nil {
ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
ctx.AbortWithStatusJSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
return
}
clientUUID := utils.GetUUID()
clientKey, err := common.EncAES(clientUUID, config.Config.StdSalt)
if err != nil {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
return
}
cfgBytes, err := genConfig(clientCfg{
@@ -119,10 +101,10 @@ func generateClient(ctx *gin.Context) {
})
if err != nil {
if err == errTooLargeEntity {
ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `${i18n|tooLargeConfig}`})
ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `${i18n|tooLargeConfig}`})
return
}
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
return
}
ctx.Header(`Accept-Ranges`, `none`)

View File

@@ -7,13 +7,14 @@ import (
"net/http"
)
var AuthHandler gin.HandlerFunc
// InitRouter will initialize http and websocket routers.
func InitRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
func InitRouter(ctx *gin.RouterGroup) {
ctx.Any(`/bridge/push`, bridgePush)
ctx.Any(`/bridge/pull`, bridgePull)
ctx.Any(`/device/terminal`, initTerminal) // Browser, handle websocket events for web terminal.
ctx.Any(`/client/update`, checkUpdate) // Client, for update.
group := ctx.Group(`/`, auth)
group := ctx.Group(`/`, AuthHandler)
{
group.POST(`/device/screenshot/get`, getScreenshot)
group.POST(`/device/process/list`, listDeviceProcesses)
@@ -26,6 +27,7 @@ func InitRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
group.POST(`/device/:act`, callDevice)
group.POST(`/client/check`, checkClient)
group.POST(`/client/generate`, generateClient)
group.Any(`/device/terminal`, initTerminal) // Browser, handle websocket events for web terminal.
}
}
@@ -37,16 +39,16 @@ func checkForm(ctx *gin.Context, form interface{}) (string, bool) {
Device string `json:"device" yaml:"device" form:"device"`
}
if form != nil && ctx.ShouldBind(form) != nil {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return ``, false
}
if ctx.ShouldBind(&base) != nil || (len(base.Conn) == 0 && len(base.Device) == 0) {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return ``, false
}
connUUID, ok := common.CheckDevice(base.Device, base.Conn)
if !ok {
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `${i18n|deviceNotExists}`})
ctx.AbortWithStatusJSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `${i18n|deviceNotExists}`})
return ``, false
}
return connUUID, true

View File

@@ -21,13 +21,13 @@ func listDeviceProcesses(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Act: `listProcesses`, Event: trigger}, connUUID)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: p.Data})
}
}, connUUID, trigger, 5*time.Second)
if !ok {
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
}
}
@@ -45,12 +45,12 @@ func killDeviceProcess(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Code: 0, Act: `killProcess`, Data: gin.H{`pid`: strconv.FormatInt(int64(form.Pid), 10)}, Event: trigger}, target)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}, target, trigger, 5*time.Second)
if !ok {
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
}
}

View File

@@ -26,7 +26,7 @@ func getScreenshot(ctx *gin.Context) {
called = true
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
}, target, trigger)
instance := addBridgeWithDest(nil, bridgeID, ctx)
instance.OnPush = func(bridge *bridge) {
@@ -43,9 +43,10 @@ func getScreenshot(ctx *gin.Context) {
if !called {
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else {
<-wait
}
}
close(wait)
}

View File

@@ -4,7 +4,6 @@ import (
"Spark/modules"
"Spark/server/common"
"Spark/utils"
"Spark/utils/cmap"
"Spark/utils/melody"
"crypto/aes"
"crypto/cipher"
@@ -22,29 +21,131 @@ type terminal struct {
deviceConn *melody.Session
}
var terminals = cmap.New()
var wsSessions = melody.New()
func init() {
wsSessions.HandleConnect(func(session *melody.Session) {
wsSessions.HandleConnect(onConnect)
wsSessions.HandleMessage(onMessage)
wsSessions.HandleMessageBinary(onMessage)
wsSessions.HandleDisconnect(onDisconnect)
go wsHealthCheck(wsSessions)
}
// initTerminal handles terminal websocket handshake event
func initTerminal(ctx *gin.Context) {
if !ctx.IsWebsocket() {
ctx.AbortWithStatus(http.StatusBadRequest)
return
}
secretStr, ok := ctx.GetQuery(`secret`)
if !ok || len(secretStr) != 32 {
ctx.AbortWithStatus(http.StatusBadRequest)
return
}
secret, err := hex.DecodeString(secretStr)
if err != nil {
ctx.AbortWithStatus(http.StatusBadRequest)
return
}
device, ok := ctx.GetQuery(`device`)
if !ok {
ctx.AbortWithStatus(http.StatusBadRequest)
return
}
if _, ok := common.CheckDevice(device, ``); !ok {
ctx.AbortWithStatus(http.StatusBadRequest)
return
}
wsSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
`Secret`: secret,
`Device`: device,
`LastPack`: common.Unix,
})
}
// eventWrapper returns a eventCb function that will be called when
// device need to send a packet to browser terminal
func eventWrapper(terminal *terminal) common.EventCallback {
return func(pack modules.Packet, device *melody.Session) {
if pack.Act == `initTerminal` {
if pack.Code != 0 {
msg := `${i18n|terminalSessionCreationFailed}`
if len(pack.Msg) > 0 {
msg += `: ` + pack.Msg
} else {
msg += `${i18n|unknownError}`
}
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
common.RemoveEvent(terminal.event)
terminal.session.Close()
}
return
}
if pack.Act == `quitTerminal` {
msg := `${i18n|terminalSessionClosed}`
if len(pack.Msg) > 0 {
msg = pack.Msg
}
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
common.RemoveEvent(terminal.event)
terminal.session.Close()
return
}
if pack.Act == `outputTerminal` {
if pack.Data == nil {
return
}
if output, ok := pack.Data[`output`]; ok {
simpleSendPack(modules.Packet{Act: `outputTerminal`, Data: gin.H{
`output`: output,
}}, terminal.session)
}
}
}
}
func wsHealthCheck(container *melody.Melody) {
const MaxIdleSeconds = 300
ping := func(uuid string, s *melody.Session) {
if !simpleSendPack(modules.Packet{Act: `ping`}, s) {
s.Close()
}
}
for now := range time.NewTicker(60 * time.Second).C {
timestamp := now.Unix()
// stores sessions to be disconnected
queue := make([]*melody.Session, 0)
container.IterSessions(func(uuid string, s *melody.Session) bool {
go ping(uuid, s)
val, ok := s.Get(`LastPack`)
if !ok {
queue = append(queue, s)
return true
}
lastPack, ok := val.(int64)
if !ok {
queue = append(queue, s)
return true
}
if timestamp-lastPack > MaxIdleSeconds {
queue = append(queue, s)
}
return true
})
for i := 0; i < len(queue); i++ {
queue[i].Close()
}
}
}
func onConnect(session *melody.Session) {
device, ok := session.Get(`Device`)
if !ok {
simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
session.Close()
return
}
val, ok := session.Get(`Terminal`)
if !ok {
simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
session.Close()
return
}
termUUID, ok := val.(string)
if !ok {
simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
session.Close()
return
}
connUUID, ok := common.CheckDevice(device.(string), ``)
if !ok {
simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session)
@@ -57,6 +158,7 @@ func init() {
session.Close()
return
}
termUUID := utils.GetStrUUID()
eventUUID := utils.GetStrUUID()
terminal := &terminal{
uuid: termUUID,
@@ -65,112 +167,94 @@ func init() {
session: session,
deviceConn: deviceConn,
}
terminals.Set(termUUID, terminal)
session.Set(`Terminal`, terminal)
common.AddEvent(eventWrapper(terminal), connUUID, eventUUID)
common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{
`terminal`: termUUID,
}, Event: eventUUID}, deviceConn)
})
wsSessions.HandleMessage(onMessage)
wsSessions.HandleMessageBinary(onMessage)
wsSessions.HandleDisconnect(func(session *melody.Session) {
}
func onMessage(session *melody.Session, data []byte) {
var pack modules.Packet
data, ok := simpleDecrypt(data, session)
if !(ok && utils.JSON.Unmarshal(data, &pack) == nil) {
simpleSendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
session.Set(`LastPack`, common.Unix)
if pack.Act == `inputTerminal` {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
termUUID, ok := val.(string)
if !ok {
terminal := val.(*terminal)
if pack.Data == nil {
return
}
val, ok = terminals.Get(termUUID)
if input, ok := pack.Data[`input`]; ok {
common.SendPack(modules.Packet{Act: `inputTerminal`, Data: gin.H{
`input`: input,
`terminal`: terminal.uuid,
}, Event: terminal.event}, terminal.deviceConn)
}
return
}
if pack.Act == `resizeTerminal` {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
terminal := val.(*terminal)
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
`terminal`: termUUID,
}, Event: terminal.event}, terminal.deviceConn)
terminals.Remove(termUUID)
common.RemoveEvent(terminal.event)
})
go common.HealthCheckWS(300, wsSessions)
}
// initTerminal handles terminal websocket handshake event
func initTerminal(ctx *gin.Context) {
if !ctx.IsWebsocket() {
ctx.Status(http.StatusUpgradeRequired)
return
}
secretStr, ok := ctx.GetQuery(`secret`)
if !ok || len(secretStr) != 32 {
ctx.Status(http.StatusBadRequest)
return
}
secret, err := hex.DecodeString(secretStr)
if err != nil {
ctx.Status(http.StatusBadRequest)
return
}
device, ok := ctx.GetQuery(`device`)
if !ok {
ctx.Status(http.StatusBadRequest)
return
}
if _, ok := common.CheckDevice(device, ``); !ok {
ctx.Status(http.StatusBadRequest)
return
}
wsSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
`Secret`: secret,
`Device`: device,
`LastPack`: time.Now().Unix(),
`Terminal`: utils.GetStrUUID(),
})
}
// eventWrapper returns a eventCb function that will be called when
// device need to send a packet to browser terminal
func eventWrapper(terminal *terminal) common.EventCallback {
return func(pack modules.Packet, device *melody.Session) {
if pack.Act == `initTerminal` {
if pack.Code != 0 {
msg := `${i18n|terminalSessionCreationFailed}`
if len(pack.Msg) > 0 {
msg += `: ` + pack.Msg
} else {
msg += `${i18n|unknownError}`
}
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
terminals.Remove(terminal.uuid)
common.RemoveEvent(terminal.event)
terminal.session.Close()
}
return
}
if pack.Act == `quitTerminal` {
msg := `${i18n|terminalSessionClosed}`
if len(pack.Msg) > 0 {
msg = pack.Msg
}
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
terminals.Remove(terminal.uuid)
common.RemoveEvent(terminal.event)
terminal.session.Close()
return
}
if pack.Act == `outputTerminal` {
if pack.Data == nil {
return
}
if output, ok := pack.Data[`output`]; ok {
simpleSendPack(modules.Packet{Act: `outputTerminal`, Data: gin.H{
`output`: output,
}}, terminal.session)
if width, ok := pack.Data[`width`]; ok {
if height, ok := pack.Data[`height`]; ok {
common.SendPack(modules.Packet{Act: `resizeTerminal`, Data: gin.H{
`width`: width,
`height`: height,
`terminal`: terminal.uuid,
}, Event: terminal.event}, terminal.deviceConn)
}
}
return
}
if pack.Act == `killTerminal` {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
terminal := val.(*terminal)
if pack.Data == nil {
return
}
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
`terminal`: terminal.uuid,
}, Event: terminal.event}, terminal.deviceConn)
return
}
if pack.Act == `pong` {
return
}
session.Close()
}
func onDisconnect(session *melody.Session) {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
terminal, ok := val.(*terminal)
if !ok {
return
}
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
`terminal`: terminal.uuid,
}, Event: terminal.event}, terminal.deviceConn)
common.RemoveEvent(terminal.event)
session.Set(`Terminal`, nil)
terminal = nil
}
func simpleEncrypt(data []byte, session *melody.Session) ([]byte, bool) {
@@ -221,102 +305,24 @@ func simpleSendPack(pack modules.Packet, session *melody.Session) bool {
return err == nil
}
func onMessage(session *melody.Session, data []byte) {
var pack modules.Packet
data, ok := simpleDecrypt(data, session)
if !(ok && utils.JSON.Unmarshal(data, &pack) == nil) {
simpleSendPack(modules.Packet{Code: -1}, session)
session.Close()
return
}
session.Set(`LastPack`, time.Now().Unix())
if pack.Act == `inputTerminal` {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
termUUID, ok := val.(string)
if !ok {
return
}
val, ok = terminals.Get(termUUID)
if !ok {
return
}
terminal := val.(*terminal)
if pack.Data == nil {
return
}
if input, ok := pack.Data[`input`]; ok {
common.SendPack(modules.Packet{Act: `inputTerminal`, Data: gin.H{
`input`: input,
`terminal`: terminal.uuid,
}, Event: terminal.event}, terminal.deviceConn)
}
}
if pack.Act == `resizeTerminal` {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
termUUID, ok := val.(string)
if !ok {
return
}
val, ok = terminals.Get(termUUID)
if !ok {
return
}
terminal := val.(*terminal)
if pack.Data == nil {
return
}
if width, ok := pack.Data[`width`]; ok {
if height, ok := pack.Data[`height`]; ok {
common.SendPack(modules.Packet{Act: `resizeTerminal`, Data: gin.H{
`width`: width,
`height`: height,
`terminal`: terminal.uuid,
}, Event: terminal.event}, terminal.deviceConn)
}
}
}
if pack.Act == `killTerminal` {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
termUUID, ok := val.(string)
if !ok {
return
}
val, ok = terminals.Get(termUUID)
if !ok {
return
}
terminal := val.(*terminal)
if pack.Data == nil {
return
}
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
`terminal`: termUUID,
}, Event: terminal.event}, terminal.deviceConn)
}
}
func CloseSessionsByDevice(deviceID string) {
var queue []string
terminals.IterCb(func(key string, val interface{}) bool {
terminal := val.(*terminal)
var queue []*melody.Session
wsSessions.IterSessions(func(_ string, session *melody.Session) bool {
val, ok := session.Get(`Terminal`)
if !ok {
return true
}
terminal, ok := val.(*terminal)
if !ok {
return true
}
if terminal.device == deviceID {
common.RemoveEvent(terminal.event)
terminal.session.Close()
queue = append(queue, key)
queue = append(queue, session)
return false
}
return true
})
for _, key := range queue {
terminals.Remove(key)
for _, session := range queue {
session.Close()
}
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/kataras/golog"
"net/http"
"os"
"strconv"
"time"
)
@@ -59,24 +60,19 @@ func OnDevicePack(data []byte, session *melody.Session) error {
if len(exSession) > 0 {
common.Devices.Remove(exSession)
}
}
common.SendPack(modules.Packet{Code: 0}, session)
{
common.Devices.Set(session.UUID, &pack.Device)
} else {
val, ok := common.Devices.Get(session.UUID)
if ok {
deviceInfo := val.(*modules.Device)
deviceInfo.CPU = pack.Device.CPU
deviceInfo.RAM = pack.Device.RAM
deviceInfo.Net = pack.Device.Net
if pack.Device.Disk.Total > 0 {
deviceInfo.Disk = pack.Device.Disk
}
deviceInfo.Uptime = pack.Device.Uptime
return nil
}
common.Devices.Set(session.UUID, &pack.Device)
}
common.SendPack(modules.Packet{Code: 0}, session)
return nil
}
@@ -89,32 +85,32 @@ func checkUpdate(ctx *gin.Context) {
}
if err := ctx.ShouldBind(&form); err != nil {
golog.Error(err)
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
if form.Commit == config.COMMIT {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
return
}
tpl, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
tpl, err := os.Open(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch))
if err != nil {
ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
ctx.AbortWithStatusJSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
return
}
const MaxBodySize = 384 // This is size of client config buffer.
if ctx.Request.ContentLength > MaxBodySize {
ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
return
}
body, err := ctx.GetRawData()
if err != nil {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1})
return
}
session := common.CheckClientReq(ctx)
if session == nil {
ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
ctx.AbortWithStatusJSON(http.StatusUnauthorized, modules.Packet{Code: 1})
return
}
@@ -162,7 +158,7 @@ func getDevices(ctx *gin.Context) {
func callDevice(ctx *gin.Context) {
act := ctx.Param(`act`)
if len(act) == 0 {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
{
@@ -175,7 +171,7 @@ func callDevice(ctx *gin.Context) {
}
}
if !ok {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
}
@@ -187,7 +183,7 @@ func callDevice(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Act: act, Event: trigger}, connUUID)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}

View File

@@ -5,30 +5,32 @@ import (
"Spark/server/common"
"Spark/server/config"
"Spark/server/handler"
"Spark/utils/cmap"
"bytes"
"context"
"encoding/hex"
"fmt"
"github.com/rakyll/statik/fs"
"os"
"os/signal"
"syscall"
"time"
"github.com/rakyll/statik/fs"
_ "Spark/server/embed/built"
_ "Spark/server/embed/web"
"Spark/utils"
"Spark/utils/melody"
"encoding/hex"
"io/ioutil"
"net/http"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
"github.com/kataras/golog"
)
var lastRequest = time.Now().Unix()
func main() {
golog.SetTimeFormat(`2006/01/02 15:04:05`)
gin.SetMode(`release`)
data, err := ioutil.ReadFile(`./Config.json`)
if err != nil {
@@ -53,17 +55,20 @@ func main() {
golog.Fatal(`Failed to load static resources: `, err)
return
}
common.BuiltFS, err = fs.NewWithNamespace(`built`)
if err != nil {
golog.Fatal(`Failed to load prebuilt clients: `, err)
return
if config.Config.Debug.Gin {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
app := gin.New()
if config.Config.Debug.Pprof {
pprof.Register(app)
}
{
auth := gin.BasicAuth(config.Config.Auth)
handler.InitRouter(app.Group(`/api`), auth)
handler.AuthHandler = authCheck()
handler.InitRouter(app.Group(`/api`))
app.Any(`/ws`, wsHandshake)
app.NoRoute(auth, func(ctx *gin.Context) {
app.NoRoute(handler.AuthHandler, func(ctx *gin.Context) {
http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request)
})
}
@@ -73,7 +78,7 @@ func main() {
common.Melody.HandleMessage(wsOnMessage)
common.Melody.HandleMessageBinary(wsOnMessageBinary)
common.Melody.HandleDisconnect(wsOnDisconnect)
go common.HealthCheckWS(90, common.Melody)
go wsHealthCheck(common.Melody)
srv := &http.Server{Addr: config.Config.Listen, Handler: app}
go func() {
@@ -96,16 +101,38 @@ func main() {
}
func wsHandshake(ctx *gin.Context) {
if ctx.IsWebsocket() {
if !ctx.IsWebsocket() {
// When message is too large to transport via websocket,
// client will try to send these data via http.
const MaxBodySize = 2 << 18 //524288 512KB
if ctx.Request.ContentLength > MaxBodySize {
ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
return
}
body, err := ctx.GetRawData()
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1})
return
}
session := common.CheckClientReq(ctx)
if session == nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, modules.Packet{Code: 1})
return
}
wsOnMessageBinary(session, body)
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
return
}
clientUUID, _ := hex.DecodeString(ctx.GetHeader(`UUID`))
clientKey, _ := hex.DecodeString(ctx.GetHeader(`Key`))
if len(clientUUID) != 16 || len(clientKey) != 32 {
ctx.Status(http.StatusUnauthorized)
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
decrypted, err := common.DecAES(clientKey, config.Config.StdSalt)
if err != nil || !bytes.Equal(decrypted, clientUUID) {
ctx.Status(http.StatusUnauthorized)
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
secret := append(utils.GetUUID(), utils.GetUUID()...)
@@ -113,38 +140,18 @@ func wsHandshake(ctx *gin.Context) {
`Secret`: []string{hex.EncodeToString(secret)},
}, gin.H{
`Secret`: secret,
`LastPack`: time.Now().Unix(),
`LastPack`: common.Unix,
`Address`: common.GetRemoteAddr(ctx),
})
if err != nil {
golog.Error(err)
ctx.Status(http.StatusUpgradeRequired)
ctx.AbortWithStatus(http.StatusBadRequest)
return
}
} else {
// When message is too large to transport via websocket,
// client will try to send these data via http.
const MaxBodySize = 2 << 18 //524288 512KB
if ctx.Request.ContentLength > MaxBodySize {
ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
return
}
body, err := ctx.GetRawData()
if err != nil {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
return
}
session := common.CheckClientReq(ctx)
if session == nil {
ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
return
}
wsOnMessageBinary(session, body)
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}
func wsOnConnect(session *melody.Session) {
pingDevice(session)
}
func wsOnMessage(session *melody.Session, bytes []byte) {
@@ -160,7 +167,7 @@ func wsOnMessageBinary(session *melody.Session, data []byte) {
return
}
if pack.Act == `report` || pack.Act == `setDevice` {
session.Set(`LastPack`, time.Now().Unix())
session.Set(`LastPack`, common.Unix)
handler.OnDevicePack(data, session)
return
}
@@ -169,7 +176,7 @@ func wsOnMessageBinary(session *melody.Session, data []byte) {
return
}
common.CallEvent(pack, session)
session.Set(`LastPack`, time.Now().Unix())
session.Set(`LastPack`, common.Unix)
}
func wsOnDisconnect(session *melody.Session) {
@@ -178,5 +185,111 @@ func wsOnDisconnect(session *melody.Session) {
handler.CloseSessionsByDevice(deviceInfo.ID)
}
common.Devices.Remove(session.UUID)
}
func wsHealthCheck(container *melody.Melody) {
const MaxIdleSeconds = 150
const MaxPingInterval = 60
go func() {
// Ping clients with a dynamic interval.
// Interval will be greater than 3 seconds and less than MaxPingInterval.
var tick int64 = 0
var pingInterval int64 = 3
for range time.NewTicker(3 * time.Second).C {
tick += 3
if tick >= common.Unix-lastRequest {
pingInterval = 3
}
if tick >= 3 && (tick >= pingInterval || tick >= MaxPingInterval) {
pingInterval += 3
if pingInterval > MaxPingInterval {
pingInterval = MaxPingInterval
}
tick = 0
container.IterSessions(func(uuid string, s *melody.Session) bool {
go pingDevice(s)
return true
})
}
}
}()
for now := range time.NewTicker(60 * time.Second).C {
timestamp := now.Unix()
// Store sessions to be disconnected.
queue := make([]*melody.Session, 0)
container.IterSessions(func(uuid string, s *melody.Session) bool {
val, ok := s.Get(`LastPack`)
if !ok {
queue = append(queue, s)
return true
}
lastPack, ok := val.(int64)
if !ok {
queue = append(queue, s)
return true
}
if timestamp-lastPack > MaxIdleSeconds {
queue = append(queue, s)
}
return true
})
for i := 0; i < len(queue); i++ {
queue[i].Close()
}
}
}
func pingDevice(s *melody.Session) {
t := time.Now().UnixMilli()
trigger := utils.GetStrUUID()
common.SendPack(modules.Packet{Act: `ping`, Event: trigger}, s)
common.AddEventOnce(func(packet modules.Packet, session *melody.Session) {
val, ok := common.Devices.Get(s.UUID)
if ok {
deviceInfo := val.(*modules.Device)
deviceInfo.Latency = uint(time.Now().UnixMilli()-t) / 2
}
}, s.UUID, trigger, 3*time.Second)
}
func authCheck() gin.HandlerFunc {
// Token as key and update timestamp as value.
// Stores authenticated tokens.
tokens := cmap.New()
go func() {
for now := range time.NewTicker(60 * time.Second).C {
var queue []string
tokens.IterCb(func(key string, v interface{}) bool {
if now.Unix()-v.(int64) > 1800 {
queue = append(queue, key)
}
return true
})
tokens.Remove(queue...)
}
}()
auth := gin.BasicAuth(config.Config.Auth)
return func(ctx *gin.Context) {
now := common.Unix
passed := false
if token, err := ctx.Cookie(`Authorization`); err == nil {
if tokens.Has(token) {
lastRequest = now
tokens.Set(token, now)
passed = true
return
}
}
if !passed {
auth(ctx)
if ctx.IsAborted() {
return
}
token := utils.GetStrUUID()
tokens.Set(token, now)
ctx.Header(`Set-Cookie`, fmt.Sprintf(`Authorization=%s; Path=/; HttpOnly`, token))
}
lastRequest = now
}
}

View File

@@ -114,13 +114,15 @@ func (m ConcurrentMap) Has(key string) bool {
}
// Remove removes an element from the map.
func (m ConcurrentMap) Remove(key string) {
func (m ConcurrentMap) Remove(key ...string) {
// Try to get shard.
shard := m.GetShard(key)
for _, k := range key {
shard := m.GetShard(k)
shard.Lock()
delete(shard.items, key)
delete(shard.items, k)
shard.Unlock()
}
}
// RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held
// If returns true, the element will be removed from the map

View File

@@ -316,9 +316,7 @@ func (m *Melody) IterSessions(fn func(uuid string, s *Session) bool) {
return fn(uuid, s)
}
})
for i := range invalid {
m.hub.sessions.Remove(invalid[i])
}
m.hub.sessions.Remove(invalid...)
}
// Close closes the melody instance and all connected sessions.

View File

@@ -62,6 +62,7 @@ func (s *Session) close() {
s.open = false
s.conn.Close()
close(s.output)
s.Keys = nil
s.rwmutex.Unlock()
}
}
@@ -185,19 +186,25 @@ func (s *Session) CloseWithMsg(msg []byte) error {
return nil
}
// Set is used to store a new key/value pair exclusivelly for this session.
// It also lazy initializes s.Keys if it was not used previously.
func (s *Session) Set(key string, value interface{}) {
// Set is used to store a new key/value pair exclusively for this session.
func (s *Session) Set(key string, value interface{}) bool {
if s.closed() {
return false
}
if s.Keys == nil {
s.Keys = make(map[string]interface{})
}
s.Keys[key] = value
return true
}
// Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false)
// If the key does not exist, it returns (nil, false)
func (s *Session) Get(key string) (value interface{}, exists bool) {
if s.closed() {
return
}
if s.Keys != nil {
value, exists = s.Keys[key]
}
@@ -207,6 +214,9 @@ func (s *Session) Get(key string) (value interface{}, exists bool) {
// MustGet returns the value for the given key if it exists, otherwise it panics.
func (s *Session) MustGet(key string) interface{} {
if s.closed() {
panic("session is closed")
}
if value, exists := s.Get(key); exists {
return value
}

View File

@@ -12,7 +12,6 @@ import (
)
var (
ErrUnsupported = errors.New(`unsupported operation`)
ErrEntityInvalid = errors.New(`entity is not valid`)
ErrFailedVerification = errors.New(`failed to verify entity`)
JSON = jsoniter.ConfigCompatibleWithStandardLibrary

20
web/package-lock.json generated
View File

@@ -24,6 +24,7 @@
"react-dom": "^17.0.2",
"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",
@@ -7764,6 +7765,19 @@
"node": ">= 0.8"
}
},
"node_modules/virtuallist-antd": {
"version": "0.7.4-beta.0",
"resolved": "https://registry.npmjs.org/virtuallist-antd/-/virtuallist-antd-0.7.4-beta.0.tgz",
"integrity": "sha512-mawNCiBxNMsiq2toqvvI4USyFy69yYJXazgt/9CRGMdiPKA6azyMG56tcIoE2C51hUpgRscQuumtzNj7jsiX3Q==",
"engines": {
"node": ">=8",
"npm": ">=5"
},
"peerDependencies": {
"antd": "^4.1.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
@@ -13999,6 +14013,12 @@
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"dev": true
},
"virtuallist-antd": {
"version": "0.7.4-beta.0",
"resolved": "https://registry.npmjs.org/virtuallist-antd/-/virtuallist-antd-0.7.4-beta.0.tgz",
"integrity": "sha512-mawNCiBxNMsiq2toqvvI4USyFy69yYJXazgt/9CRGMdiPKA6azyMG56tcIoE2C51hUpgRscQuumtzNj7jsiX3Q==",
"requires": {}
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",

View File

@@ -24,6 +24,7 @@
"react-dom": "^17.0.2",
"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",

View File

@@ -8,6 +8,20 @@
min-height: 300px;
}
.ant-breadcrumb {
overflow-x: hidden;
white-space: nowrap;
}
.ant-pro-table-list-toolbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.ant-pro-table-list-toolbar::-webkit-scrollbar {
display: none;
}
.upload-progress-square > .ant-progress-outer > .ant-progress-inner {
border-radius: 0 !important;
}

View File

@@ -1,17 +1,20 @@
import React, {useEffect, useRef, useState} from 'react';
import {message, Modal, Popconfirm, Progress} from "antd";
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {Breadcrumb, Card, Image, message, Modal, Popconfirm, Progress} from "antd";
import ProTable from '@ant-design/pro-table';
import {formatSize, post, request, waitTime} from "../utils/utils";
import {formatSize, post, request, translate, waitTime} from "../utils/utils";
import dayjs from "dayjs";
import i18n from "../locale/locale";
import './explorer.css';
import {ReloadOutlined, UploadOutlined} from "@ant-design/icons";
import { VList } from "virtuallist-antd";
import {HomeOutlined, ReloadOutlined, UploadOutlined} from "@ant-design/icons";
import axios from "axios";
import Qs from "qs";
let position = '';
let fileList = [];
function FileBrowser(props) {
const [path, setPath] = useState(`/`);
const [preview, setPreview] = useState('');
const [loading, setLoading] = useState(false);
const [upload, setUpload] = useState(false);
const columns = [
@@ -55,7 +58,13 @@ function FileBrowser(props) {
setting: false,
};
const tableRef = useRef();
const virtualTable = useMemo(() => {
return VList({
height: 300
})
}, []);
useEffect(() => {
position = '/';
setPath(`/`);
if (props.visible) {
setLoading(false);
@@ -81,7 +90,7 @@ function FileBrowser(props) {
return [
<a
key='download'
onClick={downloadFile.bind(null, file.name)}
onClick={downloadFile.bind(null, file)}
>{i18n.t('download')}</a>,
remove,
];
@@ -96,7 +105,7 @@ function FileBrowser(props) {
function onRowClick(file) {
let separator = props.isWindows ? '\\' : '/';
if (file.name === '..') {
listFiles(getParentPath());
listFiles(getParentPath(position));
return;
}
if (file.type !== 0) {
@@ -107,15 +116,56 @@ function FileBrowser(props) {
}
}
listFiles(path + file.name + separator);
return;
}
let ext = file.name.split('.').pop();
if (ext === 'jpg' || ext === 'png' || ext === 'bmp' || ext === 'gif' || ext === 'jpeg') {
imgPreview(file);
return;
}
downloadFile(file);
}
function imgPreview(file) {
// Only preview image file smaller than 8MB.
if (file.size > 2 << 22) {
return;
}
setLoading(true);
request('/api/device/file/get', {device: props.device, file: path + file.name}, {}, {
responseType: 'blob',
timeout: 10000
}).then((res) => {
if ((res.data.type ?? '').substring(0, 16) === 'application/json') {
res.data.text().then((str) => {
let data = {};
try {
data = JSON.parse(str);
} catch (e) {
}
message.warn(data.msg ? translate(data.msg) : i18n.t('requestFailed'));
});
} else {
if (preview.length > 0) {
URL.revokeObjectURL(preview);
}
setPreview(URL.createObjectURL(res.data));
}
}).finally(() => {
setLoading(false);
});
}
function listFiles(newPath) {
if (loading) {
return;
}
position = newPath;
setPath(newPath);
tableRef.current.reload();
}
function getParentPath() {
function getParentPath(path) {
let separator = props.isWindows ? '\\' : '/';
// remove the last separator
// or there'll be an empty element after split
@@ -159,6 +209,7 @@ function FileBrowser(props) {
}
}
}
function uploadFile() {
if (path === '/' || path === '\\' || path.length === 0) {
if (props.isWindows) {
@@ -168,17 +219,19 @@ function FileBrowser(props) {
}
document.getElementById('uploader').click();
}
function onUploadSuccess() {
tableRef.current.reload();
setUpload(false);
}
function onUploadCancel() {
setUpload(false);
}
function downloadFile(file) {
post(location.origin + location.pathname + 'api/device/file/get', {
file: path + file,
file: path + file.name,
device: props.device
});
}
@@ -195,7 +248,7 @@ function FileBrowser(props) {
async function getData(form) {
await waitTime(300);
let res = await request('/api/device/file/list', {path: path, device: props.device});
let res = await request('/api/device/file/list', {path: position, device: props.device});
setLoading(false);
let data = res.data;
if (data.code === 0) {
@@ -211,13 +264,14 @@ function FileBrowser(props) {
modTime: 0
});
}
setPath(position);
return ({
data: data.data.files,
success: true,
total: data.data.files.length - (addParentShortcut ? 1 : 0)
});
}
setPath(getParentPath());
setPath(getParentPath(position));
return ({data: [], success: false, total: 0});
}
@@ -268,6 +322,7 @@ function FileBrowser(props) {
request={getData}
pagination={false}
actionRef={tableRef}
components={virtualTable}
>
</ProTable>
<input
@@ -283,10 +338,81 @@ function FileBrowser(props) {
onSuccess={onUploadSuccess}
onCanel={onUploadCancel}
/>
<Image
preview={{
visible: preview,
src: preview,
onVisibleChange: () => {
URL.revokeObjectURL(preview);
setPreview('');
}
}}
/>
</Modal>
)
}
function Navigator(props) {
let separator = props.isWindows ? '\\' : '/';
let path = [];
let pathItems = [];
let tempPath = props.path;
if (tempPath.endsWith(separator)) {
tempPath = tempPath.substring(0, tempPath.length - 1);
}
if (tempPath.length > 0 && tempPath !== '/' && tempPath !== '\\') {
path = tempPath.split(separator);
}
for (let i = 0; i < path.length; i++) {
let name = path[i];
if (i === 0 && props.isWindows) {
if (name.endsWith(':')) {
name = name.substring(0, name.length - 1);
}
}
pathItems.push({
name: name,
path: path.slice(0, i + 1).join(separator) + separator
});
}
if (path.length > 0 && props.isWindows) {
let first = path[0];
if (first.endsWith(':')) {
first = first.substring(0, first.length - 1);
}
path[0] = first;
}
pathItems.pop();
return (
<Breadcrumb
style={{marginLeft: '10px', marginRight: '10px'}}
disabled={props.loading}
>
<Breadcrumb.Item
style={{cursor: 'pointer'}}
onClick={props.onClick.bind(null, '/')}
>
<HomeOutlined/>
</Breadcrumb.Item>
{pathItems.map(item => (
<Breadcrumb.Item
key={item.path}
style={{cursor: 'pointer'}}
onClick={props.onClick.bind(null, item.path)}
>
{item.name}
</Breadcrumb.Item>
))}
{path.length > 0 ? (
<Breadcrumb.Item>
{path[path.length - 1]}
</Breadcrumb.Item>
) : null}
</Breadcrumb>
)
}
let abortController = null;
function UploadModal(props) {
const [visible, setVisible] = useState(!!props.file);
@@ -364,6 +490,7 @@ function UploadModal(props) {
}, 1500);
});
}
function onCancel() {
if (status === 0) {
setVisible(false);

View File

@@ -1,8 +1,9 @@
import React, {useEffect, useRef, useState} from 'react';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {message, Modal, Popconfirm} from "antd";
import ProTable from '@ant-design/pro-table';
import {request, waitTime} from "../utils/utils";
import i18n from "../locale/locale";
import {VList} from "virtuallist-antd";
function ProcessMgr(props) {
const [loading, setLoading] = useState(false);
@@ -37,6 +38,11 @@ function ProcessMgr(props) {
setting: false,
};
const tableRef = useRef();
const virtualTable = useMemo(() => {
return VList({
height: 300
})
}, []);
useEffect(() => {
if (props.visible) {
setLoading(false);
@@ -113,6 +119,7 @@ function ProcessMgr(props) {
request={getData}
pagination={false}
actionRef={tableRef}
components={virtualTable}
>
</ProTable>
</Modal>

View File

@@ -192,6 +192,9 @@ class TerminalModal extends React.Component {
if (data?.act === 'warn') {
message.warn(data.msg ? translate(data.msg) : i18n.t('unknownError'));
}
if (data?.act === 'ping') {
this.sendData({act: 'pong'});
}
}
}
this.ws.onclose = (e) => {

View File

@@ -60,6 +60,8 @@
"fileOrDirNotExist": "File or folder does not exist",
"fileOverwriteConfirm": "File [ {0} ] already exists, overwrite?",
"fileOverwrite": "Overwrite",
"fileTooLarge": "File is too large to read",
"fileEncodingUnsupported": "File encoding is not supported",
"host": "Host",
"port": "Port",

View File

@@ -61,6 +61,8 @@
"fileOrDirNotExist": "文件或目录不存在",
"fileOverwriteConfirm": "文件 [ {0} ] 已经存在,是否覆盖?",
"fileOverwrite": "覆盖",
"fileTooLarge": "文件太大,读取失败",
"fileEncodingUnsupported": "不支持该文件编码",
"registryEditor": "注册表编辑器",
"unknownRegistryKey": "注册表键有误",

View File

@@ -103,11 +103,11 @@ module.exports = (env, args) => {
hot: true,
proxy: {
'/api/': {
target: 'http://localhost:8000/',
target: 'http://localhost:8001/',
secure: false
},
'/api/device/terminal': {
target: 'ws://localhost:8000/',
target: 'ws://localhost:8001/',
ws: true
},
}