`(basic auth).
@@ -18,6 +20,10 @@ Example:
Authorization: Basic WFpCOjEyNDg=
```
+After basic authentication, server will assign you a `Authorization` cookie.
+
+You can use this token cookie to authenticate rest of your requests.
+
---
## Response
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30e9c77..bdde6d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,13 @@
+## v0.0.9
+
+* Optimize: performance of front-end and back-end.
+* Optimize: security vulnerability.
+
+* 优化:前后端性能。
+* 优化:安全问题。
+
+
+
## v0.0.8
* Add: file upload.
diff --git a/README.ZH.md b/README.ZH.md
index 5fb3ce3..725074f 100644
--- a/README.ZH.md
+++ b/README.ZH.md
@@ -8,6 +8,18 @@
---
+
+
+|![GitHub repo size](https://img.shields.io/github/repo-size/DGP-Studio/Snap.Genshin?style=flat-square)|![GitHub issues](https://img.shields.io/github/issues/XZB-1248/Spark?style=flat-square)|![GitHub closed issues](https://img.shields.io/github/issues-closed/XZB-1248/Spark?style=flat-square)|
+|-|-|-|
+
+|[![GitHub downloads](https://img.shields.io/github/downloads/XZB-1248/Spark/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases)|[![GitHub release (latest by date)](https://img.shields.io/github/downloads/XZB-1248/Spark/latest/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases/latest)|
+|-|-|
+
+
+
+---
+
### **免责声明**
**本项目及其源代码和发行版,旨在用于学习和交流。使用本项目所带来的风险由使用者本人承担。作者和开发者不会对你的错误使用而造成的损害承担任何责任。**
@@ -123,12 +135,12 @@ $ statik -m -src="./web/dist" -f -dest="./server/embed" -p web -ns web
# 在使用类Unix系统时,运行以下命令。
$ go mod tidy
$ go mod download
-$ ./build.client.sh
+$ ./scripts/build.client.sh
$ statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
# 最终开始编译服务端。
-$ ./build.server.sh
+$ ./scripts/build.server.sh
```
然后打开`releases`目录,放入上文提到的配置文件,选择对应平台的服务端运行即可。
diff --git a/README.md b/README.md
index f867f33..12ed2ff 100644
--- a/README.md
+++ b/README.md
@@ -3,15 +3,30 @@
**Spark** is a free, safe, open-source, web-based, cross-platform and full-featured RAT (Remote Administration Tool)
that allow you to control all your devices via browser anywhere.
-We **won't** collect any data, thus the server will never self-upgrade. Your clients will only communicate with your server forever.
+We **won't** collect any data, thus the server will never self-upgrade. Your clients will only communicate with your
+server forever.
### [English] [[中文]](./README.ZH.md) [[API Document]](./API.md) [[API文档]](./API.ZH.md)
---
+
+
+|![GitHub repo size](https://img.shields.io/github/repo-size/DGP-Studio/Snap.Genshin?style=flat-square)|![GitHub issues](https://img.shields.io/github/issues/XZB-1248/Spark?style=flat-square)|![GitHub closed issues](https://img.shields.io/github/issues-closed/XZB-1248/Spark?style=flat-square)|
+|-|-|-|
+
+|[![GitHub downloads](https://img.shields.io/github/downloads/XZB-1248/Spark/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases)|[![GitHub release (latest by date)](https://img.shields.io/github/downloads/XZB-1248/Spark/latest/total?style=flat-square)](https://github.com/XZB-1248/Spark/releases/latest)|
+|-|-|
+
+
+
+---
+
## **Disclaimer**
-**THIS PROJECT, ITS SOURCE CODE, AND ITS RELEASES SHOULD ONLY BE USED FOR EDUCATIONAL PURPOSES.YOU SHALL USE THIS PROJECT AT YOUR OWN RISK.THE AUTHORS AND DEVELOPERS ARE NOT RESPONSIBLE FOR ANY DAMAGE CAUSED BY YOUR MISUSE OF THIS PROJECT.**
+**THIS PROJECT, ITS SOURCE CODE, AND ITS RELEASES SHOULD ONLY BE USED FOR EDUCATIONAL PURPOSES.YOU SHALL USE THIS
+PROJECT AT YOUR OWN RISK.THE AUTHORS AND DEVELOPERS ARE NOT RESPONSIBLE FOR ANY DAMAGE CAUSED BY YOUR MISUSE OF THIS
+PROJECT.**
**YOUR DATA IS PRICELESS. THINK TWICE BEFORE YOU CLICK ANY BUTTON OR ENTER ANY COMMAND.**
@@ -123,12 +138,12 @@ $ statik -m -src="./web/dist" -f -dest="./server/embed" -p web -ns web
# When you're using unix-like OS, you can use this.
$ go mod tidy
$ go mod download
-$ ./build.client.sh
+$ ./scripts/build.client.sh
$ statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
# Finally we're compiling the server side.
-$ ./build.server.sh
+$ ./scripts/build.server.sh
```
Then you can find executable files in `releases` directory.
diff --git a/client/config/config.go b/client/config/config.go
index ff8c53e..573fcd3 100644
--- a/client/config/config.go
+++ b/client/config/config.go
@@ -16,7 +16,7 @@ type Cfg struct {
// Localhost for my development only.
// Shall be commented out when development is done.
-//var CfgBuffer = "\x00\xcd\x90\x50\x43\xfc\x3d\x36\x56\x6d\xf6\x01\xd1\xcd\x81\xc3\x1b\x80\xc9\x61\xd8\xdf\x5b\x76\x48\x88\xc5\xb1\x74\x22\x23\xab\x3b\xfc\x8b\xbe\x98\x27\xed\x05\xec\xbb\x40\x4f\xe9\xe7\xe5\xe0\x84\xaa\xb7\xfd\x4a\x30\x71\x08\x6c\x02\x50\xe9\xc5\x22\xcf\xcb\x89\x16\x0a\x89\x08\xd4\x26\xdc\x5c\xc1\xc9\xbf\xc4\xac\x0d\x92\x2f\x34\x7f\x45\xeb\x55\xa0\x6d\xf6\x64\xbc\xd5\x15\x40\x96\x43\x64\xe0\x24\x51\xfb\xe8\xc9\x7f\x48\x60\xcd\x30\x5e\x5e\x78\xba\xb6\x6f\x07\x64\xe8\x59\x81\x0b\x91\x13\x92\x1a\xdd\x49\x8f\x28\xe7\x74\xea\xff\x5b\x45\x0e\x4a\x2d\x60\x4e\xc9\xde\x9c\xbe\x50\xc6\x12\xc7\x45\xa2\x15\xa0\x58\x62\x45\x86\x74\x9f\xa5\x14\x5c\x17\x8a\xcc\x56\x73\xa7\x75\xb7\xf6\x6d\x52\x0f\xb8\xc1\xff\x9c\x39\x39\x00\x74\xe1\x4d\x65\x73\x9c\x02\x57\x8b\xcf\xdf\x0a\x20\x4c\xed\xe2\x25\xea\x01\x36\x12\x37\x12\x2e\x1a\x03\x41\x19\x2e\xc9\xdd\x71\xac\x73\x90\xfa\x5e\x60\x08\x43\x35\xef\x61\x45\xf9\xe3\xba\xcb\xb1\xc5\x7c\xf0\x11\xcd\x47\x57\x53\xdc\x35\x6b\x9f\xac\xad\x43\x4a\xc7\x54\x20\xb8\xd0\xf8\xb5\x0c\x45\x76\x57\xb9\xee\x4a\x3f\xd2\xda\xf7\x94\x54\x74\xf3\x91\xf3\x4d\x49\x98\xc6\xf8\x60\x80\xad\x84\x04\xef\x35\xca\x3a\xcf\xd3\x7e\x74\xc2\x4b\xb8\xb3\x9f\xb2\x83\xb8\xbd\x29\x13\x9f\x2b\xaa\x60\x47\x24\x7e\x20\xb2\x85\xdc\x47\xfe\x8f\x68\xb6\xc3\x43\xad\x61\x3d\x9b\x35\x60\x2e\x6c\x44\xf0\xaf\xb2\xf3\xdb\xe2\x1b\x8a\xec\x0a\x48\x5e\x43\xa9\xb3\x3a\x5e\xb6\x90\xa9\x3d\xee\x4f\xa1\x57\x7c\x94\xf4\xb1\x36\xda\x04\xa8\x5e\x48\x2a\xc3\xa1\xf0\x97\xf0\xe0\x10\x46\x32\x10\xe5\xd8\x36\x5a\x56\xa5\xbb\x37\x3c\x9f\xbd\xef\xf5\x2f"
+//var CfgBuffer = "\x00\xcd\xc6\x68\x5d\xf5\x83\x53\x1c\x49\xa2\x35\x7b\x5b\xaf\xf2\x9e\x6d\x74\x00\x95\x23\x73\x00\x77\xa0\xe1\x46\x64\xd2\x33\x2b\x04\xb2\xca\x70\xda\x4b\xed\xec\x43\x6b\xeb\x6e\x10\x53\x6e\x62\x13\x3c\xb1\x0a\xdd\xc0\x48\x2d\x77\xfa\x4a\x9b\x26\xb5\x1b\x50\x62\x05\xcc\xc9\x3b\x22\xf5\x19\x5b\xac\x41\x74\xc9\x9e\x02\x9f\xe8\x75\xce\x3a\xe0\x50\x67\x0f\x81\x01\xca\x47\x0d\xb2\x09\x8b\x74\x6c\xfd\xc5\x73\xf9\x2a\xf0\x13\x52\xb7\x79\xff\xeb\xab\xcd\x9f\xe8\xb7\xae\xff\xa9\x50\xb2\x90\x11\x35\x4d\x94\x6e\x67\x55\x37\x66\x58\x21\xc0\x0d\xab\x3b\x6f\xc4\x00\x56\xd6\x06\xa0\x7e\x73\xdf\x46\x76\xe0\xb3\x89\x0d\xa2\x33\x07\x39\x81\x2b\x59\x30\x24\xc7\x4f\xe9\xb9\xf6\x3c\xb6\x24\xc5\x44\xde\xe6\x66\x66\x92\x49\xe1\x38\x50\xff\xb5\xf3\x20\xb9\x15\x60\x4a\xdf\xba\xd5\xae\x85\x7e\x3f\x8a\xf0\xb8\xf5\x23\x39\xf0\x46\x11\x64\x42\x04\x8c\xf0\x8a\x5e\xc7\x43\xd2\x0c\x89\xd1\xc4\x14\x26\xb1\x67\x64\x28\x77\xf4\xc8\xf3\x51\x69\xba\xf2\xca\xfa\x2f\x11\xe0\x8d\x6c\x4e\x8c\xb7\x28\xf5\x2a\x67\xe3\x8f\xf0\x7f\x79\xc5\xa5\x1a\xb5\xa1\x22\xe9\x55\x61\xdd\xce\x39\x13\x4b\xdd\x19\xf1\x5c\x86\x9b\x16\x89\x45\xba\x16\x68\xfc\x88\x4b\xd5\x13\xa4\x7e\x26\xce\x35\x2d\x42\x4d\x21\xf1\xc3\x6d\xf5\x64\x16\xc9\x05\xed\x9b\x6c\xbf\x26\xe3\xad\x40\x1d\xc6\x64\x03\xb9\xcb\xca\x3c\x62\x5d\x07\x6b\x07\x8b\xa9\x86\x60\x27\x28\xe7\xa3\xc2\x8d\x6f\xc0\x3d\x8e\x14\xa6\xcc\xe0\x50\x51\x22\x20\x6b\x16\x10\xe9\xe0\x4a\xd2\x4e\x77\xc8\xd1\xf7\x60\x4c\xed\xca\x3f\x1e\x13\x0a\x2e\x84\x15\xd3\xf6\x3e\x13\x4e\x68\xaf\xfd\x7a\xd7\x5b\xaa\x5b\x28\x7c\x3f\xb3\xd0\xd0"
// None
var CfgBuffer = "\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19"
diff --git a/client/core/core.go b/client/core/core.go
index c41f732..f3e9db0 100644
--- a/client/core/core.go
+++ b/client/core/core.go
@@ -27,6 +27,7 @@ var (
errNoSecretHeader = errors.New(`can not find secret header`)
)
var handlers = map[string]func(pack modules.Packet, wsConn *common.Conn){
+ `ping`: ping,
`offline`: offline,
`lock`: lock,
`logoff`: logoff,
@@ -69,8 +70,6 @@ func Start() {
checkUpdate(common.WSConn)
- go heartbeat(common.WSConn)
-
err = handleWS(common.WSConn)
if err != nil && !stop {
golog.Error(`Execution error: `, err)
@@ -212,25 +211,3 @@ func handleAct(pack modules.Packet, wsConn *common.Conn) {
act(pack, wsConn)
}
}
-
-func heartbeat(wsConn *common.Conn) error {
- t := 0
- for range time.NewTicker(2 * time.Second).C {
- t++
- // GetPartialInfo always costs more than 1 second.
- // So it is actually get disk info every 20*3 seconds (1 minute).
- device, err := GetPartialInfo(t >= 20)
- if err != nil {
- golog.Error(err)
- continue
- }
- if t >= 20 {
- t = 0
- }
- err = common.SendPack(modules.CommonPack{Act: `setDevice`, Data: *device}, wsConn)
- if err != nil {
- return err
- }
- }
- return nil
-}
diff --git a/client/core/device.go b/client/core/device.go
index 4fd726d..b65e509 100644
--- a/client/core/device.go
+++ b/client/core/device.go
@@ -245,7 +245,7 @@ func GetDevice() (*modules.Device, error) {
}, nil
}
-func GetPartialInfo(getDisk bool) (*modules.Device, error) {
+func GetPartialInfo() (*modules.Device, error) {
cpuInfo, err := GetCPUInfo()
if err != nil {
cpuInfo = modules.CPU{
diff --git a/client/core/handler.go b/client/core/handler.go
index a8714d6..d1706e8 100644
--- a/client/core/handler.go
+++ b/client/core/handler.go
@@ -8,11 +8,22 @@ import (
Screenshot "Spark/client/service/screenshot"
"Spark/client/service/terminal"
"Spark/modules"
+ "github.com/kataras/golog"
"os"
"reflect"
"strconv"
)
+func ping(pack modules.Packet, wsConn *common.Conn) {
+ common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
+ device, err := GetPartialInfo()
+ if err != nil {
+ golog.Error(err)
+ return
+ }
+ common.SendPack(modules.CommonPack{Act: `setDevice`, Data: *device}, wsConn)
+}
+
func offline(pack modules.Packet, wsConn *common.Conn) {
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
stop = true
diff --git a/client/service/file/file.go b/client/service/file/file.go
index e9cea12..3c50b5f 100644
--- a/client/service/file/file.go
+++ b/client/service/file/file.go
@@ -3,13 +3,13 @@ package file
import (
"Spark/client/config"
"errors"
+ "github.com/imroc/req/v3"
"io"
"io/ioutil"
"os"
"path"
"strconv"
-
- "github.com/imroc/req/v3"
+ "unicode/utf8"
)
type File struct {
@@ -41,6 +41,53 @@ func listFiles(path string) ([]File, error) {
return result, nil
}
+func ReadText(path, bridge string) error {
+ file, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ uploadReq := req.R()
+ stat, err := file.Stat()
+ if err != nil {
+ return err
+ }
+ size := stat.Size()
+ // Check if size larger than 2MB.
+ if size > 2<<20 {
+ return errors.New(`${i18n|fileTooLarge}`)
+ }
+ headers := map[string]string{
+ `FileName`: stat.Name(),
+ `FileSize`: strconv.FormatInt(size, 10),
+ }
+ uploadReq.RawRequest.ContentLength = size
+
+ // Check file if is a text file.
+ // UTF-8 and GBK are only supported yet.
+ buf := make([]byte, size)
+ _, err = file.Read(buf)
+ if err != nil {
+ return err
+ }
+ if utf8.Valid(buf) {
+ headers[`FileEncoding`] = `utf-8`
+ } else if gbkValidate(buf) {
+ headers[`FileEncoding`] = `gbk`
+ } else {
+ return errors.New(`${i18n|fileEncodingUnsupported}`)
+ }
+
+ file.Seek(0, 0)
+ url := config.GetBaseURL(false) + `/api/bridge/push`
+ _, err = uploadReq.
+ SetBody(file).
+ SetHeaders(headers).
+ SetQueryParam(`bridge`, bridge).
+ Send(`PUT`, url)
+ return err
+}
+
// FetchFile saves file from bridge to local.
// Save body as temp file and when done, rename it to file.
func FetchFile(dir, file, bridge string) error {
@@ -162,6 +209,26 @@ func UploadFile(path, bridge string, start, end int64) error {
return err
}
+func gbkValidate(b []byte) bool {
+ length := len(b)
+ var i int = 0
+ for i < length {
+ if b[i] <= 0x7f {
+ i++
+ continue
+ } else {
+ if i+1 < length {
+ if b[i] >= 0x81 && b[i] <= 0xfe && b[i+1] >= 0x40 && b[i+1] <= 0xfe && b[i+1] != 0xf7 {
+ i += 2
+ continue
+ }
+ }
+ return false
+ }
+ }
+ return true
+}
+
func getTempFileName(dir, file string) string {
exists := true
tempFile := ``
diff --git a/client/service/screenshot/unsupported.go b/client/service/screenshot/unsupported.go
index ced7966..4ae94a3 100644
--- a/client/service/screenshot/unsupported.go
+++ b/client/service/screenshot/unsupported.go
@@ -2,6 +2,8 @@
package screenshot
+import "errors"
+
func GetScreenshot(bridge string) error {
- return utils.ErrUnsupported
+ return errors.New(`${i18n|operationNotSupported}`)
}
diff --git a/go.mod b/go.mod
index ba41fcc..7b9a222 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.17
require (
github.com/creack/pty v1.1.18
github.com/denisbrodbeck/machineid v1.0.1
+ github.com/gin-contrib/pprof v1.3.0
github.com/gin-gonic/gin v1.7.7
github.com/gorilla/websocket v1.5.0
github.com/imroc/req/v3 v3.8.2
diff --git a/go.sum b/go.sum
index 8b1f81d..f4a47d4 100644
--- a/go.sum
+++ b/go.sum
@@ -7,8 +7,11 @@ github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMS
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY=
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
+github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
+github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@@ -19,6 +22,7 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
diff --git a/scripts/build.client.bat b/scripts/build.client.bat
index c61e11c..b4b2646 100644
--- a/scripts/build.client.bat
+++ b/scripts/build.client.bat
@@ -7,10 +7,10 @@ set GOOS=linux
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_arm Spark/client
-set GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_arm64 Spark/client
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_i386 Spark/client
+set GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_arm64 Spark/client
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/linux_amd64 Spark/client
@@ -20,10 +20,10 @@ set GOOS=windows
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_arm Spark/client
-set GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_arm64 Spark/client
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_i386 Spark/client
+set GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_arm64 Spark/client
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/windows_amd64 Spark/client
@@ -37,16 +37,16 @@ go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/wi
@REM set CXX=armv7a-linux-androideabi21-clang++
@REM go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/android_arm Spark/client
-@REM set GOARCH=arm64
-@REM set CC=aarch64-linux-android21-clang
-@REM set CXX=aarch64-linux-android21-clang++
-@REM go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/android_arm64 Spark/client
-
@REM set GOARCH=386
@REM set CC=i686-linux-android21-clang
@REM set CXX=i686-linux-android21-clang++
@REM go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/android_i386 Spark/client
+@REM set GOARCH=arm64
+@REM set CC=aarch64-linux-android21-clang
+@REM set CXX=aarch64-linux-android21-clang++
+@REM go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=%COMMIT%'" -o ./built/android_arm64 Spark/client
+
@REM set GOARCH=amd64
@REM set CC=x86_64-linux-android21-clang
@REM set CXX=x86_64-linux-android21-clang++
diff --git a/scripts/build.client.sh b/scripts/build.client.sh
index 979e795..963ee61 100644
--- a/scripts/build.client.sh
+++ b/scripts/build.client.sh
@@ -7,10 +7,10 @@ export GOOS=linux
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_arm Spark/client
-export GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_arm64 Spark/client
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_i386 Spark/client
+export GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_arm64 Spark/client
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/linux_amd64 Spark/client
@@ -20,10 +20,10 @@ export GOOS=windows
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_arm Spark/client
-export GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_arm64 Spark/client
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_i386 Spark/client
+export GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_arm64 Spark/client
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/windows_amd64 Spark/client
@@ -37,16 +37,16 @@ go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/win
# export CXX=armv7a-linux-androideabi21-clang++
# go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/android_arm Spark/client
-# export GOARCH=arm64
-# export CC=aarch64-linux-android21-clang
-# export CXX=aarch64-linux-android21-clang++
-# go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/android_arm64 Spark/client
-
# export GOARCH=386
# export CC=i686-linux-android21-clang
# export CXX=i686-linux-android21-clang++
# go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/android_i386 Spark/client
+# export GOARCH=arm64
+# export CC=aarch64-linux-android21-clang
+# export CXX=aarch64-linux-android21-clang++
+# go build -ldflags "-s -w -X 'Spark/client/config.COMMIT=$COMMIT'" -o ./built/android_arm64 Spark/client
+
# export GOARCH=amd64
# export CC=x86_64-linux-android21-clang
# export CXX=x86_64-linux-android21-clang++
diff --git a/scripts/build.server.bat b/scripts/build.server.bat
index 57d2333..023f725 100644
--- a/scripts/build.server.bat
+++ b/scripts/build.server.bat
@@ -1,17 +1,25 @@
-cd ..
-mkdir .\releases
+set GO111MODULE=auto
for /F %%i in ('git rev-parse HEAD') do ( set COMMIT=%%i)
+set GOOS=darwin
+
+set GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
+set GOARCH=amd64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
+
+
+
set GOOS=linux
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_arm Spark/Server
-set GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_arm64 Spark/Server
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_i386 Spark/Server
+set GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_arm64 Spark/Server
set GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_linux_amd64 Spark/Server
@@ -21,18 +29,9 @@ set GOOS=windows
set GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_arm.exe Spark/Server
-set GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_arm64.exe Spark/Server
set GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/Server
-set GOARCH=amd64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/Server
-
-
-
-set GOOS=darwin
-
set GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_arm64.exe Spark/Server
set GOARCH=amd64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=%COMMIT%'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/Server
diff --git a/scripts/build.server.sh b/scripts/build.server.sh
index 4c3ecb7..0aee937 100644
--- a/scripts/build.server.sh
+++ b/scripts/build.server.sh
@@ -3,14 +3,23 @@ export COMMIT=`git rev-parse HEAD`
+export GOOS=darwin
+
+export GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
+export GOARCH=amd64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
+
+
+
export GOOS=linux
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_arm Spark/server
-export GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_arm64 Spark/server
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_i386 Spark/server
+export GOARCH=arm64
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_arm64 Spark/server
export GOARCH=amd64
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_linux_amd64 Spark/server
@@ -20,18 +29,9 @@ export GOOS=windows
export GOARCH=arm
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_arm.exe Spark/server
-export GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_arm64.exe Spark/server
export GOARCH=386
go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_i386.exe Spark/server
-export GOARCH=amd64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/server
-
-
-
-export GOOS=darwin
-
export GOARCH=arm64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_arm64 Spark/server
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_arm64.exe Spark/server
export GOARCH=amd64
-go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_darwin_amd64 Spark/server
+go build -ldflags "-s -w -X 'Spark/server/config.COMMIT=$COMMIT'" -tags=jsoniter -o ./releases/server_windows_amd64.exe Spark/server
diff --git a/server/common/common.go b/server/common/common.go
index 849ffee..29e271b 100644
--- a/server/common/common.go
+++ b/server/common/common.go
@@ -11,14 +11,11 @@ import (
"encoding/hex"
"github.com/gin-gonic/gin"
"net"
- "net/http"
"strings"
- "time"
)
var Melody = melody.New()
var Devices = cmap.New()
-var BuiltFS http.FileSystem
func SendPackByUUID(pack modules.Packet, uuid string) bool {
session, ok := Melody.GetSessionByUUID(uuid)
@@ -70,54 +67,6 @@ func Decrypt(data []byte, session *melody.Session) ([]byte, bool) {
return dec, true
}
-func HealthCheckWS(maxIdleSeconds int64, container *melody.Melody) {
- go func() {
- // ping client and update latency every 3 seconds
- ping := func(uuid string, s *melody.Session) {
- t := time.Now().UnixMilli()
- trigger := utils.GetStrUUID()
- SendPack(modules.Packet{Act: `ping`, Event: trigger}, s)
- AddEventOnce(func(packet modules.Packet, session *melody.Session) {
- val, ok := Devices.Get(uuid)
- if ok {
- deviceInfo := val.(*modules.Device)
- deviceInfo.Latency = uint(time.Now().UnixMilli()-t) / 2
- }
- }, uuid, trigger, 3*time.Second)
- }
- for range time.NewTicker(3 * time.Second).C {
- container.IterSessions(func(uuid string, s *melody.Session) bool {
- go ping(uuid, s)
- return true
- })
- }
- }()
- for now := range time.NewTicker(30 * time.Second).C {
- timestamp := now.Unix()
- // stores sessions to be disconnected
- queue := make([]*melody.Session, 0)
- container.IterSessions(func(uuid string, s *melody.Session) bool {
- val, ok := s.Get(`LastPack`)
- if !ok {
- queue = append(queue, s)
- return true
- }
- lastPack, ok := val.(int64)
- if !ok {
- queue = append(queue, s)
- return true
- }
- if timestamp-lastPack > maxIdleSeconds {
- queue = append(queue, s)
- }
- return true
- })
- for i := 0; i < len(queue); i++ {
- queue[i].Close()
- }
- }
-}
-
func GetRemoteAddr(ctx *gin.Context) string {
if remote, ok := ctx.RemoteIP(); ok {
if remote.IsLoopback() {
diff --git a/server/common/event.go b/server/common/event.go
index 2f9e517..3e6aa83 100644
--- a/server/common/event.go
+++ b/server/common/event.go
@@ -48,9 +48,9 @@ func AddEventOnce(fn EventCallback, connUUID, trigger string, timeout time.Durat
remove: make(chan bool),
}
events.Set(trigger, ev)
- defer events.Remove(trigger)
- defer close(ev.finish)
defer close(ev.remove)
+ defer close(ev.finish)
+ defer events.Remove(trigger)
select {
case ok := <-ev.finish:
return ok
@@ -79,11 +79,16 @@ func RemoveEvent(trigger string, ok ...bool) {
return
}
events.Remove(trigger)
- if ev := v.(*event); ev.remove != nil {
+ ev := v.(*event)
+ if ev.remove != nil {
if len(ok) > 0 {
ev.remove <- ok[0]
+ } else {
+ ev.remove <- false
}
}
+ v = nil
+ ev = nil
}
// HasEvent returns if the event exists.
diff --git a/server/common/time.go b/server/common/time.go
new file mode 100644
index 0000000..de70438
--- /dev/null
+++ b/server/common/time.go
@@ -0,0 +1,14 @@
+package common
+
+import "time"
+
+var Unix int64 = time.Now().Unix()
+
+// To prevent call time.Now().Unix() too often.
+func init() {
+ go func() {
+ for now := range time.NewTicker(time.Second).C {
+ Unix = now.Unix()
+ }
+ }()
+}
diff --git a/server/config/config.go b/server/config/config.go
index 334abbc..839d1c6 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -1,6 +1,10 @@
package config
type Cfg struct {
+ Debug struct {
+ Pprof bool `json:"pprof"`
+ Gin bool `json:"gin"`
+ } `json:"debug,omitempty"`
Listen string `json:"listen"`
Salt string `json:"salt"`
Auth map[string]string `json:"auth"`
@@ -8,6 +12,7 @@ type Cfg struct {
}
var Config Cfg
+var BuiltPath = `./built/%v_%v`
// COMMIT means this commit hash, for auto upgrade.
var COMMIT = ``
diff --git a/server/embed/built/statik.go b/server/embed/built/statik.go
deleted file mode 100644
index ad62327..0000000
--- a/server/embed/built/statik.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Code generated by statik. DO NOT EDIT.
-
-package built
-
-import (
- "github.com/rakyll/statik/fs"
-)
-
-
-const Built = "built" // static asset namespace
-
-func init() {
- data := "PK\x05\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
- fs.RegisterWithNamespace("built", data)
- }
-
\ No newline at end of file
diff --git a/server/handler/bridge.go b/server/handler/bridge.go
index f3182a7..864f145 100644
--- a/server/handler/bridge.go
+++ b/server/handler/bridge.go
@@ -2,6 +2,7 @@ package handler
import (
"Spark/modules"
+ "Spark/server/common"
"Spark/utils/cmap"
"github.com/gin-gonic/gin"
"github.com/kataras/golog"
@@ -31,26 +32,25 @@ var bridges = cmap.New()
func init() {
go func() {
- for now := range time.NewTicker(10 * time.Second).C {
- var queue []*bridge
+ for now := range time.NewTicker(15 * time.Second).C {
+ var queue []string
+ timestamp := now.Unix()
bridges.IterCb(func(k string, v interface{}) bool {
b := v.(*bridge)
- if b.creation < now.Unix()-60 && !b.using {
- queue = append(queue, b)
+ if timestamp-b.creation > 60 && !b.using {
+ b.lock.Lock()
+ if b.src != nil && b.src.Request.Body != nil {
+ b.src.Request.Body.Close()
+ }
+ b.src = nil
+ b.dest = nil
+ b.lock.Unlock()
+ b = nil
+ queue = append(queue, b.uuid)
}
return true
})
- for _, b := range queue {
- bridges.Remove(b.uuid)
- b.lock.Lock()
- if b.src != nil && b.src.Request.Body != nil {
- b.src.Request.Body.Close()
- }
- b.src = nil
- b.dest = nil
- b.lock.Unlock()
- b = nil
- }
+ bridges.Remove(queue...)
}
}()
}
@@ -61,12 +61,12 @@ func checkBridge(ctx *gin.Context) *bridge {
}
if err := ctx.ShouldBind(&form); err != nil {
golog.Error(err)
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return nil
}
val, ok := bridges.Get(form.Bridge)
if !ok {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidBridgeID}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidBridgeID}`})
return nil
}
return val.(*bridge)
@@ -80,7 +80,7 @@ func bridgePush(ctx *gin.Context) {
bridge.lock.Lock()
if bridge.using || (bridge.src != nil && bridge.dest != nil) {
bridge.lock.Unlock()
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
return
}
bridge.src = ctx
@@ -108,7 +108,7 @@ func bridgePull(ctx *gin.Context) {
bridge.lock.Lock()
if bridge.using || (bridge.src != nil && bridge.dest != nil) {
bridge.lock.Unlock()
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1, Msg: `${i18n|bridgeAlreadyInUse}`})
return
}
bridge.dest = ctx
@@ -130,7 +130,7 @@ func bridgePull(ctx *gin.Context) {
func addBridge(ext interface{}, uuid string) *bridge {
bridge := &bridge{
- creation: time.Now().Unix(),
+ creation: common.Unix,
uuid: uuid,
using: false,
lock: &sync.Mutex{},
@@ -142,7 +142,7 @@ func addBridge(ext interface{}, uuid string) *bridge {
func addBridgeWithSrc(ext interface{}, uuid string, src *gin.Context) *bridge {
bridge := &bridge{
- creation: time.Now().Unix(),
+ creation: common.Unix,
uuid: uuid,
using: false,
lock: &sync.Mutex{},
@@ -155,7 +155,7 @@ func addBridgeWithSrc(ext interface{}, uuid string, src *gin.Context) *bridge {
func addBridgeWithDest(ext interface{}, uuid string, dest *gin.Context) *bridge {
bridge := &bridge{
- creation: time.Now().Unix(),
+ creation: common.Unix,
uuid: uuid,
using: false,
lock: &sync.Mutex{},
diff --git a/server/handler/file.go b/server/handler/file.go
index c69623b..d340cd4 100644
--- a/server/handler/file.go
+++ b/server/handler/file.go
@@ -29,13 +29,13 @@ func removeDeviceFile(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Code: 0, Act: `removeFile`, Data: gin.H{`file`: form.File}, Event: trigger}, target)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}, target, trigger, 5*time.Second)
if !ok {
- ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+ ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
}
}
@@ -52,13 +52,13 @@ func listDeviceFiles(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Act: `listFiles`, Data: gin.H{`path`: form.Path}, Event: trigger}, target)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: p.Data})
}
}, target, trigger, 5*time.Second)
if !ok {
- ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+ ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
}
}
@@ -66,7 +66,8 @@ func listDeviceFiles(ctx *gin.Context) {
// client and let it upload the file specified.
func getDeviceFile(ctx *gin.Context) {
var form struct {
- File string `json:"file" yaml:"file" form:"file" binding:"required"`
+ File string `json:"file" yaml:"file" form:"file" binding:"required"`
+ Preview bool `json:"preview" yaml:"preview" form:"preview"`
}
target, ok := checkForm(ctx, &form)
if !ok {
@@ -82,29 +83,29 @@ func getDeviceFile(ctx *gin.Context) {
rangeHeader := ctx.GetHeader(`Range`)
if len(rangeHeader) > 6 {
if rangeHeader[:6] != `bytes=` {
- ctx.Status(http.StatusRequestedRangeNotSatisfiable)
+ ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
rangeHeader = strings.TrimSpace(rangeHeader[6:])
rangesList := strings.Split(rangeHeader, `,`)
if len(rangesList) > 1 {
- ctx.Status(http.StatusRequestedRangeNotSatisfiable)
+ ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
r := strings.Split(rangesList[0], `-`)
rangeStart, err = strconv.ParseInt(r[0], 10, 64)
if err != nil {
- ctx.Status(http.StatusRequestedRangeNotSatisfiable)
+ ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
if len(r[1]) > 0 {
rangeEnd, err = strconv.ParseInt(r[1], 10, 64)
if err != nil {
- ctx.Status(http.StatusRequestedRangeNotSatisfiable)
+ ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
if rangeEnd < rangeStart {
- ctx.Status(http.StatusRequestedRangeNotSatisfiable)
+ ctx.AbortWithStatus(http.StatusRequestedRangeNotSatisfiable)
return
}
command[`end`] = rangeEnd
@@ -121,25 +122,32 @@ func getDeviceFile(ctx *gin.Context) {
called = true
removeBridge(bridgeID)
common.RemoveEvent(trigger)
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
}, target, trigger)
instance := addBridgeWithDest(nil, bridgeID, ctx)
instance.OnPush = func(bridge *bridge) {
called = true
common.RemoveEvent(trigger)
src := bridge.src
+ for k, v := range src.Request.Header {
+ if strings.HasPrefix(k, `File`) {
+ ctx.Header(k, v[0])
+ }
+ }
if src.Request.ContentLength > 0 {
ctx.Header(`Content-Length`, strconv.FormatInt(src.Request.ContentLength, 10))
}
- ctx.Header(`Accept-Ranges`, `bytes`)
- ctx.Header(`Content-Transfer-Encoding`, `binary`)
- ctx.Header(`Content-Type`, `application/octet-stream`)
- filename := src.GetHeader(`FileName`)
- if len(filename) == 0 {
- filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
+ if !form.Preview {
+ ctx.Header(`Accept-Ranges`, `bytes`)
+ ctx.Header(`Content-Transfer-Encoding`, `binary`)
+ ctx.Header(`Content-Type`, `application/octet-stream`)
+ filename := src.GetHeader(`FileName`)
+ if len(filename) == 0 {
+ filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
+ }
+ filename = url.PathEscape(filename)
+ ctx.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
}
- filename = url.PathEscape(filename)
- ctx.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
if partial {
if rangeEnd == 0 {
@@ -164,11 +172,12 @@ func getDeviceFile(ctx *gin.Context) {
if !called {
removeBridge(bridgeID)
common.RemoveEvent(trigger)
- ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+ ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else {
<-wait
}
}
+ close(wait)
}
// uploadToDevice handles file from browser
@@ -191,7 +200,7 @@ func uploadToDevice(ctx *gin.Context) {
called = true
removeBridge(bridgeID)
common.RemoveEvent(trigger)
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
}, target, trigger)
instance := addBridgeWithSrc(nil, bridgeID, ctx)
instance.OnPull = func(bridge *bridge) {
@@ -223,10 +232,11 @@ func uploadToDevice(ctx *gin.Context) {
if !called {
removeBridge(bridgeID)
common.RemoveEvent(trigger)
- ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+ ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else {
<-wait
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}
+ close(wait)
}
diff --git a/server/handler/generate.go b/server/handler/generate.go
index e261dd3..e3e70be 100644
--- a/server/handler/generate.go
+++ b/server/handler/generate.go
@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"math/big"
"net/http"
+ "os"
"strconv"
"strings"
)
@@ -29,25 +30,6 @@ var (
errTooLargeEntity = errors.New(`length of data can not excess buffer size`)
)
-//func init() {
-// clientUUID := utils.GetUUID()
-// clientKey, _ := common.EncAES(clientUUID, append([]byte("XZB_Spark"), bytes.Repeat([]byte{25}, 24-9)...))
-// cfg, _ := genConfig(clientCfg{
-// Secure: false,
-// Host: "47.102.136.182",
-// Port: 1025,
-// Path: "/",
-// UUID: hex.EncodeToString(clientUUID),
-// Key: hex.EncodeToString(clientKey),
-// })
-// output := ``
-// temp := hex.EncodeToString(cfg)
-// for i := 0; i < len(temp); i += 2 {
-// output += `\x` + temp[i:i+2]
-// }
-// ioutil.WriteFile(`./Client.cfg`, []byte(output), 0755)
-//}
-
func checkClient(ctx *gin.Context) {
var form struct {
OS string `json:"os" yaml:"os" form:"os" binding:"required"`
@@ -58,12 +40,12 @@ func checkClient(ctx *gin.Context) {
Secure string `json:"secure" yaml:"secure" form:"secure"`
}
if err := ctx.ShouldBind(&form); err != nil {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
- _, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
+ _, err := os.Open(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch))
if err != nil {
- ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
+ ctx.AbortWithStatusJSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
return
}
_, err = genConfig(clientCfg{
@@ -76,10 +58,10 @@ func checkClient(ctx *gin.Context) {
})
if err != nil {
if err == errTooLargeEntity {
- ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `${i18n|tooLargeConfig}`})
+ ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `${i18n|tooLargeConfig}`})
return
}
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
return
}
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
@@ -95,18 +77,18 @@ func generateClient(ctx *gin.Context) {
Secure string `json:"secure" yaml:"secure" form:"secure"`
}
if err := ctx.ShouldBind(&form); err != nil {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
- tpl, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
+ tpl, err := os.Open(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch))
if err != nil {
- ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
+ ctx.AbortWithStatusJSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
return
}
clientUUID := utils.GetUUID()
clientKey, err := common.EncAES(clientUUID, config.Config.StdSalt)
if err != nil {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
return
}
cfgBytes, err := genConfig(clientCfg{
@@ -119,10 +101,10 @@ func generateClient(ctx *gin.Context) {
})
if err != nil {
if err == errTooLargeEntity {
- ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `${i18n|tooLargeConfig}`})
+ ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `${i18n|tooLargeConfig}`})
return
}
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`})
return
}
ctx.Header(`Accept-Ranges`, `none`)
diff --git a/server/handler/handler.go b/server/handler/handler.go
index b09c109..1cdd716 100644
--- a/server/handler/handler.go
+++ b/server/handler/handler.go
@@ -7,13 +7,14 @@ import (
"net/http"
)
+var AuthHandler gin.HandlerFunc
+
// InitRouter will initialize http and websocket routers.
-func InitRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
+func InitRouter(ctx *gin.RouterGroup) {
ctx.Any(`/bridge/push`, bridgePush)
ctx.Any(`/bridge/pull`, bridgePull)
- ctx.Any(`/device/terminal`, initTerminal) // Browser, handle websocket events for web terminal.
- ctx.Any(`/client/update`, checkUpdate) // Client, for update.
- group := ctx.Group(`/`, auth)
+ ctx.Any(`/client/update`, checkUpdate) // Client, for update.
+ group := ctx.Group(`/`, AuthHandler)
{
group.POST(`/device/screenshot/get`, getScreenshot)
group.POST(`/device/process/list`, listDeviceProcesses)
@@ -26,6 +27,7 @@ func InitRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
group.POST(`/device/:act`, callDevice)
group.POST(`/client/check`, checkClient)
group.POST(`/client/generate`, generateClient)
+ group.Any(`/device/terminal`, initTerminal) // Browser, handle websocket events for web terminal.
}
}
@@ -37,16 +39,16 @@ func checkForm(ctx *gin.Context, form interface{}) (string, bool) {
Device string `json:"device" yaml:"device" form:"device"`
}
if form != nil && ctx.ShouldBind(form) != nil {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return ``, false
}
if ctx.ShouldBind(&base) != nil || (len(base.Conn) == 0 && len(base.Device) == 0) {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return ``, false
}
connUUID, ok := common.CheckDevice(base.Device, base.Conn)
if !ok {
- ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `${i18n|deviceNotExists}`})
+ ctx.AbortWithStatusJSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `${i18n|deviceNotExists}`})
return ``, false
}
return connUUID, true
diff --git a/server/handler/process.go b/server/handler/process.go
index f30896f..07181e2 100644
--- a/server/handler/process.go
+++ b/server/handler/process.go
@@ -21,13 +21,13 @@ func listDeviceProcesses(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Act: `listProcesses`, Event: trigger}, connUUID)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: p.Data})
}
}, connUUID, trigger, 5*time.Second)
if !ok {
- ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+ ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
}
}
@@ -45,12 +45,12 @@ func killDeviceProcess(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Code: 0, Act: `killProcess`, Data: gin.H{`pid`: strconv.FormatInt(int64(form.Pid), 10)}, Event: trigger}, target)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
}, target, trigger, 5*time.Second)
if !ok {
- ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+ ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
}
}
diff --git a/server/handler/screenshot.go b/server/handler/screenshot.go
index fc6c84f..28e5e76 100644
--- a/server/handler/screenshot.go
+++ b/server/handler/screenshot.go
@@ -26,7 +26,7 @@ func getScreenshot(ctx *gin.Context) {
called = true
removeBridge(bridgeID)
common.RemoveEvent(trigger)
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
}, target, trigger)
instance := addBridgeWithDest(nil, bridgeID, ctx)
instance.OnPush = func(bridge *bridge) {
@@ -43,9 +43,10 @@ func getScreenshot(ctx *gin.Context) {
if !called {
removeBridge(bridgeID)
common.RemoveEvent(trigger)
- ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
+ ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`})
} else {
<-wait
}
}
+ close(wait)
}
diff --git a/server/handler/terminal.go b/server/handler/terminal.go
index 5e97127..15ef2a9 100644
--- a/server/handler/terminal.go
+++ b/server/handler/terminal.go
@@ -4,7 +4,6 @@ import (
"Spark/modules"
"Spark/server/common"
"Spark/utils"
- "Spark/utils/cmap"
"Spark/utils/melody"
"crypto/aes"
"crypto/cipher"
@@ -22,111 +21,46 @@ type terminal struct {
deviceConn *melody.Session
}
-var terminals = cmap.New()
var wsSessions = melody.New()
func init() {
- wsSessions.HandleConnect(func(session *melody.Session) {
- device, ok := session.Get(`Device`)
- if !ok {
- simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
- session.Close()
- return
- }
- val, ok := session.Get(`Terminal`)
- if !ok {
- simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
- session.Close()
- return
- }
- termUUID, ok := val.(string)
- if !ok {
- simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
- session.Close()
- return
- }
- connUUID, ok := common.CheckDevice(device.(string), ``)
- if !ok {
- simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session)
- session.Close()
- return
- }
- deviceConn, ok := common.Melody.GetSessionByUUID(connUUID)
- if !ok {
- simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session)
- session.Close()
- return
- }
- eventUUID := utils.GetStrUUID()
- terminal := &terminal{
- uuid: termUUID,
- event: eventUUID,
- device: device.(string),
- session: session,
- deviceConn: deviceConn,
- }
- terminals.Set(termUUID, terminal)
- common.AddEvent(eventWrapper(terminal), connUUID, eventUUID)
- common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{
- `terminal`: termUUID,
- }, Event: eventUUID}, deviceConn)
- })
+ wsSessions.HandleConnect(onConnect)
wsSessions.HandleMessage(onMessage)
wsSessions.HandleMessageBinary(onMessage)
- wsSessions.HandleDisconnect(func(session *melody.Session) {
- val, ok := session.Get(`Terminal`)
- if !ok {
- return
- }
- termUUID, ok := val.(string)
- if !ok {
- return
- }
- val, ok = terminals.Get(termUUID)
- if !ok {
- return
- }
- terminal := val.(*terminal)
- common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
- `terminal`: termUUID,
- }, Event: terminal.event}, terminal.deviceConn)
- terminals.Remove(termUUID)
- common.RemoveEvent(terminal.event)
- })
- go common.HealthCheckWS(300, wsSessions)
+ wsSessions.HandleDisconnect(onDisconnect)
+ go wsHealthCheck(wsSessions)
}
// initTerminal handles terminal websocket handshake event
func initTerminal(ctx *gin.Context) {
if !ctx.IsWebsocket() {
- ctx.Status(http.StatusUpgradeRequired)
+ ctx.AbortWithStatus(http.StatusBadRequest)
return
}
secretStr, ok := ctx.GetQuery(`secret`)
if !ok || len(secretStr) != 32 {
- ctx.Status(http.StatusBadRequest)
+ ctx.AbortWithStatus(http.StatusBadRequest)
return
}
secret, err := hex.DecodeString(secretStr)
if err != nil {
- ctx.Status(http.StatusBadRequest)
+ ctx.AbortWithStatus(http.StatusBadRequest)
return
}
device, ok := ctx.GetQuery(`device`)
if !ok {
- ctx.Status(http.StatusBadRequest)
+ ctx.AbortWithStatus(http.StatusBadRequest)
return
}
if _, ok := common.CheckDevice(device, ``); !ok {
- ctx.Status(http.StatusBadRequest)
+ ctx.AbortWithStatus(http.StatusBadRequest)
return
}
wsSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
`Secret`: secret,
`Device`: device,
- `LastPack`: time.Now().Unix(),
- `Terminal`: utils.GetStrUUID(),
+ `LastPack`: common.Unix,
})
}
@@ -143,7 +77,6 @@ func eventWrapper(terminal *terminal) common.EventCallback {
msg += `${i18n|unknownError}`
}
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
- terminals.Remove(terminal.uuid)
common.RemoveEvent(terminal.event)
terminal.session.Close()
}
@@ -155,7 +88,6 @@ func eventWrapper(terminal *terminal) common.EventCallback {
msg = pack.Msg
}
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
- terminals.Remove(terminal.uuid)
common.RemoveEvent(terminal.event)
terminal.session.Close()
return
@@ -173,52 +105,73 @@ func eventWrapper(terminal *terminal) common.EventCallback {
}
}
-func simpleEncrypt(data []byte, session *melody.Session) ([]byte, bool) {
- temp, ok := session.Get(`Secret`)
- if !ok {
- return nil, false
+func wsHealthCheck(container *melody.Melody) {
+ const MaxIdleSeconds = 300
+ ping := func(uuid string, s *melody.Session) {
+ if !simpleSendPack(modules.Packet{Act: `ping`}, s) {
+ s.Close()
+ }
}
- secret := temp.([]byte)
- block, err := aes.NewCipher(secret)
- if err != nil {
- return nil, false
+ for now := range time.NewTicker(60 * time.Second).C {
+ timestamp := now.Unix()
+ // stores sessions to be disconnected
+ queue := make([]*melody.Session, 0)
+ container.IterSessions(func(uuid string, s *melody.Session) bool {
+ go ping(uuid, s)
+ val, ok := s.Get(`LastPack`)
+ if !ok {
+ queue = append(queue, s)
+ return true
+ }
+ lastPack, ok := val.(int64)
+ if !ok {
+ queue = append(queue, s)
+ return true
+ }
+ if timestamp-lastPack > MaxIdleSeconds {
+ queue = append(queue, s)
+ }
+ return true
+ })
+ for i := 0; i < len(queue); i++ {
+ queue[i].Close()
+ }
}
- stream := cipher.NewCTR(block, secret)
- encBuffer := make([]byte, len(data))
- stream.XORKeyStream(encBuffer, data)
- return encBuffer, true
}
-func simpleDecrypt(data []byte, session *melody.Session) ([]byte, bool) {
- temp, ok := session.Get(`Secret`)
+func onConnect(session *melody.Session) {
+ device, ok := session.Get(`Device`)
if !ok {
- return nil, false
- }
- secret := temp.([]byte)
- block, err := aes.NewCipher(secret)
- if err != nil {
- return nil, false
- }
- stream := cipher.NewCTR(block, secret)
- decBuffer := make([]byte, len(data))
- stream.XORKeyStream(decBuffer, data)
- return decBuffer, true
-}
-
-func simpleSendPack(pack modules.Packet, session *melody.Session) bool {
- if session == nil {
- return false
+ simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|terminalSessionCreationFailed}`}, session)
+ session.Close()
+ return
}
- data, err := utils.JSON.Marshal(pack)
- if err != nil {
- return false
+ connUUID, ok := common.CheckDevice(device.(string), ``)
+ if !ok {
+ simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session)
+ session.Close()
+ return
}
- data, ok := simpleEncrypt(data, session)
+ deviceConn, ok := common.Melody.GetSessionByUUID(connUUID)
if !ok {
- return false
+ simpleSendPack(modules.Packet{Act: `warn`, Msg: `${i18n|deviceNotExists}`}, session)
+ session.Close()
+ return
}
- err = session.WriteBinary(data)
- return err == nil
+ termUUID := utils.GetStrUUID()
+ eventUUID := utils.GetStrUUID()
+ terminal := &terminal{
+ uuid: termUUID,
+ event: eventUUID,
+ device: device.(string),
+ session: session,
+ deviceConn: deviceConn,
+ }
+ session.Set(`Terminal`, terminal)
+ common.AddEvent(eventWrapper(terminal), connUUID, eventUUID)
+ common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{
+ `terminal`: termUUID,
+ }, Event: eventUUID}, deviceConn)
}
func onMessage(session *melody.Session, data []byte) {
@@ -229,20 +182,12 @@ func onMessage(session *melody.Session, data []byte) {
session.Close()
return
}
- session.Set(`LastPack`, time.Now().Unix())
+ session.Set(`LastPack`, common.Unix)
if pack.Act == `inputTerminal` {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
- termUUID, ok := val.(string)
- if !ok {
- return
- }
- val, ok = terminals.Get(termUUID)
- if !ok {
- return
- }
terminal := val.(*terminal)
if pack.Data == nil {
return
@@ -253,20 +198,13 @@ func onMessage(session *melody.Session, data []byte) {
`terminal`: terminal.uuid,
}, Event: terminal.event}, terminal.deviceConn)
}
+ return
}
if pack.Act == `resizeTerminal` {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
- termUUID, ok := val.(string)
- if !ok {
- return
- }
- val, ok = terminals.Get(termUUID)
- if !ok {
- return
- }
terminal := val.(*terminal)
if pack.Data == nil {
return
@@ -280,43 +218,111 @@ func onMessage(session *melody.Session, data []byte) {
}, Event: terminal.event}, terminal.deviceConn)
}
}
+ return
}
if pack.Act == `killTerminal` {
val, ok := session.Get(`Terminal`)
if !ok {
return
}
- termUUID, ok := val.(string)
- if !ok {
- return
- }
- val, ok = terminals.Get(termUUID)
- if !ok {
- return
- }
terminal := val.(*terminal)
if pack.Data == nil {
return
}
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
- `terminal`: termUUID,
+ `terminal`: terminal.uuid,
}, Event: terminal.event}, terminal.deviceConn)
+ return
}
+ if pack.Act == `pong` {
+ return
+ }
+ session.Close()
+}
+
+func onDisconnect(session *melody.Session) {
+ val, ok := session.Get(`Terminal`)
+ if !ok {
+ return
+ }
+ terminal, ok := val.(*terminal)
+ if !ok {
+ return
+ }
+ common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
+ `terminal`: terminal.uuid,
+ }, Event: terminal.event}, terminal.deviceConn)
+ common.RemoveEvent(terminal.event)
+ session.Set(`Terminal`, nil)
+ terminal = nil
+}
+
+func simpleEncrypt(data []byte, session *melody.Session) ([]byte, bool) {
+ temp, ok := session.Get(`Secret`)
+ if !ok {
+ return nil, false
+ }
+ secret := temp.([]byte)
+ block, err := aes.NewCipher(secret)
+ if err != nil {
+ return nil, false
+ }
+ stream := cipher.NewCTR(block, secret)
+ encBuffer := make([]byte, len(data))
+ stream.XORKeyStream(encBuffer, data)
+ return encBuffer, true
+}
+
+func simpleDecrypt(data []byte, session *melody.Session) ([]byte, bool) {
+ temp, ok := session.Get(`Secret`)
+ if !ok {
+ return nil, false
+ }
+ secret := temp.([]byte)
+ block, err := aes.NewCipher(secret)
+ if err != nil {
+ return nil, false
+ }
+ stream := cipher.NewCTR(block, secret)
+ decBuffer := make([]byte, len(data))
+ stream.XORKeyStream(decBuffer, data)
+ return decBuffer, true
+}
+
+func simpleSendPack(pack modules.Packet, session *melody.Session) bool {
+ if session == nil {
+ return false
+ }
+ data, err := utils.JSON.Marshal(pack)
+ if err != nil {
+ return false
+ }
+ data, ok := simpleEncrypt(data, session)
+ if !ok {
+ return false
+ }
+ err = session.WriteBinary(data)
+ return err == nil
}
func CloseSessionsByDevice(deviceID string) {
- var queue []string
- terminals.IterCb(func(key string, val interface{}) bool {
- terminal := val.(*terminal)
+ var queue []*melody.Session
+ wsSessions.IterSessions(func(_ string, session *melody.Session) bool {
+ val, ok := session.Get(`Terminal`)
+ if !ok {
+ return true
+ }
+ terminal, ok := val.(*terminal)
+ if !ok {
+ return true
+ }
if terminal.device == deviceID {
- common.RemoveEvent(terminal.event)
- terminal.session.Close()
- queue = append(queue, key)
+ queue = append(queue, session)
+ return false
}
return true
})
-
- for _, key := range queue {
- terminals.Remove(key)
+ for _, session := range queue {
+ session.Close()
}
}
diff --git a/server/handler/utility.go b/server/handler/utility.go
index ea82f34..03f1c0a 100644
--- a/server/handler/utility.go
+++ b/server/handler/utility.go
@@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/kataras/golog"
"net/http"
+ "os"
"strconv"
"time"
)
@@ -59,24 +60,19 @@ func OnDevicePack(data []byte, session *melody.Session) error {
if len(exSession) > 0 {
common.Devices.Remove(exSession)
}
- }
- common.SendPack(modules.Packet{Code: 0}, session)
-
- {
+ common.Devices.Set(session.UUID, &pack.Device)
+ } else {
val, ok := common.Devices.Get(session.UUID)
if ok {
deviceInfo := val.(*modules.Device)
deviceInfo.CPU = pack.Device.CPU
deviceInfo.RAM = pack.Device.RAM
deviceInfo.Net = pack.Device.Net
- if pack.Device.Disk.Total > 0 {
- deviceInfo.Disk = pack.Device.Disk
- }
+ deviceInfo.Disk = pack.Device.Disk
deviceInfo.Uptime = pack.Device.Uptime
- return nil
}
- common.Devices.Set(session.UUID, &pack.Device)
}
+ common.SendPack(modules.Packet{Code: 0}, session)
return nil
}
@@ -89,32 +85,32 @@ func checkUpdate(ctx *gin.Context) {
}
if err := ctx.ShouldBind(&form); err != nil {
golog.Error(err)
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
if form.Commit == config.COMMIT {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
return
}
- tpl, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
+ tpl, err := os.Open(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch))
if err != nil {
- ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
+ ctx.AbortWithStatusJSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`})
return
}
const MaxBodySize = 384 // This is size of client config buffer.
if ctx.Request.ContentLength > MaxBodySize {
- ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
+ ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
return
}
body, err := ctx.GetRawData()
if err != nil {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1})
return
}
session := common.CheckClientReq(ctx)
if session == nil {
- ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
+ ctx.AbortWithStatusJSON(http.StatusUnauthorized, modules.Packet{Code: 1})
return
}
@@ -162,7 +158,7 @@ func getDevices(ctx *gin.Context) {
func callDevice(ctx *gin.Context) {
act := ctx.Param(`act`)
if len(act) == 0 {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
{
@@ -175,7 +171,7 @@ func callDevice(ctx *gin.Context) {
}
}
if !ok {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`})
return
}
}
@@ -187,7 +183,7 @@ func callDevice(ctx *gin.Context) {
common.SendPackByUUID(modules.Packet{Act: act, Event: trigger}, connUUID)
ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) {
if p.Code != 0 {
- ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
+ ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
} else {
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
}
diff --git a/server/main.go b/server/main.go
index ab32c06..1f0b6be 100644
--- a/server/main.go
+++ b/server/main.go
@@ -5,30 +5,32 @@ import (
"Spark/server/common"
"Spark/server/config"
"Spark/server/handler"
+ "Spark/utils/cmap"
"bytes"
"context"
+ "encoding/hex"
+ "fmt"
+ "github.com/rakyll/statik/fs"
"os"
"os/signal"
"syscall"
"time"
- "github.com/rakyll/statik/fs"
-
- _ "Spark/server/embed/built"
_ "Spark/server/embed/web"
"Spark/utils"
"Spark/utils/melody"
- "encoding/hex"
"io/ioutil"
"net/http"
+ "github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
"github.com/kataras/golog"
)
+var lastRequest = time.Now().Unix()
+
func main() {
golog.SetTimeFormat(`2006/01/02 15:04:05`)
- gin.SetMode(`release`)
data, err := ioutil.ReadFile(`./Config.json`)
if err != nil {
@@ -53,17 +55,20 @@ func main() {
golog.Fatal(`Failed to load static resources: `, err)
return
}
- common.BuiltFS, err = fs.NewWithNamespace(`built`)
- if err != nil {
- golog.Fatal(`Failed to load prebuilt clients: `, err)
- return
+ if config.Config.Debug.Gin {
+ gin.SetMode(gin.DebugMode)
+ } else {
+ gin.SetMode(gin.ReleaseMode)
}
app := gin.New()
+ if config.Config.Debug.Pprof {
+ pprof.Register(app)
+ }
{
- auth := gin.BasicAuth(config.Config.Auth)
- handler.InitRouter(app.Group(`/api`), auth)
+ handler.AuthHandler = authCheck()
+ handler.InitRouter(app.Group(`/api`))
app.Any(`/ws`, wsHandshake)
- app.NoRoute(auth, func(ctx *gin.Context) {
+ app.NoRoute(handler.AuthHandler, func(ctx *gin.Context) {
http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request)
})
}
@@ -73,7 +78,7 @@ func main() {
common.Melody.HandleMessage(wsOnMessage)
common.Melody.HandleMessageBinary(wsOnMessageBinary)
common.Melody.HandleDisconnect(wsOnDisconnect)
- go common.HealthCheckWS(90, common.Melody)
+ go wsHealthCheck(common.Melody)
srv := &http.Server{Addr: config.Config.Listen, Handler: app}
go func() {
@@ -96,55 +101,57 @@ func main() {
}
func wsHandshake(ctx *gin.Context) {
- if ctx.IsWebsocket() {
- clientUUID, _ := hex.DecodeString(ctx.GetHeader(`UUID`))
- clientKey, _ := hex.DecodeString(ctx.GetHeader(`Key`))
- if len(clientUUID) != 16 || len(clientKey) != 32 {
- ctx.Status(http.StatusUnauthorized)
- return
- }
- decrypted, err := common.DecAES(clientKey, config.Config.StdSalt)
- if err != nil || !bytes.Equal(decrypted, clientUUID) {
- ctx.Status(http.StatusUnauthorized)
- return
- }
- secret := append(utils.GetUUID(), utils.GetUUID()...)
- err = common.Melody.HandleRequestWithKeys(ctx.Writer, ctx.Request, http.Header{
- `Secret`: []string{hex.EncodeToString(secret)},
- }, gin.H{
- `Secret`: secret,
- `LastPack`: time.Now().Unix(),
- `Address`: common.GetRemoteAddr(ctx),
- })
- if err != nil {
- golog.Error(err)
- ctx.Status(http.StatusUpgradeRequired)
- return
- }
- } else {
+ if !ctx.IsWebsocket() {
// When message is too large to transport via websocket,
// client will try to send these data via http.
const MaxBodySize = 2 << 18 //524288 512KB
if ctx.Request.ContentLength > MaxBodySize {
- ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
+ ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1})
return
}
body, err := ctx.GetRawData()
if err != nil {
- ctx.JSON(http.StatusBadRequest, modules.Packet{Code: 1})
+ ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1})
return
}
session := common.CheckClientReq(ctx)
if session == nil {
- ctx.JSON(http.StatusUnauthorized, modules.Packet{Code: 1})
+ ctx.AbortWithStatusJSON(http.StatusUnauthorized, modules.Packet{Code: 1})
return
}
wsOnMessageBinary(session, body)
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
+ return
+ }
+
+ clientUUID, _ := hex.DecodeString(ctx.GetHeader(`UUID`))
+ clientKey, _ := hex.DecodeString(ctx.GetHeader(`Key`))
+ if len(clientUUID) != 16 || len(clientKey) != 32 {
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+ decrypted, err := common.DecAES(clientKey, config.Config.StdSalt)
+ if err != nil || !bytes.Equal(decrypted, clientUUID) {
+ ctx.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+ secret := append(utils.GetUUID(), utils.GetUUID()...)
+ err = common.Melody.HandleRequestWithKeys(ctx.Writer, ctx.Request, http.Header{
+ `Secret`: []string{hex.EncodeToString(secret)},
+ }, gin.H{
+ `Secret`: secret,
+ `LastPack`: common.Unix,
+ `Address`: common.GetRemoteAddr(ctx),
+ })
+ if err != nil {
+ golog.Error(err)
+ ctx.AbortWithStatus(http.StatusBadRequest)
+ return
}
}
func wsOnConnect(session *melody.Session) {
+ pingDevice(session)
}
func wsOnMessage(session *melody.Session, bytes []byte) {
@@ -160,7 +167,7 @@ func wsOnMessageBinary(session *melody.Session, data []byte) {
return
}
if pack.Act == `report` || pack.Act == `setDevice` {
- session.Set(`LastPack`, time.Now().Unix())
+ session.Set(`LastPack`, common.Unix)
handler.OnDevicePack(data, session)
return
}
@@ -169,7 +176,7 @@ func wsOnMessageBinary(session *melody.Session, data []byte) {
return
}
common.CallEvent(pack, session)
- session.Set(`LastPack`, time.Now().Unix())
+ session.Set(`LastPack`, common.Unix)
}
func wsOnDisconnect(session *melody.Session) {
@@ -178,5 +185,111 @@ func wsOnDisconnect(session *melody.Session) {
handler.CloseSessionsByDevice(deviceInfo.ID)
}
common.Devices.Remove(session.UUID)
+}
+func wsHealthCheck(container *melody.Melody) {
+ const MaxIdleSeconds = 150
+ const MaxPingInterval = 60
+ go func() {
+ // Ping clients with a dynamic interval.
+ // Interval will be greater than 3 seconds and less than MaxPingInterval.
+ var tick int64 = 0
+ var pingInterval int64 = 3
+ for range time.NewTicker(3 * time.Second).C {
+ tick += 3
+ if tick >= common.Unix-lastRequest {
+ pingInterval = 3
+ }
+ if tick >= 3 && (tick >= pingInterval || tick >= MaxPingInterval) {
+ pingInterval += 3
+ if pingInterval > MaxPingInterval {
+ pingInterval = MaxPingInterval
+ }
+ tick = 0
+ container.IterSessions(func(uuid string, s *melody.Session) bool {
+ go pingDevice(s)
+ return true
+ })
+ }
+ }
+ }()
+ for now := range time.NewTicker(60 * time.Second).C {
+ timestamp := now.Unix()
+ // Store sessions to be disconnected.
+ queue := make([]*melody.Session, 0)
+ container.IterSessions(func(uuid string, s *melody.Session) bool {
+ val, ok := s.Get(`LastPack`)
+ if !ok {
+ queue = append(queue, s)
+ return true
+ }
+ lastPack, ok := val.(int64)
+ if !ok {
+ queue = append(queue, s)
+ return true
+ }
+ if timestamp-lastPack > MaxIdleSeconds {
+ queue = append(queue, s)
+ }
+ return true
+ })
+ for i := 0; i < len(queue); i++ {
+ queue[i].Close()
+ }
+ }
+}
+
+func pingDevice(s *melody.Session) {
+ t := time.Now().UnixMilli()
+ trigger := utils.GetStrUUID()
+ common.SendPack(modules.Packet{Act: `ping`, Event: trigger}, s)
+ common.AddEventOnce(func(packet modules.Packet, session *melody.Session) {
+ val, ok := common.Devices.Get(s.UUID)
+ if ok {
+ deviceInfo := val.(*modules.Device)
+ deviceInfo.Latency = uint(time.Now().UnixMilli()-t) / 2
+ }
+ }, s.UUID, trigger, 3*time.Second)
+}
+
+func authCheck() gin.HandlerFunc {
+ // Token as key and update timestamp as value.
+ // Stores authenticated tokens.
+ tokens := cmap.New()
+ go func() {
+ for now := range time.NewTicker(60 * time.Second).C {
+ var queue []string
+ tokens.IterCb(func(key string, v interface{}) bool {
+ if now.Unix()-v.(int64) > 1800 {
+ queue = append(queue, key)
+ }
+ return true
+ })
+ tokens.Remove(queue...)
+ }
+ }()
+
+ auth := gin.BasicAuth(config.Config.Auth)
+ return func(ctx *gin.Context) {
+ now := common.Unix
+ passed := false
+ if token, err := ctx.Cookie(`Authorization`); err == nil {
+ if tokens.Has(token) {
+ lastRequest = now
+ tokens.Set(token, now)
+ passed = true
+ return
+ }
+ }
+ if !passed {
+ auth(ctx)
+ if ctx.IsAborted() {
+ return
+ }
+ token := utils.GetStrUUID()
+ tokens.Set(token, now)
+ ctx.Header(`Set-Cookie`, fmt.Sprintf(`Authorization=%s; Path=/; HttpOnly`, token))
+ }
+ lastRequest = now
+ }
}
diff --git a/utils/cmap/concurrent_map.go b/utils/cmap/concurrent_map.go
index 4bca4a7..687a827 100644
--- a/utils/cmap/concurrent_map.go
+++ b/utils/cmap/concurrent_map.go
@@ -114,12 +114,14 @@ func (m ConcurrentMap) Has(key string) bool {
}
// Remove removes an element from the map.
-func (m ConcurrentMap) Remove(key string) {
+func (m ConcurrentMap) Remove(key ...string) {
// Try to get shard.
- shard := m.GetShard(key)
- shard.Lock()
- delete(shard.items, key)
- shard.Unlock()
+ for _, k := range key {
+ shard := m.GetShard(k)
+ shard.Lock()
+ delete(shard.items, k)
+ shard.Unlock()
+ }
}
// RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held
@@ -260,7 +262,7 @@ type IterCb func(key string, v interface{}) bool
// IterCb callback based iterator, the cheapest way to read
// all elements in a map.
func (m ConcurrentMap) IterCb(fn IterCb) {
- escape:=false
+ escape := false
for idx := range m {
shard := (m)[idx]
shard.RLock()
diff --git a/utils/melody/melody.go b/utils/melody/melody.go
index 36afcd3..815552c 100644
--- a/utils/melody/melody.go
+++ b/utils/melody/melody.go
@@ -316,9 +316,7 @@ func (m *Melody) IterSessions(fn func(uuid string, s *Session) bool) {
return fn(uuid, s)
}
})
- for i := range invalid {
- m.hub.sessions.Remove(invalid[i])
- }
+ m.hub.sessions.Remove(invalid...)
}
// Close closes the melody instance and all connected sessions.
diff --git a/utils/melody/session.go b/utils/melody/session.go
index af398b6..7ad1414 100644
--- a/utils/melody/session.go
+++ b/utils/melody/session.go
@@ -62,6 +62,7 @@ func (s *Session) close() {
s.open = false
s.conn.Close()
close(s.output)
+ s.Keys = nil
s.rwmutex.Unlock()
}
}
@@ -185,19 +186,25 @@ func (s *Session) CloseWithMsg(msg []byte) error {
return nil
}
-// Set is used to store a new key/value pair exclusivelly for this session.
-// It also lazy initializes s.Keys if it was not used previously.
-func (s *Session) Set(key string, value interface{}) {
+// Set is used to store a new key/value pair exclusively for this session.
+func (s *Session) Set(key string, value interface{}) bool {
+ if s.closed() {
+ return false
+ }
if s.Keys == nil {
s.Keys = make(map[string]interface{})
}
s.Keys[key] = value
+ return true
}
// Get returns the value for the given key, ie: (value, true).
-// If the value does not exists it returns (nil, false)
+// If the key does not exist, it returns (nil, false)
func (s *Session) Get(key string) (value interface{}, exists bool) {
+ if s.closed() {
+ return
+ }
if s.Keys != nil {
value, exists = s.Keys[key]
}
@@ -207,6 +214,9 @@ func (s *Session) Get(key string) (value interface{}, exists bool) {
// MustGet returns the value for the given key if it exists, otherwise it panics.
func (s *Session) MustGet(key string) interface{} {
+ if s.closed() {
+ panic("session is closed")
+ }
if value, exists := s.Get(key); exists {
return value
}
diff --git a/utils/utils.go b/utils/utils.go
index f16d32b..1d871b3 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -12,7 +12,6 @@ import (
)
var (
- ErrUnsupported = errors.New(`unsupported operation`)
ErrEntityInvalid = errors.New(`entity is not valid`)
ErrFailedVerification = errors.New(`failed to verify entity`)
JSON = jsoniter.ConfigCompatibleWithStandardLibrary
diff --git a/web/package-lock.json b/web/package-lock.json
index 0d026f2..5a48a0c 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -24,6 +24,7 @@
"react-dom": "^17.0.2",
"react-router": "^6.2.2",
"react-router-dom": "^6.2.2",
+ "virtuallist-antd": "^0.7.4-beta.0",
"wcwidth": "^1.0.1",
"xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0",
@@ -7764,6 +7765,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/virtuallist-antd": {
+ "version": "0.7.4-beta.0",
+ "resolved": "https://registry.npmjs.org/virtuallist-antd/-/virtuallist-antd-0.7.4-beta.0.tgz",
+ "integrity": "sha512-mawNCiBxNMsiq2toqvvI4USyFy69yYJXazgt/9CRGMdiPKA6azyMG56tcIoE2C51hUpgRscQuumtzNj7jsiX3Q==",
+ "engines": {
+ "node": ">=8",
+ "npm": ">=5"
+ },
+ "peerDependencies": {
+ "antd": "^4.1.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
@@ -13999,6 +14013,12 @@
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"dev": true
},
+ "virtuallist-antd": {
+ "version": "0.7.4-beta.0",
+ "resolved": "https://registry.npmjs.org/virtuallist-antd/-/virtuallist-antd-0.7.4-beta.0.tgz",
+ "integrity": "sha512-mawNCiBxNMsiq2toqvvI4USyFy69yYJXazgt/9CRGMdiPKA6azyMG56tcIoE2C51hUpgRscQuumtzNj7jsiX3Q==",
+ "requires": {}
+ },
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
diff --git a/web/package.json b/web/package.json
index 9062e16..ec84ed4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -24,6 +24,7 @@
"react-dom": "^17.0.2",
"react-router": "^6.2.2",
"react-router-dom": "^6.2.2",
+ "virtuallist-antd": "^0.7.4-beta.0",
"wcwidth": "^1.0.1",
"xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0",
diff --git a/web/src/components/explorer.css b/web/src/components/explorer.css
index 7a9afca..eb6fd67 100644
--- a/web/src/components/explorer.css
+++ b/web/src/components/explorer.css
@@ -8,6 +8,20 @@
min-height: 300px;
}
+.ant-breadcrumb {
+ overflow-x: hidden;
+ white-space: nowrap;
+}
+
+.ant-pro-table-list-toolbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.ant-pro-table-list-toolbar::-webkit-scrollbar {
+ display: none;
+}
+
.upload-progress-square > .ant-progress-outer > .ant-progress-inner {
border-radius: 0 !important;
}
\ No newline at end of file
diff --git a/web/src/components/explorer.js b/web/src/components/explorer.js
index 6da8c8c..82c7e41 100644
--- a/web/src/components/explorer.js
+++ b/web/src/components/explorer.js
@@ -1,17 +1,20 @@
-import React, {useEffect, useRef, useState} from 'react';
-import {message, Modal, Popconfirm, Progress} from "antd";
+import React, {useEffect, useMemo, useRef, useState} from 'react';
+import {Breadcrumb, Card, Image, message, Modal, Popconfirm, Progress} from "antd";
import ProTable from '@ant-design/pro-table';
-import {formatSize, post, request, waitTime} from "../utils/utils";
+import {formatSize, post, request, translate, waitTime} from "../utils/utils";
import dayjs from "dayjs";
import i18n from "../locale/locale";
import './explorer.css';
-import {ReloadOutlined, UploadOutlined} from "@ant-design/icons";
+import { VList } from "virtuallist-antd";
+import {HomeOutlined, ReloadOutlined, UploadOutlined} from "@ant-design/icons";
import axios from "axios";
import Qs from "qs";
+let position = '';
let fileList = [];
function FileBrowser(props) {
const [path, setPath] = useState(`/`);
+ const [preview, setPreview] = useState('');
const [loading, setLoading] = useState(false);
const [upload, setUpload] = useState(false);
const columns = [
@@ -55,7 +58,13 @@ function FileBrowser(props) {
setting: false,
};
const tableRef = useRef();
+ const virtualTable = useMemo(() => {
+ return VList({
+ height: 300
+ })
+ }, []);
useEffect(() => {
+ position = '/';
setPath(`/`);
if (props.visible) {
setLoading(false);
@@ -81,7 +90,7 @@ function FileBrowser(props) {
return [
{i18n.t('download')} ,
remove,
];
@@ -96,7 +105,7 @@ function FileBrowser(props) {
function onRowClick(file) {
let separator = props.isWindows ? '\\' : '/';
if (file.name === '..') {
- listFiles(getParentPath());
+ listFiles(getParentPath(position));
return;
}
if (file.type !== 0) {
@@ -107,15 +116,56 @@ function FileBrowser(props) {
}
}
listFiles(path + file.name + separator);
+ return;
+ }
+ let ext = file.name.split('.').pop();
+ if (ext === 'jpg' || ext === 'png' || ext === 'bmp' || ext === 'gif' || ext === 'jpeg') {
+ imgPreview(file);
+ return;
}
+ downloadFile(file);
+ }
+
+ function imgPreview(file) {
+ // Only preview image file smaller than 8MB.
+ if (file.size > 2 << 22) {
+ return;
+ }
+ setLoading(true);
+ request('/api/device/file/get', {device: props.device, file: path + file.name}, {}, {
+ responseType: 'blob',
+ timeout: 10000
+ }).then((res) => {
+ if ((res.data.type ?? '').substring(0, 16) === 'application/json') {
+ res.data.text().then((str) => {
+ let data = {};
+ try {
+ data = JSON.parse(str);
+ } catch (e) {
+ }
+ message.warn(data.msg ? translate(data.msg) : i18n.t('requestFailed'));
+ });
+ } else {
+ if (preview.length > 0) {
+ URL.revokeObjectURL(preview);
+ }
+ setPreview(URL.createObjectURL(res.data));
+ }
+ }).finally(() => {
+ setLoading(false);
+ });
}
function listFiles(newPath) {
+ if (loading) {
+ return;
+ }
+ position = newPath;
setPath(newPath);
tableRef.current.reload();
}
- function getParentPath() {
+ function getParentPath(path) {
let separator = props.isWindows ? '\\' : '/';
// remove the last separator
// or there'll be an empty element after split
@@ -159,6 +209,7 @@ function FileBrowser(props) {
}
}
}
+
function uploadFile() {
if (path === '/' || path === '\\' || path.length === 0) {
if (props.isWindows) {
@@ -168,17 +219,19 @@ function FileBrowser(props) {
}
document.getElementById('uploader').click();
}
+
function onUploadSuccess() {
tableRef.current.reload();
setUpload(false);
}
+
function onUploadCancel() {
setUpload(false);
}
function downloadFile(file) {
post(location.origin + location.pathname + 'api/device/file/get', {
- file: path + file,
+ file: path + file.name,
device: props.device
});
}
@@ -195,7 +248,7 @@ function FileBrowser(props) {
async function getData(form) {
await waitTime(300);
- let res = await request('/api/device/file/list', {path: path, device: props.device});
+ let res = await request('/api/device/file/list', {path: position, device: props.device});
setLoading(false);
let data = res.data;
if (data.code === 0) {
@@ -211,13 +264,14 @@ function FileBrowser(props) {
modTime: 0
});
}
+ setPath(position);
return ({
data: data.data.files,
success: true,
total: data.data.files.length - (addParentShortcut ? 1 : 0)
});
}
- setPath(getParentPath());
+ setPath(getParentPath(position));
return ({data: [], success: false, total: 0});
}
@@ -242,7 +296,7 @@ function FileBrowser(props) {
toolbar={{
settings: [
{
- icon: ,
+ icon: ,
tooltip: i18n.t('upload'),
key: 'upload',
onClick: uploadFile
@@ -268,12 +322,13 @@ function FileBrowser(props) {
request={getData}
pagination={false}
actionRef={tableRef}
+ components={virtualTable}
>
+ {
+ URL.revokeObjectURL(preview);
+ setPreview('');
+ }
+ }}
+ />
)
}
+function Navigator(props) {
+ let separator = props.isWindows ? '\\' : '/';
+ let path = [];
+ let pathItems = [];
+ let tempPath = props.path;
+ if (tempPath.endsWith(separator)) {
+ tempPath = tempPath.substring(0, tempPath.length - 1);
+ }
+ if (tempPath.length > 0 && tempPath !== '/' && tempPath !== '\\') {
+ path = tempPath.split(separator);
+ }
+ for (let i = 0; i < path.length; i++) {
+ let name = path[i];
+ if (i === 0 && props.isWindows) {
+ if (name.endsWith(':')) {
+ name = name.substring(0, name.length - 1);
+ }
+ }
+ pathItems.push({
+ name: name,
+ path: path.slice(0, i + 1).join(separator) + separator
+ });
+ }
+ if (path.length > 0 && props.isWindows) {
+ let first = path[0];
+ if (first.endsWith(':')) {
+ first = first.substring(0, first.length - 1);
+ }
+ path[0] = first;
+ }
+ pathItems.pop();
+
+ return (
+
+
+
+
+ {pathItems.map(item => (
+
+ {item.name}
+
+ ))}
+ {path.length > 0 ? (
+
+ {path[path.length - 1]}
+
+ ) : null}
+
+ )
+}
+
let abortController = null;
function UploadModal(props) {
const [visible, setVisible] = useState(!!props.file);
@@ -364,6 +490,7 @@ function UploadModal(props) {
}, 1500);
});
}
+
function onCancel() {
if (status === 0) {
setVisible(false);
@@ -413,7 +540,7 @@ function UploadModal(props) {
maskClosable={false}
destroyOnClose={true}
confirmLoading={status === 1}
- okText={i18n.t(status===1?'uploading':'upload')}
+ okText={i18n.t(status === 1 ? 'uploading' : 'upload')}
onOk={onConfirm}
onCancel={onCancel}
okButtonProps={{disabled: status !== 0}}
diff --git a/web/src/components/processes.js b/web/src/components/processes.js
index 9bfd2f5..d4b4ab3 100644
--- a/web/src/components/processes.js
+++ b/web/src/components/processes.js
@@ -1,8 +1,9 @@
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useEffect, useMemo, useRef, useState} from 'react';
import {message, Modal, Popconfirm} from "antd";
import ProTable from '@ant-design/pro-table';
import {request, waitTime} from "../utils/utils";
import i18n from "../locale/locale";
+import {VList} from "virtuallist-antd";
function ProcessMgr(props) {
const [loading, setLoading] = useState(false);
@@ -37,6 +38,11 @@ function ProcessMgr(props) {
setting: false,
};
const tableRef = useRef();
+ const virtualTable = useMemo(() => {
+ return VList({
+ height: 300
+ })
+ }, []);
useEffect(() => {
if (props.visible) {
setLoading(false);
@@ -113,6 +119,7 @@ function ProcessMgr(props) {
request={getData}
pagination={false}
actionRef={tableRef}
+ components={virtualTable}
>
diff --git a/web/src/components/terminal.js b/web/src/components/terminal.js
index 7ee4ec2..de968ab 100644
--- a/web/src/components/terminal.js
+++ b/web/src/components/terminal.js
@@ -192,6 +192,9 @@ class TerminalModal extends React.Component {
if (data?.act === 'warn') {
message.warn(data.msg ? translate(data.msg) : i18n.t('unknownError'));
}
+ if (data?.act === 'ping') {
+ this.sendData({act: 'pong'});
+ }
}
}
this.ws.onclose = (e) => {
diff --git a/web/src/locale/en.json b/web/src/locale/en.json
index 7adc2a6..0264ced 100644
--- a/web/src/locale/en.json
+++ b/web/src/locale/en.json
@@ -60,6 +60,8 @@
"fileOrDirNotExist": "File or folder does not exist",
"fileOverwriteConfirm": "File [ {0} ] already exists, overwrite?",
"fileOverwrite": "Overwrite",
+ "fileTooLarge": "File is too large to read",
+ "fileEncodingUnsupported": "File encoding is not supported",
"host": "Host",
"port": "Port",
diff --git a/web/src/locale/zh-CN.json b/web/src/locale/zh-CN.json
index bdbad00..644160e 100644
--- a/web/src/locale/zh-CN.json
+++ b/web/src/locale/zh-CN.json
@@ -61,6 +61,8 @@
"fileOrDirNotExist": "文件或目录不存在",
"fileOverwriteConfirm": "文件 [ {0} ] 已经存在,是否覆盖?",
"fileOverwrite": "覆盖",
+ "fileTooLarge": "文件太大,读取失败",
+ "fileEncodingUnsupported": "不支持该文件编码",
"registryEditor": "注册表编辑器",
"unknownRegistryKey": "注册表键有误",
diff --git a/web/webpack.config.js b/web/webpack.config.js
index 8445ad2..15231b6 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -103,11 +103,11 @@ module.exports = (env, args) => {
hot: true,
proxy: {
'/api/': {
- target: 'http://localhost:8000/',
+ target: 'http://localhost:8001/',
secure: false
},
'/api/device/terminal': {
- target: 'ws://localhost:8000/',
+ target: 'ws://localhost:8001/',
ws: true
},
}