add: file upload
optimize: project structure
98
.github/workflows/build.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- '!v*.*.*'
|
||||
|
||||
jobs:
|
||||
build-macOS-clients:
|
||||
build-clients-macOS:
|
||||
runs-on: macos-latest
|
||||
|
||||
strategy:
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
|
||||
build-others:
|
||||
needs: [ build-macOS-clients ]
|
||||
needs: [ build-clients-macOS ]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
@@ -77,29 +77,18 @@ jobs:
|
||||
export PATH=$PATH:~/go/bin/
|
||||
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
|
||||
with:
|
||||
name: darwin_arm64
|
||||
path: ./built
|
||||
|
||||
- name: Get artifacts from previous job (amd64)
|
||||
- name: Get artifact from previous job (darwin_amd64)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: darwin_amd64
|
||||
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
|
||||
run: |
|
||||
cd ./web
|
||||
@@ -107,17 +96,23 @@ jobs:
|
||||
npm run build-prod
|
||||
statik -m -src="./dist" -f -dest="../server/embed" -p web -ns web
|
||||
cd ..
|
||||
zip -q -r ./embed.zip ./server/embed
|
||||
|
||||
- name: Build server
|
||||
- name: Set up go dependencies
|
||||
run: |
|
||||
chmod +x ./build.server.sh
|
||||
export GOMOD=`pwd`/go.mod
|
||||
export CGO_ENABLED=0
|
||||
go mod tidy
|
||||
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
|
||||
./build.server.sh
|
||||
./scripts/build.server.sh
|
||||
|
||||
- name: Prepare release note
|
||||
run: |
|
||||
@@ -128,6 +123,8 @@ jobs:
|
||||
run: |
|
||||
cd ./releases
|
||||
sudo apt install zip tar -y
|
||||
tar -zcvf server_darwin_arm64.tar.gz server_darwin_arm64
|
||||
tar -zcvf server_darwin_amd64.tar.gz server_darwin_amd64
|
||||
tar -zcvf server_linux_arm.tar.gz server_linux_arm
|
||||
tar -zcvf server_linux_arm64.tar.gz server_linux_arm64
|
||||
tar -zcvf server_linux_i386.tar.gz server_linux_i386
|
||||
@@ -137,17 +134,13 @@ jobs:
|
||||
zip -r server_windows_i386.zip server_windows_i386.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
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: CHANGELOG.md
|
||||
files: |
|
||||
releases/server_darwin_arm64.tar.gz
|
||||
releases/server_darwin_amd64.tar.gz
|
||||
releases/server_linux_arm.tar.gz
|
||||
releases/server_linux_arm64.tar.gz
|
||||
releases/server_linux_i386.tar.gz
|
||||
@@ -157,64 +150,9 @@ jobs:
|
||||
releases/server_windows_i386.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
|
||||
uses: geekyeggo/delete-artifact@v1
|
||||
with:
|
||||
name: |
|
||||
embed.zip
|
||||
darwin_arm64
|
||||
darwin_amd64
|
||||
|
73
API.ZH.md
@@ -5,21 +5,25 @@
|
||||
## 通用
|
||||
|
||||
所有请求均为`POST`。
|
||||
|
||||
<br />
|
||||
每次请求都必须在Header中带上`Authorization`。
|
||||
|
||||
<br />
|
||||
`Authorization`请求头格式:`Basic <token>`(basic auth)。
|
||||
|
||||
```
|
||||
Authorization: Basic <base64('username:password')>
|
||||
```
|
||||
例如:
|
||||
```
|
||||
Authorization: Basic WFpCOjEyNDg=
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 响应
|
||||
|
||||
所有响应均是JSON格式。
|
||||
|
||||
<br />
|
||||
`code` 有三种结果,分别为`-1`,`0`和`1`,含义如下。
|
||||
|
||||
| code | meaning |
|
||||
@@ -55,9 +59,13 @@ Authorization: Basic <base64('username:password')>
|
||||
|
||||
参数:**无**
|
||||
|
||||
设备的`id`是一串64位的字符串,每台设备独一无二,一般不会变化。识别设备主要靠这个。下文中提到的设备ID也指的是这个。
|
||||
|
||||
每个device对象所对应的key,是它的本次连接的连接ID,这个ID是随机、临时的,每次重连就会变化,不建议使用。
|
||||
设备的`id`是一串64位的字符串,每台设备独一无二,一般不会变化。
|
||||
<br />
|
||||
识别设备主要靠这个。下文中提到的设备ID也指的是这个。
|
||||
<br />
|
||||
每个device对象所对应的key,是它的本次连接的连接ID。
|
||||
<br />
|
||||
连接ID是随机、临时的,每次重连就会变化,不建议使用。
|
||||
|
||||
```
|
||||
{
|
||||
@@ -123,7 +131,9 @@ Authorization: Basic <base64('username:password')>
|
||||
|
||||
参数:`device`(设备ID)
|
||||
|
||||
如果截屏获取成功,则会直接以图片的形式输出。如果截屏失败,如下响应会被输出(错误信息不止这一个)。
|
||||
如果截屏获取成功,则会直接以图片的形式输出。
|
||||
<br />
|
||||
如果截屏失败,如下响应会被输出(错误信息不一定是这一个)。
|
||||
|
||||
```
|
||||
{
|
||||
@@ -138,7 +148,9 @@ Authorization: Basic <base64('username:password')>
|
||||
|
||||
参数:`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`
|
||||
|
||||
参数:`path`(父目录路径) 以及 `device`(设备ID)
|
||||
|
||||
如果`path`为空,windows下会给出磁盘列表,其它系统会默认输出`/`目录下的文件和目录。
|
||||
如果`path`为空,windows下会给出磁盘列表。
|
||||
<br />
|
||||
其它系统会默认输出`/`目录下的文件和目录。
|
||||
|
||||
`type`有三种结果:`0`代表文件,`1`代表目录,`2`代表磁盘(windows)。
|
||||
|
||||
|
57
API.md
@@ -5,14 +5,18 @@
|
||||
## Common
|
||||
|
||||
Only `POST` requests are allowed.
|
||||
|
||||
<br />
|
||||
For every request, you should have `Authorization` on its header.
|
||||
|
||||
<br />
|
||||
Authorization header is a string like `Basic <token>`(basic auth).
|
||||
|
||||
```
|
||||
Authorization: Basic <base64('username:password')>
|
||||
```
|
||||
Example:
|
||||
```
|
||||
Authorization: Basic WFpCOjEyNDg=
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -53,8 +57,12 @@ All responses are JSON encoded.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
@@ -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`
|
||||
|
||||
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
|
||||
|
||||
* 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使用了许多第三方的开源项目。
|
||||
|
28
README.md
@@ -47,7 +47,7 @@ Only local installation are available yet.
|
||||
|-----------------|---------|-------|-------|
|
||||
| Process manager | ✔ | ✔ | ✔ |
|
||||
| Kill process | ✔ | ✔ | ✔ |
|
||||
| Network Traffic | ✔ | ✔ | ✔ |
|
||||
| Network traffic | ✔ | ✔ | ✔ |
|
||||
| File explorer | ✔ | ✔ | ✔ |
|
||||
| File transfer | ✔ | ✔ | ✔ |
|
||||
| Delete file | ✔ | ✔ | ✔ |
|
||||
@@ -66,6 +66,20 @@ Only local installation are available yet.
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
Spark contains many third-party open-source projects.
|
||||
|
@@ -48,6 +48,11 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
update()
|
||||
core.Start()
|
||||
}
|
||||
|
||||
func update() {
|
||||
if len(os.Args) > 1 && os.Args[1] == `--update` {
|
||||
thisPath := os.Args[0]
|
||||
destPath := thisPath[:len(thisPath)-4]
|
||||
@@ -69,7 +74,6 @@ func main() {
|
||||
<-time.After(time.Second)
|
||||
os.Remove(os.Args[0] + `.tmp`)
|
||||
}
|
||||
core.Start()
|
||||
}
|
||||
|
||||
func decrypt(data []byte, key []byte) ([]byte, error) {
|
||||
|
@@ -14,10 +14,11 @@ type Cfg struct {
|
||||
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"
|
||||
|
||||
// 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"
|
||||
|
||||
// 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,
|
||||
`killTerminal`: killTerminal,
|
||||
`listFiles`: listFiles,
|
||||
`fetchFile`: fetchFile,
|
||||
`removeFile`: removeFile,
|
||||
`uploadFile`: uploadFile,
|
||||
`listProcesses`: listProcesses,
|
||||
@@ -54,14 +55,14 @@ func Start() {
|
||||
}
|
||||
common.WSConn, err = connectWS()
|
||||
if err != nil && !stop {
|
||||
golog.Error(err)
|
||||
golog.Error(`Connection error: `, err)
|
||||
<-time.After(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
err = reportWS(common.WSConn)
|
||||
if err != nil && !stop {
|
||||
golog.Error(err)
|
||||
golog.Error(`Register error: `, err)
|
||||
<-time.After(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
@@ -72,7 +73,7 @@ func Start() {
|
||||
|
||||
err = handleWS(common.WSConn)
|
||||
if err != nil && !stop {
|
||||
golog.Error(err)
|
||||
golog.Error(`Execution error: `, err)
|
||||
<-time.After(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
@@ -103,7 +104,7 @@ func reportWS(wsConn *common.Conn) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack := modules.CommonPack{Act: `report`, Data: device}
|
||||
pack := modules.CommonPack{Act: `report`, Data: *device}
|
||||
err = common.SendPack(pack, wsConn)
|
||||
common.WSConn.SetWriteDeadline(time.Time{})
|
||||
if err != nil {
|
||||
@@ -226,7 +227,7 @@ func heartbeat(wsConn *common.Conn) error {
|
||||
if t >= 20 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@@ -245,7 +245,7 @@ func GetDevice() (*modules.Device, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetPartialInfo(getDisk bool) (modules.Device, error) {
|
||||
func GetPartialInfo(getDisk bool) (*modules.Device, error) {
|
||||
cpuInfo, err := GetCPUInfo()
|
||||
if err != nil {
|
||||
cpuInfo = modules.CPU{
|
||||
@@ -280,7 +280,7 @@ func GetPartialInfo(getDisk bool) (modules.Device, error) {
|
||||
if err != nil {
|
||||
uptime = 0
|
||||
}
|
||||
return modules.Device{
|
||||
return &modules.Device{
|
||||
Net: netInfo,
|
||||
CPU: cpuInfo,
|
||||
RAM: memInfo,
|
||||
|
@@ -13,41 +13,6 @@ import (
|
||||
"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) {
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
stop = true
|
||||
@@ -110,8 +75,16 @@ func shutdown(pack modules.Packet, wsConn *common.Conn) {
|
||||
}
|
||||
|
||||
func screenshot(pack modules.Packet, wsConn *common.Conn) {
|
||||
if len(pack.Event) > 0 {
|
||||
Screenshot.GetScreenshot(pack.Event)
|
||||
var bridge 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 := 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) {
|
||||
path := `/`
|
||||
if val, ok := getPackData(pack, `path`, reflect.String); ok {
|
||||
if val, ok := pack.GetData(`path`, reflect.String); ok {
|
||||
path = val.(string)
|
||||
}
|
||||
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) {
|
||||
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)
|
||||
return
|
||||
} else {
|
||||
@@ -165,18 +164,24 @@ func removeFile(pack modules.Packet, wsConn *common.Conn) {
|
||||
|
||||
func uploadFile(pack modules.Packet, wsConn *common.Conn) {
|
||||
var start, end int64
|
||||
var path string
|
||||
if val, ok := getPackData(pack, `file`, reflect.String); !ok {
|
||||
var path, bridge string
|
||||
if val, ok := pack.GetData(`file`, reflect.String); !ok {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `${i18n|fileOrDirNotExist}`}, pack, wsConn)
|
||||
return
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
if val, ok := getPackData(pack, `end`, reflect.Float64); ok {
|
||||
if val, ok := pack.GetData(`end`, reflect.Float64); ok {
|
||||
end = int64(val.(float64))
|
||||
if end > 0 {
|
||||
end++
|
||||
@@ -187,7 +192,7 @@ func uploadFile(pack modules.Packet, wsConn *common.Conn) {
|
||||
return
|
||||
}
|
||||
}
|
||||
err := file.UploadFile(path, pack.Event, start, end)
|
||||
err := file.UploadFile(path, bridge, start, end)
|
||||
if err != nil {
|
||||
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
|
||||
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)
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
return
|
||||
|
@@ -6,12 +6,13 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
type File struct {
|
||||
Name string `json:"name"`
|
||||
Size uint64 `json:"size"`
|
||||
Time int64 `json:"time"`
|
||||
@@ -19,8 +20,8 @@ type file struct {
|
||||
}
|
||||
|
||||
// listFiles returns files and directories find in path.
|
||||
func listFiles(path string) ([]file, error) {
|
||||
result := make([]file, 0)
|
||||
func listFiles(path string) ([]File, error) {
|
||||
result := make([]File, 0)
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -30,7 +31,7 @@ func listFiles(path string) ([]file, error) {
|
||||
if files[i].IsDir() {
|
||||
itemType = 1
|
||||
}
|
||||
result = append(result, file{
|
||||
result = append(result, File{
|
||||
Name: files[i].Name(),
|
||||
Size: uint64(files[i].Size()),
|
||||
Time: files[i].ModTime().Unix(),
|
||||
@@ -40,6 +41,62 @@ func listFiles(path string) ([]file, error) {
|
||||
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 {
|
||||
if path == `\` || path == `/` || len(path) == 0 {
|
||||
return errors.New(`${i18n|fileOrDirNotExist}`)
|
||||
@@ -51,7 +108,7 @@ func RemoveFile(path string) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -65,7 +122,6 @@ func UploadFile(path, trigger string, start, end int64) error {
|
||||
}
|
||||
size := stat.Size()
|
||||
headers := map[string]string{
|
||||
`Trigger`: trigger,
|
||||
`FileName`: stat.Name(),
|
||||
`FileSize`: strconv.FormatInt(size, 10),
|
||||
}
|
||||
@@ -96,8 +152,25 @@ func UploadFile(path, trigger string, start, end int64) error {
|
||||
}
|
||||
writer.Close()
|
||||
}()
|
||||
url := config.GetBaseURL(false) + `/api/device/file/put`
|
||||
_, err = uploadReq.SetBody(reader).SetHeaders(headers).Send(`PUT`, url)
|
||||
url := config.GetBaseURL(false) + `/api/bridge/push`
|
||||
_, err = uploadReq.
|
||||
SetBody(reader).
|
||||
SetHeaders(headers).
|
||||
SetQueryParam(`bridge`, bridge).
|
||||
Send(`PUT`, url)
|
||||
reader.Close()
|
||||
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
|
||||
|
||||
package file
|
||||
|
||||
func ListFiles(path string) ([]file, error) {
|
||||
func ListFiles(path string) ([]File, error) {
|
||||
if len(path) == 0 {
|
||||
path = `/`
|
||||
}
|
||||
|
@@ -8,8 +8,8 @@ import "github.com/shirou/gopsutil/v3/disk"
|
||||
// ListFiles will only be called when path is root and
|
||||
// current system is Windows.
|
||||
// It will return mount points of all volumes.
|
||||
func ListFiles(path string) ([]file, error) {
|
||||
result := make([]file, 0)
|
||||
func ListFiles(path string) ([]File, error) {
|
||||
result := make([]File, 0)
|
||||
if len(path) == 0 || path == `\` || path == `/` {
|
||||
partitions, err := disk.Partitions(true)
|
||||
if err != nil {
|
||||
@@ -23,7 +23,7 @@ func ListFiles(path string) ([]file, error) {
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
import (
|
||||
"Spark/client/config"
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/kbinani/screenshot"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
func GetScreenshot(trigger string) error {
|
||||
func GetScreenshot(bridge string) error {
|
||||
writer := new(bytes.Buffer)
|
||||
num := screenshot.NumActiveDisplays()
|
||||
if num == 0 {
|
||||
err := errors.New(`${i18n|noDisplayFound}`)
|
||||
putScreenshot(trigger, err.Error(), nil)
|
||||
return err
|
||||
}
|
||||
img, err := screenshot.CaptureDisplay(0)
|
||||
if err != nil {
|
||||
putScreenshot(trigger, err.Error(), nil)
|
||||
return err
|
||||
}
|
||||
err = png.Encode(writer, img)
|
||||
if err != nil {
|
||||
putScreenshot(trigger, err.Error(), nil)
|
||||
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
|
||||
}
|
||||
|
@@ -2,9 +2,6 @@
|
||||
|
||||
package screenshot
|
||||
|
||||
import "Spark/utils"
|
||||
|
||||
func GetScreenshot(trigger string) error {
|
||||
_, err := putScreenshot(trigger, utils.ErrUnsupported.Error(), nil)
|
||||
return err
|
||||
func GetScreenshot(bridge string) error {
|
||||
return utils.ErrUnsupported
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/creack/pty"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -55,86 +56,54 @@ func InitTerminal(pack modules.Packet) error {
|
||||
}
|
||||
|
||||
func InputTerminal(pack modules.Packet) error {
|
||||
if pack.Data == nil {
|
||||
return errDataNotFound
|
||||
}
|
||||
val, ok := pack.Data[`input`]
|
||||
val, ok := pack.GetData(`input`, reflect.String)
|
||||
if !ok {
|
||||
return errDataNotFound
|
||||
}
|
||||
hexStr, ok := val.(string)
|
||||
if !ok {
|
||||
return errDataNotFound
|
||||
}
|
||||
data, err := hex.DecodeString(hexStr)
|
||||
data, err := hex.DecodeString(val.(string))
|
||||
if err != nil {
|
||||
return errDataInvalid
|
||||
}
|
||||
|
||||
val, ok = pack.Data[`terminal`]
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID, ok := val.(string)
|
||||
val, ok = pack.GetData(`terminal`, reflect.String)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID := val.(string)
|
||||
val, ok = terminals.Get(termUUID)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
|
||||
terminal := val.(*terminal)
|
||||
terminal.lastPack = time.Now().Unix()
|
||||
terminal.pty.Write(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResizeTerminal(pack modules.Packet) error {
|
||||
if pack.Data == nil {
|
||||
return errDataNotFound
|
||||
}
|
||||
val, ok := pack.Data[`width`]
|
||||
val, ok := pack.GetData(`width`, reflect.Float64)
|
||||
if !ok {
|
||||
return errDataInvalid
|
||||
}
|
||||
width, ok := val.(float64)
|
||||
if !ok {
|
||||
return errDataInvalid
|
||||
}
|
||||
val, ok = pack.Data[`height`]
|
||||
if !ok {
|
||||
return errDataInvalid
|
||||
}
|
||||
height, ok := val.(float64)
|
||||
width := val.(float64)
|
||||
val, ok = pack.GetData(`height`, reflect.Float64)
|
||||
if !ok {
|
||||
return errDataInvalid
|
||||
}
|
||||
height := val.(float64)
|
||||
|
||||
val, ok = pack.Data[`terminal`]
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID, ok := val.(string)
|
||||
val, ok = pack.GetData(`terminal`, reflect.String)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID := val.(string)
|
||||
val, ok = terminals.Get(termUUID)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
|
||||
terminal := val.(*terminal)
|
||||
pty.Setsize(terminal.pty, &pty.Winsize{
|
||||
Rows: uint16(height),
|
||||
Cols: uint16(width),
|
||||
@@ -143,28 +112,17 @@ func ResizeTerminal(pack modules.Packet) error {
|
||||
}
|
||||
|
||||
func KillTerminal(pack modules.Packet) error {
|
||||
if pack.Data == nil {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
val, ok := pack.Data[`terminal`]
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID, ok := val.(string)
|
||||
val, ok := pack.GetData(`terminal`, reflect.String)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID := val.(string)
|
||||
val, ok = terminals.Get(termUUID)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
terminals.Remove(termUUID)
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
terminal := val.(*terminal)
|
||||
doKillTerminal(terminal)
|
||||
return nil
|
||||
}
|
||||
@@ -187,17 +145,13 @@ func getTerminal() string {
|
||||
}
|
||||
|
||||
func healthCheck() {
|
||||
const MaxInterval = 180
|
||||
const MaxInterval = 300
|
||||
for now := range time.NewTicker(30 * time.Second).C {
|
||||
timestamp := now.Unix()
|
||||
// stores sessions to be disconnected
|
||||
queue := make([]string, 0)
|
||||
terminals.IterCb(func(uuid string, t interface{}) bool {
|
||||
termSession, ok := t.(*terminal)
|
||||
if !ok {
|
||||
queue = append(queue, uuid)
|
||||
return true
|
||||
}
|
||||
termSession := t.(*terminal)
|
||||
if timestamp-termSession.lastPack > MaxInterval {
|
||||
queue = append(queue, uuid)
|
||||
doKillTerminal(termSession)
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
@@ -86,41 +87,26 @@ func InitTerminal(pack modules.Packet) error {
|
||||
}
|
||||
|
||||
func InputTerminal(pack modules.Packet) error {
|
||||
if pack.Data == nil {
|
||||
return errDataNotFound
|
||||
}
|
||||
val, ok := pack.Data[`input`]
|
||||
val, ok := pack.GetData(`input`, reflect.String)
|
||||
if !ok {
|
||||
return errDataNotFound
|
||||
}
|
||||
hexStr, ok := val.(string)
|
||||
if !ok {
|
||||
return errDataNotFound
|
||||
}
|
||||
data, err := hex.DecodeString(hexStr)
|
||||
data, err := hex.DecodeString(val.(string))
|
||||
if err != nil {
|
||||
return errDataInvalid
|
||||
}
|
||||
|
||||
val, ok = pack.Data[`terminal`]
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID, ok := val.(string)
|
||||
val, ok = pack.GetData(`terminal`, reflect.String)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID := val.(string)
|
||||
val, ok = terminals.Get(termUUID)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
|
||||
terminal := val.(*terminal)
|
||||
terminal.lastPack = time.Now().Unix()
|
||||
if len(data) == 1 && data[0] == '\x03' {
|
||||
terminal.cmd.Process.Signal(os.Interrupt)
|
||||
@@ -136,28 +122,17 @@ func ResizeTerminal(pack modules.Packet) error {
|
||||
}
|
||||
|
||||
func KillTerminal(pack modules.Packet) error {
|
||||
if pack.Data == nil {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
val, ok := pack.Data[`terminal`]
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID, ok := val.(string)
|
||||
val, ok := pack.GetData(`terminal`, reflect.String)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID := val.(string)
|
||||
val, ok = terminals.Get(termUUID)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
terminals.Remove(termUUID)
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `${i18n|terminalSessionClosed}`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
terminal := val.(*terminal)
|
||||
doKillTerminal(terminal)
|
||||
return nil
|
||||
}
|
||||
@@ -210,17 +185,13 @@ func utf8ToGbk(s []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func healthCheck() {
|
||||
const MaxInterval = 180
|
||||
const MaxInterval = 300
|
||||
for now := range time.NewTicker(30 * time.Second).C {
|
||||
timestamp := now.Unix()
|
||||
// stores sessions to be disconnected
|
||||
queue := make([]string, 0)
|
||||
terminals.IterCb(func(uuid string, t interface{}) bool {
|
||||
termSession, ok := t.(*terminal)
|
||||
if !ok {
|
||||
queue = append(queue, uuid)
|
||||
return true
|
||||
}
|
||||
termSession := t.(*terminal)
|
||||
if timestamp-termSession.lastPack > MaxInterval {
|
||||
queue = append(queue, uuid)
|
||||
doKillTerminal(termSession)
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package modules
|
||||
|
||||
import "reflect"
|
||||
|
||||
type Packet struct {
|
||||
Code int `json:"code"`
|
||||
Act string `json:"act,omitempty"`
|
||||
@@ -52,3 +54,41 @@ type Net struct {
|
||||
Sent uint64 `json:"sent"`
|
||||
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
|
||||
export GOARCH=amd64
|
||||
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/server
|
||||
|
||||
|
||||
|
||||
export GOOS=darwin
|
||||
|
||||
export GOARCH=arm64
|
||||
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
|
||||
export GOARCH=amd64
|
||||
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
|
@@ -10,7 +10,9 @@ import (
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,7 +20,7 @@ var Melody = melody.New()
|
||||
var Devices = cmap.New()
|
||||
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)
|
||||
if !ok {
|
||||
return false
|
||||
@@ -68,8 +70,7 @@ func Decrypt(data []byte, session *melody.Session) ([]byte, bool) {
|
||||
return dec, true
|
||||
}
|
||||
|
||||
func WSHealthCheck(container *melody.Melody) {
|
||||
const MaxInterval = 90
|
||||
func HealthCheckWS(maxIdleSeconds int64, container *melody.Melody) {
|
||||
go func() {
|
||||
// ping client and update latency every 3 seconds
|
||||
ping := func(uuid string, s *melody.Session) {
|
||||
@@ -79,11 +80,9 @@ func WSHealthCheck(container *melody.Melody) {
|
||||
AddEventOnce(func(packet modules.Packet, session *melody.Session) {
|
||||
val, ok := Devices.Get(uuid)
|
||||
if ok {
|
||||
deviceInfo, ok := val.(*modules.Device)
|
||||
if ok {
|
||||
deviceInfo := val.(*modules.Device)
|
||||
deviceInfo.Latency = uint(time.Now().UnixMilli()-t) / 2
|
||||
}
|
||||
}
|
||||
}, uuid, trigger, 3*time.Second)
|
||||
}
|
||||
for range time.NewTicker(3 * time.Second).C {
|
||||
@@ -108,7 +107,7 @@ func WSHealthCheck(container *melody.Melody) {
|
||||
queue = append(queue, s)
|
||||
return true
|
||||
}
|
||||
if timestamp-lastPack > MaxInterval {
|
||||
if timestamp-lastPack > maxIdleSeconds {
|
||||
queue = append(queue, s)
|
||||
}
|
||||
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`))
|
||||
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 {
|
||||
if val, ok := s.Get(`Secret`); ok {
|
||||
// Check if there's a connection matches this secret.
|
||||
if b, ok := val.([]byte); ok && bytes.Equal(b, secret) {
|
||||
find = true
|
||||
if cb != nil {
|
||||
cb(s)
|
||||
}
|
||||
result = s
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return find
|
||||
return result
|
||||
}
|
||||
|
||||
func CheckDevice(deviceID, connUUID string) (string, bool) {
|
||||
|
@@ -7,14 +7,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type EventCallback func(modules.Packet, *melody.Session)
|
||||
type event struct {
|
||||
connection string
|
||||
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
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
v, ok := eventTable.Get(pack.Event)
|
||||
v, ok := events.Get(pack.Event)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -31,12 +32,8 @@ func CallEvent(pack modules.Packet, session *melody.Session) {
|
||||
return
|
||||
}
|
||||
ev.callback(pack, session)
|
||||
if ev.channel != nil {
|
||||
defer close(ev.channel)
|
||||
select {
|
||||
case ev.channel <- true:
|
||||
default:
|
||||
}
|
||||
if ev.finish != nil {
|
||||
ev.finish <- true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,17 +41,21 @@ func CallEvent(pack modules.Packet, session *melody.Session) {
|
||||
// can call back the event with the given event trigger.
|
||||
// Event trigger should be uuid to make every event unique.
|
||||
func AddEventOnce(fn EventCallback, connUUID, trigger string, timeout time.Duration) bool {
|
||||
done := make(chan bool)
|
||||
ev := &event{
|
||||
connection: connUUID,
|
||||
callback: fn,
|
||||
channel: done,
|
||||
finish: make(chan bool),
|
||||
remove: make(chan bool),
|
||||
}
|
||||
eventTable.Set(trigger, ev)
|
||||
defer eventTable.Remove(trigger)
|
||||
events.Set(trigger, ev)
|
||||
defer events.Remove(trigger)
|
||||
defer close(ev.finish)
|
||||
defer close(ev.remove)
|
||||
select {
|
||||
case <-done:
|
||||
return true
|
||||
case ok := <-ev.finish:
|
||||
return ok
|
||||
case ok := <-ev.remove:
|
||||
return ok
|
||||
case <-time.After(timeout):
|
||||
return false
|
||||
}
|
||||
@@ -66,17 +67,26 @@ func AddEvent(fn EventCallback, connUUID, trigger string) {
|
||||
ev := &event{
|
||||
connection: connUUID,
|
||||
callback: fn,
|
||||
channel: nil,
|
||||
}
|
||||
eventTable.Set(trigger, ev)
|
||||
events.Set(trigger, ev)
|
||||
}
|
||||
|
||||
// RemoveEvent deletes the event with the given event trigger.
|
||||
func RemoveEvent(trigger string) {
|
||||
eventTable.Remove(trigger)
|
||||
// The ok will be returned to caller if the event is temp (only once).
|
||||
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.
|
||||
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"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -27,7 +26,7 @@ func removeDeviceFile(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
@@ -50,7 +49,7 @@ func listDeviceFiles(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
@@ -73,12 +72,13 @@ func getDeviceFile(ctx *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
bridgeID := utils.GetStrUUID()
|
||||
trigger := utils.GetStrUUID()
|
||||
var rangeStart, rangeEnd int64
|
||||
var err error
|
||||
partial := false
|
||||
{
|
||||
command := gin.H{`file`: form.File}
|
||||
command := gin.H{`file`: form.File, `bridge`: bridgeID}
|
||||
rangeHeader := ctx.GetHeader(`Range`)
|
||||
if len(rangeHeader) > 6 {
|
||||
if rangeHeader[:6] != `bytes=` {
|
||||
@@ -112,39 +112,29 @@ func getDeviceFile(ctx *gin.Context) {
|
||||
command[`start`] = rangeStart
|
||||
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)
|
||||
called := false
|
||||
common.AddEvent(func(p modules.Packet, _ *melody.Session) {
|
||||
wait <- false
|
||||
called = true
|
||||
removeBridge(bridgeID)
|
||||
common.RemoveEvent(trigger)
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
}, target, trigger)
|
||||
instance := addBridgeWithDest(nil, bridgeID, ctx)
|
||||
instance.OnPush = func(bridge *bridge) {
|
||||
called = true
|
||||
common.RemoveEvent(trigger)
|
||||
if p.Code != 0 {
|
||||
wait <- false
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
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))
|
||||
src := bridge.src
|
||||
if src.Request.ContentLength > 0 {
|
||||
ctx.Header(`Content-Length`, strconv.FormatInt(src.Request.ContentLength, 10))
|
||||
}
|
||||
ctx.Header(`Accept-Ranges`, `bytes`)
|
||||
ctx.Header(`Content-Transfer-Encoding`, `binary`)
|
||||
ctx.Header(`Content-Type`, `application/octet-stream`)
|
||||
filename := ctx.GetHeader(`FileName`)
|
||||
filename := src.GetHeader(`FileName`)
|
||||
if len(filename) == 0 {
|
||||
filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
|
||||
}
|
||||
@@ -153,35 +143,26 @@ func getDeviceFile(ctx *gin.Context) {
|
||||
|
||||
if partial {
|
||||
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 {
|
||||
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%d`, rangeStart, rangeEnd-1, rangeEnd))
|
||||
}
|
||||
} else {
|
||||
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%v`, rangeStart, rangeEnd, req.Header.Get(`FileSize`)))
|
||||
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %d-%d/%v`, rangeStart, rangeEnd, src.GetHeader(`FileSize`)))
|
||||
}
|
||||
ctx.Status(http.StatusPartialContent)
|
||||
} else {
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
for {
|
||||
buffer := make([]byte, 8192)
|
||||
n, err := req.Body.Read(buffer)
|
||||
buffer = buffer[:n]
|
||||
ctx.Writer.Write(buffer)
|
||||
ctx.Writer.Flush()
|
||||
if n == 0 || err != nil {
|
||||
}
|
||||
instance.OnFinish = func(bridge *bridge) {
|
||||
wait <- false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}, target, trigger)
|
||||
select {
|
||||
case <-wait:
|
||||
case <-time.After(5 * time.Second):
|
||||
if !called {
|
||||
removeBridge(bridgeID)
|
||||
common.RemoveEvent(trigger)
|
||||
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
|
||||
} else {
|
||||
@@ -190,36 +171,62 @@ func getDeviceFile(ctx *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// putDeviceFile will be called by client.
|
||||
// It will transfer binary stream from client to browser.
|
||||
func putDeviceFile(ctx *gin.Context) {
|
||||
original := ctx.Request.Body
|
||||
ctx.Request.Body = ioutil.NopCloser(ctx.Request.Body)
|
||||
|
||||
errMsg := ctx.GetHeader(`Error`)
|
||||
trigger := ctx.GetHeader(`Trigger`)
|
||||
if len(trigger) == 0 {
|
||||
original.Close()
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
|
||||
// uploadToDevice handles file from browser
|
||||
// and transfer it to device.
|
||||
func uploadToDevice(ctx *gin.Context) {
|
||||
var form struct {
|
||||
Path string `json:"path" yaml:"path" form:"path" binding:"required"`
|
||||
File string `json:"file" yaml:"file" form:"file" binding:"required"`
|
||||
}
|
||||
target, ok := checkForm(ctx, &form)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if len(errMsg) > 0 {
|
||||
common.CallEvent(modules.Packet{
|
||||
Code: 1,
|
||||
Msg: fmt.Sprintf(`${i18n|fileUploadFailed}: %v`, errMsg),
|
||||
Event: trigger,
|
||||
}, nil)
|
||||
original.Close()
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
return
|
||||
bridgeID := utils.GetStrUUID()
|
||||
trigger := utils.GetStrUUID()
|
||||
wait := make(chan bool)
|
||||
called := false
|
||||
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 := 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{
|
||||
Code: 0,
|
||||
Data: map[string]interface{}{
|
||||
`request`: ctx.Request,
|
||||
},
|
||||
Event: trigger,
|
||||
}, nil)
|
||||
original.Close()
|
||||
dest.Header(`Accept-Ranges`, `none`)
|
||||
dest.Header(`Content-Transfer-Encoding`, `binary`)
|
||||
dest.Header(`Content-Type`, `application/octet-stream`)
|
||||
filename := form.File
|
||||
filename = url.PathEscape(filename)
|
||||
dest.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
|
||||
}
|
||||
instance.OnFinish = func(bridge *bridge) {
|
||||
wait <- false
|
||||
}
|
||||
common.SendPackByUUID(modules.Packet{Code: 0, Act: `fetchFile`, Data: gin.H{
|
||||
`path`: form.Path,
|
||||
`file`: form.File,
|
||||
`bridge`: bridgeID,
|
||||
}, Event: trigger}, target)
|
||||
select {
|
||||
case <-wait:
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,22 +3,14 @@ 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"
|
||||
)
|
||||
|
||||
// APIRouter will initialize http and websocket routers.
|
||||
func APIRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
|
||||
ctx.PUT(`/device/screenshot/put`, putScreenshot) // Client, upload screenshot and forward to browser.
|
||||
ctx.PUT(`/device/file/put`, putDeviceFile) // Client, to upload file and forward to browser.
|
||||
// InitRouter will initialize http and websocket routers.
|
||||
func InitRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
|
||||
ctx.Any(`/bridge/push`, bridgePush)
|
||||
ctx.Any(`/bridge/pull`, bridgePull)
|
||||
ctx.Any(`/device/terminal`, initTerminal) // Browser, handle websocket events for web terminal.
|
||||
ctx.Any(`/client/update`, checkUpdate) // Client, for update.
|
||||
group := ctx.Group(`/`, auth)
|
||||
@@ -27,6 +19,7 @@ func APIRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
|
||||
group.POST(`/device/process/list`, listDeviceProcesses)
|
||||
group.POST(`/device/process/kill`, killDeviceProcess)
|
||||
group.POST(`/device/file/remove`, removeDeviceFile)
|
||||
group.POST(`/device/file/upload`, uploadToDevice)
|
||||
group.POST(`/device/file/list`, listDeviceFiles)
|
||||
group.POST(`/device/file/get`, getDeviceFile)
|
||||
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.
|
||||
// Every request must contain connection UUID or device ID.
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
@@ -42,7 +42,7 @@ func killDeviceProcess(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
|
@@ -5,82 +5,47 @@ import (
|
||||
"Spark/server/common"
|
||||
"Spark/utils"
|
||||
"Spark/utils/melody"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"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.
|
||||
func getScreenshot(ctx *gin.Context) {
|
||||
target, ok := checkForm(ctx, nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
bridgeID := utils.GetStrUUID()
|
||||
trigger := utils.GetStrUUID()
|
||||
common.SendPackUUID(modules.Packet{Code: 0, Act: `screenshot`, Event: trigger}, target)
|
||||
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
|
||||
if p.Code != 0 {
|
||||
wait := make(chan bool)
|
||||
called := false
|
||||
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})
|
||||
} else {
|
||||
data, ok := p.Data[`screenshot`]
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|screenshotObtainFailed}`})
|
||||
return
|
||||
}, target, trigger)
|
||||
instance := addBridgeWithDest(nil, bridgeID, ctx)
|
||||
instance.OnPush = func(bridge *bridge) {
|
||||
called = true
|
||||
common.RemoveEvent(trigger)
|
||||
ctx.Header(`Content-Type`, `image/png`)
|
||||
}
|
||||
screenshot, ok := data.([]byte)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|screenshotObtainFailed}`})
|
||||
return
|
||||
instance.OnFinish = func(bridge *bridge) {
|
||||
wait <- false
|
||||
}
|
||||
ctx.Data(200, `image/png`, screenshot)
|
||||
}
|
||||
}, target, trigger, 5*time.Second)
|
||||
if !ok {
|
||||
select {
|
||||
case <-wait:
|
||||
case <-time.After(5 * time.Second):
|
||||
if !called {
|
||||
removeBridge(bridgeID)
|
||||
common.RemoveEvent(trigger)
|
||||
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
|
||||
} else {
|
||||
<-wait
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,18 +15,18 @@ import (
|
||||
)
|
||||
|
||||
type terminal struct {
|
||||
uuid string
|
||||
event string
|
||||
device string
|
||||
session *melody.Session
|
||||
deviceConn *melody.Session
|
||||
device string
|
||||
termUUID string
|
||||
eventUUID string
|
||||
}
|
||||
|
||||
var terminals = cmap.New()
|
||||
var wsTerminals = melody.New()
|
||||
var wsSessions = melody.New()
|
||||
|
||||
func init() {
|
||||
wsTerminals.HandleConnect(func(session *melody.Session) {
|
||||
wsSessions.HandleConnect(func(session *melody.Session) {
|
||||
device, ok := session.Get(`Device`)
|
||||
if !ok {
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
|
||||
@@ -59,11 +59,11 @@ func init() {
|
||||
}
|
||||
eventUUID := utils.GetStrUUID()
|
||||
terminal := &terminal{
|
||||
uuid: termUUID,
|
||||
event: eventUUID,
|
||||
device: device.(string),
|
||||
session: session,
|
||||
deviceConn: deviceConn,
|
||||
device: device.(string),
|
||||
termUUID: termUUID,
|
||||
eventUUID: eventUUID,
|
||||
}
|
||||
terminals.Set(termUUID, terminal)
|
||||
common.AddEvent(eventWrapper(terminal), connUUID, eventUUID)
|
||||
@@ -71,9 +71,9 @@ func init() {
|
||||
`terminal`: termUUID,
|
||||
}, Event: eventUUID}, deviceConn)
|
||||
})
|
||||
wsTerminals.HandleMessage(onMessage)
|
||||
wsTerminals.HandleMessageBinary(onMessage)
|
||||
wsTerminals.HandleDisconnect(func(session *melody.Session) {
|
||||
wsSessions.HandleMessage(onMessage)
|
||||
wsSessions.HandleMessageBinary(onMessage)
|
||||
wsSessions.HandleDisconnect(func(session *melody.Session) {
|
||||
val, ok := session.Get(`Terminal`)
|
||||
if !ok {
|
||||
return
|
||||
@@ -86,17 +86,14 @@ func init() {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
terminal := val.(*terminal)
|
||||
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
|
||||
`terminal`: terminal.termUUID,
|
||||
}, Event: terminal.eventUUID}, terminal.deviceConn)
|
||||
`terminal`: termUUID,
|
||||
}, Event: terminal.event}, terminal.deviceConn)
|
||||
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
|
||||
@@ -125,7 +122,7 @@ func initTerminal(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
wsTerminals.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
|
||||
wsSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
|
||||
`Secret`: secret,
|
||||
`Device`: device,
|
||||
`LastPack`: time.Now().Unix(),
|
||||
@@ -146,8 +143,8 @@ func eventWrapper(terminal *terminal) common.EventCallback {
|
||||
msg += `${i18n|unknownError}`
|
||||
}
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
|
||||
terminals.Remove(terminal.termUUID)
|
||||
common.RemoveEvent(terminal.eventUUID)
|
||||
terminals.Remove(terminal.uuid)
|
||||
common.RemoveEvent(terminal.event)
|
||||
terminal.session.Close()
|
||||
}
|
||||
return
|
||||
@@ -158,8 +155,8 @@ func eventWrapper(terminal *terminal) common.EventCallback {
|
||||
msg = pack.Msg
|
||||
}
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
|
||||
terminals.Remove(terminal.termUUID)
|
||||
common.RemoveEvent(terminal.eventUUID)
|
||||
terminals.Remove(terminal.uuid)
|
||||
common.RemoveEvent(terminal.event)
|
||||
terminal.session.Close()
|
||||
return
|
||||
}
|
||||
@@ -246,18 +243,15 @@ func onMessage(session *melody.Session, data []byte) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
terminal := val.(*terminal)
|
||||
if pack.Data == nil {
|
||||
return
|
||||
}
|
||||
if input, ok := pack.Data[`input`]; ok {
|
||||
common.SendPack(modules.Packet{Act: `inputTerminal`, Data: gin.H{
|
||||
`input`: input,
|
||||
`terminal`: terminal.termUUID,
|
||||
}, Event: terminal.eventUUID}, terminal.deviceConn)
|
||||
`terminal`: terminal.uuid,
|
||||
}, Event: terminal.event}, terminal.deviceConn)
|
||||
}
|
||||
}
|
||||
if pack.Act == `resizeTerminal` {
|
||||
@@ -273,10 +267,7 @@ func onMessage(session *melody.Session, data []byte) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
terminal := val.(*terminal)
|
||||
if pack.Data == nil {
|
||||
return
|
||||
}
|
||||
@@ -285,26 +276,44 @@ func onMessage(session *melody.Session, data []byte) {
|
||||
common.SendPack(modules.Packet{Act: `resizeTerminal`, Data: gin.H{
|
||||
`width`: width,
|
||||
`height`: height,
|
||||
`terminal`: terminal.termUUID,
|
||||
}, Event: terminal.eventUUID}, terminal.deviceConn)
|
||||
`terminal`: terminal.uuid,
|
||||
}, Event: terminal.event}, terminal.deviceConn)
|
||||
}
|
||||
}
|
||||
}
|
||||
if pack.Act == `killTerminal` {
|
||||
val, ok := session.Get(`Terminal`)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
termUUID, ok := val.(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
val, ok = terminals.Get(termUUID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
terminal := val.(*terminal)
|
||||
if pack.Data == nil {
|
||||
return
|
||||
}
|
||||
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
|
||||
`terminal`: termUUID,
|
||||
}, Event: terminal.event}, terminal.deviceConn)
|
||||
}
|
||||
}
|
||||
|
||||
func CloseSessionsByDevice(deviceID string) {
|
||||
var queue []string
|
||||
terminals.IterCb(func(key string, val interface{}) bool {
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
terminal := val.(*terminal)
|
||||
if terminal.device == deviceID {
|
||||
common.RemoveEvent(terminal.eventUUID)
|
||||
common.RemoveEvent(terminal.event)
|
||||
terminal.session.Close()
|
||||
queue = append(queue, key)
|
||||
}
|
||||
return false
|
||||
return true
|
||||
})
|
||||
|
||||
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"
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -61,19 +59,21 @@ func main() {
|
||||
return
|
||||
}
|
||||
app := gin.New()
|
||||
{
|
||||
auth := gin.BasicAuth(config.Config.Auth)
|
||||
handler.APIRouter(app.Group(`/api`), auth)
|
||||
handler.InitRouter(app.Group(`/api`), auth)
|
||||
app.Any(`/ws`, wsHandshake)
|
||||
app.NoRoute(auth, func(ctx *gin.Context) {
|
||||
http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request)
|
||||
})
|
||||
}
|
||||
|
||||
common.Melody.Config.MaxMessageSize = 1024
|
||||
common.Melody.HandleConnect(wsOnConnect)
|
||||
common.Melody.HandleMessage(wsOnMessage)
|
||||
common.Melody.HandleMessageBinary(wsOnMessageBinary)
|
||||
common.Melody.HandleDisconnect(wsOnDisconnect)
|
||||
go common.WSHealthCheck(common.Melody)
|
||||
go common.HealthCheckWS(90, common.Melody)
|
||||
|
||||
srv := &http.Server{Addr: config.Config.Listen, Handler: app}
|
||||
go func() {
|
||||
@@ -114,7 +114,7 @@ func wsHandshake(ctx *gin.Context) {
|
||||
}, gin.H{
|
||||
`Secret`: secret,
|
||||
`LastPack`: time.Now().Unix(),
|
||||
`Address`: getRemoteAddr(ctx),
|
||||
`Address`: common.GetRemoteAddr(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
golog.Error(err)
|
||||
@@ -134,12 +134,12 @@ func wsHandshake(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
|
||||
return
|
||||
}
|
||||
auth := common.CheckClientReq(ctx, func(s *melody.Session) {
|
||||
wsOnMessageBinary(s, body)
|
||||
})
|
||||
if !auth {
|
||||
session := common.CheckClientReq(ctx)
|
||||
if session == nil {
|
||||
ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
|
||||
return
|
||||
}
|
||||
wsOnMessageBinary(session, body)
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
}
|
||||
}
|
||||
@@ -161,71 +161,22 @@ func wsOnMessageBinary(session *melody.Session, data []byte) {
|
||||
}
|
||||
if pack.Act == `report` || pack.Act == `setDevice` {
|
||||
session.Set(`LastPack`, time.Now().Unix())
|
||||
handler.WSDevice(data, session)
|
||||
handler.OnDevicePack(data, session)
|
||||
return
|
||||
}
|
||||
if !common.Devices.Has(session.UUID) {
|
||||
session.Close()
|
||||
return
|
||||
}
|
||||
handler.WSRouter(pack, session)
|
||||
common.CallEvent(pack, session)
|
||||
session.Set(`LastPack`, time.Now().Unix())
|
||||
}
|
||||
|
||||
func wsOnDisconnect(session *melody.Session) {
|
||||
if val, ok := common.Devices.Get(session.UUID); ok {
|
||||
if deviceInfo, ok := val.(*modules.Device); ok {
|
||||
deviceInfo := val.(*modules.Device)
|
||||
handler.CloseSessionsByDevice(deviceInfo.ID)
|
||||
}
|
||||
}
|
||||
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.Write(data)
|
||||
result := hash.Sum(nil)
|
||||
hash.Reset()
|
||||
return result, hex.EncodeToString(result)
|
||||
}
|
||||
|
||||
@@ -73,9 +74,13 @@ func Decrypt(data []byte, key []byte) ([]byte, error) {
|
||||
|
||||
hash, _ := GetMD5(decBuffer)
|
||||
if !bytes.Equal(hash, data[:16]) {
|
||||
data = nil
|
||||
decBuffer = nil
|
||||
return nil, ErrFailedVerification
|
||||
}
|
||||
data = nil
|
||||
decBuffer = 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;
|
||||
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 {message, Modal, Popconfirm} from "antd";
|
||||
import {message, Modal, Popconfirm, Progress} from "antd";
|
||||
import ProTable from '@ant-design/pro-table';
|
||||
import {formatSize, post, request, waitTime} from "../utils/utils";
|
||||
import dayjs from "dayjs";
|
||||
import i18n from "../locale/locale";
|
||||
import './explorer.css';
|
||||
import {ReloadOutlined, UploadOutlined} from "@ant-design/icons";
|
||||
import axios from "axios";
|
||||
import Qs from "qs";
|
||||
|
||||
let fileList = [];
|
||||
function FileBrowser(props) {
|
||||
const [path, setPath] = useState(`/`);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [upload, setUpload] = useState(false);
|
||||
const columns = [
|
||||
{
|
||||
key: 'Name',
|
||||
@@ -91,7 +96,7 @@ function FileBrowser(props) {
|
||||
function onRowClick(file) {
|
||||
let separator = props.isWindows ? '\\' : '/';
|
||||
if (file.name === '..') {
|
||||
listFiles(getLastPath());
|
||||
listFiles(getParentPath());
|
||||
return;
|
||||
}
|
||||
if (file.type !== 0) {
|
||||
@@ -110,7 +115,7 @@ function FileBrowser(props) {
|
||||
tableRef.current.reload();
|
||||
}
|
||||
|
||||
function getLastPath() {
|
||||
function getParentPath() {
|
||||
let separator = props.isWindows ? '\\' : '/';
|
||||
// remove the last separator
|
||||
// or there'll be an empty element after split
|
||||
@@ -125,6 +130,52 @@ function FileBrowser(props) {
|
||||
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) {
|
||||
post(location.origin + location.pathname + 'api/device/file/get', {
|
||||
file: path + file,
|
||||
@@ -150,6 +201,7 @@ function FileBrowser(props) {
|
||||
if (data.code === 0) {
|
||||
let addParentShortcut = false;
|
||||
data.data.files = data.data.files.sort((first, second) => (second.type - first.type));
|
||||
fileList = [].concat(data.data.files);
|
||||
if (path.length > 0 && path !== '/' && path !== '\\') {
|
||||
addParentShortcut = true;
|
||||
data.data.files.unshift({
|
||||
@@ -165,7 +217,7 @@ function FileBrowser(props) {
|
||||
total: data.data.files.length - (addParentShortcut ? 1 : 0)
|
||||
});
|
||||
}
|
||||
setPath(getLastPath());
|
||||
setPath(getParentPath());
|
||||
return ({data: [], success: false, total: 0});
|
||||
}
|
||||
|
||||
@@ -188,7 +240,22 @@ function FileBrowser(props) {
|
||||
onDoubleClick: onRowClick.bind(null, file),
|
||||
})}
|
||||
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}}
|
||||
search={false}
|
||||
@@ -203,6 +270,173 @@ function FileBrowser(props) {
|
||||
actionRef={tableRef}
|
||||
>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import CryptoJS from 'crypto-js';
|
||||
import wcwidth from 'wcwidth';
|
||||
import "xterm/css/xterm.css";
|
||||
import i18n from "../locale/locale";
|
||||
import {translate} from "../utils/utils";
|
||||
import {getBaseURL, translate} from "../utils/utils";
|
||||
|
||||
function hex2buf(hex) {
|
||||
if (typeof hex !== 'string') {
|
||||
@@ -59,13 +59,6 @@ function ab2str(buffer) {
|
||||
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) {
|
||||
return [...Array(length)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||
}
|
||||
@@ -120,7 +113,7 @@ class TerminalModal extends React.Component {
|
||||
ev?.dispose();
|
||||
let buffer = '';
|
||||
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') {
|
||||
let cmd = '';
|
||||
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.onopen = () => {
|
||||
this.conn = true;
|
||||
@@ -192,7 +185,6 @@ class TerminalModal extends React.Component {
|
||||
data = data.substring(buffer.length);
|
||||
buffer = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.term.write(data);
|
||||
return;
|
||||
@@ -263,6 +255,9 @@ class TerminalModal extends React.Component {
|
||||
if (prevProps.visible) {
|
||||
clearInterval(this.ticker);
|
||||
if (this.conn) {
|
||||
this.sendData({
|
||||
act: 'killTerminal'
|
||||
});
|
||||
this.ws.close();
|
||||
}
|
||||
this?.termEv?.dispose();
|
||||
|
@@ -27,18 +27,20 @@ axios.interceptors.response.use(async (res) => {
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
if (err.code === 'ECONNABORTED') {
|
||||
message.warn(i18n.t('requestTimeout'));
|
||||
return Promise.resolve(err);
|
||||
message.error(i18n.t('requestTimeout'));
|
||||
return Promise.reject(err);
|
||||
}
|
||||
let res = err.response;
|
||||
let data = res.data;
|
||||
let data = res?.data ?? {};
|
||||
if (data.hasOwnProperty('code')) {
|
||||
if (data.code !== 0){
|
||||
message.warn(translate(data.msg));
|
||||
}
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
}
|
||||
}
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
|
@@ -44,12 +44,22 @@
|
||||
"modifyTime": "Modify Time",
|
||||
"file": "file",
|
||||
"folder": "folder",
|
||||
"reload": "Reload",
|
||||
"upload": "Upload",
|
||||
"delete": "Delete",
|
||||
"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}?",
|
||||
"deleteSuccess": "File or folder deleted",
|
||||
"dateTimeFormat": "MMM D, YYYY h:mm A",
|
||||
"fileOrDirNotExist": "File or folder does not exist",
|
||||
"fileOverwriteConfirm": "File [ {0} ] already exists, overwrite?",
|
||||
"fileOverwrite": "Overwrite",
|
||||
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
|
@@ -45,12 +45,22 @@
|
||||
"modifyTime": "修改时间",
|
||||
"file": "文件",
|
||||
"folder": "文件夹",
|
||||
"reload": "刷新",
|
||||
"upload": "上传",
|
||||
"delete": "删除",
|
||||
"download": "下载",
|
||||
"uploading": "上传中...",
|
||||
"uploadFailed": "上传失败",
|
||||
"uploadAborted": "取消上传",
|
||||
"uploadSuccess": "上传完成",
|
||||
"uploadInvalidPath": "该路径无法上传文件",
|
||||
"uploadCancelConfirm": "确定要取消上传吗?",
|
||||
"deleteConfirm": "确定要删除该{0}吗?",
|
||||
"deleteSuccess": "文件或目录已被删除",
|
||||
"dateTimeFormat": "YYYY/MM/DD HH:mm",
|
||||
"fileOrDirNotExist": "文件或目录不存在",
|
||||
"fileOverwriteConfirm": "文件 [ {0} ] 已经存在,是否覆盖?",
|
||||
"fileOverwrite": "覆盖",
|
||||
|
||||
"registryEditor": "注册表编辑器",
|
||||
"unknownRegistryKey": "注册表键有误",
|
||||
|
@@ -24,11 +24,13 @@ function waitTime(time) {
|
||||
};
|
||||
|
||||
function formatSize(size) {
|
||||
if (size === 0) return '0 B';
|
||||
size = isNaN(size) ? 0 : (size??0);
|
||||
size = Math.max(size, 0);
|
||||
let k = 1024,
|
||||
i = Math.floor(Math.log(size) / Math.log(k)),
|
||||
units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
return (size / Math.pow(k, i)).toFixed(2) + ' ' + units[i];
|
||||
i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(k)),
|
||||
units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
||||
result = size / Math.pow(k, i);
|
||||
return (Math.round(result * 100) / 100) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function tsToTime(ts) {
|
||||
@@ -39,6 +41,15 @@ function tsToTime(ts) {
|
||||
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) {
|
||||
let form = document.createElement('form');
|
||||
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};
|