add: file upload
optimize: project structure
98
.github/workflows/build.yml
vendored
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build-macOS-servers:
|
|
||||||
needs: [ build-others ]
|
|
||||||
runs-on: macos-latest
|
|
||||||
|
|
||||||
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
|
- name: Clean up
|
||||||
uses: geekyeggo/delete-artifact@v1
|
uses: geekyeggo/delete-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: |
|
name: |
|
||||||
embed.zip
|
|
||||||
darwin_arm64
|
darwin_arm64
|
||||||
darwin_amd64
|
darwin_amd64
|
||||||
|
73
API.ZH.md
@@ -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
@@ -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)
|
||||||
|
10
CHANGELOG.md
@@ -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.
|
||||||
|
26
README.ZH.md
@@ -66,6 +66,20 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 截图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## **开发**
|
## **开发**
|
||||||
|
|
||||||
### 注意
|
### 注意
|
||||||
@@ -121,18 +135,6 @@ $ ./build.server.sh
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 截图
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 项目依赖
|
## 项目依赖
|
||||||
|
|
||||||
Spark使用了许多第三方的开源项目。
|
Spark使用了许多第三方的开源项目。
|
||||||
|
28
README.md
@@ -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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## **Development**
|
## **Development**
|
||||||
|
|
||||||
### note
|
### note
|
||||||
@@ -123,18 +137,6 @@ Copy configuration file mentioned above into this dir, and then you can execute
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Spark contains many third-party open-source projects.
|
Spark contains many third-party open-source projects.
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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.
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -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 = `/`
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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`)
|
|
||||||
}
|
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 43 KiB |
BIN
screenshots/overview.cpu.ZH.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
screenshots/overview.cpu.png
Normal file
After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 36 KiB |
38
scripts/build.server.bat
Normal 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
|
@@ -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
|
@@ -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,11 +80,9 @@ 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)
|
||||||
}
|
}
|
||||||
for range time.NewTicker(3 * time.Second).C {
|
for range time.NewTicker(3 * time.Second).C {
|
||||||
@@ -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) {
|
||||||
|
@@ -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
@@ -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
|
||||||
|
}
|
@@ -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,39 +112,29 @@ 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(`Accept-Ranges`, `bytes`)
|
||||||
ctx.Header(`Content-Transfer-Encoding`, `binary`)
|
ctx.Header(`Content-Transfer-Encoding`, `binary`)
|
||||||
ctx.Header(`Content-Type`, `application/octet-stream`)
|
ctx.Header(`Content-Type`, `application/octet-stream`)
|
||||||
filename := ctx.GetHeader(`FileName`)
|
filename := src.GetHeader(`FileName`)
|
||||||
if len(filename) == 0 {
|
if len(filename) == 0 {
|
||||||
filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
|
filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
|
||||||
}
|
}
|
||||||
@@ -153,35 +143,26 @@ func getDeviceFile(ctx *gin.Context) {
|
|||||||
|
|
||||||
if partial {
|
if partial {
|
||||||
if rangeEnd == 0 {
|
if rangeEnd == 0 {
|
||||||
rangeEnd, err = strconv.ParseInt(req.Header.Get(`FileSize`), 10, 64)
|
rangeEnd, err = strconv.ParseInt(src.GetHeader(`FileSize`), 10, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%d`, rangeStart, rangeEnd-1, rangeEnd))
|
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%d`, rangeStart, rangeEnd-1, rangeEnd))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%v`, rangeStart, rangeEnd, req.Header.Get(`FileSize`)))
|
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%v`, rangeStart, rangeEnd, src.GetHeader(`FileSize`)))
|
||||||
}
|
}
|
||||||
ctx.Status(http.StatusPartialContent)
|
ctx.Status(http.StatusPartialContent)
|
||||||
} else {
|
} else {
|
||||||
ctx.Status(http.StatusOK)
|
ctx.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for {
|
instance.OnFinish = func(bridge *bridge) {
|
||||||
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
|
wait <- false
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}, target, trigger)
|
|
||||||
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
|
||||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
removeBridge(bridgeID)
|
||||||
return
|
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))
|
||||||
}
|
}
|
||||||
common.CallEvent(modules.Packet{
|
dest.Header(`Accept-Ranges`, `none`)
|
||||||
Code: 0,
|
dest.Header(`Content-Transfer-Encoding`, `binary`)
|
||||||
Data: map[string]interface{}{
|
dest.Header(`Content-Type`, `application/octet-stream`)
|
||||||
`request`: ctx.Request,
|
filename := form.File
|
||||||
},
|
filename = url.PathEscape(filename)
|
||||||
Event: trigger,
|
dest.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
|
||||||
}, nil)
|
}
|
||||||
original.Close()
|
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})
|
||||||
|
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})
|
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -3,22 +3,14 @@ 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)
|
||||||
@@ -27,6 +19,7 @@ func APIRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
|
|||||||
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)
|
|
||||||
}
|
|
||||||
|
@@ -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})
|
||||||
|
@@ -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)
|
||||||
|
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})
|
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||||
} else {
|
}, target, trigger)
|
||||||
data, ok := p.Data[`screenshot`]
|
instance := addBridgeWithDest(nil, bridgeID, ctx)
|
||||||
if !ok {
|
instance.OnPush = func(bridge *bridge) {
|
||||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|screenshotObtainFailed}`})
|
called = true
|
||||||
return
|
common.RemoveEvent(trigger)
|
||||||
|
ctx.Header(`Content-Type`, `image/png`)
|
||||||
}
|
}
|
||||||
screenshot, ok := data.([]byte)
|
instance.OnFinish = func(bridge *bridge) {
|
||||||
if !ok {
|
wait <- false
|
||||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|screenshotObtainFailed}`})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ctx.Data(200, `image/png`, screenshot)
|
select {
|
||||||
}
|
case <-wait:
|
||||||
}, target, trigger, 5*time.Second)
|
case <-time.After(5 * time.Second):
|
||||||
if !ok {
|
if !called {
|
||||||
|
removeBridge(bridgeID)
|
||||||
|
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 {
|
||||||
|
<-wait
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
@@ -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})
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
auth := gin.BasicAuth(config.Config.Auth)
|
||||||
handler.APIRouter(app.Group(`/api`), auth)
|
handler.InitRouter(app.Group(`/api`), auth)
|
||||||
app.Any(`/ws`, wsHandshake)
|
app.Any(`/ws`, wsHandshake)
|
||||||
app.NoRoute(auth, func(ctx *gin.Context) {
|
app.NoRoute(auth, func(ctx *gin.Context) {
|
||||||
http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request)
|
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
|
|
||||||
}
|
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -7,3 +7,7 @@
|
|||||||
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;
|
||||||
|
}
|
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
|
@@ -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(
|
||||||
|
@@ -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",
|
||||||
|
@@ -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": "注册表键有误",
|
||||||
|
@@ -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};
|