From b9a551114a2bfc6ee4fd1e949b0f9d0a53ae80f3 Mon Sep 17 00:00:00 2001 From: XZB Date: Sun, 22 May 2022 23:41:23 +0800 Subject: [PATCH] optimize: performance of front-end and back-end optimize: security vulnerability --- .github/workflows/build.yml | 24 +- API.ZH.md | 8 +- API.md | 8 +- CHANGELOG.md | 10 + README.ZH.md | 16 +- README.md | 23 +- client/config/config.go | 2 +- client/core/core.go | 25 +- client/core/device.go | 2 +- client/core/handler.go | 11 + client/service/file/file.go | 71 +++++- client/service/screenshot/unsupported.go | 4 +- go.mod | 1 + go.sum | 4 + scripts/build.client.bat | 18 +- scripts/build.client.sh | 18 +- scripts/build.server.bat | 29 ++- scripts/build.server.sh | 26 +- server/common/common.go | 51 ---- server/common/event.go | 11 +- server/common/time.go | 14 ++ server/config/config.go | 5 + server/embed/built/statik.go | 16 -- server/handler/bridge.go | 44 ++-- server/handler/file.go | 54 ++-- server/handler/generate.go | 42 +--- server/handler/handler.go | 16 +- server/handler/process.go | 8 +- server/handler/screenshot.go | 5 +- server/handler/terminal.go | 304 ++++++++++++----------- server/handler/utility.go | 32 ++- server/main.go | 201 +++++++++++---- utils/cmap/concurrent_map.go | 14 +- utils/melody/melody.go | 4 +- utils/melody/session.go | 18 +- utils/utils.go | 1 - web/package-lock.json | 20 ++ web/package.json | 1 + web/src/components/explorer.css | 14 ++ web/src/components/explorer.js | 153 +++++++++++- web/src/components/processes.js | 9 +- web/src/components/terminal.js | 3 + web/src/locale/en.json | 2 + web/src/locale/zh-CN.json | 2 + web/webpack.config.js | 4 +- 45 files changed, 855 insertions(+), 493 deletions(-) create mode 100644 server/common/time.go delete mode 100644 server/embed/built/statik.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28108eb..871d3d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/API.ZH.md b/API.ZH.md index 18daeba..798f928 100644 --- a/API.ZH.md +++ b/API.ZH.md @@ -5,7 +5,9 @@ ## 通用 所有请求均为`POST`。 -
+ +### 鉴权 + 每次请求都必须在Header中带上`Authorization`。
`Authorization`请求头格式:`Basic `(basic auth)。 @@ -18,6 +20,10 @@ Authorization: Basic Authorization: Basic WFpCOjEyNDg= ``` +在最初的Basic Authentication之后,服务端会分配一个`Authorization`的Cookie。 +
+该Cookie可用于请求的后续鉴权,可以不再附带Authorization头。 + --- ## 响应 diff --git a/API.md b/API.md index 13273ba..33796ae 100644 --- a/API.md +++ b/API.md @@ -5,7 +5,9 @@ ## Common Only `POST` requests are allowed. -
+ +### Authenticate + For every request, you should have `Authorization` on its header.
Authorization header is a string like `Basic `(basic auth). @@ -18,6 +20,10 @@ Example: Authorization: Basic WFpCOjEyNDg= ``` +After basic authentication, server will assign you a `Authorization` cookie. +
+You can use this token cookie to authenticate rest of your requests. + --- ## Response diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e9c77..bdde6d1 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/README.ZH.md b/README.ZH.md index 5fb3ce3..725074f 100644 --- a/README.ZH.md +++ b/README.ZH.md @@ -8,6 +8,18 @@ --- +
+ +|![GitHub repo size](https://img.shields.io/github/repo-size/DGP-Studio/Snap.Genshin?style=flat-square)|![GitHub issues](https://img.shields.io/github/issues/XZB-1248/Spark?style=flat-square)|![GitHub closed issues](https://img.shields.io/github/issues-closed/XZB-1248/Spark?style=flat-square)| +|-|-|-| + +|[![GitHub downloads](https://img.shields.io/github/downloads/XZB-1248/Spark/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases)|[![GitHub release (latest by date)](https://img.shields.io/github/downloads/XZB-1248/Spark/latest/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases/latest)| +|-|-| + +
+ +--- + ### **免责声明** **本项目及其源代码和发行版,旨在用于学习和交流。使用本项目所带来的风险由使用者本人承担。作者和开发者不会对你的错误使用而造成的损害承担任何责任。** @@ -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`目录,放入上文提到的配置文件,选择对应平台的服务端运行即可。 diff --git a/README.md b/README.md index f867f33..12ed2ff 100644 --- a/README.md +++ b/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) --- +
+ +|![GitHub repo size](https://img.shields.io/github/repo-size/DGP-Studio/Snap.Genshin?style=flat-square)|![GitHub issues](https://img.shields.io/github/issues/XZB-1248/Spark?style=flat-square)|![GitHub closed issues](https://img.shields.io/github/issues-closed/XZB-1248/Spark?style=flat-square)| +|-|-|-| + +|[![GitHub downloads](https://img.shields.io/github/downloads/XZB-1248/Spark/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases)|[![GitHub release (latest by date)](https://img.shields.io/github/downloads/XZB-1248/Spark/latest/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases/latest)| +|-|-| + +
+ +--- + ## **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. diff --git a/client/config/config.go b/client/config/config.go index ff8c53e..573fcd3 100644 --- a/client/config/config.go +++ b/client/config/config.go @@ -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" diff --git a/client/core/core.go b/client/core/core.go index c41f732..f3e9db0 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -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 -} diff --git a/client/core/device.go b/client/core/device.go index 4fd726d..b65e509 100644 --- a/client/core/device.go +++ b/client/core/device.go @@ -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{ diff --git a/client/core/handler.go b/client/core/handler.go index a8714d6..d1706e8 100644 --- a/client/core/handler.go +++ b/client/core/handler.go @@ -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 diff --git a/client/service/file/file.go b/client/service/file/file.go index e9cea12..3c50b5f 100644 --- a/client/service/file/file.go +++ b/client/service/file/file.go @@ -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 := `` diff --git a/client/service/screenshot/unsupported.go b/client/service/screenshot/unsupported.go index ced7966..4ae94a3 100644 --- a/client/service/screenshot/unsupported.go +++ b/client/service/screenshot/unsupported.go @@ -2,6 +2,8 @@ package screenshot +import "errors" + func GetScreenshot(bridge string) error { - return utils.ErrUnsupported + return errors.New(`${i18n|operationNotSupported}`) } diff --git a/go.mod b/go.mod index ba41fcc..7b9a222 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 8b1f81d..f4a47d4 100644 --- a/go.sum +++ b/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= diff --git a/scripts/build.client.bat b/scripts/build.client.bat index c61e11c..b4b2646 100644 --- a/scripts/build.client.bat +++ b/scripts/build.client.bat @@ -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++ diff --git a/scripts/build.client.sh b/scripts/build.client.sh index 979e795..963ee61 100644 --- a/scripts/build.client.sh +++ b/scripts/build.client.sh @@ -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++ diff --git a/scripts/build.server.bat b/scripts/build.server.bat index 57d2333..023f725 100644 --- a/scripts/build.server.bat +++ b/scripts/build.server.bat @@ -1,17 +1,25 @@ -cd .. -mkdir .\releases +set GO111MODULE=auto for /F %%i in ('git rev-parse HEAD') do ( set COMMIT=%%i) +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=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=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 @@ -21,18 +29,9 @@ 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 +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_darwin_amd64 Spark/server +go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/Server diff --git a/scripts/build.server.sh b/scripts/build.server.sh index 4c3ecb7..0aee937 100644 --- a/scripts/build.server.sh +++ b/scripts/build.server.sh @@ -3,14 +3,23 @@ export COMMIT=`git rev-parse HEAD` +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=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=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 @@ -20,18 +29,9 @@ 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 +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_darwin_amd64 Spark/server +go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/server diff --git a/server/common/common.go b/server/common/common.go index 849ffee..29e271b 100644 --- a/server/common/common.go +++ b/server/common/common.go @@ -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() { diff --git a/server/common/event.go b/server/common/event.go index 2f9e517..3e6aa83 100644 --- a/server/common/event.go +++ b/server/common/event.go @@ -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. diff --git a/server/common/time.go b/server/common/time.go new file mode 100644 index 0000000..de70438 --- /dev/null +++ b/server/common/time.go @@ -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() + } + }() +} diff --git a/server/config/config.go b/server/config/config.go index 334abbc..839d1c6 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -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 = `` diff --git a/server/embed/built/statik.go b/server/embed/built/statik.go deleted file mode 100644 index ad62327..0000000 --- a/server/embed/built/statik.go +++ /dev/null @@ -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) - } - \ No newline at end of file diff --git a/server/handler/bridge.go b/server/handler/bridge.go index f3182a7..864f145 100644 --- a/server/handler/bridge.go +++ b/server/handler/bridge.go @@ -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,26 +32,25 @@ 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) + if timestamp-b.creation > 60 && !b.using { + b.lock.Lock() + if b.src != nil && b.src.Request.Body != nil { + b.src.Request.Body.Close() + } + b.src = nil + b.dest = nil + b.lock.Unlock() + b = nil + queue = append(queue, b.uuid) } return true }) - for _, b := range queue { - bridges.Remove(b.uuid) - b.lock.Lock() - if b.src != nil && b.src.Request.Body != nil { - b.src.Request.Body.Close() - } - b.src = nil - b.dest = nil - b.lock.Unlock() - b = nil - } + 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{}, diff --git a/server/handler/file.go b/server/handler/file.go index c69623b..d340cd4 100644 --- a/server/handler/file.go +++ b/server/handler/file.go @@ -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}`}) } } @@ -66,7 +66,8 @@ func listDeviceFiles(ctx *gin.Context) { // client and let it upload the file specified. func getDeviceFile(ctx *gin.Context) { var form struct { - File string `json:"file" yaml:"file" form:"file" binding:"required"` + 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,25 +122,32 @@ 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)) } - ctx.Header(`Accept-Ranges`, `bytes`) - ctx.Header(`Content-Transfer-Encoding`, `binary`) - ctx.Header(`Content-Type`, `application/octet-stream`) - filename := src.GetHeader(`FileName`) - if len(filename) == 0 { - filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`)) + if !form.Preview { + ctx.Header(`Accept-Ranges`, `bytes`) + ctx.Header(`Content-Transfer-Encoding`, `binary`) + ctx.Header(`Content-Type`, `application/octet-stream`) + filename := src.GetHeader(`FileName`) + if len(filename) == 0 { + filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`)) + } + filename = url.PathEscape(filename) + ctx.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`) } - 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) } diff --git a/server/handler/generate.go b/server/handler/generate.go index e261dd3..e3e70be 100644 --- a/server/handler/generate.go +++ b/server/handler/generate.go @@ -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`) diff --git a/server/handler/handler.go b/server/handler/handler.go index b09c109..1cdd716 100644 --- a/server/handler/handler.go +++ b/server/handler/handler.go @@ -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) + ctx.Any(`/client/update`, checkUpdate) // Client, for update. + 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 diff --git a/server/handler/process.go b/server/handler/process.go index f30896f..07181e2 100644 --- a/server/handler/process.go +++ b/server/handler/process.go @@ -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}`}) } } diff --git a/server/handler/screenshot.go b/server/handler/screenshot.go index fc6c84f..28e5e76 100644 --- a/server/handler/screenshot.go +++ b/server/handler/screenshot.go @@ -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) } diff --git a/server/handler/terminal.go b/server/handler/terminal.go index 5e97127..15ef2a9 100644 --- a/server/handler/terminal.go +++ b/server/handler/terminal.go @@ -4,7 +4,6 @@ import ( "Spark/modules" "Spark/server/common" "Spark/utils" - "Spark/utils/cmap" "Spark/utils/melody" "crypto/aes" "crypto/cipher" @@ -22,111 +21,46 @@ type terminal struct { deviceConn *melody.Session } -var terminals = cmap.New() var wsSessions = melody.New() func init() { - wsSessions.HandleConnect(func(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) - session.Close() - return - } - deviceConn, ok := common.Melody.GetSessionByUUID(connUUID) - if !ok { - simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session) - session.Close() - return - } - eventUUID := utils.GetStrUUID() - terminal := &terminal{ - uuid: termUUID, - event: eventUUID, - device: device.(string), - session: session, - deviceConn: deviceConn, - } - terminals.Set(termUUID, terminal) - common.AddEvent(eventWrapper(terminal), connUUID, eventUUID) - common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{ - `terminal`: termUUID, - }, Event: eventUUID}, deviceConn) - }) + wsSessions.HandleConnect(onConnect) wsSessions.HandleMessage(onMessage) wsSessions.HandleMessageBinary(onMessage) - wsSessions.HandleDisconnect(func(session *melody.Session) { - 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) - 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) + wsSessions.HandleDisconnect(onDisconnect) + go wsHealthCheck(wsSessions) } // initTerminal handles terminal websocket handshake event func initTerminal(ctx *gin.Context) { if !ctx.IsWebsocket() { - ctx.Status(http.StatusUpgradeRequired) + ctx.AbortWithStatus(http.StatusBadRequest) return } secretStr, ok := ctx.GetQuery(`secret`) if !ok || len(secretStr) != 32 { - ctx.Status(http.StatusBadRequest) + ctx.AbortWithStatus(http.StatusBadRequest) return } secret, err := hex.DecodeString(secretStr) if err != nil { - ctx.Status(http.StatusBadRequest) + ctx.AbortWithStatus(http.StatusBadRequest) return } device, ok := ctx.GetQuery(`device`) if !ok { - ctx.Status(http.StatusBadRequest) + ctx.AbortWithStatus(http.StatusBadRequest) return } if _, ok := common.CheckDevice(device, ``); !ok { - ctx.Status(http.StatusBadRequest) + ctx.AbortWithStatus(http.StatusBadRequest) return } wsSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{ `Secret`: secret, `Device`: device, - `LastPack`: time.Now().Unix(), - `Terminal`: utils.GetStrUUID(), + `LastPack`: common.Unix, }) } @@ -143,7 +77,6 @@ func eventWrapper(terminal *terminal) common.EventCallback { msg += `${i18n|unknownError}` } simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session) - terminals.Remove(terminal.uuid) common.RemoveEvent(terminal.event) terminal.session.Close() } @@ -155,7 +88,6 @@ func eventWrapper(terminal *terminal) common.EventCallback { msg = pack.Msg } simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session) - terminals.Remove(terminal.uuid) common.RemoveEvent(terminal.event) terminal.session.Close() return @@ -173,52 +105,73 @@ func eventWrapper(terminal *terminal) common.EventCallback { } } -func simpleEncrypt(data []byte, session *melody.Session) ([]byte, bool) { - temp, ok := session.Get(`Secret`) - if !ok { - return nil, false +func wsHealthCheck(container *melody.Melody) { + const MaxIdleSeconds = 300 + ping := func(uuid string, s *melody.Session) { + if !simpleSendPack(modules.Packet{Act: `ping`}, s) { + s.Close() + } } - secret := temp.([]byte) - block, err := aes.NewCipher(secret) - if err != nil { - return nil, false + 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() + } } - stream := cipher.NewCTR(block, secret) - encBuffer := make([]byte, len(data)) - stream.XORKeyStream(encBuffer, data) - return encBuffer, true } -func simpleDecrypt(data []byte, session *melody.Session) ([]byte, bool) { - temp, ok := session.Get(`Secret`) +func onConnect(session *melody.Session) { + device, ok := session.Get(`Device`) if !ok { - return nil, false - } - secret := temp.([]byte) - block, err := aes.NewCipher(secret) - if err != nil { - return nil, false - } - stream := cipher.NewCTR(block, secret) - decBuffer := make([]byte, len(data)) - stream.XORKeyStream(decBuffer, data) - return decBuffer, true -} - -func simpleSendPack(pack modules.Packet, session *melody.Session) bool { - if session == nil { - return false + simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session) + session.Close() + return } - data, err := utils.JSON.Marshal(pack) - if err != nil { - return false + connUUID, ok := common.CheckDevice(device.(string), ``) + if !ok { + simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session) + session.Close() + return } - data, ok := simpleEncrypt(data, session) + deviceConn, ok := common.Melody.GetSessionByUUID(connUUID) if !ok { - return false + simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session) + session.Close() + return } - err = session.WriteBinary(data) - return err == nil + termUUID := utils.GetStrUUID() + eventUUID := utils.GetStrUUID() + terminal := &terminal{ + uuid: termUUID, + event: eventUUID, + device: device.(string), + session: session, + deviceConn: deviceConn, + } + session.Set(`Terminal`, terminal) + common.AddEvent(eventWrapper(terminal), connUUID, eventUUID) + common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{ + `terminal`: termUUID, + }, Event: eventUUID}, deviceConn) } func onMessage(session *melody.Session, data []byte) { @@ -229,20 +182,12 @@ func onMessage(session *melody.Session, data []byte) { session.Close() return } - session.Set(`LastPack`, time.Now().Unix()) + session.Set(`LastPack`, common.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 @@ -253,20 +198,13 @@ func onMessage(session *melody.Session, data []byte) { `terminal`: terminal.uuid, }, Event: terminal.event}, terminal.deviceConn) } + return } 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 @@ -280,43 +218,111 @@ func onMessage(session *melody.Session, data []byte) { }, Event: terminal.event}, terminal.deviceConn) } } + return } 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, + `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) { + temp, ok := session.Get(`Secret`) + if !ok { + return nil, false + } + secret := temp.([]byte) + block, err := aes.NewCipher(secret) + if err != nil { + return nil, false + } + stream := cipher.NewCTR(block, secret) + encBuffer := make([]byte, len(data)) + stream.XORKeyStream(encBuffer, data) + return encBuffer, true +} + +func simpleDecrypt(data []byte, session *melody.Session) ([]byte, bool) { + temp, ok := session.Get(`Secret`) + if !ok { + return nil, false + } + secret := temp.([]byte) + block, err := aes.NewCipher(secret) + if err != nil { + return nil, false + } + stream := cipher.NewCTR(block, secret) + decBuffer := make([]byte, len(data)) + stream.XORKeyStream(decBuffer, data) + return decBuffer, true +} + +func simpleSendPack(pack modules.Packet, session *melody.Session) bool { + if session == nil { + return false + } + data, err := utils.JSON.Marshal(pack) + if err != nil { + return false + } + data, ok := simpleEncrypt(data, session) + if !ok { + return false + } + err = session.WriteBinary(data) + return err == nil } 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() } } diff --git a/server/handler/utility.go b/server/handler/utility.go index ea82f34..03f1c0a 100644 --- a/server/handler/utility.go +++ b/server/handler/utility.go @@ -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.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}) } diff --git a/server/main.go b/server/main.go index ab32c06..1f0b6be 100644 --- a/server/main.go +++ b/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,55 +101,57 @@ func main() { } func wsHandshake(ctx *gin.Context) { - if ctx.IsWebsocket() { - clientUUID, _ := hex.DecodeString(ctx.GetHeader(`UUID`)) - clientKey, _ := hex.DecodeString(ctx.GetHeader(`Key`)) - if len(clientUUID) != 16 || len(clientKey) != 32 { - ctx.Status(http.StatusUnauthorized) - return - } - decrypted, err := common.DecAES(clientKey, config.Config.StdSalt) - if err != nil || !bytes.Equal(decrypted, clientUUID) { - ctx.Status(http.StatusUnauthorized) - return - } - secret := append(utils.GetUUID(), utils.GetUUID()...) - err = common.Melody.HandleRequestWithKeys(ctx.Writer, ctx.Request, http.Header{ - `Secret`: []string{hex.EncodeToString(secret)}, - }, gin.H{ - `Secret`: secret, - `LastPack`: time.Now().Unix(), - `Address`: common.GetRemoteAddr(ctx), - }) - if err != nil { - golog.Error(err) - ctx.Status(http.StatusUpgradeRequired) - return - } - } else { + 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.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 } 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.AbortWithStatus(http.StatusUnauthorized) + return + } + decrypted, err := common.DecAES(clientKey, config.Config.StdSalt) + if err != nil || !bytes.Equal(decrypted, clientUUID) { + ctx.AbortWithStatus(http.StatusUnauthorized) + return + } + secret := append(utils.GetUUID(), utils.GetUUID()...) + err = common.Melody.HandleRequestWithKeys(ctx.Writer, ctx.Request, http.Header{ + `Secret`: []string{hex.EncodeToString(secret)}, + }, gin.H{ + `Secret`: secret, + `LastPack`: common.Unix, + `Address`: common.GetRemoteAddr(ctx), + }) + if err != nil { + golog.Error(err) + ctx.AbortWithStatus(http.StatusBadRequest) + return } } 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 + } } diff --git a/utils/cmap/concurrent_map.go b/utils/cmap/concurrent_map.go index 4bca4a7..687a827 100644 --- a/utils/cmap/concurrent_map.go +++ b/utils/cmap/concurrent_map.go @@ -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) - shard.Lock() - delete(shard.items, key) - shard.Unlock() + for _, k := range key { + shard := m.GetShard(k) + shard.Lock() + 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() diff --git a/utils/melody/melody.go b/utils/melody/melody.go index 36afcd3..815552c 100644 --- a/utils/melody/melody.go +++ b/utils/melody/melody.go @@ -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. diff --git a/utils/melody/session.go b/utils/melody/session.go index af398b6..7ad1414 100644 --- a/utils/melody/session.go +++ b/utils/melody/session.go @@ -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 } diff --git a/utils/utils.go b/utils/utils.go index f16d32b..1d871b3 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -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 diff --git a/web/package-lock.json b/web/package-lock.json index 0d026f2..5a48a0c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index 9062e16..ec84ed4 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/components/explorer.css b/web/src/components/explorer.css index 7a9afca..eb6fd67 100644 --- a/web/src/components/explorer.css +++ b/web/src/components/explorer.css @@ -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; } \ No newline at end of file diff --git a/web/src/components/explorer.js b/web/src/components/explorer.js index 6da8c8c..82c7e41 100644 --- a/web/src/components/explorer.js +++ b/web/src/components/explorer.js @@ -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 [ {i18n.t('download')}, 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: , + icon: , tooltip: i18n.t('upload'), key: 'upload', onClick: uploadFile @@ -268,12 +322,13 @@ function FileBrowser(props) { request={getData} pagination={false} actionRef={tableRef} + components={virtualTable} > + { + URL.revokeObjectURL(preview); + setPreview(''); + } + }} + /> ) } +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 ( + + + + + {pathItems.map(item => ( + + {item.name} + + ))} + {path.length > 0 ? ( + + {path[path.length - 1]} + + ) : null} + + ) +} + 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}} diff --git a/web/src/components/processes.js b/web/src/components/processes.js index 9bfd2f5..d4b4ab3 100644 --- a/web/src/components/processes.js +++ b/web/src/components/processes.js @@ -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} > diff --git a/web/src/components/terminal.js b/web/src/components/terminal.js index 7ee4ec2..de968ab 100644 --- a/web/src/components/terminal.js +++ b/web/src/components/terminal.js @@ -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) => { diff --git a/web/src/locale/en.json b/web/src/locale/en.json index 7adc2a6..0264ced 100644 --- a/web/src/locale/en.json +++ b/web/src/locale/en.json @@ -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", diff --git a/web/src/locale/zh-CN.json b/web/src/locale/zh-CN.json index bdbad00..644160e 100644 --- a/web/src/locale/zh-CN.json +++ b/web/src/locale/zh-CN.json @@ -61,6 +61,8 @@ "fileOrDirNotExist": "文件或目录不存在", "fileOverwriteConfirm": "文件 [ {0} ] 已经存在,是否覆盖?", "fileOverwrite": "覆盖", + "fileTooLarge": "文件太大,读取失败", + "fileEncodingUnsupported": "不支持该文件编码", "registryEditor": "注册表编辑器", "unknownRegistryKey": "注册表键有误", diff --git a/web/webpack.config.js b/web/webpack.config.js index 8445ad2..15231b6 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -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 }, }