mirror of
https://github.com/XZB-1248/Spark
synced 2025-10-06 08:36:51 +08:00
optimize: performance of front-end and back-end
optimize: security vulnerability
This commit is contained in:
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
@@ -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
8
API.md
@@ -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
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,3 +1,13 @@
|
||||
## v0.0.9
|
||||
|
||||
* Optimize: performance of front-end and back-end.
|
||||
* Optimize: security vulnerability.
|
||||
|
||||
* 优化:前后端性能。
|
||||
* 优化:安全问题。
|
||||
|
||||
|
||||
|
||||
## v0.0.8
|
||||
|
||||
* Add: file upload.
|
||||
|
16
README.ZH.md
16
README.ZH.md
@@ -8,6 +8,18 @@
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
||||
|
||||
|-|-|-|
|
||||
|
||||
|[](https://github.com/XZB-1248/Spark/releases)|[](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`目录,放入上文提到的配置文件,选择对应平台的服务端运行即可。
|
||||
|
23
README.md
23
README.md
@@ -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">
|
||||
|
||||
||||
|
||||
|-|-|-|
|
||||
|
||||
|[](https://github.com/XZB-1248/Spark/releases)|[](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.
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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{
|
||||
|
@@ -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
|
||||
|
@@ -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 := ``
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
package screenshot
|
||||
|
||||
import "errors"
|
||||
|
||||
func GetScreenshot(bridge string) error {
|
||||
return utils.ErrUnsupported
|
||||
return errors.New(`${i18n|operationNotSupported}`)
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
@@ -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++
|
||||
|
@@ -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++
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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() {
|
||||
|
@@ -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
14
server/common/time.go
Normal 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()
|
||||
}
|
||||
}()
|
||||
}
|
@@ -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 = ``
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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{},
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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`)
|
||||
|
@@ -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
|
||||
|
@@ -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}`})
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
@@ -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})
|
||||
}
|
||||
|
197
server/main.go
197
server/main.go
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -114,12 +114,14 @@ 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
|
||||
@@ -260,7 +262,7 @@ type IterCb func(key string, v interface{}) bool
|
||||
// IterCb callback based iterator, the cheapest way to read
|
||||
// all elements in a map.
|
||||
func (m ConcurrentMap) IterCb(fn IterCb) {
|
||||
escape:=false
|
||||
escape := false
|
||||
for idx := range m {
|
||||
shard := (m)[idx]
|
||||
shard.RLock()
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
20
web/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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;
|
||||
}
|
@@ -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});
|
||||
}
|
||||
|
||||
@@ -242,7 +296,7 @@ function FileBrowser(props) {
|
||||
toolbar={{
|
||||
settings: [
|
||||
{
|
||||
icon: <UploadOutlined />,
|
||||
icon: <UploadOutlined/>,
|
||||
tooltip: i18n.t('upload'),
|
||||
key: 'upload',
|
||||
onClick: uploadFile
|
||||
@@ -268,12 +322,13 @@ function FileBrowser(props) {
|
||||
request={getData}
|
||||
pagination={false}
|
||||
actionRef={tableRef}
|
||||
components={virtualTable}
|
||||
>
|
||||
</ProTable>
|
||||
<input
|
||||
id='uploader'
|
||||
type='file'
|
||||
style={{ display: 'none' }}
|
||||
style={{display: 'none'}}
|
||||
onChange={onFileChange}
|
||||
/>
|
||||
<UploadModal
|
||||
@@ -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);
|
||||
@@ -413,7 +540,7 @@ function UploadModal(props) {
|
||||
maskClosable={false}
|
||||
destroyOnClose={true}
|
||||
confirmLoading={status === 1}
|
||||
okText={i18n.t(status===1?'uploading':'upload')}
|
||||
okText={i18n.t(status === 1 ? 'uploading' : 'upload')}
|
||||
onOk={onConfirm}
|
||||
onCancel={onCancel}
|
||||
okButtonProps={{disabled: status !== 0}}
|
||||
|
@@ -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>
|
||||
|
@@ -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) => {
|
||||
|
@@ -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",
|
||||
|
@@ -61,6 +61,8 @@
|
||||
"fileOrDirNotExist": "文件或目录不存在",
|
||||
"fileOverwriteConfirm": "文件 [ {0} ] 已经存在,是否覆盖?",
|
||||
"fileOverwrite": "覆盖",
|
||||
"fileTooLarge": "文件太大,读取失败",
|
||||
"fileEncodingUnsupported": "不支持该文件编码",
|
||||
|
||||
"registryEditor": "注册表编辑器",
|
||||
"unknownRegistryKey": "注册表键有误",
|
||||
|
@@ -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
|
||||
},
|
||||
}
|
||||
|
Reference in New Issue
Block a user