optimize: performance of front-end and back-end

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

View File

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

View File

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

8
API.md
View File

@@ -5,7 +5,9 @@
## Common ## Common
Only `POST` requests are allowed. Only `POST` requests are allowed.
<br />
### Authenticate
For every request, you should have `Authorization` on its header. For every request, you should have `Authorization` on its header.
<br /> <br />
Authorization header is a string like `Basic <token>`(basic auth). Authorization header is a string like `Basic <token>`(basic auth).
@@ -18,6 +20,10 @@ Example:
Authorization: Basic WFpCOjEyNDg= 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 ## Response

View File

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

View File

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

View File

@@ -3,15 +3,30 @@
**Spark** is a free, safe, open-source, web-based, cross-platform and full-featured RAT (Remote Administration Tool) **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. 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) ### [English] [[中文]](./README.ZH.md) [[API Document]](./API.md) [[API文档]](./API.ZH.md)
--- ---
<div align="center">
|![GitHub repo size](https://img.shields.io/github/repo-size/DGP-Studio/Snap.Genshin?style=flat-square)|![GitHub issues](https://img.shields.io/github/issues/XZB-1248/Spark?style=flat-square)|![GitHub closed issues](https://img.shields.io/github/issues-closed/XZB-1248/Spark?style=flat-square)|
|-|-|-|
|[![GitHub downloads](https://img.shields.io/github/downloads/XZB-1248/Spark/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases)|[![GitHub release (latest by date)](https://img.shields.io/github/downloads/XZB-1248/Spark/latest/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases/latest)|
|-|-|
</div>
---
## **Disclaimer** ## **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.** **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. # When you're using unix-like OS, you can use this.
$ go mod tidy $ go mod tidy
$ go mod download $ go mod download
$ ./build.client.sh $ ./scripts/build.client.sh
$ statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built $ statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
# Finally we're compiling the server side. # Finally we're compiling the server side.
$ ./build.server.sh $ ./scripts/build.server.sh
``` ```
Then you can find executable files in `releases` directory. Then you can find executable files in `releases` directory.

View File

@@ -16,7 +16,7 @@ type Cfg struct {
// Localhost for my development only. // Localhost for my development only.
// Shall be commented out when development is done. // 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 // 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" var CfgBuffer = "\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19"

View File

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

View File

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

View File

@@ -8,11 +8,22 @@ import (
Screenshot "Spark/client/service/screenshot" Screenshot "Spark/client/service/screenshot"
"Spark/client/service/terminal" "Spark/client/service/terminal"
"Spark/modules" "Spark/modules"
"github.com/kataras/golog"
"os" "os"
"reflect" "reflect"
"strconv" "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) { func offline(pack modules.Packet, wsConn *common.Conn) {
common.SendCb(modules.Packet{Code: 0}, pack, wsConn) common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
stop = true stop = true

View File

@@ -3,13 +3,13 @@ package file
import ( import (
"Spark/client/config" "Spark/client/config"
"errors" "errors"
"github.com/imroc/req/v3"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"strconv" "strconv"
"unicode/utf8"
"github.com/imroc/req/v3"
) )
type File struct { type File struct {
@@ -41,6 +41,53 @@ func listFiles(path string) ([]File, error) {
return result, nil 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. // FetchFile saves file from bridge to local.
// Save body as temp file and when done, rename it to file. // Save body as temp file and when done, rename it to file.
func FetchFile(dir, file, bridge string) error { func FetchFile(dir, file, bridge string) error {
@@ -162,6 +209,26 @@ func UploadFile(path, bridge string, start, end int64) error {
return err 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 { func getTempFileName(dir, file string) string {
exists := true exists := true
tempFile := `` tempFile := ``

View File

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

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.17
require ( require (
github.com/creack/pty v1.1.18 github.com/creack/pty v1.1.18
github.com/denisbrodbeck/machineid v1.0.1 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/gin-gonic/gin v1.7.7
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/imroc/req/v3 v3.8.2 github.com/imroc/req/v3 v3.8.2

4
go.sum
View File

@@ -7,8 +7,11 @@ github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMS
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/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 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY=
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo= 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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 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 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 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/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 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 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 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 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= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=

View File

@@ -7,10 +7,10 @@ set GOOS=linux
set GOARCH=arm set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_arm Spark/client 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 set GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_i386 Spark/client 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 set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_amd64 Spark/client 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 set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_arm Spark/client 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 set GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_i386 Spark/client 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 set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_amd64 Spark/client 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 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 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 GOARCH=386
@REM set CC=i686-linux-android21-clang @REM set CC=i686-linux-android21-clang
@REM set CXX=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 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 GOARCH=amd64
@REM set CC=x86_64-linux-android21-clang @REM set CC=x86_64-linux-android21-clang
@REM set CXX=x86_64-linux-android21-clang++ @REM set CXX=x86_64-linux-android21-clang++

View File

@@ -7,10 +7,10 @@ export GOOS=linux
export GOARCH=arm export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_arm Spark/client 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 export GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_i386 Spark/client 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 export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_amd64 Spark/client 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 export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_arm Spark/client 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 export GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_i386 Spark/client 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 export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_amd64 Spark/client 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++ # export CXX=armv7a-linux-androideabi21-clang++
# go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/android_arm Spark/client # 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 GOARCH=386
# export CC=i686-linux-android21-clang # export CC=i686-linux-android21-clang
# export CXX=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 # 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 GOARCH=amd64
# export CC=x86_64-linux-android21-clang # export CC=x86_64-linux-android21-clang
# export CXX=x86_64-linux-android21-clang++ # export CXX=x86_64-linux-android21-clang++

View File

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

View File

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

View File

@@ -11,14 +11,11 @@ import (
"encoding/hex" "encoding/hex"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net" "net"
"net/http"
"strings" "strings"
"time"
) )
var Melody = melody.New() var Melody = melody.New()
var Devices = cmap.New() var Devices = cmap.New()
var BuiltFS http.FileSystem
func SendPackByUUID(pack modules.Packet, uuid string) bool { func SendPackByUUID(pack modules.Packet, uuid string) bool {
session, ok := Melody.GetSessionByUUID(uuid) session, ok := Melody.GetSessionByUUID(uuid)
@@ -70,54 +67,6 @@ func Decrypt(data []byte, session *melody.Session) ([]byte, bool) {
return dec, true 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 { func GetRemoteAddr(ctx *gin.Context) string {
if remote, ok := ctx.RemoteIP(); ok { if remote, ok := ctx.RemoteIP(); ok {
if remote.IsLoopback() { if remote.IsLoopback() {

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"math/big" "math/big"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
) )
@@ -29,25 +30,6 @@ var (
errTooLargeEntity = errors.New(`length of data can not excess buffer size`) 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) { func checkClient(ctx *gin.Context) {
var form struct { var form struct {
OS string `json:"os" yaml:"os" form:"os" binding:"required"` 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"` Secure string `json:"secure" yaml:"secure" form:"secure"`
} }
if err := ctx.ShouldBind(&form); err != nil { 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 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 { 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 return
} }
_, err = genConfig(clientCfg{ _, err = genConfig(clientCfg{
@@ -76,10 +58,10 @@ func checkClient(ctx *gin.Context) {
}) })
if err != nil { if err != nil {
if err == errTooLargeEntity { 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 return
} }
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`}) ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
return return
} }
ctx.JSON(http.StatusOK, modules.Packet{Code: 0}) 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"` Secure string `json:"secure" yaml:"secure" form:"secure"`
} }
if err := ctx.ShouldBind(&form); err != nil { 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 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 { 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 return
} }
clientUUID := utils.GetUUID() clientUUID := utils.GetUUID()
clientKey, err := common.EncAES(clientUUID, config.Config.StdSalt) clientKey, err := common.EncAES(clientUUID, config.Config.StdSalt)
if err != nil { 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 return
} }
cfgBytes, err := genConfig(clientCfg{ cfgBytes, err := genConfig(clientCfg{
@@ -119,10 +101,10 @@ func generateClient(ctx *gin.Context) {
}) })
if err != nil { if err != nil {
if err == errTooLargeEntity { 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 return
} }
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`}) ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
return return
} }
ctx.Header(`Accept-Ranges`, `none`) ctx.Header(`Accept-Ranges`, `none`)

View File

@@ -7,13 +7,14 @@ import (
"net/http" "net/http"
) )
var AuthHandler gin.HandlerFunc
// InitRouter will initialize http and websocket routers. // 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/push`, bridgePush)
ctx.Any(`/bridge/pull`, bridgePull) 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. 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/screenshot/get`, getScreenshot)
group.POST(`/device/process/list`, listDeviceProcesses) 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(`/device/:act`, callDevice)
group.POST(`/client/check`, checkClient) group.POST(`/client/check`, checkClient)
group.POST(`/client/generate`, generateClient) 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"` Device string `json:"device" yaml:"device" form:"device"`
} }
if form != nil && ctx.ShouldBind(form) != nil { 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 return ``, false
} }
if ctx.ShouldBind(&base) != nil || (len(base.Conn) == 0 && len(base.Device) == 0) { 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 return ``, false
} }
connUUID, ok := common.CheckDevice(base.Device, base.Conn) connUUID, ok := common.CheckDevice(base.Device, base.Conn)
if !ok { 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 ``, false
} }
return connUUID, true return connUUID, true

View File

@@ -21,13 +21,13 @@ func listDeviceProcesses(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Act: `listProcesses`, Event: trigger}, connUUID) common.SendPackByUUID(modules.Packet{Act: `listProcesses`, Event: trigger}, connUUID)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) { ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 { 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 { } else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: p.Data}) ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: p.Data})
} }
}, connUUID, trigger, 5*time.Second) }, connUUID, trigger, 5*time.Second)
if !ok { 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) 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) { ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 { 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 { } else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0}) ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
} }
}, target, trigger, 5*time.Second) }, target, trigger, 5*time.Second)
if !ok { if !ok {
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`}) ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} }
} }

View File

@@ -26,7 +26,7 @@ func getScreenshot(ctx *gin.Context) {
called = true called = true
removeBridge(bridgeID) removeBridge(bridgeID)
common.RemoveEvent(trigger) 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) }, target, trigger)
instance := addBridgeWithDest(nil, bridgeID, ctx) instance := addBridgeWithDest(nil, bridgeID, ctx)
instance.OnPush = func(bridge *bridge) { instance.OnPush = func(bridge *bridge) {
@@ -43,9 +43,10 @@ func getScreenshot(ctx *gin.Context) {
if !called { if !called {
removeBridge(bridgeID) removeBridge(bridgeID)
common.RemoveEvent(trigger) 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 { } else {
<-wait <-wait
} }
} }
close(wait)
} }

View File

@@ -4,7 +4,6 @@ import (
"Spark/modules" "Spark/modules"
"Spark/server/common" "Spark/server/common"
"Spark/utils" "Spark/utils"
"Spark/utils/cmap"
"Spark/utils/melody" "Spark/utils/melody"
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
@@ -22,29 +21,131 @@ type terminal struct {
deviceConn *melody.Session deviceConn *melody.Session
} }
var terminals = cmap.New()
var wsSessions = melody.New() var wsSessions = melody.New()
func init() { 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`) device, ok := session.Get(`Device`)
if !ok { if !ok {
simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session) simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
session.Close() session.Close()
return 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), ``) connUUID, ok := common.CheckDevice(device.(string), ``)
if !ok { if !ok {
simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session) simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session)
@@ -57,6 +158,7 @@ func init() {
session.Close() session.Close()
return return
} }
termUUID := utils.GetStrUUID()
eventUUID := utils.GetStrUUID() eventUUID := utils.GetStrUUID()
terminal := &terminal{ terminal := &terminal{
uuid: termUUID, uuid: termUUID,
@@ -65,112 +167,94 @@ func init() {
session: session, session: session,
deviceConn: deviceConn, deviceConn: deviceConn,
} }
terminals.Set(termUUID, terminal) session.Set(`Terminal`, terminal)
common.AddEvent(eventWrapper(terminal), connUUID, eventUUID) common.AddEvent(eventWrapper(terminal), connUUID, eventUUID)
common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{ common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{
`terminal`: termUUID, `terminal`: termUUID,
}, Event: eventUUID}, deviceConn) }, Event: eventUUID}, deviceConn)
}) }
wsSessions.HandleMessage(onMessage)
wsSessions.HandleMessageBinary(onMessage) func onMessage(session *melody.Session, data []byte) {
wsSessions.HandleDisconnect(func(session *melody.Session) { 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`) val, ok := session.Get(`Terminal`)
if !ok { if !ok {
return return
} }
termUUID, ok := val.(string) terminal := val.(*terminal)
if !ok { if pack.Data == nil {
return 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 { if !ok {
return return
} }
terminal := val.(*terminal) 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 { if pack.Data == nil {
return return
} }
if output, ok := pack.Data[`output`]; ok { if width, ok := pack.Data[`width`]; ok {
simpleSendPack(modules.Packet{Act: `outputTerminal`, Data: gin.H{ if height, ok := pack.Data[`height`]; ok {
`output`: output, common.SendPack(modules.Packet{Act: `resizeTerminal`, Data: gin.H{
}}, terminal.session) `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) { 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 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) { func CloseSessionsByDevice(deviceID string) {
var queue []string var queue []*melody.Session
terminals.IterCb(func(key string, val interface{}) bool { wsSessions.IterSessions(func(_ string, session *melody.Session) bool {
terminal := val.(*terminal) val, ok := session.Get(`Terminal`)
if !ok {
return true
}
terminal, ok := val.(*terminal)
if !ok {
return true
}
if terminal.device == deviceID { if terminal.device == deviceID {
common.RemoveEvent(terminal.event) queue = append(queue, session)
terminal.session.Close() return false
queue = append(queue, key)
} }
return true return true
}) })
for _, session := range queue {
for _, key := range queue { session.Close()
terminals.Remove(key)
} }
} }

View File

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

View File

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

View File

@@ -114,12 +114,14 @@ func (m ConcurrentMap) Has(key string) bool {
} }
// Remove removes an element from the map. // Remove removes an element from the map.
func (m ConcurrentMap) Remove(key string) { func (m ConcurrentMap) Remove(key ...string) {
// Try to get shard. // Try to get shard.
shard := m.GetShard(key) for _, k := range key {
shard := m.GetShard(k)
shard.Lock() shard.Lock()
delete(shard.items, key) delete(shard.items, k)
shard.Unlock() shard.Unlock()
}
} }
// RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held // 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 // IterCb callback based iterator, the cheapest way to read
// all elements in a map. // all elements in a map.
func (m ConcurrentMap) IterCb(fn IterCb) { func (m ConcurrentMap) IterCb(fn IterCb) {
escape:=false escape := false
for idx := range m { for idx := range m {
shard := (m)[idx] shard := (m)[idx]
shard.RLock() shard.RLock()

View File

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

View File

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

View File

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

20
web/package-lock.json generated
View File

@@ -24,6 +24,7 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-router": "^6.2.2", "react-router": "^6.2.2",
"react-router-dom": "^6.2.2", "react-router-dom": "^6.2.2",
"virtuallist-antd": "^0.7.4-beta.0",
"wcwidth": "^1.0.1", "wcwidth": "^1.0.1",
"xterm": "^4.18.0", "xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0", "xterm-addon-fit": "^0.5.0",
@@ -7764,6 +7765,19 @@
"node": ">= 0.8" "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": { "node_modules/warning": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
@@ -13999,6 +14013,12 @@
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"dev": true "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": { "warning": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",

View File

@@ -24,6 +24,7 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-router": "^6.2.2", "react-router": "^6.2.2",
"react-router-dom": "^6.2.2", "react-router-dom": "^6.2.2",
"virtuallist-antd": "^0.7.4-beta.0",
"wcwidth": "^1.0.1", "wcwidth": "^1.0.1",
"xterm": "^4.18.0", "xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0", "xterm-addon-fit": "^0.5.0",

View File

@@ -8,6 +8,20 @@
min-height: 300px; 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 { .upload-progress-square > .ant-progress-outer > .ant-progress-inner {
border-radius: 0 !important; border-radius: 0 !important;
} }

View File

@@ -1,17 +1,20 @@
import React, {useEffect, useRef, useState} from 'react'; import React, {useEffect, useMemo, useRef, useState} from 'react';
import {message, Modal, Popconfirm, Progress} from "antd"; import {Breadcrumb, Card, Image, message, Modal, Popconfirm, Progress} from "antd";
import ProTable from '@ant-design/pro-table'; 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 dayjs from "dayjs";
import i18n from "../locale/locale"; import i18n from "../locale/locale";
import './explorer.css'; 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 axios from "axios";
import Qs from "qs"; import Qs from "qs";
let position = '';
let fileList = []; let fileList = [];
function FileBrowser(props) { function FileBrowser(props) {
const [path, setPath] = useState(`/`); const [path, setPath] = useState(`/`);
const [preview, setPreview] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [upload, setUpload] = useState(false); const [upload, setUpload] = useState(false);
const columns = [ const columns = [
@@ -55,7 +58,13 @@ function FileBrowser(props) {
setting: false, setting: false,
}; };
const tableRef = useRef(); const tableRef = useRef();
const virtualTable = useMemo(() => {
return VList({
height: 300
})
}, []);
useEffect(() => { useEffect(() => {
position = '/';
setPath(`/`); setPath(`/`);
if (props.visible) { if (props.visible) {
setLoading(false); setLoading(false);
@@ -81,7 +90,7 @@ function FileBrowser(props) {
return [ return [
<a <a
key='download' key='download'
onClick={downloadFile.bind(null, file.name)} onClick={downloadFile.bind(null, file)}
>{i18n.t('download')}</a>, >{i18n.t('download')}</a>,
remove, remove,
]; ];
@@ -96,7 +105,7 @@ function FileBrowser(props) {
function onRowClick(file) { function onRowClick(file) {
let separator = props.isWindows ? '\\' : '/'; let separator = props.isWindows ? '\\' : '/';
if (file.name === '..') { if (file.name === '..') {
listFiles(getParentPath()); listFiles(getParentPath(position));
return; return;
} }
if (file.type !== 0) { if (file.type !== 0) {
@@ -107,15 +116,56 @@ function FileBrowser(props) {
} }
} }
listFiles(path + file.name + separator); 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) { function listFiles(newPath) {
if (loading) {
return;
}
position = newPath;
setPath(newPath); setPath(newPath);
tableRef.current.reload(); tableRef.current.reload();
} }
function getParentPath() { function getParentPath(path) {
let separator = props.isWindows ? '\\' : '/'; let separator = props.isWindows ? '\\' : '/';
// remove the last separator // remove the last separator
// or there'll be an empty element after split // or there'll be an empty element after split
@@ -159,6 +209,7 @@ function FileBrowser(props) {
} }
} }
} }
function uploadFile() { function uploadFile() {
if (path === '/' || path === '\\' || path.length === 0) { if (path === '/' || path === '\\' || path.length === 0) {
if (props.isWindows) { if (props.isWindows) {
@@ -168,17 +219,19 @@ function FileBrowser(props) {
} }
document.getElementById('uploader').click(); document.getElementById('uploader').click();
} }
function onUploadSuccess() { function onUploadSuccess() {
tableRef.current.reload(); tableRef.current.reload();
setUpload(false); setUpload(false);
} }
function onUploadCancel() { function onUploadCancel() {
setUpload(false); setUpload(false);
} }
function downloadFile(file) { function downloadFile(file) {
post(location.origin + location.pathname + 'api/device/file/get', { post(location.origin + location.pathname + 'api/device/file/get', {
file: path + file, file: path + file.name,
device: props.device device: props.device
}); });
} }
@@ -195,7 +248,7 @@ function FileBrowser(props) {
async function getData(form) { async function getData(form) {
await waitTime(300); 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); setLoading(false);
let data = res.data; let data = res.data;
if (data.code === 0) { if (data.code === 0) {
@@ -211,13 +264,14 @@ function FileBrowser(props) {
modTime: 0 modTime: 0
}); });
} }
setPath(position);
return ({ return ({
data: data.data.files, data: data.data.files,
success: true, success: true,
total: data.data.files.length - (addParentShortcut ? 1 : 0) total: data.data.files.length - (addParentShortcut ? 1 : 0)
}); });
} }
setPath(getParentPath()); setPath(getParentPath(position));
return ({data: [], success: false, total: 0}); return ({data: [], success: false, total: 0});
} }
@@ -242,7 +296,7 @@ function FileBrowser(props) {
toolbar={{ toolbar={{
settings: [ settings: [
{ {
icon: <UploadOutlined />, icon: <UploadOutlined/>,
tooltip: i18n.t('upload'), tooltip: i18n.t('upload'),
key: 'upload', key: 'upload',
onClick: uploadFile onClick: uploadFile
@@ -268,12 +322,13 @@ function FileBrowser(props) {
request={getData} request={getData}
pagination={false} pagination={false}
actionRef={tableRef} actionRef={tableRef}
components={virtualTable}
> >
</ProTable> </ProTable>
<input <input
id='uploader' id='uploader'
type='file' type='file'
style={{ display: 'none' }} style={{display: 'none'}}
onChange={onFileChange} onChange={onFileChange}
/> />
<UploadModal <UploadModal
@@ -283,10 +338,81 @@ function FileBrowser(props) {
onSuccess={onUploadSuccess} onSuccess={onUploadSuccess}
onCanel={onUploadCancel} onCanel={onUploadCancel}
/> />
<Image
preview={{
visible: preview,
src: preview,
onVisibleChange: () => {
URL.revokeObjectURL(preview);
setPreview('');
}
}}
/>
</Modal> </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; let abortController = null;
function UploadModal(props) { function UploadModal(props) {
const [visible, setVisible] = useState(!!props.file); const [visible, setVisible] = useState(!!props.file);
@@ -364,6 +490,7 @@ function UploadModal(props) {
}, 1500); }, 1500);
}); });
} }
function onCancel() { function onCancel() {
if (status === 0) { if (status === 0) {
setVisible(false); setVisible(false);
@@ -413,7 +540,7 @@ function UploadModal(props) {
maskClosable={false} maskClosable={false}
destroyOnClose={true} destroyOnClose={true}
confirmLoading={status === 1} confirmLoading={status === 1}
okText={i18n.t(status===1?'uploading':'upload')} okText={i18n.t(status === 1 ? 'uploading' : 'upload')}
onOk={onConfirm} onOk={onConfirm}
onCancel={onCancel} onCancel={onCancel}
okButtonProps={{disabled: status !== 0}} okButtonProps={{disabled: status !== 0}}

View File

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

View File

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

View File

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

View File

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

View File

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