add: file upload

optimize: project structure
This commit is contained in:
XZB
2022-05-21 19:44:55 +08:00
parent e878ea1e44
commit dae496d934
52 changed files with 1443 additions and 869 deletions

View File

@@ -6,7 +6,7 @@ on:
- '!v*.*.*' - '!v*.*.*'
jobs: jobs:
build-macOS-clients: build-clients-macOS:
runs-on: macos-latest runs-on: macos-latest
strategy: strategy:
@@ -51,7 +51,7 @@ jobs:
build-others: build-others:
needs: [ build-macOS-clients ] needs: [ build-clients-macOS ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@@ -77,29 +77,18 @@ jobs:
export PATH=$PATH:~/go/bin/ export PATH=$PATH:~/go/bin/
go install github.com/rakyll/statik go install github.com/rakyll/statik
- name: Get artifacts from previous job (arm64) - name: Get artifact from previous job (darwin_arm64)
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: darwin_arm64 name: darwin_arm64
path: ./built path: ./built
- name: Get artifacts from previous job (amd64) - name: Get artifact from previous job (darwin_amd64)
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: darwin_amd64 name: darwin_amd64
path: ./built path: ./built
- name: Build and embed clients
run: |
chmod +x ./build.client.sh
export GOMOD=`pwd`/go.mod
export CGO_ENABLED=0
go mod tidy
go mod download
./build.client.sh
statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
- name: Build and pack static resources - name: Build and pack static resources
run: | run: |
cd ./web cd ./web
@@ -107,17 +96,23 @@ jobs:
npm run build-prod npm run build-prod
statik -m -src="./dist" -f -dest="../server/embed" -p web -ns web statik -m -src="./dist" -f -dest="../server/embed" -p web -ns web
cd .. cd ..
zip -q -r ./embed.zip ./server/embed
- name: Build server - name: Set up go dependencies
run: | run: |
chmod +x ./build.server.sh
export GOMOD=`pwd`/go.mod export GOMOD=`pwd`/go.mod
export CGO_ENABLED=0 export CGO_ENABLED=0
go mod tidy go mod tidy
go mod download go mod download
- name: Build clients and servers
run: |
chmod +x ./scripts/build.client.sh
./scripts/build.client.sh
statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
chmod +x ./scripts/build.server.sh
mkdir ./releases mkdir ./releases
./build.server.sh ./scripts/build.server.sh
- name: Prepare release note - name: Prepare release note
run: | run: |
@@ -128,6 +123,8 @@ 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 -zcvf server_darwin_amd64.tar.gz server_darwin_amd64
tar -zcvf server_linux_arm.tar.gz server_linux_arm tar -zcvf server_linux_arm.tar.gz server_linux_arm
tar -zcvf server_linux_arm64.tar.gz server_linux_arm64 tar -zcvf server_linux_arm64.tar.gz server_linux_arm64
tar -zcvf server_linux_i386.tar.gz server_linux_i386 tar -zcvf server_linux_i386.tar.gz server_linux_i386
@@ -137,17 +134,13 @@ jobs:
zip -r server_windows_i386.zip server_windows_i386.exe zip -r server_windows_i386.zip server_windows_i386.exe
zip -r server_windows_amd64.zip server_windows_amd64.exe zip -r server_windows_amd64.zip server_windows_amd64.exe
- name: Upload embedding resources (embed.zip)
uses: actions/upload-artifact@v2
with:
name: embed.zip
path: embed.zip
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
body_path: CHANGELOG.md body_path: CHANGELOG.md
files: | files: |
releases/server_darwin_arm64.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_arm64.tar.gz
releases/server_linux_i386.tar.gz releases/server_linux_i386.tar.gz
@@ -157,64 +150,9 @@ jobs:
releases/server_windows_i386.zip releases/server_windows_i386.zip
releases/server_windows_amd64.zip releases/server_windows_amd64.zip
- name: Clean up
uses: geekyeggo/delete-artifact@v1
build-macOS-servers: with:
needs: [ build-others ] name: |
runs-on: macos-latest darwin_arm64
darwin_amd64
strategy:
matrix:
go-version: [ 1.17 ]
steps:
- uses: actions/checkout@v3
- name: Set up golang
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Get artifacts from previous job (embed.zip)
uses: actions/download-artifact@v3
with:
name: embed.zip
path: .
- name: Build and compress servers
run: |
rm -rf ./server/embed
unzip -q embed.zip
export COMMIT=`git rev-parse HEAD`
export GOMOD=`pwd`/go.mod
export CGO_ENABLED=0
go mod tidy
go mod download
mkdir ./releases
export GOOS=darwin
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
cd ./releases
tar -zcvf server_darwin_arm64.tar.gz server_darwin_arm64
tar -zcvf server_darwin_amd64.tar.gz server_darwin_amd64
cd ..
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
releases/server_darwin_arm64.tar.gz
releases/server_darwin_amd64.tar.gz
- name: Clean up
uses: geekyeggo/delete-artifact@v1
with:
name: |
embed.zip
darwin_arm64
darwin_amd64

View File

@@ -5,21 +5,25 @@
## 通用 ## 通用
所有请求均为`POST` 所有请求均为`POST`
<br />
每次请求都必须在Header中带上`Authorization` 每次请求都必须在Header中带上`Authorization`
<br />
`Authorization`请求头格式:`Basic <token>`basic auth `Authorization`请求头格式:`Basic <token>`basic auth
``` ```
Authorization: Basic <base64('username:password')> Authorization: Basic <base64('username:password')>
``` ```
例如:
```
Authorization: Basic WFpCOjEyNDg=
```
--- ---
## 响应 ## 响应
所有响应均是JSON格式。 所有响应均是JSON格式。
<br />
`code` 有三种结果,分别为`-1``0``1`,含义如下。 `code` 有三种结果,分别为`-1``0``1`,含义如下。
| code | meaning | | code | meaning |
@@ -55,9 +59,13 @@ Authorization: Basic <base64('username:password')>
参数:**无** 参数:**无**
设备的`id`是一串64位的字符串每台设备独一无二一般不会变化。识别设备主要靠这个。下文中提到的设备ID也指的是这个。 设备的`id`是一串64位的字符串每台设备独一无二一般不会变化。
<br />
每个device对象所对应的key是它的本次连接的连接ID这个ID是随机、临时的每次重连就会变化不建议使用 识别设备主要靠这个。下文中提到的设备ID也指的是这个
<br />
每个device对象所对应的key是它的本次连接的连接ID。
<br />
连接ID是随机、临时的每次重连就会变化不建议使用。
``` ```
{ {
@@ -123,7 +131,9 @@ Authorization: Basic <base64('username:password')>
参数:`device`设备ID 参数:`device`设备ID
如果截屏获取成功,则会直接以图片的形式输出。如果截屏失败,如下响应会被输出(错误信息不止这一个)。 如果截屏获取成功,则会直接以图片的形式输出。
<br />
如果截屏失败,如下响应会被输出(错误信息不一定是这一个)。
``` ```
{ {
@@ -138,7 +148,9 @@ Authorization: Basic <base64('username:password')>
参数:`file`(文件路径) 以及 `device`设备ID 参数:`file`(文件路径) 以及 `device`设备ID
如果文件存在且可访问,则文件会直接输出。否则,会给出以下响应。 如果文件存在且可访问,则文件会直接输出。
<br />
否则,会给出错误原因。
``` ```
{ {
@@ -169,11 +181,54 @@ Authorization: Basic <base64('username:password')>
--- ---
### 上传文件到目录:`/device/file/upload`
**GET**参数:`file`(文件名)、`path`(路径)和`device`设备ID
文件内容需要作为**请求体body**发送。
<br />
**请求体body**中的任何内容都会被写到指定地文件中。
<br />
如果存在同名文件,则会被**覆盖**
Example:
```http request
POST http://localhost:8000/api/device/file/upload?path=D%3A%5C&file=Test.txt&device=bc7e49f8f794f80ffb0032a4ba516c86d76041bf2023e1be6c5dda3b1ee0cf4c HTTP/1.1
Host: localhost:8000
Content-Length: 12
Content-Type: application/octet-stream
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.47
Origin: http://localhost:8000
Referer: http://localhost:8000/
Hello World.
```
如果文件上传成功,则`code`为`1`。
<br />
文件`D:\Test.txt`会写入:`Hello World.`。
```
{
"code": 0
}
```
```
{
"code": 1,
"msg": "${i18n|fileOrDirNotExist}"
}
```
---
### 列举设备上的文件和目录:`/device/file/list` ### 列举设备上的文件和目录:`/device/file/list`
参数:`path`(父目录路径) 以及 `device`设备ID 参数:`path`(父目录路径) 以及 `device`设备ID
如果`path`为空windows下会给出磁盘列表,其它系统会默认输出`/`目录下的文件和目录 如果`path`为空windows下会给出磁盘列表。
<br />
其它系统会默认输出`/`目录下的文件和目录。
`type`有三种结果:`0`代表文件,`1`代表目录,`2`代表磁盘windows `type`有三种结果:`0`代表文件,`1`代表目录,`2`代表磁盘windows

57
API.md
View File

@@ -5,14 +5,18 @@
## Common ## Common
Only `POST` requests are allowed. Only `POST` requests are allowed.
<br />
For every request, you should have `Authorization` on its header. For every request, you should have `Authorization` on its header.
<br />
Authorization header is a string like `Basic <token>`(basic auth). Authorization header is a string like `Basic <token>`(basic auth).
``` ```
Authorization: Basic <base64('username:password')> Authorization: Basic <base64('username:password')>
``` ```
Example:
```
Authorization: Basic WFpCOjEyNDg=
```
--- ---
@@ -53,8 +57,12 @@ All responses are JSON encoded.
Parameters: **None** Parameters: **None**
The `id` of device is persistent, its length always equals 64. It's unique for every device and won't change, so you should identify every device by this. The `id` of device is persistent, its length always equals 64.
<br />
It's unique for every device and won't change.
<br />
You're recommend to recognize your device by device ID.
<br />
The key of the device object is its connection UUID, it's random and temporary. The key of the device object is its connection UUID, it's random and temporary.
``` ```
@@ -166,6 +174,47 @@ If file exists and is deleted successfully, then `code` will be `0`.
--- ---
### Upload file: `/device/file/upload`
**Query Parameters**: `file` (file name), `path` and `device` (device ID)
File itself should be sent in the request **body**.
<br />
**Anything** represented in the request **body** will be saved to the device.
<br />
If same file exists, then it will be **overwritten**.
Example:
```http request
POST http://localhost:8000/api/device/file/upload?path=D%3A%5C&file=Test.txt&device=bc7e49f8f794f80ffb0032a4ba516c86d76041bf2023e1be6c5dda3b1ee0cf4c HTTP/1.1
Host: localhost:8000
Content-Length: 12
Content-Type: application/octet-stream
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.47
Origin: http://localhost:8000
Referer: http://localhost:8000/
Hello World.
```
If file uploaded successfully, then `code` will be `0`.
<br />
And `D:\Test.txt` will be created with the content of `Hello World.`.
```
{
"code": 0
}
```
```
{
"code": 1,
"msg": "${i18n|fileOrDirNotExist}"
}
```
---
### List files: `/device/file/list` ### List files: `/device/file/list`
Parameters: `path` (folder to be listed) and `device` (device ID) Parameters: `path` (folder to be listed) and `device` (device ID)

View File

@@ -1,3 +1,13 @@
## v0.0.8
* Add: file upload.
* Optimize: project structure.
* 新增: 文件上传功能。
* 优化: 项目结构。
## v0.0.7 ## v0.0.7
* Add: detail info tooltip of cpu, ram and disk. * Add: detail info tooltip of cpu, ram and disk.

View File

@@ -66,6 +66,20 @@
--- ---
## 截图
![overview](./screenshots/overview.ZH.png)
![terminal](./screenshots/terminal.ZH.png)
![procmgr](./screenshots/procmgr.ZH.png)
![explorer](./screenshots/explorer.ZH.png)
![overview.cpu](./screenshots/overview.cpu.ZH.png)
---
## **开发** ## **开发**
### 注意 ### 注意
@@ -121,18 +135,6 @@ $ ./build.server.sh
--- ---
## 截图
![overview](./screenshots/overview.ZH.png)
![terminal](./screenshots/terminal.ZH.png)
![procmgr](./screenshots/procmgr.ZH.png)
![explorer](./screenshots/explorer.ZH.png)
---
## 项目依赖 ## 项目依赖
Spark使用了许多第三方的开源项目。 Spark使用了许多第三方的开源项目。

View File

@@ -47,7 +47,7 @@ Only local installation are available yet.
|-----------------|---------|-------|-------| |-----------------|---------|-------|-------|
| Process manager | ✔ | ✔ | ✔ | | Process manager | ✔ | ✔ | ✔ |
| Kill process | ✔ | ✔ | ✔ | | Kill process | ✔ | ✔ | ✔ |
| Network Traffic | ✔ | ✔ | ✔ | | Network traffic | ✔ | ✔ | ✔ |
| File explorer | ✔ | ✔ | ✔ | | File explorer | ✔ | ✔ | ✔ |
| File transfer | ✔ | ✔ | ✔ | | File transfer | ✔ | ✔ | ✔ |
| Delete file | ✔ | ✔ | ✔ | | Delete file | ✔ | ✔ | ✔ |
@@ -66,6 +66,20 @@ Only local installation are available yet.
--- ---
## Screenshots
![overview](./screenshots/overview.png)
![terminal](./screenshots/terminal.png)
![procmgr](./screenshots/procmgr.png)
![explorer](./screenshots/explorer.png)
![overview.cpu](./screenshots/overview.cpu.png)
---
## **Development** ## **Development**
### note ### note
@@ -123,18 +137,6 @@ Copy configuration file mentioned above into this dir, and then you can execute
--- ---
## Screenshots
![overview](./screenshots/overview.png)
![terminal](./screenshots/terminal.png)
![procmgr](./screenshots/procmgr.png)
![explorer](./screenshots/explorer.png)
---
## Dependencies ## Dependencies
Spark contains many third-party open-source projects. Spark contains many third-party open-source projects.

View File

@@ -48,6 +48,11 @@ func init() {
} }
func main() { func main() {
update()
core.Start()
}
func update() {
if len(os.Args) > 1 && os.Args[1] == `--update` { if len(os.Args) > 1 && os.Args[1] == `--update` {
thisPath := os.Args[0] thisPath := os.Args[0]
destPath := thisPath[:len(thisPath)-4] destPath := thisPath[:len(thisPath)-4]
@@ -69,7 +74,6 @@ func main() {
<-time.After(time.Second) <-time.After(time.Second)
os.Remove(os.Args[0] + `.tmp`) os.Remove(os.Args[0] + `.tmp`)
} }
core.Start()
} }
func decrypt(data []byte, key []byte) ([]byte, error) { func decrypt(data []byte, key []byte) ([]byte, error) {

View File

@@ -14,10 +14,11 @@ type Cfg struct {
Key string `json:"key"` Key string `json:"key"`
} }
// localhost for debug only // Localhost for my development only.
// Shall be commented out when development is done.
//var CfgBuffer = "\x00\xcd\x90\x50\x43\xfc\x3d\x36\x56\x6d\xf6\x01\xd1\xcd\x81\xc3\x1b\x80\xc9\x61\xd8\xdf\x5b\x76\x48\x88\xc5\xb1\x74\x22\x23\xab\x3b\xfc\x8b\xbe\x98\x27\xed\x05\xec\xbb\x40\x4f\xe9\xe7\xe5\xe0\x84\xaa\xb7\xfd\x4a\x30\x71\x08\x6c\x02\x50\xe9\xc5\x22\xcf\xcb\x89\x16\x0a\x89\x08\xd4\x26\xdc\x5c\xc1\xc9\xbf\xc4\xac\x0d\x92\x2f\x34\x7f\x45\xeb\x55\xa0\x6d\xf6\x64\xbc\xd5\x15\x40\x96\x43\x64\xe0\x24\x51\xfb\xe8\xc9\x7f\x48\x60\xcd\x30\x5e\x5e\x78\xba\xb6\x6f\x07\x64\xe8\x59\x81\x0b\x91\x13\x92\x1a\xdd\x49\x8f\x28\xe7\x74\xea\xff\x5b\x45\x0e\x4a\x2d\x60\x4e\xc9\xde\x9c\xbe\x50\xc6\x12\xc7\x45\xa2\x15\xa0\x58\x62\x45\x86\x74\x9f\xa5\x14\x5c\x17\x8a\xcc\x56\x73\xa7\x75\xb7\xf6\x6d\x52\x0f\xb8\xc1\xff\x9c\x39\x39\x00\x74\xe1\x4d\x65\x73\x9c\x02\x57\x8b\xcf\xdf\x0a\x20\x4c\xed\xe2\x25\xea\x01\x36\x12\x37\x12\x2e\x1a\x03\x41\x19\x2e\xc9\xdd\x71\xac\x73\x90\xfa\x5e\x60\x08\x43\x35\xef\x61\x45\xf9\xe3\xba\xcb\xb1\xc5\x7c\xf0\x11\xcd\x47\x57\x53\xdc\x35\x6b\x9f\xac\xad\x43\x4a\xc7\x54\x20\xb8\xd0\xf8\xb5\x0c\x45\x76\x57\xb9\xee\x4a\x3f\xd2\xda\xf7\x94\x54\x74\xf3\x91\xf3\x4d\x49\x98\xc6\xf8\x60\x80\xad\x84\x04\xef\x35\xca\x3a\xcf\xd3\x7e\x74\xc2\x4b\xb8\xb3\x9f\xb2\x83\xb8\xbd\x29\x13\x9f\x2b\xaa\x60\x47\x24\x7e\x20\xb2\x85\xdc\x47\xfe\x8f\x68\xb6\xc3\x43\xad\x61\x3d\x9b\x35\x60\x2e\x6c\x44\xf0\xaf\xb2\xf3\xdb\xe2\x1b\x8a\xec\x0a\x48\x5e\x43\xa9\xb3\x3a\x5e\xb6\x90\xa9\x3d\xee\x4f\xa1\x57\x7c\x94\xf4\xb1\x36\xda\x04\xa8\x5e\x48\x2a\xc3\xa1\xf0\x97\xf0\xe0\x10\x46\x32\x10\xe5\xd8\x36\x5a\x56\xa5\xbb\x37\x3c\x9f\xbd\xef\xf5\x2f" //var CfgBuffer = "\x00\xcd\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"
// 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"
// COMMIT means this commit hash, help to identify version and self upgrade. // COMMIT means this commit hash, help to identify version and self upgrade.

View File

@@ -40,6 +40,7 @@ var handlers = map[string]func(pack modules.Packet, wsConn *common.Conn){
`resizeTerminal`: resizeTerminal, `resizeTerminal`: resizeTerminal,
`killTerminal`: killTerminal, `killTerminal`: killTerminal,
`listFiles`: listFiles, `listFiles`: listFiles,
`fetchFile`: fetchFile,
`removeFile`: removeFile, `removeFile`: removeFile,
`uploadFile`: uploadFile, `uploadFile`: uploadFile,
`listProcesses`: listProcesses, `listProcesses`: listProcesses,
@@ -54,14 +55,14 @@ func Start() {
} }
common.WSConn, err = connectWS() common.WSConn, err = connectWS()
if err != nil && !stop { if err != nil && !stop {
golog.Error(err) golog.Error(`Connection error: `, err)
<-time.After(3 * time.Second) <-time.After(3 * time.Second)
continue continue
} }
err = reportWS(common.WSConn) err = reportWS(common.WSConn)
if err != nil && !stop { if err != nil && !stop {
golog.Error(err) golog.Error(`Register error: `, err)
<-time.After(3 * time.Second) <-time.After(3 * time.Second)
continue continue
} }
@@ -72,7 +73,7 @@ func Start() {
err = handleWS(common.WSConn) err = handleWS(common.WSConn)
if err != nil && !stop { if err != nil && !stop {
golog.Error(err) golog.Error(`Execution error: `, err)
<-time.After(3 * time.Second) <-time.After(3 * time.Second)
continue continue
} }
@@ -103,7 +104,7 @@ func reportWS(wsConn *common.Conn) error {
if err != nil { if err != nil {
return err return err
} }
pack := modules.CommonPack{Act: `report`, Data: device} pack := modules.CommonPack{Act: `report`, Data: *device}
err = common.SendPack(pack, wsConn) err = common.SendPack(pack, wsConn)
common.WSConn.SetWriteDeadline(time.Time{}) common.WSConn.SetWriteDeadline(time.Time{})
if err != nil { if err != nil {
@@ -226,7 +227,7 @@ func heartbeat(wsConn *common.Conn) error {
if t >= 20 { if t >= 20 {
t = 0 t = 0
} }
err = common.SendPack(modules.CommonPack{Act: `setDevice`, Data: device}, wsConn) err = common.SendPack(modules.CommonPack{Act: `setDevice`, Data: *device}, wsConn)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -245,7 +245,7 @@ func GetDevice() (*modules.Device, error) {
}, nil }, nil
} }
func GetPartialInfo(getDisk bool) (modules.Device, error) { func GetPartialInfo(getDisk bool) (*modules.Device, error) {
cpuInfo, err := GetCPUInfo() cpuInfo, err := GetCPUInfo()
if err != nil { if err != nil {
cpuInfo = modules.CPU{ cpuInfo = modules.CPU{
@@ -280,7 +280,7 @@ func GetPartialInfo(getDisk bool) (modules.Device, error) {
if err != nil { if err != nil {
uptime = 0 uptime = 0
} }
return modules.Device{ return &modules.Device{
Net: netInfo, Net: netInfo,
CPU: cpuInfo, CPU: cpuInfo,
RAM: memInfo, RAM: memInfo,

View File

@@ -13,41 +13,6 @@ import (
"strconv" "strconv"
) )
func getPackData(pack modules.Packet, key string, t reflect.Kind) (interface{}, bool) {
data, ok := pack.Data[key]
if !ok {
return nil, false
}
switch t {
case reflect.String:
val, ok := data.(string)
return val, ok
case reflect.Uint:
val, ok := data.(uint)
return val, ok
case reflect.Uint32:
val, ok := data.(uint32)
return val, ok
case reflect.Uint64:
val, ok := data.(uint64)
return val, ok
case reflect.Int:
val, ok := data.(int)
return val, ok
case reflect.Int64:
val, ok := data.(int64)
return val, ok
case reflect.Bool:
val, ok := data.(bool)
return val, ok
case reflect.Float64:
val, ok := data.(float64)
return val, ok
default:
return nil, false
}
}
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
@@ -110,8 +75,16 @@ func shutdown(pack modules.Packet, wsConn *common.Conn) {
} }
func screenshot(pack modules.Packet, wsConn *common.Conn) { func screenshot(pack modules.Packet, wsConn *common.Conn) {
if len(pack.Event) > 0 { var bridge string
Screenshot.GetScreenshot(pack.Event) if val, ok := pack.GetData(`bridge`, reflect.String); !ok {
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack, wsConn)
return
} else {
bridge = val.(string)
}
err := Screenshot.GetScreenshot(bridge)
if err != nil {
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
} }
} }
@@ -136,7 +109,7 @@ func killTerminal(pack modules.Packet, wsConn *common.Conn) {
func listFiles(pack modules.Packet, wsConn *common.Conn) { func listFiles(pack modules.Packet, wsConn *common.Conn) {
path := `/` path := `/`
if val, ok := getPackData(pack, `path`, reflect.String); ok { if val, ok := pack.GetData(`path`, reflect.String); ok {
path = val.(string) path = val.(string)
} }
files, err := file.ListFiles(path) files, err := file.ListFiles(path)
@@ -147,9 +120,35 @@ func listFiles(pack modules.Packet, wsConn *common.Conn) {
} }
} }
func fetchFile(pack modules.Packet, wsConn *common.Conn) {
var path, filename, bridge string
if val, ok := pack.GetData(`path`, reflect.String); !ok {
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|fileOrDirNotExist}`}, pack, wsConn)
return
} else {
path = val.(string)
}
if val, ok := pack.GetData(`file`, reflect.String); !ok {
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack, wsConn)
return
} else {
filename = val.(string)
}
if val, ok := pack.GetData(`bridge`, reflect.String); !ok {
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack, wsConn)
return
} else {
bridge = val.(string)
}
err := file.FetchFile(path, filename, bridge)
if err != nil {
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
}
}
func removeFile(pack modules.Packet, wsConn *common.Conn) { func removeFile(pack modules.Packet, wsConn *common.Conn) {
var path string var path string
if val, ok := getPackData(pack, `file`, reflect.String); !ok { if val, ok := pack.GetData(`file`, reflect.String); !ok {
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|fileOrDirNotExist}`}, pack, wsConn) common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|fileOrDirNotExist}`}, pack, wsConn)
return return
} else { } else {
@@ -165,18 +164,24 @@ func removeFile(pack modules.Packet, wsConn *common.Conn) {
func uploadFile(pack modules.Packet, wsConn *common.Conn) { func uploadFile(pack modules.Packet, wsConn *common.Conn) {
var start, end int64 var start, end int64
var path string var path, bridge string
if val, ok := getPackData(pack, `file`, reflect.String); !ok { if val, ok := pack.GetData(`file`, reflect.String); !ok {
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|fileOrDirNotExist}`}, pack, wsConn) common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|fileOrDirNotExist}`}, pack, wsConn)
return return
} else { } else {
path = val.(string) path = val.(string)
} }
if val, ok := pack.GetData(`bridge`, reflect.String); !ok {
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack, wsConn)
return
} else {
bridge = val.(string)
}
{ {
if val, ok := getPackData(pack, `start`, reflect.Float64); ok { if val, ok := pack.GetData(`start`, reflect.Float64); ok {
start = int64(val.(float64)) start = int64(val.(float64))
} }
if val, ok := getPackData(pack, `end`, reflect.Float64); ok { if val, ok := pack.GetData(`end`, reflect.Float64); ok {
end = int64(val.(float64)) end = int64(val.(float64))
if end > 0 { if end > 0 {
end++ end++
@@ -187,7 +192,7 @@ func uploadFile(pack modules.Packet, wsConn *common.Conn) {
return return
} }
} }
err := file.UploadFile(path, pack.Event, start, end) err := file.UploadFile(path, bridge, start, end)
if err != nil { if err != nil {
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn) common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
} }
@@ -207,7 +212,7 @@ func killProcess(pack modules.Packet, wsConn *common.Conn) {
pid int64 pid int64
err error err error
) )
if val, ok := getPackData(pack, `pid`, reflect.String); ok { if val, ok := pack.GetData(`pid`, reflect.String); ok {
pid, err = strconv.ParseInt(val.(string), 10, 32) pid, err = strconv.ParseInt(val.(string), 10, 32)
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn) common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
return return

View File

@@ -6,12 +6,13 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path"
"strconv" "strconv"
"github.com/imroc/req/v3" "github.com/imroc/req/v3"
) )
type file struct { type File struct {
Name string `json:"name"` Name string `json:"name"`
Size uint64 `json:"size"` Size uint64 `json:"size"`
Time int64 `json:"time"` Time int64 `json:"time"`
@@ -19,8 +20,8 @@ type file struct {
} }
// listFiles returns files and directories find in path. // listFiles returns files and directories find in path.
func listFiles(path string) ([]file, error) { func listFiles(path string) ([]File, error) {
result := make([]file, 0) result := make([]File, 0)
files, err := ioutil.ReadDir(path) files, err := ioutil.ReadDir(path)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -30,7 +31,7 @@ func listFiles(path string) ([]file, error) {
if files[i].IsDir() { if files[i].IsDir() {
itemType = 1 itemType = 1
} }
result = append(result, file{ result = append(result, File{
Name: files[i].Name(), Name: files[i].Name(),
Size: uint64(files[i].Size()), Size: uint64(files[i].Size()),
Time: files[i].ModTime().Unix(), Time: files[i].ModTime().Unix(),
@@ -40,6 +41,62 @@ func listFiles(path string) ([]file, error) {
return result, nil return result, nil
} }
// FetchFile saves file from bridge to local.
// Save body as temp file and when done, rename it to file.
func FetchFile(dir, file, bridge string) error {
url := config.GetBaseURL(false) + `/api/bridge/pull`
client := req.C().DisableAutoReadResponse()
resp, err := client.R().SetQueryParam(`bridge`, bridge).Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// If dest file exists, write to temp file first.
dest := path.Join(dir, file)
tmpFile := dest
destExists := false
if _, err := os.Stat(dest); !os.IsNotExist(err) {
tmpFile = getTempFileName(dir, file)
destExists = true
}
fh, err := os.Create(tmpFile)
if err != nil {
return err
}
for {
buf := make([]byte, 1024)
n, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
fh.Truncate(0)
fh.Close()
os.Remove(tmpFile)
return err
}
if n == 0 {
break
}
_, err = fh.Write(buf[:n])
if err != nil {
fh.Truncate(0)
fh.Close()
os.Remove(tmpFile)
return err
}
fh.Sync()
}
fh.Close()
// Delete old file if exists.
// Then rename temp file to file.
if destExists {
os.Remove(dest)
err = os.Rename(tmpFile, dest)
}
return err
}
func RemoveFile(path string) error { func RemoveFile(path string) error {
if path == `\` || path == `/` || len(path) == 0 { if path == `\` || path == `/` || len(path) == 0 {
return errors.New(`${i18n|fileOrDirNotExist}`) return errors.New(`${i18n|fileOrDirNotExist}`)
@@ -51,7 +108,7 @@ func RemoveFile(path string) error {
return nil return nil
} }
func UploadFile(path, trigger string, start, end int64) error { func UploadFile(path, bridge string, start, end int64) error {
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return err return err
@@ -65,7 +122,6 @@ func UploadFile(path, trigger string, start, end int64) error {
} }
size := stat.Size() size := stat.Size()
headers := map[string]string{ headers := map[string]string{
`Trigger`: trigger,
`FileName`: stat.Name(), `FileName`: stat.Name(),
`FileSize`: strconv.FormatInt(size, 10), `FileSize`: strconv.FormatInt(size, 10),
} }
@@ -96,8 +152,25 @@ func UploadFile(path, trigger string, start, end int64) error {
} }
writer.Close() writer.Close()
}() }()
url := config.GetBaseURL(false) + `/api/device/file/put` url := config.GetBaseURL(false) + `/api/bridge/push`
_, err = uploadReq.SetBody(reader).SetHeaders(headers).Send(`PUT`, url) _, err = uploadReq.
SetBody(reader).
SetHeaders(headers).
SetQueryParam(`bridge`, bridge).
Send(`PUT`, url)
reader.Close() reader.Close()
return err return err
} }
func getTempFileName(dir, file string) string {
exists := true
tempFile := ``
for i := 0; exists; i++ {
tempFile = path.Join(dir, file+`.tmp.`+strconv.Itoa(i))
_, err := os.Stat(tempFile)
if os.IsNotExist(err) {
exists = false
}
}
return tempFile
}

View File

@@ -1,8 +1,9 @@
//go:build !windows
// +build !windows // +build !windows
package file package file
func ListFiles(path string) ([]file, error) { func ListFiles(path string) ([]File, error) {
if len(path) == 0 { if len(path) == 0 {
path = `/` path = `/`
} }

View File

@@ -8,8 +8,8 @@ import "github.com/shirou/gopsutil/v3/disk"
// ListFiles will only be called when path is root and // ListFiles will only be called when path is root and
// current system is Windows. // current system is Windows.
// It will return mount points of all volumes. // It will return mount points of all volumes.
func ListFiles(path string) ([]file, error) { func ListFiles(path string) ([]File, error) {
result := make([]file, 0) result := make([]File, 0)
if len(path) == 0 || path == `\` || path == `/` { if len(path) == 0 || path == `\` || path == `/` {
partitions, err := disk.Partitions(true) partitions, err := disk.Partitions(true)
if err != nil { if err != nil {
@@ -23,7 +23,7 @@ func ListFiles(path string) ([]file, error) {
} else { } else {
size = stat.Total size = stat.Total
} }
result = append(result, file{Name: partitions[i].Mountpoint, Type: 2, Size: size}) result = append(result, File{Name: partitions[i].Mountpoint, Type: 2, Size: size})
} }
return result, nil return result, nil
} }

View File

@@ -1,16 +0,0 @@
package screenshot
import (
"Spark/client/config"
"github.com/imroc/req/v3"
)
func putScreenshot(trigger, err string, body interface{}) (*req.Response, error) {
return req.R().
SetBody(body).
SetHeaders(map[string]string{
`Trigger`: trigger,
`Error`: err,
}).
Send(`PUT`, config.GetBaseURL(false)+`/api/device/screenshot/put`)
}

View File

@@ -3,30 +3,30 @@
package screenshot package screenshot
import ( import (
"Spark/client/config"
"bytes" "bytes"
"errors" "errors"
"github.com/imroc/req/v3"
"github.com/kbinani/screenshot" "github.com/kbinani/screenshot"
"image/png" "image/png"
) )
func GetScreenshot(trigger string) error { func GetScreenshot(bridge string) error {
writer := new(bytes.Buffer) writer := new(bytes.Buffer)
num := screenshot.NumActiveDisplays() num := screenshot.NumActiveDisplays()
if num == 0 { if num == 0 {
err := errors.New(`${i18n|noDisplayFound}`) err := errors.New(`${i18n|noDisplayFound}`)
putScreenshot(trigger, err.Error(), nil)
return err return err
} }
img, err := screenshot.CaptureDisplay(0) img, err := screenshot.CaptureDisplay(0)
if err != nil { if err != nil {
putScreenshot(trigger, err.Error(), nil)
return err return err
} }
err = png.Encode(writer, img) err = png.Encode(writer, img)
if err != nil { if err != nil {
putScreenshot(trigger, err.Error(), nil)
return err return err
} }
_, err = putScreenshot(trigger, ``, writer) url := config.GetBaseURL(false) + `/api/bridge/push`
_, err = req.R().SetBody(writer.Bytes()).SetQueryParam(`bridge`, bridge).Put(url)
return err return err
} }

View File

@@ -2,9 +2,6 @@
package screenshot package screenshot
import "Spark/utils" func GetScreenshot(bridge string) error {
return utils.ErrUnsupported
func GetScreenshot(trigger string) error {
_, err := putScreenshot(trigger, utils.ErrUnsupported.Error(), nil)
return err
} }

View File

@@ -10,13 +10,14 @@ import (
"github.com/creack/pty" "github.com/creack/pty"
"os" "os"
"os/exec" "os/exec"
"reflect"
"time" "time"
) )
type terminal struct { type terminal struct {
lastPack int64 lastPack int64
event string event string
pty *os.File pty *os.File
} }
func init() { func init() {
@@ -30,8 +31,8 @@ func InitTerminal(pack modules.Packet) error {
return err return err
} }
termSession := &terminal{ termSession := &terminal{
pty: ptySession, pty: ptySession,
event: pack.Event, event: pack.Event,
lastPack: time.Now().Unix(), lastPack: time.Now().Unix(),
} }
terminals.Set(pack.Data[`terminal`].(string), termSession) terminals.Set(pack.Data[`terminal`].(string), termSession)
@@ -55,86 +56,54 @@ func InitTerminal(pack modules.Packet) error {
} }
func InputTerminal(pack modules.Packet) error { func InputTerminal(pack modules.Packet) error {
if pack.Data == nil { val, ok := pack.GetData(`input`, reflect.String)
return errDataNotFound
}
val, ok := pack.Data[`input`]
if !ok { if !ok {
return errDataNotFound return errDataNotFound
} }
hexStr, ok := val.(string) data, err := hex.DecodeString(val.(string))
if !ok {
return errDataNotFound
}
data, err := hex.DecodeString(hexStr)
if err != nil { if err != nil {
return errDataInvalid return errDataInvalid
} }
val, ok = pack.Data[`terminal`] val, ok = pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
}
termUUID, ok := val.(string)
if !ok { if !ok {
return errUUIDNotFound return errUUIDNotFound
} }
termUUID := val.(string)
val, ok = terminals.Get(termUUID) val, ok = terminals.Get(termUUID)
if !ok { if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn) common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil return nil
} }
terminal, ok := val.(*terminal) terminal := val.(*terminal)
if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
terminal.lastPack = time.Now().Unix() terminal.lastPack = time.Now().Unix()
terminal.pty.Write(data) terminal.pty.Write(data)
return nil return nil
} }
func ResizeTerminal(pack modules.Packet) error { func ResizeTerminal(pack modules.Packet) error {
if pack.Data == nil { val, ok := pack.GetData(`width`, reflect.Float64)
return errDataNotFound
}
val, ok := pack.Data[`width`]
if !ok { if !ok {
return errDataInvalid return errDataInvalid
} }
width, ok := val.(float64) width := val.(float64)
if !ok { val, ok = pack.GetData(`height`, reflect.Float64)
return errDataInvalid
}
val, ok = pack.Data[`height`]
if !ok {
return errDataInvalid
}
height, ok := val.(float64)
if !ok { if !ok {
return errDataInvalid return errDataInvalid
} }
height := val.(float64)
val, ok = pack.Data[`terminal`] val, ok = pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
}
termUUID, ok := val.(string)
if !ok { if !ok {
return errUUIDNotFound return errUUIDNotFound
} }
termUUID := val.(string)
val, ok = terminals.Get(termUUID) val, ok = terminals.Get(termUUID)
if !ok { if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn) common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil return nil
} }
terminal, ok := val.(*terminal) terminal := val.(*terminal)
if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
pty.Setsize(terminal.pty, &pty.Winsize{ pty.Setsize(terminal.pty, &pty.Winsize{
Rows: uint16(height), Rows: uint16(height),
Cols: uint16(width), Cols: uint16(width),
@@ -143,28 +112,17 @@ func ResizeTerminal(pack modules.Packet) error {
} }
func KillTerminal(pack modules.Packet) error { func KillTerminal(pack modules.Packet) error {
if pack.Data == nil { val, ok := pack.GetData(`terminal`, reflect.String)
return errUUIDNotFound
}
val, ok := pack.Data[`terminal`]
if !ok {
return errUUIDNotFound
}
termUUID, ok := val.(string)
if !ok { if !ok {
return errUUIDNotFound return errUUIDNotFound
} }
termUUID := val.(string)
val, ok = terminals.Get(termUUID) val, ok = terminals.Get(termUUID)
if !ok { if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn) common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil return nil
} }
terminal, ok := val.(*terminal) terminal := val.(*terminal)
if !ok {
terminals.Remove(termUUID)
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
doKillTerminal(terminal) doKillTerminal(terminal)
return nil return nil
} }
@@ -187,17 +145,13 @@ func getTerminal() string {
} }
func healthCheck() { func healthCheck() {
const MaxInterval = 180 const MaxInterval = 300
for now := range time.NewTicker(30 * time.Second).C { for now := range time.NewTicker(30 * time.Second).C {
timestamp := now.Unix() timestamp := now.Unix()
// stores sessions to be disconnected // stores sessions to be disconnected
queue := make([]string, 0) queue := make([]string, 0)
terminals.IterCb(func(uuid string, t interface{}) bool { terminals.IterCb(func(uuid string, t interface{}) bool {
termSession, ok := t.(*terminal) termSession := t.(*terminal)
if !ok {
queue = append(queue, uuid)
return true
}
if timestamp-termSession.lastPack > MaxInterval { if timestamp-termSession.lastPack > MaxInterval {
queue = append(queue, uuid) queue = append(queue, uuid)
doKillTerminal(termSession) doKillTerminal(termSession)

View File

@@ -9,6 +9,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"reflect"
"runtime" "runtime"
"time" "time"
@@ -18,11 +19,11 @@ import (
type terminal struct { type terminal struct {
lastPack int64 lastPack int64
event string event string
cmd *exec.Cmd cmd *exec.Cmd
stdout *io.ReadCloser stdout *io.ReadCloser
stderr *io.ReadCloser stderr *io.ReadCloser
stdin *io.WriteCloser stdin *io.WriteCloser
} }
func init() { func init() {
@@ -47,11 +48,11 @@ func InitTerminal(pack modules.Packet) error {
return err return err
} }
termSession := &terminal{ termSession := &terminal{
cmd: cmd, cmd: cmd,
event: pack.Event, event: pack.Event,
stdout: &stdout, stdout: &stdout,
stderr: &stderr, stderr: &stderr,
stdin: &stdin, stdin: &stdin,
lastPack: time.Now().Unix(), lastPack: time.Now().Unix(),
} }
terminals.Set(pack.Data[`terminal`].(string), termSession) terminals.Set(pack.Data[`terminal`].(string), termSession)
@@ -86,41 +87,26 @@ func InitTerminal(pack modules.Packet) error {
} }
func InputTerminal(pack modules.Packet) error { func InputTerminal(pack modules.Packet) error {
if pack.Data == nil { val, ok := pack.GetData(`input`, reflect.String)
return errDataNotFound
}
val, ok := pack.Data[`input`]
if !ok { if !ok {
return errDataNotFound return errDataNotFound
} }
hexStr, ok := val.(string) data, err := hex.DecodeString(val.(string))
if !ok {
return errDataNotFound
}
data, err := hex.DecodeString(hexStr)
if err != nil { if err != nil {
return errDataInvalid return errDataInvalid
} }
val, ok = pack.Data[`terminal`] val, ok = pack.GetData(`terminal`, reflect.String)
if !ok {
return errUUIDNotFound
}
termUUID, ok := val.(string)
if !ok { if !ok {
return errUUIDNotFound return errUUIDNotFound
} }
termUUID := val.(string)
val, ok = terminals.Get(termUUID) val, ok = terminals.Get(termUUID)
if !ok { if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn) common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil return nil
} }
terminal, ok := val.(*terminal) terminal := val.(*terminal)
if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
terminal.lastPack = time.Now().Unix() terminal.lastPack = time.Now().Unix()
if len(data) == 1 && data[0] == '\x03' { if len(data) == 1 && data[0] == '\x03' {
terminal.cmd.Process.Signal(os.Interrupt) terminal.cmd.Process.Signal(os.Interrupt)
@@ -136,28 +122,17 @@ func ResizeTerminal(pack modules.Packet) error {
} }
func KillTerminal(pack modules.Packet) error { func KillTerminal(pack modules.Packet) error {
if pack.Data == nil { val, ok := pack.GetData(`terminal`, reflect.String)
return errUUIDNotFound
}
val, ok := pack.Data[`terminal`]
if !ok {
return errUUIDNotFound
}
termUUID, ok := val.(string)
if !ok { if !ok {
return errUUIDNotFound return errUUIDNotFound
} }
termUUID := val.(string)
val, ok = terminals.Get(termUUID) val, ok = terminals.Get(termUUID)
if !ok { if !ok {
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn) common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil return nil
} }
terminal, ok := val.(*terminal) terminal := val.(*terminal)
if !ok {
terminals.Remove(termUUID)
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
return nil
}
doKillTerminal(terminal) doKillTerminal(terminal)
return nil return nil
} }
@@ -210,17 +185,13 @@ func utf8ToGbk(s []byte) ([]byte, error) {
} }
func healthCheck() { func healthCheck() {
const MaxInterval = 180 const MaxInterval = 300
for now := range time.NewTicker(30 * time.Second).C { for now := range time.NewTicker(30 * time.Second).C {
timestamp := now.Unix() timestamp := now.Unix()
// stores sessions to be disconnected // stores sessions to be disconnected
queue := make([]string, 0) queue := make([]string, 0)
terminals.IterCb(func(uuid string, t interface{}) bool { terminals.IterCb(func(uuid string, t interface{}) bool {
termSession, ok := t.(*terminal) termSession := t.(*terminal)
if !ok {
queue = append(queue, uuid)
return true
}
if timestamp-termSession.lastPack > MaxInterval { if timestamp-termSession.lastPack > MaxInterval {
queue = append(queue, uuid) queue = append(queue, uuid)
doKillTerminal(termSession) doKillTerminal(termSession)

View File

@@ -1,5 +1,7 @@
package modules package modules
import "reflect"
type Packet struct { type Packet struct {
Code int `json:"code"` Code int `json:"code"`
Act string `json:"act,omitempty"` Act string `json:"act,omitempty"`
@@ -52,3 +54,41 @@ type Net struct {
Sent uint64 `json:"sent"` Sent uint64 `json:"sent"`
Recv uint64 `json:"recv"` Recv uint64 `json:"recv"`
} }
func (p *Packet) GetData(key string, t reflect.Kind) (interface{}, bool) {
if p.Data == nil {
return nil, false
}
data, ok := p.Data[key]
if !ok {
return nil, false
}
switch t {
case reflect.String:
val, ok := data.(string)
return val, ok
case reflect.Uint:
val, ok := data.(uint)
return val, ok
case reflect.Uint32:
val, ok := data.(uint32)
return val, ok
case reflect.Uint64:
val, ok := data.(uint64)
return val, ok
case reflect.Int:
val, ok := data.(int)
return val, ok
case reflect.Int64:
val, ok := data.(int64)
return val, ok
case reflect.Bool:
val, ok := data.(bool)
return val, ok
case reflect.Float64:
val, ok := data.(float64)
return val, ok
default:
return nil, false
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 36 KiB

38
scripts/build.server.bat Normal file
View File

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

View File

@@ -26,3 +26,12 @@ export GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/server go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/server
export GOARCH=amd64 export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/server go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/server
export GOOS=darwin
export GOARCH=arm64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server

View File

@@ -10,7 +10,9 @@ import (
"crypto/cipher" "crypto/cipher"
"encoding/hex" "encoding/hex"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net"
"net/http" "net/http"
"strings"
"time" "time"
) )
@@ -18,7 +20,7 @@ var Melody = melody.New()
var Devices = cmap.New() var Devices = cmap.New()
var BuiltFS http.FileSystem var BuiltFS http.FileSystem
func SendPackUUID(pack modules.Packet, uuid string) bool { func SendPackByUUID(pack modules.Packet, uuid string) bool {
session, ok := Melody.GetSessionByUUID(uuid) session, ok := Melody.GetSessionByUUID(uuid)
if !ok { if !ok {
return false return false
@@ -68,8 +70,7 @@ func Decrypt(data []byte, session *melody.Session) ([]byte, bool) {
return dec, true return dec, true
} }
func WSHealthCheck(container *melody.Melody) { func HealthCheckWS(maxIdleSeconds int64, container *melody.Melody) {
const MaxInterval = 90
go func() { go func() {
// ping client and update latency every 3 seconds // ping client and update latency every 3 seconds
ping := func(uuid string, s *melody.Session) { ping := func(uuid string, s *melody.Session) {
@@ -79,10 +80,8 @@ func WSHealthCheck(container *melody.Melody) {
AddEventOnce(func(packet modules.Packet, session *melody.Session) { AddEventOnce(func(packet modules.Packet, session *melody.Session) {
val, ok := Devices.Get(uuid) val, ok := Devices.Get(uuid)
if ok { if ok {
deviceInfo, ok := val.(*modules.Device) deviceInfo := val.(*modules.Device)
if ok { deviceInfo.Latency = uint(time.Now().UnixMilli()-t) / 2
deviceInfo.Latency = uint(time.Now().UnixMilli()-t) / 2
}
} }
}, uuid, trigger, 3*time.Second) }, uuid, trigger, 3*time.Second)
} }
@@ -108,7 +107,7 @@ func WSHealthCheck(container *melody.Melody) {
queue = append(queue, s) queue = append(queue, s)
return true return true
} }
if timestamp-lastPack > MaxInterval { if timestamp-lastPack > maxIdleSeconds {
queue = append(queue, s) queue = append(queue, s)
} }
return true return true
@@ -119,26 +118,71 @@ func WSHealthCheck(container *melody.Melody) {
} }
} }
func CheckClientReq(ctx *gin.Context, cb func(*melody.Session)) bool { func GetRemoteAddr(ctx *gin.Context) string {
if remote, ok := ctx.RemoteIP(); ok {
if remote.IsLoopback() {
forwarded := ctx.GetHeader(`X-Forwarded-For`)
if len(forwarded) > 0 {
return forwarded
}
realIP := ctx.GetHeader(`X-Real-IP`)
if len(realIP) > 0 {
return realIP
}
} else {
if ip := remote.To4(); ip != nil {
return ip.String()
}
if ip := remote.To16(); ip != nil {
return ip.String()
}
}
}
remote := net.ParseIP(ctx.Request.RemoteAddr)
if remote != nil {
if remote.IsLoopback() {
forwarded := ctx.GetHeader(`X-Forwarded-For`)
if len(forwarded) > 0 {
return forwarded
}
realIP := ctx.GetHeader(`X-Real-IP`)
if len(realIP) > 0 {
return realIP
}
} else {
if ip := remote.To4(); ip != nil {
return ip.String()
}
if ip := remote.To16(); ip != nil {
return ip.String()
}
}
}
addr := ctx.Request.RemoteAddr
if pos := strings.LastIndex(addr, `:`); pos > -1 {
return strings.Trim(addr[:pos], `[]`)
}
return addr
}
func CheckClientReq(ctx *gin.Context) *melody.Session {
secret, err := hex.DecodeString(ctx.GetHeader(`Secret`)) secret, err := hex.DecodeString(ctx.GetHeader(`Secret`))
if err != nil || len(secret) != 32 { if err != nil || len(secret) != 32 {
return false return nil
} }
find := false var result *melody.Session = nil
Melody.IterSessions(func(uuid string, s *melody.Session) bool { Melody.IterSessions(func(uuid string, s *melody.Session) bool {
if val, ok := s.Get(`Secret`); ok { if val, ok := s.Get(`Secret`); ok {
// Check if there's a connection matches this secret. // Check if there's a connection matches this secret.
if b, ok := val.([]byte); ok && bytes.Equal(b, secret) { if b, ok := val.([]byte); ok && bytes.Equal(b, secret) {
find = true result = s
if cb != nil {
cb(s)
}
return false return false
} }
} }
return true return true
}) })
return find return result
} }
func CheckDevice(deviceID, connUUID string) (string, bool) { func CheckDevice(deviceID, connUUID string) (string, bool) {

View File

@@ -7,14 +7,15 @@ import (
"time" "time"
) )
type EventCallback func(modules.Packet, *melody.Session)
type event struct { type event struct {
connection string connection string
callback EventCallback callback EventCallback
channel chan bool finish chan bool
remove chan bool
} }
type EventCallback func(modules.Packet, *melody.Session)
var eventTable = cmap.New() var events = cmap.New()
// CallEvent tries to call the callback with the given uuid // CallEvent tries to call the callback with the given uuid
// after that, it will notify the caller via the channel // after that, it will notify the caller via the channel
@@ -22,7 +23,7 @@ func CallEvent(pack modules.Packet, session *melody.Session) {
if len(pack.Event) == 0 { if len(pack.Event) == 0 {
return return
} }
v, ok := eventTable.Get(pack.Event) v, ok := events.Get(pack.Event)
if !ok { if !ok {
return return
} }
@@ -31,12 +32,8 @@ func CallEvent(pack modules.Packet, session *melody.Session) {
return return
} }
ev.callback(pack, session) ev.callback(pack, session)
if ev.channel != nil { if ev.finish != nil {
defer close(ev.channel) ev.finish <- true
select {
case ev.channel <- true:
default:
}
} }
} }
@@ -44,17 +41,21 @@ func CallEvent(pack modules.Packet, session *melody.Session) {
// can call back the event with the given event trigger. // can call back the event with the given event trigger.
// Event trigger should be uuid to make every event unique. // Event trigger should be uuid to make every event unique.
func AddEventOnce(fn EventCallback, connUUID, trigger string, timeout time.Duration) bool { func AddEventOnce(fn EventCallback, connUUID, trigger string, timeout time.Duration) bool {
done := make(chan bool)
ev := &event{ ev := &event{
connection: connUUID, connection: connUUID,
callback: fn, callback: fn,
channel: done, finish: make(chan bool),
remove: make(chan bool),
} }
eventTable.Set(trigger, ev) events.Set(trigger, ev)
defer eventTable.Remove(trigger) defer events.Remove(trigger)
defer close(ev.finish)
defer close(ev.remove)
select { select {
case <-done: case ok := <-ev.finish:
return true return ok
case ok := <-ev.remove:
return ok
case <-time.After(timeout): case <-time.After(timeout):
return false return false
} }
@@ -66,17 +67,26 @@ func AddEvent(fn EventCallback, connUUID, trigger string) {
ev := &event{ ev := &event{
connection: connUUID, connection: connUUID,
callback: fn, callback: fn,
channel: nil,
} }
eventTable.Set(trigger, ev) events.Set(trigger, ev)
} }
// RemoveEvent deletes the event with the given event trigger. // RemoveEvent deletes the event with the given event trigger.
func RemoveEvent(trigger string) { // The ok will be returned to caller if the event is temp (only once).
eventTable.Remove(trigger) func RemoveEvent(trigger string, ok ...bool) {
v, found := events.Get(trigger)
if !found {
return
}
events.Remove(trigger)
if ev := v.(*event); ev.remove != nil {
if len(ok) > 0 {
ev.remove <- ok[0]
}
}
} }
// HasEvent returns if the event exists. // HasEvent returns if the event exists.
func HasEvent(trigger string) bool { func HasEvent(trigger string) bool {
return eventTable.Has(trigger) return events.Has(trigger)
} }

182
server/handler/bridge.go Normal file
View File

@@ -0,0 +1,182 @@
package handler
import (
"Spark/modules"
"Spark/utils/cmap"
"github.com/gin-gonic/gin"
"github.com/kataras/golog"
"io"
"net/http"
"sync"
"time"
)
// Bridge is a utility that handles the binary flow from the client
// to the browser or flow from the browser to the client.
type bridge struct {
creation int64
using bool
uuid string
lock *sync.Mutex
dest *gin.Context
src *gin.Context
ext interface{}
OnPull func(bridge *bridge)
OnPush func(bridge *bridge)
OnFinish func(bridge *bridge)
}
var bridges = cmap.New()
func init() {
go func() {
for now := range time.NewTicker(10 * time.Second).C {
var queue []*bridge
bridges.IterCb(func(k string, v interface{}) bool {
b := v.(*bridge)
if b.creation < now.Unix()-60 && !b.using {
queue = append(queue, b)
}
return true
})
for _, b := range queue {
bridges.Remove(b.uuid)
b.lock.Lock()
if b.src != nil && b.src.Request.Body != nil {
b.src.Request.Body.Close()
}
b.src = nil
b.dest = nil
b.lock.Unlock()
b = nil
}
}
}()
}
func checkBridge(ctx *gin.Context) *bridge {
var form struct {
Bridge string `json:"bridge" yaml:"bridge" form:"bridge" binding:"required"`
}
if err := ctx.ShouldBind(&form); err != nil {
golog.Error(err)
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return nil
}
val, ok := bridges.Get(form.Bridge)
if !ok {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidBridgeID}`})
return nil
}
return val.(*bridge)
}
func bridgePush(ctx *gin.Context) {
bridge := checkBridge(ctx)
if bridge == nil {
return
}
bridge.lock.Lock()
if bridge.using || (bridge.src != nil && bridge.dest != nil) {
bridge.lock.Unlock()
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
return
}
bridge.src = ctx
bridge.using = true
bridge.lock.Unlock()
if bridge.OnPush != nil {
bridge.OnPush(bridge)
}
if bridge.dest != nil && bridge.dest.Writer != nil {
io.Copy(bridge.dest.Writer, bridge.src.Request.Body)
bridge.src.Status(http.StatusOK)
if bridge.OnFinish != nil {
bridge.OnFinish(bridge)
}
removeBridge(bridge.uuid)
bridge = nil
}
}
func bridgePull(ctx *gin.Context) {
bridge := checkBridge(ctx)
if bridge == nil {
return
}
bridge.lock.Lock()
if bridge.using || (bridge.src != nil && bridge.dest != nil) {
bridge.lock.Unlock()
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
return
}
bridge.dest = ctx
bridge.using = true
bridge.lock.Unlock()
if bridge.OnPull != nil {
bridge.OnPull(bridge)
}
if bridge.src != nil && bridge.src.Request.Body != nil {
io.Copy(bridge.dest.Writer, bridge.src.Request.Body)
bridge.src.Status(http.StatusOK)
if bridge.OnFinish != nil {
bridge.OnFinish(bridge)
}
removeBridge(bridge.uuid)
bridge = nil
}
}
func addBridge(ext interface{}, uuid string) *bridge {
bridge := &bridge{
creation: time.Now().Unix(),
uuid: uuid,
using: false,
lock: &sync.Mutex{},
ext: ext,
}
bridges.Set(uuid, bridge)
return bridge
}
func addBridgeWithSrc(ext interface{}, uuid string, src *gin.Context) *bridge {
bridge := &bridge{
creation: time.Now().Unix(),
uuid: uuid,
using: false,
lock: &sync.Mutex{},
ext: ext,
src: src,
}
bridges.Set(uuid, bridge)
return bridge
}
func addBridgeWithDest(ext interface{}, uuid string, dest *gin.Context) *bridge {
bridge := &bridge{
creation: time.Now().Unix(),
uuid: uuid,
using: false,
lock: &sync.Mutex{},
ext: ext,
dest: dest,
}
bridges.Set(uuid, bridge)
return bridge
}
func removeBridge(uuid string) {
val, ok := bridges.Get(uuid)
if !ok {
return
}
bridges.Remove(uuid)
b := val.(*bridge)
if b.src != nil && b.src.Request.Body != nil {
b.src.Request.Body.Close()
}
b.src = nil
b.dest = nil
b = nil
}

View File

@@ -7,7 +7,6 @@ import (
"Spark/utils/melody" "Spark/utils/melody"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
@@ -27,7 +26,7 @@ func removeDeviceFile(ctx *gin.Context) {
return return
} }
trigger := utils.GetStrUUID() trigger := utils.GetStrUUID()
common.SendPackUUID(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.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
@@ -50,7 +49,7 @@ func listDeviceFiles(ctx *gin.Context) {
return return
} }
trigger := utils.GetStrUUID() trigger := utils.GetStrUUID()
common.SendPackUUID(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.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
@@ -73,12 +72,13 @@ func getDeviceFile(ctx *gin.Context) {
if !ok { if !ok {
return return
} }
bridgeID := utils.GetStrUUID()
trigger := utils.GetStrUUID() trigger := utils.GetStrUUID()
var rangeStart, rangeEnd int64 var rangeStart, rangeEnd int64
var err error var err error
partial := false partial := false
{ {
command := gin.H{`file`: form.File} command := gin.H{`file`: form.File, `bridge`: bridgeID}
rangeHeader := ctx.GetHeader(`Range`) rangeHeader := ctx.GetHeader(`Range`)
if len(rangeHeader) > 6 { if len(rangeHeader) > 6 {
if rangeHeader[:6] != `bytes=` { if rangeHeader[:6] != `bytes=` {
@@ -112,76 +112,57 @@ func getDeviceFile(ctx *gin.Context) {
command[`start`] = rangeStart command[`start`] = rangeStart
partial = true partial = true
} }
common.SendPackUUID(modules.Packet{Code: 0, Act: `uploadFile`, Data: command, Event: trigger}, target) common.SendPackByUUID(modules.Packet{Code: 0, Act: `uploadFile`, Data: command, Event: trigger}, target)
} }
wait := make(chan bool) wait := make(chan bool)
called := false called := false
common.AddEvent(func(p modules.Packet, _ *melody.Session) { common.AddEvent(func(p modules.Packet, _ *melody.Session) {
wait <- false
called = true
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
}, target, trigger)
instance := addBridgeWithDest(nil, bridgeID, ctx)
instance.OnPush = func(bridge *bridge) {
called = true called = true
common.RemoveEvent(trigger) common.RemoveEvent(trigger)
if p.Code != 0 { src := bridge.src
wait <- false if src.Request.ContentLength > 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) ctx.Header(`Content-Length`, strconv.FormatInt(src.Request.ContentLength, 10))
return
} else {
val, ok := p.Data[`request`]
if !ok {
wait <- false
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|fileUploadFailed}`})
return
}
req, ok := val.(*http.Request)
if !ok || req == nil || req.Body == nil {
wait <- false
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|fileUploadFailed}`})
return
}
if req.ContentLength > 0 {
ctx.Header(`Content-Length`, strconv.FormatInt(req.ContentLength, 10))
}
ctx.Header(`Accept-Ranges`, `bytes`)
ctx.Header(`Content-Transfer-Encoding`, `binary`)
ctx.Header(`Content-Type`, `application/octet-stream`)
filename := ctx.GetHeader(`FileName`)
if len(filename) == 0 {
filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
}
filename = url.PathEscape(filename)
ctx.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
if partial {
if rangeEnd == 0 {
rangeEnd, err = strconv.ParseInt(req.Header.Get(`FileSize`), 10, 64)
if err == nil {
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%d`, rangeStart, rangeEnd-1, rangeEnd))
}
} else {
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%v`, rangeStart, rangeEnd, req.Header.Get(`FileSize`)))
}
ctx.Status(http.StatusPartialContent)
} else {
ctx.Status(http.StatusOK)
}
for {
buffer := make([]byte, 8192)
n, err := req.Body.Read(buffer)
buffer = buffer[:n]
ctx.Writer.Write(buffer)
ctx.Writer.Flush()
if n == 0 || err != nil {
wait <- false
break
}
}
} }
}, target, trigger) ctx.Header(`Accept-Ranges`, `bytes`)
ctx.Header(`Content-Transfer-Encoding`, `binary`)
ctx.Header(`Content-Type`, `application/octet-stream`)
filename := src.GetHeader(`FileName`)
if len(filename) == 0 {
filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
}
filename = url.PathEscape(filename)
ctx.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
if partial {
if rangeEnd == 0 {
rangeEnd, err = strconv.ParseInt(src.GetHeader(`FileSize`), 10, 64)
if err == nil {
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%d`, rangeStart, rangeEnd-1, rangeEnd))
}
} else {
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%v`, rangeStart, rangeEnd, src.GetHeader(`FileSize`)))
}
ctx.Status(http.StatusPartialContent)
} else {
ctx.Status(http.StatusOK)
}
}
instance.OnFinish = func(bridge *bridge) {
wait <- false
}
select { select {
case <-wait: case <-wait:
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):
if !called { if !called {
removeBridge(bridgeID)
common.RemoveEvent(trigger) common.RemoveEvent(trigger)
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`}) ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else { } else {
@@ -190,36 +171,62 @@ func getDeviceFile(ctx *gin.Context) {
} }
} }
// putDeviceFile will be called by client. // uploadToDevice handles file from browser
// It will transfer binary stream from client to browser. // and transfer it to device.
func putDeviceFile(ctx *gin.Context) { func uploadToDevice(ctx *gin.Context) {
original := ctx.Request.Body var form struct {
ctx.Request.Body = ioutil.NopCloser(ctx.Request.Body) Path string `json:"path" yaml:"path" form:"path" binding:"required"`
File string `json:"file" yaml:"file" form:"file" binding:"required"`
errMsg := ctx.GetHeader(`Error`) }
trigger := ctx.GetHeader(`Trigger`) target, ok := checkForm(ctx, &form)
if len(trigger) == 0 { if !ok {
original.Close()
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return return
} }
if len(errMsg) > 0 { bridgeID := utils.GetStrUUID()
common.CallEvent(modules.Packet{ trigger := utils.GetStrUUID()
Code: 1, wait := make(chan bool)
Msg: fmt.Sprintf(`${i18n|fileUploadFailed}: %v`, errMsg), called := false
Event: trigger, common.AddEvent(func(p modules.Packet, _ *melody.Session) {
}, nil) wait <- false
original.Close() called = true
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
}, target, trigger)
instance := addBridgeWithSrc(nil, bridgeID, ctx)
instance.OnPull = func(bridge *bridge) {
called = true
common.RemoveEvent(trigger)
dest := bridge.dest
if ctx.Request.ContentLength > 0 {
dest.Header(`Content-Length`, strconv.FormatInt(ctx.Request.ContentLength, 10))
}
dest.Header(`Accept-Ranges`, `none`)
dest.Header(`Content-Transfer-Encoding`, `binary`)
dest.Header(`Content-Type`, `application/octet-stream`)
filename := form.File
filename = url.PathEscape(filename)
dest.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
}
instance.OnFinish = func(bridge *bridge) {
wait <- false
}
common.SendPackByUUID(modules.Packet{Code: 0, Act: `fetchFile`, Data: gin.H{
`path`: form.Path,
`file`: form.File,
`bridge`: bridgeID,
}, Event: trigger}, target)
select {
case <-wait:
ctx.JSON(http.StatusOK, modules.Packet{Code: 0}) ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
return case <-time.After(5 * time.Second):
if !called {
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else {
<-wait
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
} }
common.CallEvent(modules.Packet{
Code: 0,
Data: map[string]interface{}{
`request`: ctx.Request,
},
Event: trigger,
}, nil)
original.Close()
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
} }

View File

@@ -3,30 +3,23 @@ package handler
import ( import (
"Spark/modules" "Spark/modules"
"Spark/server/common" "Spark/server/common"
"Spark/server/config"
"Spark/utils"
"Spark/utils/melody"
"bytes"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/kataras/golog"
"net/http" "net/http"
"strconv"
"time"
) )
// APIRouter will initialize http and websocket routers. // InitRouter will initialize http and websocket routers.
func APIRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) { func InitRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
ctx.PUT(`/device/screenshot/put`, putScreenshot) // Client, upload screenshot and forward to browser. ctx.Any(`/bridge/push`, bridgePush)
ctx.PUT(`/device/file/put`, putDeviceFile) // Client, to upload file and forward to browser. ctx.Any(`/bridge/pull`, bridgePull)
ctx.Any(`/device/terminal`, initTerminal) // Browser, handle websocket events for web terminal. 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(`/`, auth)
{ {
group.POST(`/device/screenshot/get`, getScreenshot) group.POST(`/device/screenshot/get`, getScreenshot)
group.POST(`/device/process/list`, listDeviceProcesses) group.POST(`/device/process/list`, listDeviceProcesses)
group.POST(`/device/process/kill`, killDeviceProcess) group.POST(`/device/process/kill`, killDeviceProcess)
group.POST(`/device/file/remove`, removeDeviceFile) group.POST(`/device/file/remove`, removeDeviceFile)
group.POST(`/device/file/upload`, uploadToDevice)
group.POST(`/device/file/list`, listDeviceFiles) group.POST(`/device/file/list`, listDeviceFiles)
group.POST(`/device/file/get`, getDeviceFile) group.POST(`/device/file/get`, getDeviceFile)
group.POST(`/device/list`, getDevices) group.POST(`/device/list`, getDevices)
@@ -36,126 +29,6 @@ func APIRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
} }
} }
// checkUpdate will check if client need update and return latest client if so.
func checkUpdate(ctx *gin.Context) {
var form struct {
OS string `form:"os" binding:"required"`
Arch string `form:"arch" binding:"required"`
Commit string `form:"commit" binding:"required"`
}
if err := ctx.ShouldBind(&form); err != nil {
golog.Error(err)
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
if form.Commit == config.COMMIT {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
return
}
tpl, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
if err != nil {
ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
return
}
const MaxBodySize = 384 // This is size of client config buffer.
if ctx.Request.ContentLength > MaxBodySize {
ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
return
}
body, err := ctx.GetRawData()
if err != nil {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
return
}
auth := common.CheckClientReq(ctx, nil)
if !auth {
ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
}
ctx.Header(`Accept-Ranges`, `none`)
ctx.Header(`Content-Transfer-Encoding`, `binary`)
ctx.Header(`Content-Type`, `application/octet-stream`)
if stat, err := tpl.Stat(); err == nil {
ctx.Header(`Content-Length`, strconv.FormatInt(stat.Size(), 10))
}
cfgBuffer := bytes.Repeat([]byte{'\x19'}, 384)
prevBuffer := make([]byte, 0)
for {
thisBuffer := make([]byte, 1024)
n, err := tpl.Read(thisBuffer)
thisBuffer = thisBuffer[:n]
tempBuffer := append(prevBuffer, thisBuffer...)
bufIndex := bytes.Index(tempBuffer, cfgBuffer)
if bufIndex > -1 {
tempBuffer = bytes.Replace(tempBuffer, cfgBuffer, body, -1)
}
ctx.Writer.Write(tempBuffer[:len(prevBuffer)])
prevBuffer = tempBuffer[len(prevBuffer):]
if err != nil {
break
}
}
if len(prevBuffer) > 0 {
ctx.Writer.Write(prevBuffer)
prevBuffer = []byte{}
}
}
// getDevices will return all info about all clients.
func getDevices(ctx *gin.Context) {
devices := make(map[string]modules.Device)
common.Devices.IterCb(func(uuid string, v interface{}) bool {
device, ok := v.(*modules.Device)
if ok {
devices[uuid] = *device
}
return true
})
ctx.JSON(http.StatusOK, modules.CommonPack{Code: 0, Data: devices})
}
// callDevice will call client with command from browser.
func callDevice(ctx *gin.Context) {
act := ctx.Param(`act`)
if len(act) == 0 {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
{
actions := []string{`lock`, `logoff`, `hibernate`, `suspend`, `restart`, `shutdown`, `offline`}
ok := false
for _, v := range actions {
if v == act {
ok = true
break
}
}
if !ok {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
}
connUUID, ok := checkForm(ctx, nil)
if !ok {
return
}
trigger := utils.GetStrUUID()
common.SendPackUUID(modules.Packet{Act: act, Event: trigger}, connUUID)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}, connUUID, trigger, 5*time.Second)
if !ok {
//This means the client is offline.
//So we take this as a success.
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}
// checkForm checks if the form contains the required fields. // checkForm checks if the form contains the required fields.
// Every request must contain connection UUID or device ID. // Every request must contain connection UUID or device ID.
func checkForm(ctx *gin.Context, form interface{}) (string, bool) { func checkForm(ctx *gin.Context, form interface{}) (string, bool) {
@@ -178,75 +51,3 @@ func checkForm(ctx *gin.Context, form interface{}) (string, bool) {
} }
return connUUID, true return connUUID, true
} }
// WSDevice handles events about device info.
// Such as websocket handshake and update device info.
func WSDevice(data []byte, session *melody.Session) error {
var pack struct {
Code int `json:"code,omitempty"`
Act string `json:"act,omitempty"`
Msg string `json:"msg,omitempty"`
Device modules.Device `json:"data"`
}
err := utils.JSON.Unmarshal(data, &pack)
if err != nil {
golog.Error(err)
session.Close()
return err
}
addr, ok := session.Get(`Address`)
if ok {
pack.Device.WAN = addr.(string)
} else {
pack.Device.WAN = `Unknown`
}
if pack.Act == `report` {
// Check if this device has already connected.
// If so, then find the session and let client quit.
// This will keep only one connection remained per device.
exSession := ``
common.Devices.IterCb(func(uuid string, v interface{}) bool {
device := v.(*modules.Device)
if device.ID == pack.Device.ID {
exSession = uuid
target, ok := common.Melody.GetSessionByUUID(uuid)
if ok {
common.SendPack(modules.Packet{Act: `offline`}, target)
target.Close()
}
return false
}
return true
})
if len(exSession) > 0 {
common.Devices.Remove(exSession)
}
}
common.SendPack(modules.Packet{Code: 0}, session)
{
val, ok := common.Devices.Get(session.UUID)
if ok {
deviceInfo, ok := val.(*modules.Device)
if ok {
deviceInfo.CPU = pack.Device.CPU
deviceInfo.RAM = pack.Device.RAM
deviceInfo.Net = pack.Device.Net
if pack.Device.Disk.Total > 0 {
deviceInfo.Disk = pack.Device.Disk
}
deviceInfo.Uptime = pack.Device.Uptime
return nil
}
}
common.Devices.Set(session.UUID, &pack.Device)
}
return nil
}
// WSRouter handles all packets from client.
func WSRouter(pack modules.Packet, session *melody.Session) {
common.CallEvent(pack, session)
}

View File

@@ -18,7 +18,7 @@ func listDeviceProcesses(ctx *gin.Context) {
return return
} }
trigger := utils.GetStrUUID() trigger := utils.GetStrUUID()
common.SendPackUUID(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.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
@@ -42,7 +42,7 @@ func killDeviceProcess(ctx *gin.Context) {
return return
} }
trigger := utils.GetStrUUID() trigger := utils.GetStrUUID()
common.SendPackUUID(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.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})

View File

@@ -5,82 +5,47 @@ import (
"Spark/server/common" "Spark/server/common"
"Spark/utils" "Spark/utils"
"Spark/utils/melody" "Spark/utils/melody"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
"time" "time"
) )
// putScreenshot will forward screenshot image from client to browser.
func putScreenshot(ctx *gin.Context) {
errMsg := ctx.GetHeader(`Error`)
trigger := ctx.GetHeader(`Trigger`)
if len(trigger) == 0 {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
if len(errMsg) > 0 {
common.CallEvent(modules.Packet{
Code: 1,
Msg: fmt.Sprintf(`${i18n|screenshotFailed}: %v`, errMsg),
Event: trigger,
}, nil)
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
return
}
data, err := ctx.GetRawData()
if len(data) == 0 {
msg := ``
if err != nil {
msg = fmt.Sprintf(`${i18n|screenshotObtainFailed}: %v`, err)
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: msg})
} else {
msg = `${i18n|screenshotFailed}: ${i18n|unknownError}`
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
common.CallEvent(modules.Packet{
Code: 1,
Msg: msg,
Event: trigger,
}, nil)
return
}
common.CallEvent(modules.Packet{
Code: 0,
Data: map[string]interface{}{
`screenshot`: data,
},
Event: trigger,
}, nil)
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
// getScreenshot will call client to screenshot. // getScreenshot will call client to screenshot.
func getScreenshot(ctx *gin.Context) { func getScreenshot(ctx *gin.Context) {
target, ok := checkForm(ctx, nil) target, ok := checkForm(ctx, nil)
if !ok { if !ok {
return return
} }
bridgeID := utils.GetStrUUID()
trigger := utils.GetStrUUID() trigger := utils.GetStrUUID()
common.SendPackUUID(modules.Packet{Code: 0, Act: `screenshot`, Event: trigger}, target) wait := make(chan bool)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) { called := false
if p.Code != 0 { common.SendPackByUUID(modules.Packet{Code: 0, Act: `screenshot`, Data: gin.H{`bridge`: bridgeID}, Event: trigger}, target)
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) common.AddEvent(func(p modules.Packet, _ *melody.Session) {
wait <- false
called = true
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
}, target, trigger)
instance := addBridgeWithDest(nil, bridgeID, ctx)
instance.OnPush = func(bridge *bridge) {
called = true
common.RemoveEvent(trigger)
ctx.Header(`Content-Type`, `image/png`)
}
instance.OnFinish = func(bridge *bridge) {
wait <- false
}
select {
case <-wait:
case <-time.After(5 * time.Second):
if !called {
removeBridge(bridgeID)
common.RemoveEvent(trigger)
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else { } else {
data, ok := p.Data[`screenshot`] <-wait
if !ok {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|screenshotObtainFailed}`})
return
}
screenshot, ok := data.([]byte)
if !ok {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|screenshotObtainFailed}`})
return
}
ctx.Data(200, `image/png`, screenshot)
} }
}, target, trigger, 5*time.Second)
if !ok {
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} }
} }

View File

@@ -15,18 +15,18 @@ import (
) )
type terminal struct { type terminal struct {
uuid string
event string
device string
session *melody.Session session *melody.Session
deviceConn *melody.Session deviceConn *melody.Session
device string
termUUID string
eventUUID string
} }
var terminals = cmap.New() var terminals = cmap.New()
var wsTerminals = melody.New() var wsSessions = melody.New()
func init() { func init() {
wsTerminals.HandleConnect(func(session *melody.Session) { wsSessions.HandleConnect(func(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)
@@ -59,11 +59,11 @@ func init() {
} }
eventUUID := utils.GetStrUUID() eventUUID := utils.GetStrUUID()
terminal := &terminal{ terminal := &terminal{
uuid: termUUID,
event: eventUUID,
device: device.(string),
session: session, session: session,
deviceConn: deviceConn, deviceConn: deviceConn,
device: device.(string),
termUUID: termUUID,
eventUUID: eventUUID,
} }
terminals.Set(termUUID, terminal) terminals.Set(termUUID, terminal)
common.AddEvent(eventWrapper(terminal), connUUID, eventUUID) common.AddEvent(eventWrapper(terminal), connUUID, eventUUID)
@@ -71,9 +71,9 @@ func init() {
`terminal`: termUUID, `terminal`: termUUID,
}, Event: eventUUID}, deviceConn) }, Event: eventUUID}, deviceConn)
}) })
wsTerminals.HandleMessage(onMessage) wsSessions.HandleMessage(onMessage)
wsTerminals.HandleMessageBinary(onMessage) wsSessions.HandleMessageBinary(onMessage)
wsTerminals.HandleDisconnect(func(session *melody.Session) { wsSessions.HandleDisconnect(func(session *melody.Session) {
val, ok := session.Get(`Terminal`) val, ok := session.Get(`Terminal`)
if !ok { if !ok {
return return
@@ -86,17 +86,14 @@ func init() {
if !ok { if !ok {
return return
} }
terminal, ok := val.(*terminal) terminal := val.(*terminal)
if !ok {
return
}
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{ common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
`terminal`: terminal.termUUID, `terminal`: termUUID,
}, Event: terminal.eventUUID}, terminal.deviceConn) }, Event: terminal.event}, terminal.deviceConn)
terminals.Remove(termUUID) terminals.Remove(termUUID)
common.RemoveEvent(terminal.eventUUID) common.RemoveEvent(terminal.event)
}) })
go common.WSHealthCheck(wsTerminals) go common.HealthCheckWS(300, wsSessions)
} }
// initTerminal handles terminal websocket handshake event // initTerminal handles terminal websocket handshake event
@@ -125,7 +122,7 @@ func initTerminal(ctx *gin.Context) {
return return
} }
wsTerminals.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{ wsSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
`Secret`: secret, `Secret`: secret,
`Device`: device, `Device`: device,
`LastPack`: time.Now().Unix(), `LastPack`: time.Now().Unix(),
@@ -146,8 +143,8 @@ func eventWrapper(terminal *terminal) common.EventCallback {
msg += `${i18n|unknownError}` msg += `${i18n|unknownError}`
} }
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session) simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
terminals.Remove(terminal.termUUID) terminals.Remove(terminal.uuid)
common.RemoveEvent(terminal.eventUUID) common.RemoveEvent(terminal.event)
terminal.session.Close() terminal.session.Close()
} }
return return
@@ -158,8 +155,8 @@ func eventWrapper(terminal *terminal) common.EventCallback {
msg = pack.Msg msg = pack.Msg
} }
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session) simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
terminals.Remove(terminal.termUUID) terminals.Remove(terminal.uuid)
common.RemoveEvent(terminal.eventUUID) common.RemoveEvent(terminal.event)
terminal.session.Close() terminal.session.Close()
return return
} }
@@ -246,18 +243,15 @@ func onMessage(session *melody.Session, data []byte) {
if !ok { if !ok {
return return
} }
terminal, ok := val.(*terminal) terminal := val.(*terminal)
if !ok {
return
}
if pack.Data == nil { if pack.Data == nil {
return return
} }
if input, ok := pack.Data[`input`]; ok { if input, ok := pack.Data[`input`]; ok {
common.SendPack(modules.Packet{Act: `inputTerminal`, Data: gin.H{ common.SendPack(modules.Packet{Act: `inputTerminal`, Data: gin.H{
`input`: input, `input`: input,
`terminal`: terminal.termUUID, `terminal`: terminal.uuid,
}, Event: terminal.eventUUID}, terminal.deviceConn) }, Event: terminal.event}, terminal.deviceConn)
} }
} }
if pack.Act == `resizeTerminal` { if pack.Act == `resizeTerminal` {
@@ -273,10 +267,7 @@ func onMessage(session *melody.Session, data []byte) {
if !ok { if !ok {
return return
} }
terminal, ok := val.(*terminal) terminal := val.(*terminal)
if !ok {
return
}
if pack.Data == nil { if pack.Data == nil {
return return
} }
@@ -285,26 +276,44 @@ func onMessage(session *melody.Session, data []byte) {
common.SendPack(modules.Packet{Act: `resizeTerminal`, Data: gin.H{ common.SendPack(modules.Packet{Act: `resizeTerminal`, Data: gin.H{
`width`: width, `width`: width,
`height`: height, `height`: height,
`terminal`: terminal.termUUID, `terminal`: terminal.uuid,
}, Event: terminal.eventUUID}, terminal.deviceConn) }, 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 []string
terminals.IterCb(func(key string, val interface{}) bool { terminals.IterCb(func(key string, val interface{}) bool {
terminal, ok := val.(*terminal) terminal := val.(*terminal)
if !ok {
return false
}
if terminal.device == deviceID { if terminal.device == deviceID {
common.RemoveEvent(terminal.eventUUID) common.RemoveEvent(terminal.event)
terminal.session.Close() terminal.session.Close()
queue = append(queue, key) queue = append(queue, key)
} }
return false return true
}) })
for _, key := range queue { for _, key := range queue {

200
server/handler/utility.go Normal file
View File

@@ -0,0 +1,200 @@
package handler
import (
"Spark/modules"
"Spark/server/common"
"Spark/server/config"
"Spark/utils"
"Spark/utils/melody"
"bytes"
"fmt"
"github.com/gin-gonic/gin"
"github.com/kataras/golog"
"net/http"
"strconv"
"time"
)
// OnDevicePack handles events about device info.
// Such as websocket handshake and update device info.
func OnDevicePack(data []byte, session *melody.Session) error {
var pack struct {
Code int `json:"code,omitempty"`
Act string `json:"act,omitempty"`
Msg string `json:"msg,omitempty"`
Device modules.Device `json:"data"`
}
err := utils.JSON.Unmarshal(data, &pack)
if err != nil {
golog.Error(err)
session.Close()
return err
}
addr, ok := session.Get(`Address`)
if ok {
pack.Device.WAN = addr.(string)
} else {
pack.Device.WAN = `Unknown`
}
if pack.Act == `report` {
// Check if this device has already connected.
// If so, then find the session and let client quit.
// This will keep only one connection remained per device.
exSession := ``
common.Devices.IterCb(func(uuid string, v interface{}) bool {
device := v.(*modules.Device)
if device.ID == pack.Device.ID {
exSession = uuid
target, ok := common.Melody.GetSessionByUUID(uuid)
if ok {
common.SendPack(modules.Packet{Act: `offline`}, target)
target.Close()
}
return false
}
return true
})
if len(exSession) > 0 {
common.Devices.Remove(exSession)
}
}
common.SendPack(modules.Packet{Code: 0}, session)
{
val, ok := common.Devices.Get(session.UUID)
if ok {
deviceInfo := val.(*modules.Device)
deviceInfo.CPU = pack.Device.CPU
deviceInfo.RAM = pack.Device.RAM
deviceInfo.Net = pack.Device.Net
if pack.Device.Disk.Total > 0 {
deviceInfo.Disk = pack.Device.Disk
}
deviceInfo.Uptime = pack.Device.Uptime
return nil
}
common.Devices.Set(session.UUID, &pack.Device)
}
return nil
}
// checkUpdate will check if client need update and return latest client if so.
func checkUpdate(ctx *gin.Context) {
var form struct {
OS string `form:"os" binding:"required"`
Arch string `form:"arch" binding:"required"`
Commit string `form:"commit" binding:"required"`
}
if err := ctx.ShouldBind(&form); err != nil {
golog.Error(err)
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
if form.Commit == config.COMMIT {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
return
}
tpl, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
if err != nil {
ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
return
}
const MaxBodySize = 384 // This is size of client config buffer.
if ctx.Request.ContentLength > MaxBodySize {
ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
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
}
ctx.Header(`Accept-Ranges`, `none`)
ctx.Header(`Content-Transfer-Encoding`, `binary`)
ctx.Header(`Content-Type`, `application/octet-stream`)
if stat, err := tpl.Stat(); err == nil {
ctx.Header(`Content-Length`, strconv.FormatInt(stat.Size(), 10))
}
cfgBuffer := bytes.Repeat([]byte{'\x19'}, 384)
prevBuffer := make([]byte, 0)
for {
thisBuffer := make([]byte, 1024)
n, err := tpl.Read(thisBuffer)
thisBuffer = thisBuffer[:n]
tempBuffer := append(prevBuffer, thisBuffer...)
bufIndex := bytes.Index(tempBuffer, cfgBuffer)
if bufIndex > -1 {
tempBuffer = bytes.Replace(tempBuffer, cfgBuffer, body, -1)
}
ctx.Writer.Write(tempBuffer[:len(prevBuffer)])
prevBuffer = tempBuffer[len(prevBuffer):]
if err != nil {
break
}
}
if len(prevBuffer) > 0 {
ctx.Writer.Write(prevBuffer)
prevBuffer = []byte{}
}
}
// getDevices will return all info about all clients.
func getDevices(ctx *gin.Context) {
devices := map[string]interface{}{}
common.Devices.IterCb(func(uuid string, v interface{}) bool {
device := v.(*modules.Device)
devices[uuid] = *device
return true
})
ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: devices})
}
// callDevice will call client with command from browser.
func callDevice(ctx *gin.Context) {
act := ctx.Param(`act`)
if len(act) == 0 {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
{
actions := []string{`lock`, `logoff`, `hibernate`, `suspend`, `restart`, `shutdown`, `offline`}
ok := false
for _, v := range actions {
if v == act {
ok = true
break
}
}
if !ok {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
}
connUUID, ok := checkForm(ctx, nil)
if !ok {
return
}
trigger := utils.GetStrUUID()
common.SendPackByUUID(modules.Packet{Act: act, Event: trigger}, connUUID)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}, connUUID, trigger, 5*time.Second)
if !ok {
//This means the client is offline.
//So we take this as a success.
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}

View File

@@ -7,10 +7,8 @@ import (
"Spark/server/handler" "Spark/server/handler"
"bytes" "bytes"
"context" "context"
"net"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
@@ -61,19 +59,21 @@ func main() {
return return
} }
app := gin.New() app := gin.New()
auth := gin.BasicAuth(config.Config.Auth) {
handler.APIRouter(app.Group(`/api`), auth) auth := gin.BasicAuth(config.Config.Auth)
app.Any(`/ws`, wsHandshake) handler.InitRouter(app.Group(`/api`), auth)
app.NoRoute(auth, func(ctx *gin.Context) { app.Any(`/ws`, wsHandshake)
http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request) app.NoRoute(auth, func(ctx *gin.Context) {
}) http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request)
})
}
common.Melody.Config.MaxMessageSize = 1024 common.Melody.Config.MaxMessageSize = 1024
common.Melody.HandleConnect(wsOnConnect) common.Melody.HandleConnect(wsOnConnect)
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.WSHealthCheck(common.Melody) go common.HealthCheckWS(90, common.Melody)
srv := &http.Server{Addr: config.Config.Listen, Handler: app} srv := &http.Server{Addr: config.Config.Listen, Handler: app}
go func() { go func() {
@@ -114,7 +114,7 @@ func wsHandshake(ctx *gin.Context) {
}, gin.H{ }, gin.H{
`Secret`: secret, `Secret`: secret,
`LastPack`: time.Now().Unix(), `LastPack`: time.Now().Unix(),
`Address`: getRemoteAddr(ctx), `Address`: common.GetRemoteAddr(ctx),
}) })
if err != nil { if err != nil {
golog.Error(err) golog.Error(err)
@@ -134,12 +134,12 @@ func wsHandshake(ctx *gin.Context) {
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1}) ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
return return
} }
auth := common.CheckClientReq(ctx, func(s *melody.Session) { session := common.CheckClientReq(ctx)
wsOnMessageBinary(s, body) if session == nil {
})
if !auth {
ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1}) ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
return
} }
wsOnMessageBinary(session, body)
ctx.JSON(http.StatusOK, modules.Packet{Code: 0}) ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
} }
} }
@@ -161,71 +161,22 @@ func wsOnMessageBinary(session *melody.Session, data []byte) {
} }
if pack.Act == `report` || pack.Act == `setDevice` { if pack.Act == `report` || pack.Act == `setDevice` {
session.Set(`LastPack`, time.Now().Unix()) session.Set(`LastPack`, time.Now().Unix())
handler.WSDevice(data, session) handler.OnDevicePack(data, session)
return return
} }
if !common.Devices.Has(session.UUID) { if !common.Devices.Has(session.UUID) {
session.Close() session.Close()
return return
} }
handler.WSRouter(pack, session) common.CallEvent(pack, session)
session.Set(`LastPack`, time.Now().Unix()) session.Set(`LastPack`, time.Now().Unix())
} }
func wsOnDisconnect(session *melody.Session) { func wsOnDisconnect(session *melody.Session) {
if val, ok := common.Devices.Get(session.UUID); ok { if val, ok := common.Devices.Get(session.UUID); ok {
if deviceInfo, ok := val.(*modules.Device); ok { deviceInfo := val.(*modules.Device)
handler.CloseSessionsByDevice(deviceInfo.ID) handler.CloseSessionsByDevice(deviceInfo.ID)
}
} }
common.Devices.Remove(session.UUID) common.Devices.Remove(session.UUID)
} }
func getRemoteAddr(ctx *gin.Context) string {
if remote, ok := ctx.RemoteIP(); ok {
if remote.IsLoopback() {
forwarded := ctx.GetHeader(`X-Forwarded-For`)
if len(forwarded) > 0 {
return forwarded
}
realIP := ctx.GetHeader(`X-Real-IP`)
if len(realIP) > 0 {
return realIP
}
} else {
if ip := remote.To4(); ip != nil {
return ip.String()
}
if ip := remote.To16(); ip != nil {
return ip.String()
}
}
}
remote := net.ParseIP(ctx.Request.RemoteAddr)
if remote != nil {
if remote.IsLoopback() {
forwarded := ctx.GetHeader(`X-Forwarded-For`)
if len(forwarded) > 0 {
return forwarded
}
realIP := ctx.GetHeader(`X-Real-IP`)
if len(realIP) > 0 {
return realIP
}
} else {
if ip := remote.To4(); ip != nil {
return ip.String()
}
if ip := remote.To16(); ip != nil {
return ip.String()
}
}
}
addr := ctx.Request.RemoteAddr
if pos := strings.LastIndex(addr, `:`); pos > -1 {
return strings.Trim(addr[:pos], `[]`)
}
return addr
}

View File

@@ -36,6 +36,7 @@ func GetMD5(data []byte) ([]byte, string) {
hash := md5.New() hash := md5.New()
hash.Write(data) hash.Write(data)
result := hash.Sum(nil) result := hash.Sum(nil)
hash.Reset()
return result, hex.EncodeToString(result) return result, hex.EncodeToString(result)
} }
@@ -73,9 +74,13 @@ func Decrypt(data []byte, key []byte) ([]byte, error) {
hash, _ := GetMD5(decBuffer) hash, _ := GetMD5(decBuffer)
if !bytes.Equal(hash, data[:16]) { if !bytes.Equal(hash, data[:16]) {
data = nil
decBuffer = nil
return nil, ErrFailedVerification return nil, ErrFailedVerification
} }
data = nil
decBuffer = decBuffer[:dataLen-16-64]
//fmt.Println(`Recv: `, string(decBuffer[:dataLen-16-64])) //fmt.Println(`Recv: `, string(decBuffer[:dataLen-16-64]))
return decBuffer[:dataLen-16-64], nil return decBuffer, nil
} }

View File

@@ -6,4 +6,8 @@
.ant-table-body { .ant-table-body {
max-height: 300px; max-height: 300px;
min-height: 300px; min-height: 300px;
}
.upload-progress-square > .ant-progress-outer > .ant-progress-inner {
border-radius: 0 !important;
} }

View File

@@ -1,14 +1,19 @@
import React, {useEffect, useRef, useState} from 'react'; import React, {useEffect, useRef, useState} from 'react';
import {message, Modal, Popconfirm} from "antd"; import {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, 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 axios from "axios";
import Qs from "qs";
let fileList = [];
function FileBrowser(props) { function FileBrowser(props) {
const [path, setPath] = useState(`/`); const [path, setPath] = useState(`/`);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [upload, setUpload] = useState(false);
const columns = [ const columns = [
{ {
key: 'Name', key: 'Name',
@@ -91,7 +96,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(getLastPath()); listFiles(getParentPath());
return; return;
} }
if (file.type !== 0) { if (file.type !== 0) {
@@ -110,7 +115,7 @@ function FileBrowser(props) {
tableRef.current.reload(); tableRef.current.reload();
} }
function getLastPath() { function getParentPath() {
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
@@ -125,6 +130,52 @@ function FileBrowser(props) {
return pathArr.join(separator) + separator; return pathArr.join(separator) + separator;
} }
function onFileChange(e) {
let file = e.target.files[0];
if (file === undefined) return;
e.target.value = null;
{
let exists = false;
for (let i = 0; i < fileList.length; i++) {
if (fileList[i].type === 0 && fileList[i].name === file.name) {
exists = true;
break;
}
}
if (exists) {
Modal.confirm({
autoFocusButton: 'cancel',
content: i18n.t('fileOverwriteConfirm').replace('{0}', file.name),
okText: i18n.t('fileOverwrite'),
onOk: () => {
setUpload(file);
},
okButtonProps: {
danger: true,
},
});
} else {
setUpload(file);
}
}
}
function uploadFile() {
if (path === '/' || path === '\\' || path.length === 0) {
if (props.isWindows) {
message.error(i18n.t('uploadInvalidPath'));
return;
}
}
document.getElementById('uploader').click();
}
function onUploadSuccess() {
tableRef.current.reload();
setUpload(false);
}
function onUploadCancel() {
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,
@@ -150,6 +201,7 @@ function FileBrowser(props) {
if (data.code === 0) { if (data.code === 0) {
let addParentShortcut = false; let addParentShortcut = false;
data.data.files = data.data.files.sort((first, second) => (second.type - first.type)); data.data.files = data.data.files.sort((first, second) => (second.type - first.type));
fileList = [].concat(data.data.files);
if (path.length > 0 && path !== '/' && path !== '\\') { if (path.length > 0 && path !== '/' && path !== '\\') {
addParentShortcut = true; addParentShortcut = true;
data.data.files.unshift({ data.data.files.unshift({
@@ -165,7 +217,7 @@ function FileBrowser(props) {
total: data.data.files.length - (addParentShortcut ? 1 : 0) total: data.data.files.length - (addParentShortcut ? 1 : 0)
}); });
} }
setPath(getLastPath()); setPath(getParentPath());
return ({data: [], success: false, total: 0}); return ({data: [], success: false, total: 0});
} }
@@ -188,7 +240,22 @@ function FileBrowser(props) {
onDoubleClick: onRowClick.bind(null, file), onDoubleClick: onRowClick.bind(null, file),
})} })}
toolbar={{ toolbar={{
actions: [] settings: [
{
icon: <UploadOutlined />,
tooltip: i18n.t('upload'),
key: 'upload',
onClick: uploadFile
},
{
icon: <ReloadOutlined/>,
tooltip: i18n.t('reload'),
key: 'reload',
onClick: () => {
tableRef.current.reload();
}
}
]
}} }}
scroll={{scrollToFirstRowOnChange: true, y: 300}} scroll={{scrollToFirstRowOnChange: true, y: 300}}
search={false} search={false}
@@ -203,6 +270,173 @@ function FileBrowser(props) {
actionRef={tableRef} actionRef={tableRef}
> >
</ProTable> </ProTable>
<input
id='uploader'
type='file'
style={{ display: 'none' }}
onChange={onFileChange}
/>
<UploadModal
path={path}
file={upload}
device={props.device}
onSuccess={onUploadSuccess}
onCanel={onUploadCancel}
/>
</Modal>
)
}
let abortController = null;
function UploadModal(props) {
const [visible, setVisible] = useState(!!props.file);
const [percent, setPercent] = useState(0);
const [status, setStatus] = useState(0);
// 0: ready, 1: uploading, 2: success, 3: fail, 4: cancel
useEffect(() => {
if (props.file) {
setVisible(true);
setPercent(0);
setStatus(0);
}
}, [props.file]);
function onPageUnload(e) {
e.preventDefault();
e.returnValue = '';
return '';
}
function onConfirm() {
if (status !== 0) {
onCancel();
return;
}
let params = Qs.stringify({
device: props.device,
path: props.path,
file: props.file.name
});
setStatus(1);
window.onbeforeunload = onPageUnload;
abortController = new AbortController();
axios.post(
'/api/device/file/upload?' + params,
props.file,
{
headers: {
'Content-Type': 'application/octet-stream'
},
timeout: 0,
onUploadProgress: (progressEvent) => {
let percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
setPercent(percentCompleted);
},
signal: abortController.signal
}
).then(res => {
let data = res.data;
if (data.code === 0) {
setStatus(2);
message.success(i18n.t('uploadSuccess'));
} else {
setStatus(3);
}
}).catch((err) => {
if (axios.isCancel(err)) {
setStatus(4);
message.error(i18n.t('uploadAborted'));
} else {
setStatus(3);
message.error(i18n.t('uploadFailed') + i18n.t('colon') + err.message);
}
}).finally(() => {
abortController = null;
window.onbeforeunload = null;
setTimeout(() => {
setVisible(false);
if (status === 2) {
props.onSuccess();
} else {
props.onCanel();
}
}, 1500);
});
}
function onCancel() {
if (status === 0) {
setVisible(false);
setTimeout(props.onCanel, 300);
return;
}
if (status === 1) {
Modal.confirm({
autoFocusButton: 'cancel',
content: i18n.t('uploadCancelConfirm'),
onOk: () => {
abortController.abort();
},
okButtonProps: {
danger: true,
},
});
return;
}
setTimeout(() => {
setVisible(false);
setTimeout(props.onCanel, 300);
}, 1500);
}
function getDescription() {
switch (status) {
case 1:
return `${percent}%`;
case 2:
return i18n.t('uploadSuccess');
case 3:
return i18n.t('uploadFailed');
case 4:
return i18n.t('uploadAborted');
default:
return i18n.t('upload');
}
}
return (
<Modal
visible={visible}
closable={false}
keyboard={false}
maskClosable={false}
destroyOnClose={true}
confirmLoading={status === 1}
okText={i18n.t(status===1?'uploading':'upload')}
onOk={onConfirm}
onCancel={onCancel}
okButtonProps={{disabled: status !== 0}}
cancelButtonProps={{disabled: status > 1}}
width={550}
>
<>
<span
style={{
fontSize: '20px',
marginRight: '10px',
}}
>
{getDescription()}
</span>
{props.file.name + ` (${formatSize(props.file.size)})`}
</>
<Progress
className='upload-progress-square'
strokeLinecap='square'
percent={percent}
showInfo={false}
/>
</Modal> </Modal>
) )
} }

View File

@@ -8,7 +8,7 @@ import CryptoJS from 'crypto-js';
import wcwidth from 'wcwidth'; import wcwidth from 'wcwidth';
import "xterm/css/xterm.css"; import "xterm/css/xterm.css";
import i18n from "../locale/locale"; import i18n from "../locale/locale";
import {translate} from "../utils/utils"; import {getBaseURL, translate} from "../utils/utils";
function hex2buf(hex) { function hex2buf(hex) {
if (typeof hex !== 'string') { if (typeof hex !== 'string') {
@@ -59,13 +59,6 @@ function ab2str(buffer) {
return out; return out;
} }
function getBaseURL() {
if (location.protocol === 'https:') {
return `wss://${location.host}${location.pathname}api/device/terminal`;
}
return `ws://${location.host}${location.pathname}api/device/terminal`;
}
function genRandHex(length) { function genRandHex(length) {
return [...Array(length)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); return [...Array(length)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
} }
@@ -120,7 +113,7 @@ class TerminalModal extends React.Component {
ev?.dispose(); ev?.dispose();
let buffer = ''; let buffer = '';
let termEv = null; let termEv = null;
// Windows don't support pty, so we still use traditional way. // Windows doesn't support pty, so we still use traditional way.
if (this.props.device.os === 'windows') { if (this.props.device.os === 'windows') {
let cmd = ''; let cmd = '';
termEv = this.term.onData((e) => { termEv = this.term.onData((e) => {
@@ -173,7 +166,7 @@ class TerminalModal extends React.Component {
}); });
} }
this.ws = new WebSocket(`${getBaseURL()}?device=${this.props.device.id}&secret=${this.secret}`); this.ws = new WebSocket(`${getBaseURL(true)}?device=${this.props.device.id}&secret=${this.secret}`);
this.ws.binaryType = 'arraybuffer'; this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => { this.ws.onopen = () => {
this.conn = true; this.conn = true;
@@ -192,7 +185,6 @@ class TerminalModal extends React.Component {
data = data.substring(buffer.length); data = data.substring(buffer.length);
buffer = ''; buffer = '';
} }
return;
} }
this.term.write(data); this.term.write(data);
return; return;
@@ -263,6 +255,9 @@ class TerminalModal extends React.Component {
if (prevProps.visible) { if (prevProps.visible) {
clearInterval(this.ticker); clearInterval(this.ticker);
if (this.conn) { if (this.conn) {
this.sendData({
act: 'killTerminal'
});
this.ws.close(); this.ws.close();
} }
this?.termEv?.dispose(); this?.termEv?.dispose();

View File

@@ -27,18 +27,20 @@ axios.interceptors.response.use(async (res) => {
} }
return Promise.resolve(res); return Promise.resolve(res);
}, (err) => { }, (err) => {
console.error(err);
if (err.code === 'ECONNABORTED') { if (err.code === 'ECONNABORTED') {
message.warn(i18n.t('requestTimeout')); message.error(i18n.t('requestTimeout'));
return Promise.resolve(err); return Promise.reject(err);
} }
let res = err.response; let res = err.response;
let data = res.data; let data = res?.data ?? {};
if (data.hasOwnProperty('code')) { if (data.hasOwnProperty('code')) {
if (data.code !== 0){ if (data.code !== 0){
message.warn(translate(data.msg)); message.warn(translate(data.msg));
return Promise.resolve(res);
} }
} }
return Promise.resolve(res); return Promise.reject(err);
}); });
ReactDOM.render( ReactDOM.render(

View File

@@ -44,12 +44,22 @@
"modifyTime": "Modify Time", "modifyTime": "Modify Time",
"file": "file", "file": "file",
"folder": "folder", "folder": "folder",
"reload": "Reload",
"upload": "Upload",
"delete": "Delete", "delete": "Delete",
"download": "Download", "download": "Download",
"uploading": "Uploading...",
"uploadFailed": "Upload Failed",
"uploadAborted": "Upload Aborted",
"uploadSuccess": "Upload Success",
"uploadInvalidPath": "Cannot upload here",
"uploadCancelConfirm": "Are you sure to cancel uploading?",
"deleteConfirm": "Are you sure to delete this {0}?", "deleteConfirm": "Are you sure to delete this {0}?",
"deleteSuccess": "File or folder deleted", "deleteSuccess": "File or folder deleted",
"dateTimeFormat": "MMM D, YYYY h:mm A", "dateTimeFormat": "MMM D, YYYY h:mm A",
"fileOrDirNotExist": "File or folder does not exist", "fileOrDirNotExist": "File or folder does not exist",
"fileOverwriteConfirm": "File [ {0} ] already exists, overwrite?",
"fileOverwrite": "Overwrite",
"host": "Host", "host": "Host",
"port": "Port", "port": "Port",

View File

@@ -45,12 +45,22 @@
"modifyTime": "修改时间", "modifyTime": "修改时间",
"file": "文件", "file": "文件",
"folder": "文件夹", "folder": "文件夹",
"reload": "刷新",
"upload": "上传",
"delete": "删除", "delete": "删除",
"download": "下载", "download": "下载",
"uploading": "上传中...",
"uploadFailed": "上传失败",
"uploadAborted": "取消上传",
"uploadSuccess": "上传完成",
"uploadInvalidPath": "该路径无法上传文件",
"uploadCancelConfirm": "确定要取消上传吗?",
"deleteConfirm": "确定要删除该{0}吗?", "deleteConfirm": "确定要删除该{0}吗?",
"deleteSuccess": "文件或目录已被删除", "deleteSuccess": "文件或目录已被删除",
"dateTimeFormat": "YYYY/MM/DD HH:mm", "dateTimeFormat": "YYYY/MM/DD HH:mm",
"fileOrDirNotExist": "文件或目录不存在", "fileOrDirNotExist": "文件或目录不存在",
"fileOverwriteConfirm": "文件 [ {0} ] 已经存在,是否覆盖?",
"fileOverwrite": "覆盖",
"registryEditor": "注册表编辑器", "registryEditor": "注册表编辑器",
"unknownRegistryKey": "注册表键有误", "unknownRegistryKey": "注册表键有误",

View File

@@ -24,11 +24,13 @@ function waitTime(time) {
}; };
function formatSize(size) { function formatSize(size) {
if (size === 0) return '0 B'; size = isNaN(size) ? 0 : (size??0);
size = Math.max(size, 0);
let k = 1024, let k = 1024,
i = Math.floor(Math.log(size) / Math.log(k)), i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(k)),
units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
return (size / Math.pow(k, i)).toFixed(2) + ' ' + units[i]; result = size / Math.pow(k, i);
return (Math.round(result * 100) / 100) + ' ' + units[i];
} }
function tsToTime(ts) { function tsToTime(ts) {
@@ -39,6 +41,15 @@ function tsToTime(ts) {
return `${String(hours) + i18n.t('hours') + ' ' + String(minutes) + i18n.t('minutes')}`; return `${String(hours) + i18n.t('hours') + ' ' + String(minutes) + i18n.t('minutes')}`;
} }
function getBaseURL(ws) {
if (location.protocol === 'https:') {
let scheme = ws ? 'wss' : 'https';
return scheme + `://${location.host}${location.pathname}api/device/terminal`;
}
let scheme = ws ? 'ws' : 'http';
return scheme + `://${location.host}${location.pathname}api/device/terminal`;
}
function post(url, data, ext) { function post(url, data, ext) {
let form = document.createElement('form'); let form = document.createElement('form');
form.action = url; form.action = url;
@@ -65,4 +76,4 @@ function translate(text) {
}); });
} }
export {post, request, waitTime, formatSize, tsToTime, translate}; export {post, request, waitTime, formatSize, tsToTime, getBaseURL, translate};