From 844a0bab924384c9e8c1cd6bced41c5459f1c81c Mon Sep 17 00:00:00 2001 From: XZB-1248 Date: Fri, 7 Oct 2022 20:55:35 +0800 Subject: [PATCH] add: log system --- .gitignore | 1 + API.ZH.md | 31 ++++- API.md | 27 +++- CHANGELOG.md | 12 ++ README.ZH.md | 15 ++- README.md | 11 +- client/client.go | 11 +- client/common/common.go | 8 +- client/config/config.go | 4 +- client/core/core.go | 4 +- client/core/handler.go | 38 ++++++ client/service/desktop/desktop.go | 4 +- client/service/file/file.go | 22 ++-- client/service/terminal/terminal_others.go | 9 +- client/service/terminal/terminal_windows.go | 34 ++--- modules/modules.go | 22 ++-- server/common/common.go | 23 ++++ server/common/log.go | 123 ++++++++++++++++++ server/common/time.go | 16 --- server/config/config.go | 117 ++++++++++++++--- server/handler/bridge/bridge.go | 16 +-- server/handler/desktop/desktop.go | 62 +++++---- server/handler/file/file.go | 65 +++++++++- server/handler/generate/generate.go | 2 +- server/handler/handler.go | 1 + server/handler/process/process.go | 9 ++ server/handler/screenshot/screenshot.go | 7 +- server/handler/terminal/terminal.go | 52 +++++--- server/handler/utility/utility.go | 110 +++++++++++++++- server/main.go | 133 ++++++++++++-------- {client/common => utils}/time.go | 2 +- utils/utils.go | 52 ++++++++ web/public/index.html | 4 +- web/src/components/explorer.js | 2 +- web/src/components/generate.js | 12 +- web/src/components/runner.js | 52 ++++++++ web/src/locale/en.json | 5 + web/src/locale/zh-CN.json | 5 + web/src/pages/overview.js | 22 +++- 39 files changed, 923 insertions(+), 222 deletions(-) create mode 100644 server/common/log.go delete mode 100644 server/common/time.go rename {client/common => utils}/time.go (94%) create mode 100644 web/src/components/runner.js diff --git a/.gitignore b/.gitignore index b701baf..5176f85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /releases /built /tools +/logs /.idea /Config.json dist/ diff --git a/API.ZH.md b/API.ZH.md index 515ef81..75a9144 100644 --- a/API.ZH.md +++ b/API.ZH.md @@ -133,13 +133,38 @@ Authorization: Basic WFpCOjEyNDg= --- +### 执行命令:`/device/exec` + +参数:`cmd`、`args`以及`device`(设备ID) + +示例: +```http request +POST http://localhost:8000/api/device/exec HTTP/1.1 +Host: localhost:8000 +Content-Length: 116 +Content-Type: application/x-www-form-urlencoded +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/ + +cmd=taskkill&args=%2Ff%20%2Fim%20regedit.exe&device=bc7e49f8f794f80ffb0032a4ba516c86d76041bf2023e1be6c5dda3b1ee0cf4c +``` + +``` +{ + "code": 0 +} +``` + +--- + ### 获取截屏:`/device/screenshot/get` 参数:`device`(设备ID) 如果截屏获取成功,则会直接以图片的形式输出。
-如果截屏失败,如下响应会被输出(错误信息不一定是这一个)。 +如果截屏失败,如下响应会被输出(错误信息不唯一)。 ``` { @@ -199,7 +224,7 @@ Authorization: Basic WFpCOjEyNDg=
如果存在同名文件,则会被**覆盖**! -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 @@ -212,7 +237,7 @@ Referer: http://localhost:8000/ Hello World. ``` -如果文件上传成功,则`code`为`1`。 +如果文件上传成功,则`code`为`0`。
文件`D:\Test.txt`会写入:`Hello World.`。 diff --git a/API.md b/API.md index 94fc688..b74a8e3 100644 --- a/API.md +++ b/API.md @@ -20,7 +20,7 @@ Example: Authorization: Basic WFpCOjEyNDg= ``` -After basic authentication, server will assign you a `Authorization` cookie. +After basic authentication, server will assign you an `Authorization` cookie.
You can use this token cookie to authenticate rest of your requests. @@ -130,6 +130,31 @@ For example, when you call `/device/restart`, your device will restart. --- +### Execute command: `/device/exec` + +Parameters: `cmd`, `args` and `device` (device ID) + +Example: +```http request +POST http://localhost:8000/api/device/exec HTTP/1.1 +Host: localhost:8000 +Content-Length: 116 +Content-Type: application/x-www-form-urlencoded +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/ + +cmd=taskkill&args=%2Ff%20%2Fim%20regedit.exe&device=bc7e49f8f794f80ffb0032a4ba516c86d76041bf2023e1be6c5dda3b1ee0cf4c +``` + +``` +{ + "code": 0 +} +``` + +--- + ### Take screenshot: `/device/screenshot/get` Parameters: `device` (device ID) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56368fe..8037c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## v0.1.6 + +* Optimize: potential crash problem of explorer. +* Add: set config through command line arguments. +* Add: log system. + +* 优化:文件管理器可能导致服务崩溃。 +* 添加:支持通过命令行设置相关配置。 +* 新增:日志机制。 + + + ## v0.1.5 * Optimize: performance of desktop viewer on Windows. diff --git a/README.ZH.md b/README.ZH.md index 79197b8..440fc4f 100644 --- a/README.ZH.md +++ b/README.ZH.md @@ -24,9 +24,11 @@ **本项目及其源代码和发行版,旨在用于学习和交流。**
+**禁止用于任何非法用途!** +
**使用本项目所带来的风险由使用者本人承担。**
-**作者和开发者不会对你的错误使用而造成的损害承担任何责任。** +**作者和开发者不会对你错误使用而造成的损害承担任何责任。** **数据无价,在点击任何按钮、输入任何命令之前,请三思。** @@ -40,14 +42,19 @@ * 从 [Releases](https://github.com/XZB-1248/Spark/releases) 页面下载对应系统的可执行文件。 * 解压文件,**不要删除**`built`文件夹。 -* 在目录下创建一个名为`Config.json`的配置文件,修改其中的信息。 +* 在目录下创建一个名为`config.json`的配置文件,设置好相关的配置信息。 ```json { "listen": ":8000", - "salt": "some random string", + "salt": "随机字符串(英文数字符号,小于24位)", "auth": { - "username": "password" + "用户名": "密码(英文数字符号)" + }, + "log": { + "level": "info", + "path": "./logs", + "days": 7 } } ``` diff --git a/README.md b/README.md index a5a4c7a..85cce6a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ server forever. **THIS PROJECT, ITS SOURCE CODE, AND ITS RELEASES SHOULD ONLY BE USED FOR EDUCATIONAL PURPOSES.**
+**ALL ILLEGAL USAGE IS PROHIBITED!** +
**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.** @@ -41,14 +43,19 @@ Only local installation are available yet. ### Local installation * Get prebuilt executable file from [Releases](https://github.com/XZB-1248/Spark/releases) page. * Extract all files and **do not** delete `built` directory. -* Create a configuration file named `Config.json` and set your own salt. +* Create a configuration file named `config.json` and set your own salt. ```json { "listen": ":8000", - "salt": "some random string", + "salt": "some random string length <= 24", "auth": { "username": "password" + }, + "log": { + "level": "info", + "path": "./logs", + "days": 7 } } ``` diff --git a/client/client.go b/client/client.go index cbe14c8..1d1e33c 100644 --- a/client/client.go +++ b/client/client.go @@ -8,8 +8,6 @@ import ( "crypto/aes" "crypto/cipher" "math/big" - "net/http" - _ "net/http/pprof" "os" "os/exec" "strings" @@ -21,18 +19,18 @@ import ( func init() { golog.SetTimeFormat(`2006/01/02 15:04:05`) - if len(strings.Trim(config.CfgBuffer, "\x19")) == 0 { + if len(strings.Trim(config.ConfigBuffer, "\x19")) == 0 { os.Exit(0) return } // Convert first 2 bytes to int, which is the length of the encrypted config. - dataLen := int(big.NewInt(0).SetBytes([]byte(config.CfgBuffer[:2])).Uint64()) - if dataLen > len(config.CfgBuffer)-2 { + dataLen := int(big.NewInt(0).SetBytes([]byte(config.ConfigBuffer[:2])).Uint64()) + if dataLen > len(config.ConfigBuffer)-2 { os.Exit(0) return } - cfgBytes := []byte(config.CfgBuffer[2 : 2+dataLen]) + cfgBytes := utils.StringToBytes(config.ConfigBuffer, 2, 2+dataLen) cfgBytes, err := decrypt(cfgBytes[16:], cfgBytes[:16]) if err != nil { os.Exit(0) @@ -49,7 +47,6 @@ func init() { } func main() { - go http.ListenAndServe(`:6060`, nil) update() core.Start() } diff --git a/client/common/common.go b/client/common/common.go index 5568c8c..c88cbe9 100644 --- a/client/common/common.go +++ b/client/common/common.go @@ -18,12 +18,12 @@ type Conn struct { secretHex string } +const MaxMessageSize = (2 << 15) + 1024 + var WSConn *Conn var Mutex = &sync.Mutex{} var HTTP = CreateClient() -const MaxMessageSize = 32768 + 1024 - func CreateConn(wsConn *ws.Conn, secret []byte) *Conn { return &Conn{ Conn: wsConn, @@ -42,7 +42,7 @@ func (wsConn *Conn) SendData(data []byte) error { if WSConn == nil { return errors.New(`${i18n|wsClosed}`) } - wsConn.SetWriteDeadline(Now.Add(5 * time.Second)) + wsConn.SetWriteDeadline(utils.Now.Add(5 * time.Second)) defer wsConn.SetWriteDeadline(time.Time{}) return wsConn.WriteMessage(ws.BinaryMessage, data) } @@ -68,7 +68,7 @@ func (wsConn *Conn) SendPack(pack any) error { if WSConn == nil { return errors.New(`${i18n|wsClosed}`) } - wsConn.SetWriteDeadline(Now.Add(5 * time.Second)) + wsConn.SetWriteDeadline(utils.Now.Add(5 * time.Second)) defer wsConn.SetWriteDeadline(time.Time{}) return wsConn.WriteMessage(ws.BinaryMessage, data) } diff --git a/client/config/config.go b/client/config/config.go index 573fcd3..5f3fc42 100644 --- a/client/config/config.go +++ b/client/config/config.go @@ -16,10 +16,10 @@ type Cfg struct { // Localhost for my development only. // Shall be commented out when development is done. -//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" +//var ConfigBuffer = "\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" +var ConfigBuffer = "\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\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. var COMMIT = `` diff --git a/client/core/core.go b/client/core/core.go index 6900a3b..f984b11 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -91,7 +91,7 @@ func reportWS(wsConn *common.Conn) error { if err != nil { return err } - common.WSConn.SetReadDeadline(common.Now.Add(5 * time.Second)) + common.WSConn.SetReadDeadline(utils.Now.Add(5 * time.Second)) _, data, err := common.WSConn.ReadMessage() common.WSConn.SetReadDeadline(time.Time{}) if err != nil { @@ -116,7 +116,7 @@ func checkUpdate(wsConn *common.Conn) error { return nil } resp, err := common.HTTP.R(). - SetBody(config.CfgBuffer). + SetBody(config.ConfigBuffer). SetQueryParam(`os`, runtime.GOOS). SetQueryParam(`arch`, runtime.GOARCH). SetQueryParam(`commit`, config.COMMIT). diff --git a/client/core/handler.go b/client/core/handler.go index 928a377..3299ad1 100644 --- a/client/core/handler.go +++ b/client/core/handler.go @@ -11,7 +11,9 @@ import ( "Spark/modules" "github.com/kataras/golog" "os" + "os/exec" "reflect" + "strings" ) var handlers = map[string]func(pack modules.Packet, wsConn *common.Conn){ @@ -40,6 +42,7 @@ var handlers = map[string]func(pack modules.Packet, wsConn *common.Conn){ `pingDesktop`: pingDesktop, `killDesktop`: killDesktop, `getDesktop`: getDesktop, + `execCommand`: execCommand, } func ping(pack modules.Packet, wsConn *common.Conn) { @@ -131,6 +134,8 @@ func initTerminal(pack modules.Packet, wsConn *common.Conn) { err := terminal.InitTerminal(pack) if err != nil { wsConn.SendCallback(modules.Packet{Act: `initTerminal`, Code: 1, Msg: err.Error()}, pack) + } else { + wsConn.SendCallback(modules.Packet{Act: `initTerminal`, Code: 0}, pack) } } @@ -318,6 +323,8 @@ func initDesktop(pack modules.Packet, wsConn *common.Conn) { err := desktop.InitDesktop(pack) if err != nil { wsConn.SendCallback(modules.Packet{Act: `initDesktop`, Code: 1, Msg: err.Error()}, pack) + } else { + wsConn.SendCallback(modules.Packet{Act: `initDesktop`, Code: 0}, pack) } } @@ -332,3 +339,34 @@ func killDesktop(pack modules.Packet, wsConn *common.Conn) { func getDesktop(pack modules.Packet, wsConn *common.Conn) { desktop.GetDesktop(pack) } + +func execCommand(pack modules.Packet, wsConn *common.Conn) { + var proc *exec.Cmd + var cmd, args string + if val, ok := pack.Data[`cmd`]; !ok { + wsConn.SendCallback(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack) + return + } else { + cmd = val.(string) + } + if val, ok := pack.Data[`args`]; !ok { + wsConn.SendCallback(modules.Packet{Code: 1, Msg: `${i18n|invalidParameter}`}, pack) + return + } else { + args = val.(string) + } + if len(args) == 0 { + proc = exec.Command(cmd) + } else { + proc = exec.Command(cmd, strings.Split(args, ` `)...) + } + err := proc.Start() + if err != nil { + wsConn.SendCallback(modules.Packet{Code: 1, Msg: err.Error()}, pack) + } else { + wsConn.SendCallback(modules.Packet{Code: 0, Data: map[string]any{ + `pid`: proc.Process.Pid, + }}, pack) + proc.Process.Release() + } +} diff --git a/client/service/desktop/desktop.go b/client/service/desktop/desktop.go index a7e4616..795ed64 100644 --- a/client/service/desktop/desktop.go +++ b/client/service/desktop/desktop.go @@ -300,7 +300,7 @@ func InitDesktop(pack modules.Packet) error { desktop := &session{ event: pack.Event, rawEvent: rawEvent, - lastPack: common.Unix, + lastPack: utils.Unix, escape: false, channel: make(chan message, 4), lock: &sync.Mutex{}, @@ -345,7 +345,7 @@ func PingDesktop(pack modules.Packet) { return } else { desktop = val.(*session) - desktop.lastPack = common.Unix + desktop.lastPack = utils.Unix } } diff --git a/client/service/file/file.go b/client/service/file/file.go index f804306..abb63f5 100644 --- a/client/service/file/file.go +++ b/client/service/file/file.go @@ -115,17 +115,21 @@ func FetchFile(dir, file, bridge string) error { } func getTempFile(dir, file string) (string, os.FileMode) { - exists := true - tempFile := `` - for i := 0; exists; i++ { - tempFile = path.Join(dir, file+`.tmp.`+strconv.Itoa(i)) - stat, err := os.Stat(tempFile) - if os.IsNotExist(err) { - exists = false + fileMode := os.FileMode(0644) + origin := path.Join(dir, file) + stat, err := os.Stat(origin) + if stat != nil { + fileMode = stat.Mode() + tempFile := `` + for i := 0; i < 5; i++ { + tempFile = path.Join(dir, file+`.tmp.`+strconv.Itoa(i)) + stat, err = os.Stat(tempFile) + if os.IsNotExist(err) { + return tempFile, fileMode + } } - return tempFile, stat.Mode() } - return tempFile, 0644 + return origin, fileMode } func RemoveFiles(files []string) error { diff --git a/client/service/terminal/terminal_others.go b/client/service/terminal/terminal_others.go index a865fc7..467c3ff 100644 --- a/client/service/terminal/terminal_others.go +++ b/client/service/terminal/terminal_others.go @@ -5,6 +5,7 @@ package terminal import ( "Spark/client/common" "Spark/modules" + "Spark/utils" "encoding/hex" "errors" "github.com/creack/pty" @@ -33,7 +34,7 @@ func InitTerminal(pack modules.Packet) error { termSession := &terminal{ pty: ptySession, event: pack.Event, - lastPack: common.Unix, + lastPack: utils.Unix, } terminals.Set(pack.Data[`terminal`].(string), termSession) go func() { @@ -44,7 +45,7 @@ func InitTerminal(pack modules.Packet) error { common.WSConn.SendCallback(modules.Packet{Act: `outputTerminal`, Data: map[string]any{ `output`: hex.EncodeToString(buffer), }}, pack) - termSession.lastPack = common.Unix + termSession.lastPack = utils.Unix if err != nil { common.WSConn.SendCallback(modules.Packet{Act: `quitTerminal`}, pack) break @@ -77,7 +78,7 @@ func InputTerminal(pack modules.Packet) error { } terminal := val.(*terminal) terminal.pty.Write(data) - terminal.lastPack = common.Unix + terminal.lastPack = utils.Unix return nil } @@ -140,7 +141,7 @@ func PingTerminal(pack modules.Packet) { return } else { termSession = val.(*terminal) - termSession.lastPack = common.Unix + termSession.lastPack = utils.Unix } } diff --git a/client/service/terminal/terminal_windows.go b/client/service/terminal/terminal_windows.go index d6f4b79..d493092 100644 --- a/client/service/terminal/terminal_windows.go +++ b/client/service/terminal/terminal_windows.go @@ -3,6 +3,7 @@ package terminal import ( "Spark/client/common" "Spark/modules" + "Spark/utils" "encoding/hex" "io" "os/exec" @@ -14,6 +15,7 @@ import ( type terminal struct { lastPack int64 event string + stop bool cmd *exec.Cmd stdout *io.ReadCloser stderr *io.ReadCloser @@ -26,7 +28,11 @@ func init() { defer func() { recover() }() - syscall.NewLazyDLL(`kernel32.dll`).NewProc(`SetConsoleCP`).Call(65001) + { + kernel32 := syscall.NewLazyDLL(`kernel32.dll`) + kernel32.NewProc(`SetConsoleCP`).Call(65001) + kernel32.NewProc(`SetConsoleOutputCP`).Call(65001) + } go healthCheck() } @@ -34,34 +40,28 @@ func InitTerminal(pack modules.Packet) error { cmd := exec.Command(getTerminal()) stdout, err := cmd.StdoutPipe() if err != nil { - cmd.Process.Kill() - cmd.Process.Release() return err } stderr, err := cmd.StderrPipe() if err != nil { - cmd.Process.Kill() - cmd.Process.Release() return err } stdin, err := cmd.StdinPipe() if err != nil { - cmd.Process.Kill() - cmd.Process.Release() return err } termSession := &terminal{ cmd: cmd, + stop: false, event: pack.Event, stdout: &stdout, stderr: &stderr, stdin: &stdin, - lastPack: common.Unix, + lastPack: utils.Unix, } - terminals.Set(pack.Data[`terminal`].(string), termSession) readSender := func(rc io.ReadCloser) { - for { + for !termSession.stop { buffer := make([]byte, 512) n, err := rc.Read(buffer) buffer = buffer[:n] @@ -69,8 +69,9 @@ func InitTerminal(pack modules.Packet) error { common.WSConn.SendCallback(modules.Packet{Act: `outputTerminal`, Data: map[string]any{ `output`: hex.EncodeToString(buffer), }}, pack) - termSession.lastPack = common.Unix + termSession.lastPack = utils.Unix if err != nil { + termSession.stop = true common.WSConn.SendCallback(modules.Packet{Act: `quitTerminal`}, pack) break } @@ -79,7 +80,12 @@ func InitTerminal(pack modules.Packet) error { go readSender(stdout) go readSender(stderr) - cmd.Start() + err = cmd.Start() + if err != nil { + termSession.stop = true + return err + } + terminals.Set(pack.Data[`terminal`].(string), termSession) return nil } @@ -105,7 +111,7 @@ func InputTerminal(pack modules.Packet) error { } terminal := val.(*terminal) (*terminal.stdin).Write(data) - terminal.lastPack = common.Unix + terminal.lastPack = utils.Unix return nil } @@ -142,7 +148,7 @@ func PingTerminal(pack modules.Packet) { return } else { termSession = val.(*terminal) - termSession.lastPack = common.Unix + termSession.lastPack = utils.Unix } } diff --git a/modules/modules.go b/modules/modules.go index c1664f6..4532fa9 100644 --- a/modules/modules.go +++ b/modules/modules.go @@ -3,19 +3,19 @@ package modules import "reflect" type Packet struct { - Code int `json:"code"` - Act string `json:"act,omitempty"` - Msg string `json:"msg,omitempty"` - Data map[string]interface{} `json:"data,omitempty"` - Event string `json:"event,omitempty"` + Code int `json:"code"` + Act string `json:"act,omitempty"` + Msg string `json:"msg,omitempty"` + Data map[string]any `json:"data,omitempty"` + Event string `json:"event,omitempty"` } type CommonPack struct { - Code int `json:"code"` - Act string `json:"act,omitempty"` - Msg string `json:"msg,omitempty"` - Data interface{} `json:"data,omitempty"` - Event string `json:"event,omitempty"` + Code int `json:"code"` + Act string `json:"act,omitempty"` + Msg string `json:"msg,omitempty"` + Data any `json:"data,omitempty"` + Event string `json:"event,omitempty"` } type Device struct { @@ -55,7 +55,7 @@ type Net struct { Recv uint64 `json:"recv"` } -func (p *Packet) GetData(key string, t reflect.Kind) (interface{}, bool) { +func (p *Packet) GetData(key string, t reflect.Kind) (any, bool) { if p.Data == nil { return nil, false } diff --git a/server/common/common.go b/server/common/common.go index 0f8268f..245f1a0 100644 --- a/server/common/common.go +++ b/server/common/common.go @@ -14,6 +14,8 @@ import ( "strings" ) +const MaxMessageSize = (2 << 15) + 1024 + var Melody = melody.New() var Devices = cmap.New() @@ -67,6 +69,27 @@ func Decrypt(data []byte, session *melody.Session) ([]byte, bool) { return dec, true } +func GetAddrIP(addr net.Addr) string { + switch addr.(type) { + case *net.TCPAddr: + return addr.(*net.TCPAddr).IP.String() + case *net.UDPAddr: + return addr.(*net.UDPAddr).IP.String() + case *net.IPAddr: + return addr.(*net.IPAddr).IP.String() + default: + return addr.String() + } +} + +func GetRealIP(ctx *gin.Context) string { + addr, ok := ctx.Request.Context().Value(`ClientIP`).(string) + if !ok { + return GetRemoteAddr(ctx) + } + return addr +} + func GetRemoteAddr(ctx *gin.Context) string { if remote, ok := ctx.RemoteIP(); ok { if remote.IsLoopback() { diff --git a/server/common/log.go b/server/common/log.go new file mode 100644 index 0000000..30884de --- /dev/null +++ b/server/common/log.go @@ -0,0 +1,123 @@ +package common + +import ( + "Spark/modules" + "Spark/server/config" + "Spark/utils" + "Spark/utils/melody" + "fmt" + "github.com/gin-gonic/gin" + "github.com/kataras/golog" + "io" + "os" + "time" +) + +var logWriter *os.File +var disposed bool + +func init() { + setLogDst := func() { + var err error + if logWriter != nil { + logWriter.Close() + } + if config.Config.Log.Level == `disable` || disposed { + golog.SetOutput(os.Stdout) + return + } + os.Mkdir(config.Config.Log.Path, 0666) + now := utils.Now.Add(time.Second) + logFile := fmt.Sprintf(`%s/%s.log`, config.Config.Log.Path, now.Format(`2006-01-02`)) + logWriter, err = os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) + if err != nil { + golog.Warn(`Failed to open log file: %v`, err) + } + golog.SetOutput(io.MultiWriter(os.Stdout, logWriter)) + + staleDate := time.Unix(now.Unix()-int64(config.Config.Log.Days*86400)-86400, 0) + staleLog := fmt.Sprintf(`%s/%s.log`, config.Config.Log.Path, staleDate.Format(`2006-01-02`)) + os.Remove(staleLog) + } + setLogDst() + go func() { + waitSecs := 86400 - (utils.Now.Hour()*3600 + utils.Now.Minute()*60 + utils.Now.Second()) + <-time.After(time.Duration(waitSecs) * time.Second) + setLogDst() + for range time.NewTicker(time.Second * 86400).C { + setLogDst() + } + }() +} + +func getLog(ctx any, event, status, msg string, args map[string]any) string { + if args == nil { + args = map[string]any{} + } + args[`event`] = event + if len(msg) > 0 { + args[`msg`] = msg + } + if len(status) > 0 { + args[`status`] = status + } + if ctx != nil { + var connUUID string + var targetInfo bool + switch ctx.(type) { + case *gin.Context: + c := ctx.(*gin.Context) + args[`from`] = GetRealIP(c) + connUUID, targetInfo = c.Request.Context().Value(`ConnUUID`).(string) + case *melody.Session: + s := ctx.(*melody.Session) + args[`from`] = GetAddrIP(s.GetWSConn().UnderlyingConn().RemoteAddr()) + if deviceConn, ok := args[`deviceConn`]; ok { + delete(args, `deviceConn`) + connUUID = deviceConn.(*melody.Session).UUID + targetInfo = true + } + } + if targetInfo { + val, ok := Devices.Get(connUUID) + if ok { + device := val.(*modules.Device) + args[`target`] = map[string]any{ + `name`: device.Hostname, + `ip`: device.WAN, + } + } + } + } + output, _ := utils.JSON.MarshalToString(args) + return output +} + +func Info(ctx any, event, status, msg string, args map[string]any) { + golog.Infof(getLog(ctx, event, status, msg, args)) +} + +func Warn(ctx any, event, status, msg string, args map[string]any) { + golog.Warnf(getLog(ctx, event, status, msg, args)) +} + +func Error(ctx any, event, status, msg string, args map[string]any) { + golog.Error(getLog(ctx, event, status, msg, args)) +} + +func Fatal(ctx any, event, status, msg string, args map[string]any) { + golog.Fatalf(getLog(ctx, event, status, msg, args)) +} + +func Debug(ctx any, event, status, msg string, args map[string]any) { + golog.Debugf(getLog(ctx, event, status, msg, args)) +} + +func CloseLog() { + disposed = true + golog.SetOutput(os.Stdout) + if logWriter != nil { + logWriter.Close() + logWriter = nil + } +} diff --git a/server/common/time.go b/server/common/time.go deleted file mode 100644 index 4615592..0000000 --- a/server/common/time.go +++ /dev/null @@ -1,16 +0,0 @@ -package common - -import "time" - -var Now time.Time = time.Now() -var Unix int64 = Now.Unix() - -// To prevent call time.Now().Unix() too often. -func init() { - go func() { - for now := range time.NewTicker(time.Second).C { - Now = now - Unix = now.Unix() - } - }() -} diff --git a/server/config/config.go b/server/config/config.go index afadaa5..407f5f0 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,31 +1,112 @@ package config import ( + "Spark/utils" + "bytes" + "flag" + "github.com/kataras/golog" "os" - "path/filepath" ) -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"` - StdSalt []byte `json:"-"` +type config struct { + Listen string `json:"listen"` + Salt string `json:"salt"` + Auth map[string]string `json:"auth"` + Log *log `json:"log"` + SaltBytes []byte `json:"-"` +} +type log struct { + Level string `json:"level"` + Path string `json:"path"` + Days uint `json:"days"` } - -var Config Cfg -var BuiltPath = getBuiltPath() // COMMIT is hash of this commit, for auto upgrade. var COMMIT = `` +var Config config +var BuiltPath = `./built/%v_%v` + +func init() { + golog.SetTimeFormat(`2006/01/02 15:04:05`) -func getBuiltPath() string { - dir, err := filepath.Abs(filepath.Dir(os.Args[0])) - if err != nil { - return `./built/%v_%v` + var ( + err error + configData []byte + configPath, listen, salt string + username, password string + logLevel, logPath string + logDays uint + ) + flag.StringVar(&configPath, `config`, `config.json`, `config file path, default: config.json`) + flag.StringVar(&listen, `listen`, `:8000`, `required, listen address, default: :8000`) + flag.StringVar(&salt, `salt`, ``, `required, salt of server`) + flag.StringVar(&username, `username`, ``, `username of web interface`) + flag.StringVar(&password, `password`, ``, `password of web interface`) + flag.StringVar(&logLevel, `log-level`, `info`, `log level, default: info`) + flag.StringVar(&logPath, `log-path`, `./logs`, `log file path, default: ./logs`) + flag.UintVar(&logDays, `log-days`, 7, `max days of logs, default: 7`) + flag.Parse() + + if len(configPath) > 0 { + configData, err = os.ReadFile(configPath) + if err != nil { + configData, err = os.ReadFile(`Config.json`) + if err != nil { + fatal(map[string]any{ + `event`: `CONFIG_LOAD`, + `status`: `fail`, + `msg`: err.Error(), + }) + return + } + } + err = utils.JSON.Unmarshal(configData, &Config) + if err != nil { + fatal(map[string]any{ + `event`: `CONFIG_PARSE`, + `status`: `fail`, + `msg`: err.Error(), + }) + return + } + if Config.Log == nil { + Config.Log = &log{ + Level: `info`, + Path: `./logs`, + Days: 7, + } + } + } else { + Config = config{ + Listen: listen, + Salt: salt, + Auth: map[string]string{ + username: password, + }, + Log: &log{ + Level: logLevel, + Path: logPath, + Days: logDays, + }, + } } - return filepath.Join(dir, `built/%v_%v`) + + if len(Config.Salt) > 24 { + fatal(map[string]any{ + `event`: `CONFIG_PARSE`, + `status`: `fail`, + `msg`: `length of salt should less than 24`, + }) + return + } + Config.SaltBytes = []byte(Config.Salt) + Config.SaltBytes = append(Config.SaltBytes, bytes.Repeat([]byte{25}, 24)...) + Config.SaltBytes = Config.SaltBytes[:24] + + golog.SetLevel(utils.If(len(Config.Log.Level) == 0, `info`, Config.Log.Level)) +} + +func fatal(args map[string]any) { + output, _ := utils.JSON.MarshalToString(args) + golog.Fatal(output) } diff --git a/server/handler/bridge/bridge.go b/server/handler/bridge/bridge.go index 04a735a..a0f0e1a 100644 --- a/server/handler/bridge/bridge.go +++ b/server/handler/bridge/bridge.go @@ -2,7 +2,7 @@ package bridge import ( "Spark/modules" - "Spark/server/common" + "Spark/utils" "Spark/utils/cmap" "github.com/gin-gonic/gin" "github.com/kataras/golog" @@ -98,7 +98,7 @@ func BridgePush(ctx *gin.Context) { for { eof := false buf := make([]byte, 2<<14) - SrcConn.SetReadDeadline(common.Now.Add(5 * time.Second)) + SrcConn.SetReadDeadline(utils.Now.Add(5 * time.Second)) n, err := bridge.Src.Request.Body.Read(buf) if n == 0 { break @@ -109,7 +109,7 @@ func BridgePush(ctx *gin.Context) { break } } - DstConn.SetWriteDeadline(common.Now.Add(10 * time.Second)) + DstConn.SetWriteDeadline(utils.Now.Add(10 * time.Second)) _, err = bridge.Dst.Writer.Write(buf[:n]) if eof || err != nil { break @@ -152,7 +152,7 @@ func BridgePull(ctx *gin.Context) { for { eof := false buf := make([]byte, 2<<14) - SrcConn.SetReadDeadline(common.Now.Add(5 * time.Second)) + SrcConn.SetReadDeadline(utils.Now.Add(5 * time.Second)) n, err := bridge.Src.Request.Body.Read(buf) if n == 0 { break @@ -163,7 +163,7 @@ func BridgePull(ctx *gin.Context) { break } } - DstConn.SetWriteDeadline(common.Now.Add(10 * time.Second)) + DstConn.SetWriteDeadline(utils.Now.Add(10 * time.Second)) _, err = bridge.Dst.Writer.Write(buf[:n]) if eof || err != nil { break @@ -183,7 +183,7 @@ func BridgePull(ctx *gin.Context) { func AddBridge(ext any, uuid string) *Bridge { bridge := &Bridge{ - creation: common.Unix, + creation: utils.Unix, uuid: uuid, using: false, lock: &sync.Mutex{}, @@ -195,7 +195,7 @@ func AddBridge(ext any, uuid string) *Bridge { func AddBridgeWithSrc(ext any, uuid string, Src *gin.Context) *Bridge { bridge := &Bridge{ - creation: common.Unix, + creation: utils.Unix, uuid: uuid, using: false, lock: &sync.Mutex{}, @@ -208,7 +208,7 @@ func AddBridgeWithSrc(ext any, uuid string, Src *gin.Context) *Bridge { func AddBridgeWithDst(ext any, uuid string, Dst *gin.Context) *Bridge { bridge := &Bridge{ - creation: common.Unix, + creation: utils.Unix, uuid: uuid, using: false, lock: &sync.Mutex{}, diff --git a/server/handler/desktop/desktop.go b/server/handler/desktop/desktop.go index 7d5c8ea..eadf8f1 100644 --- a/server/handler/desktop/desktop.go +++ b/server/handler/desktop/desktop.go @@ -13,16 +13,15 @@ import ( type desktop struct { uuid string - event string device string - targetConn *melody.Session + srcConn *melody.Session deviceConn *melody.Session } var desktopSessions = melody.New() func init() { - desktopSessions.Config.MaxMessageSize = 32768 + 1024 + desktopSessions.Config.MaxMessageSize = common.MaxMessageSize desktopSessions.HandleConnect(onDesktopConnect) desktopSessions.HandleMessage(onDesktopMessage) desktopSessions.HandleMessageBinary(onDesktopMessage) @@ -59,7 +58,7 @@ func InitDesktop(ctx *gin.Context) { desktopSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{ `Secret`: secret, `Device`: device, - `LastPack`: common.Unix, + `LastPack`: utils.Unix, }) } @@ -72,7 +71,7 @@ func desktopEventWrapper(desktop *desktop) common.EventCallback { return } if data, ok := pack.Data[`data`]; ok { - desktop.targetConn.WriteBinary(*data.(*[]byte)) + desktop.srcConn.WriteBinary(*data.(*[]byte)) } return } @@ -84,9 +83,16 @@ func desktopEventWrapper(desktop *desktop) common.EventCallback { } else { msg += `${i18n|unknownError}` } - sendPack(modules.Packet{Act: `quit`, Msg: msg}, desktop.targetConn) - common.RemoveEvent(desktop.event) - desktop.targetConn.Close() + sendPack(modules.Packet{Act: `quit`, Msg: msg}, desktop.srcConn) + common.RemoveEvent(desktop.uuid) + desktop.srcConn.Close() + common.Warn(desktop.srcConn, `DESKTOP_INIT`, `fail`, msg, map[string]any{ + `deviceConn`: desktop.deviceConn, + }) + } else { + common.Info(desktop.srcConn, `DESKTOP_INIT`, `success`, ``, map[string]any{ + `deviceConn`: desktop.deviceConn, + }) } return } @@ -95,9 +101,12 @@ func desktopEventWrapper(desktop *desktop) common.EventCallback { if len(pack.Msg) > 0 { msg = pack.Msg } - sendPack(modules.Packet{Act: `quit`, Msg: msg}, desktop.targetConn) - common.RemoveEvent(desktop.event) - desktop.targetConn.Close() + sendPack(modules.Packet{Act: `quit`, Msg: msg}, desktop.srcConn) + common.RemoveEvent(desktop.uuid) + desktop.srcConn.Close() + common.Info(desktop.srcConn, `DESKTOP_QUIT`, `success`, ``, map[string]any{ + `deviceConn`: desktop.deviceConn, + }) return } } @@ -122,20 +131,21 @@ func onDesktopConnect(session *melody.Session) { session.Close() return } - eventUUID := utils.GetStrUUID() desktopUUID := utils.GetStrUUID() desktop := &desktop{ uuid: desktopUUID, - event: eventUUID, device: device.(string), - targetConn: session, + srcConn: session, deviceConn: deviceConn, } session.Set(`Desktop`, desktop) - common.AddEvent(desktopEventWrapper(desktop), connUUID, eventUUID) + common.AddEvent(desktopEventWrapper(desktop), connUUID, desktopUUID) common.SendPack(modules.Packet{Act: `initDesktop`, Data: gin.H{ `desktop`: desktopUUID, - }, Event: eventUUID}, deviceConn) + }, Event: desktopUUID}, deviceConn) + common.Info(desktop.srcConn, `DESKTOP_CONN`, `success`, ``, map[string]any{ + `deviceConn`: desktop.deviceConn, + }) } func onDesktopMessage(session *melody.Session, data []byte) { @@ -146,7 +156,7 @@ func onDesktopMessage(session *melody.Session, data []byte) { desktop := val.(*desktop) common.SendPack(modules.Packet{Act: `killDesktop`, Data: gin.H{ `desktop`: desktop.uuid, - }, Event: desktop.event}, desktop.deviceConn) + }, Event: desktop.uuid}, desktop.deviceConn) } sendPack(modules.Packet{Code: -1}, session) session.Close() @@ -157,29 +167,33 @@ func onDesktopMessage(session *melody.Session, data []byte) { return } desktop := val.(*desktop) - session.Set(`LastPack`, common.Unix) + session.Set(`LastPack`, utils.Unix) if pack.Act == `pingDesktop` { common.SendPack(modules.Packet{Act: `pingDesktop`, Data: gin.H{ `desktop`: desktop.uuid, - }, Event: desktop.event}, desktop.deviceConn) + }, Event: desktop.uuid}, desktop.deviceConn) return } if pack.Act == `killDesktop` { + common.Info(desktop.srcConn, `DESKTOP_KILL`, `success`, ``, map[string]any{ + `deviceConn`: desktop.deviceConn, + }) common.SendPack(modules.Packet{Act: `killDesktop`, Data: gin.H{ `desktop`: desktop.uuid, - }, Event: desktop.event}, desktop.deviceConn) + }, Event: desktop.uuid}, desktop.deviceConn) return } if pack.Act == `getDesktop` { common.SendPack(modules.Packet{Act: `getDesktop`, Data: gin.H{ `desktop`: desktop.uuid, - }, Event: desktop.event}, desktop.deviceConn) + }, Event: desktop.uuid}, desktop.deviceConn) return } session.Close() } func onDesktopDisconnect(session *melody.Session) { + common.Info(session, `DESKTOP_CLOSE`, `success`, ``, nil) val, ok := session.Get(`Desktop`) if !ok { return @@ -190,8 +204,8 @@ func onDesktopDisconnect(session *melody.Session) { } common.SendPack(modules.Packet{Act: `killDesktop`, Data: gin.H{ `desktop`: desktop.uuid, - }, Event: desktop.event}, desktop.deviceConn) - common.RemoveEvent(desktop.event) + }, Event: desktop.uuid}, desktop.deviceConn) + common.RemoveEvent(desktop.uuid) session.Set(`Desktop`, nil) desktop = nil } @@ -224,7 +238,7 @@ func CloseSessionsByDevice(deviceID string) { return true } if desktop.device == deviceID { - sendPack(modules.Packet{Act: `quit`, Msg: `${i18n|desktopSessionClosed}`}, desktop.targetConn) + sendPack(modules.Packet{Act: `quit`, Msg: `${i18n|desktopSessionClosed}`}, desktop.srcConn) queue = append(queue, session) return false } diff --git a/server/handler/file/file.go b/server/handler/file/file.go index aba0f66..22cf7c5 100644 --- a/server/handler/file/file.go +++ b/server/handler/file/file.go @@ -27,16 +27,29 @@ func RemoveDeviceFiles(ctx *gin.Context) { if !ok { return } + if len(form.Files) == 0 { + ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`}) + return + } trigger := utils.GetStrUUID() common.SendPackByUUID(modules.Packet{Code: 0, Act: `removeFiles`, Data: gin.H{`files`: form.Files}, Event: trigger}, target) ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) { if p.Code != 0 { + common.Warn(ctx, `REMOVE_FILES`, `fail`, p.Msg, map[string]any{ + `files`: form.Files, + }) ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) } else { + common.Info(ctx, `REMOVE_FILES`, `success`, ``, map[string]any{ + `files`: form.Files, + }) ctx.JSON(http.StatusOK, modules.Packet{Code: 0}) } }, target, trigger, 5*time.Second) if !ok { + common.Warn(ctx, `REMOVE_FILES`, `fail`, `timeout`, map[string]any{ + `files`: form.Files, + }) ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`}) } } @@ -124,11 +137,14 @@ func GetDeviceFiles(ctx *gin.Context) { wait := make(chan bool) called := false common.AddEvent(func(p modules.Packet, _ *melody.Session) { - wait <- false called = true bridge.RemoveBridge(bridgeID) common.RemoveEvent(trigger) + common.Warn(ctx, `READ_FILES`, `fail`, p.Msg, map[string]any{ + `files`: form.Files, + }) ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) + wait <- false }, target, trigger) instance := bridge.AddBridgeWithDst(nil, bridgeID, ctx) instance.OnPush = func(bridge *bridge.Bridge) { @@ -177,6 +193,11 @@ func GetDeviceFiles(ctx *gin.Context) { } } instance.OnFinish = func(bridge *bridge.Bridge) { + if called { + common.Info(ctx, `READ_FILES`, `success`, ``, map[string]any{ + `files`: form.Files, + }) + } wait <- false } select { @@ -185,6 +206,9 @@ func GetDeviceFiles(ctx *gin.Context) { if !called { bridge.RemoveBridge(bridgeID) common.RemoveEvent(trigger) + common.Warn(ctx, `READ_FILES`, `fail`, `timeout`, map[string]any{ + `files`: form.Files, + }) ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`}) } else { <-wait @@ -203,6 +227,10 @@ func GetDeviceTextFile(ctx *gin.Context) { if !ok { return } + if len(form.File) == 0 { + ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`}) + return + } bridgeID := utils.GetStrUUID() trigger := utils.GetStrUUID() common.SendPackByUUID(modules.Packet{Code: 0, Act: `uploadTextFile`, Data: gin.H{ @@ -212,11 +240,14 @@ func GetDeviceTextFile(ctx *gin.Context) { wait := make(chan bool) called := false common.AddEvent(func(p modules.Packet, _ *melody.Session) { - wait <- false called = true bridge.RemoveBridge(bridgeID) common.RemoveEvent(trigger) + common.Warn(ctx, `READ_TEXT_FILE`, `fail`, p.Msg, map[string]any{ + `file`: form.File, + }) ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) + wait <- false }, target, trigger) instance := bridge.AddBridgeWithDst(nil, bridgeID, ctx) instance.OnPush = func(bridge *bridge.Bridge) { @@ -239,6 +270,11 @@ func GetDeviceTextFile(ctx *gin.Context) { ctx.Status(http.StatusOK) } instance.OnFinish = func(bridge *bridge.Bridge) { + if called { + common.Info(ctx, `READ_TEXT_FILE`, `success`, ``, map[string]any{ + `file`: form.File, + }) + } wait <- false } select { @@ -247,6 +283,9 @@ func GetDeviceTextFile(ctx *gin.Context) { if !called { bridge.RemoveBridge(bridgeID) common.RemoveEvent(trigger) + common.Warn(ctx, `READ_TEXT_FILE`, `fail`, `timeout`, map[string]any{ + `file`: form.File, + }) ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`}) } else { <-wait @@ -266,18 +305,28 @@ func UploadToDevice(ctx *gin.Context) { if !ok { return } + if len(form.File) == 0 || len(form.Path) == 0 { + ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`}) + return + } bridgeID := utils.GetStrUUID() trigger := utils.GetStrUUID() wait := make(chan bool) called := false response := false + fileDest := path.Join(form.Path, form.File) + fileSize := ctx.Request.ContentLength common.AddEvent(func(p modules.Packet, _ *melody.Session) { - wait <- false called = true response = true bridge.RemoveBridge(bridgeID) common.RemoveEvent(trigger) + common.Warn(ctx, `UPLOAD_FILE`, `fail`, p.Msg, map[string]any{ + `dest`: fileDest, + `size`: fileSize, + }) ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) + wait <- false }, target, trigger) instance := bridge.AddBridgeWithSrc(nil, bridgeID, ctx) instance.OnPull = func(bridge *bridge.Bridge) { @@ -293,6 +342,12 @@ func UploadToDevice(ctx *gin.Context) { dst.Header(`Content-Disposition`, fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, form.File, url.PathEscape(form.File))) } instance.OnFinish = func(bridge *bridge.Bridge) { + if called { + common.Info(ctx, `UPLOAD_FILE`, `success`, ``, map[string]any{ + `dest`: fileDest, + `size`: fileSize, + }) + } wait <- false } common.SendPackByUUID(modules.Packet{Code: 0, Act: `fetchFile`, Data: gin.H{ @@ -310,6 +365,10 @@ func UploadToDevice(ctx *gin.Context) { bridge.RemoveBridge(bridgeID) common.RemoveEvent(trigger) if !response { + common.Warn(ctx, `UPLOAD_FILE`, `fail`, `timeout`, map[string]any{ + `dest`: fileDest, + `size`: fileSize, + }) ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`}) } } else { diff --git a/server/handler/generate/generate.go b/server/handler/generate/generate.go index d2c73bf..07ca993 100644 --- a/server/handler/generate/generate.go +++ b/server/handler/generate/generate.go @@ -86,7 +86,7 @@ func GenerateClient(ctx *gin.Context) { return } clientUUID := utils.GetUUID() - clientKey, err := common.EncAES(clientUUID, config.Config.StdSalt) + clientKey, err := common.EncAES(clientUUID, config.Config.SaltBytes) if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `${i18n|configGenerateFailed}`}) return diff --git a/server/handler/handler.go b/server/handler/handler.go index 59f96c3..9e4efa9 100644 --- a/server/handler/handler.go +++ b/server/handler/handler.go @@ -29,6 +29,7 @@ func InitRouter(ctx *gin.RouterGroup) { group.POST(`/device/file/list`, file.ListDeviceFiles) group.POST(`/device/file/text`, file.GetDeviceTextFile) group.POST(`/device/file/get`, file.GetDeviceFiles) + group.POST(`/device/exec`, utility.ExecDeviceCmd) group.POST(`/device/list`, utility.GetDevices) group.POST(`/device/:act`, utility.CallDevice) group.POST(`/client/check`, generate.CheckClient) diff --git a/server/handler/process/process.go b/server/handler/process/process.go index 248dc9f..12e250d 100644 --- a/server/handler/process/process.go +++ b/server/handler/process/process.go @@ -46,11 +46,20 @@ func KillDeviceProcess(ctx *gin.Context) { ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) { if p.Code != 0 { ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) + common.Warn(ctx, `KILL_PROCESS`, `fail`, p.Msg, map[string]any{ + `pid`: form.Pid, + }) } else { ctx.JSON(http.StatusOK, modules.Packet{Code: 0}) + common.Info(ctx, `KILL_PROCESS`, `success`, ``, map[string]any{ + `pid`: form.Pid, + }) } }, target, trigger, 5*time.Second) if !ok { ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`}) + common.Warn(ctx, `KILL_PROCESS`, `fail`, `timeout`, map[string]any{ + `pid`: form.Pid, + }) } } diff --git a/server/handler/screenshot/screenshot.go b/server/handler/screenshot/screenshot.go index b02a49a..ab19033 100644 --- a/server/handler/screenshot/screenshot.go +++ b/server/handler/screenshot/screenshot.go @@ -24,11 +24,12 @@ func GetScreenshot(ctx *gin.Context) { 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 bridge.RemoveBridge(bridgeID) common.RemoveEvent(trigger) ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) + common.Warn(ctx, `TAKE_SCREENSHOT`, `fail`, p.Msg, nil) + wait <- false }, target, trigger) instance := bridge.AddBridgeWithDst(nil, bridgeID, ctx) instance.OnPush = func(bridge *bridge.Bridge) { @@ -37,6 +38,9 @@ func GetScreenshot(ctx *gin.Context) { ctx.Header(`Content-Type`, `image/png`) } instance.OnFinish = func(bridge *bridge.Bridge) { + if called { + common.Info(ctx, `TAKE_SCREENSHOT`, `success`, ``, nil) + } wait <- false } select { @@ -46,6 +50,7 @@ func GetScreenshot(ctx *gin.Context) { bridge.RemoveBridge(bridgeID) common.RemoveEvent(trigger) ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`}) + common.Warn(ctx, `TAKE_SCREENSHOT`, `fail`, `timeout`, nil) } else { <-wait } diff --git a/server/handler/terminal/terminal.go b/server/handler/terminal/terminal.go index 8fca215..592a80c 100644 --- a/server/handler/terminal/terminal.go +++ b/server/handler/terminal/terminal.go @@ -9,11 +9,11 @@ import ( "encoding/hex" "github.com/gin-gonic/gin" "net/http" + "reflect" ) type terminal struct { uuid string - event string device string session *melody.Session deviceConn *melody.Session @@ -58,7 +58,7 @@ func InitTerminal(ctx *gin.Context) { terminalSessions.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{ `Secret`: secret, `Device`: device, - `LastPack`: common.Unix, + `LastPack`: utils.Unix, }) } @@ -75,8 +75,15 @@ func terminalEventWrapper(terminal *terminal) common.EventCallback { msg += `${i18n|unknownError}` } sendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session) - common.RemoveEvent(terminal.event) + common.RemoveEvent(terminal.uuid) terminal.session.Close() + common.Warn(terminal.session, `TERMINAL_INIT`, `fail`, msg, map[string]any{ + `deviceConn`: terminal.deviceConn, + }) + } else { + common.Info(terminal.session, `TERMINAL_INIT`, `success`, ``, map[string]any{ + `deviceConn`: terminal.deviceConn, + }) } return } @@ -86,8 +93,11 @@ func terminalEventWrapper(terminal *terminal) common.EventCallback { msg = pack.Msg } sendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session) - common.RemoveEvent(terminal.event) + common.RemoveEvent(terminal.uuid) terminal.session.Close() + common.Info(terminal.session, `TERMINAL_QUIT`, ``, msg, map[string]any{ + `deviceConn`: terminal.deviceConn, + }) return } if pack.Act == `outputTerminal` { @@ -123,19 +133,20 @@ func onTerminalConnect(session *melody.Session) { return } 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(terminalEventWrapper(terminal), connUUID, eventUUID) + common.AddEvent(terminalEventWrapper(terminal), connUUID, termUUID) common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{ `terminal`: termUUID, - }, Event: eventUUID}, deviceConn) + }, Event: termUUID}, deviceConn) + common.Info(terminal.session, `TERMINAL_CONN`, `success`, ``, map[string]any{ + `deviceConn`: terminal.deviceConn, + }) } func onTerminalMessage(session *melody.Session, data []byte) { @@ -151,16 +162,21 @@ func onTerminalMessage(session *melody.Session, data []byte) { return } terminal := val.(*terminal) - session.Set(`LastPack`, common.Unix) + session.Set(`LastPack`, utils.Unix) if pack.Act == `inputTerminal` { if pack.Data == nil { return } - if input, ok := pack.Data[`input`]; ok { + if input, ok := pack.GetData(`input`, reflect.String); ok { + rawInput, _ := hex.DecodeString(input.(string)) + common.Info(terminal.session, `TERMINAL_INPUT`, ``, ``, map[string]any{ + `deviceConn`: terminal.deviceConn, + `input`: utils.BytesToString(rawInput), + }) common.SendPack(modules.Packet{Act: `inputTerminal`, Data: gin.H{ `input`: input, `terminal`: terminal.uuid, - }, Event: terminal.event}, terminal.deviceConn) + }, Event: terminal.uuid}, terminal.deviceConn) } return } @@ -174,7 +190,7 @@ func onTerminalMessage(session *melody.Session, data []byte) { `width`: width, `height`: height, `terminal`: terminal.uuid, - }, Event: terminal.event}, terminal.deviceConn) + }, Event: terminal.uuid}, terminal.deviceConn) } } return @@ -183,9 +199,12 @@ func onTerminalMessage(session *melody.Session, data []byte) { if pack.Data == nil { return } + common.Info(terminal.session, `TERMINAL_KILL`, `success`, ``, map[string]any{ + `deviceConn`: terminal.deviceConn, + }) common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{ `terminal`: terminal.uuid, - }, Event: terminal.event}, terminal.deviceConn) + }, Event: terminal.uuid}, terminal.deviceConn) return } if pack.Act == `ping` { @@ -194,13 +213,14 @@ func onTerminalMessage(session *melody.Session, data []byte) { } common.SendPack(modules.Packet{Act: `pingTerminal`, Data: gin.H{ `terminal`: terminal.uuid, - }, Event: terminal.event}, terminal.deviceConn) + }, Event: terminal.uuid}, terminal.deviceConn) return } session.Close() } func onTerminalDisconnect(session *melody.Session) { + common.Info(session, `TERMINAL_CLOSE`, `success`, ``, nil) val, ok := session.Get(`Terminal`) if !ok { return @@ -211,8 +231,8 @@ func onTerminalDisconnect(session *melody.Session) { } common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{ `terminal`: terminal.uuid, - }, Event: terminal.event}, terminal.deviceConn) - common.RemoveEvent(terminal.event) + }, Event: terminal.uuid}, terminal.deviceConn) + common.RemoveEvent(terminal.uuid) session.Set(`Terminal`, nil) terminal = nil } diff --git a/server/handler/utility/utility.go b/server/handler/utility/utility.go index 7beeca6..95c7c37 100644 --- a/server/handler/utility/utility.go +++ b/server/handler/utility/utility.go @@ -7,6 +7,7 @@ import ( "Spark/utils" "Spark/utils/melody" "bytes" + "context" "crypto/aes" "crypto/cipher" "fmt" @@ -40,6 +41,7 @@ func CheckForm(ctx *gin.Context, form any) (string, bool) { ctx.AbortWithStatusJSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `${i18n|deviceNotExists}`}) return ``, false } + ctx.Request = ctx.Request.WithContext(context.WithValue(ctx.Request.Context(), `ConnUUID`, connUUID)) return connUUID, true } @@ -88,6 +90,12 @@ func OnDevicePack(data []byte, session *melody.Session) error { common.Devices.Remove(exSession) } common.Devices.Set(session.UUID, &pack.Device) + common.Info(nil, `CLIENT_ONLINE`, ``, ``, map[string]any{ + `device`: map[string]any{ + `name`: pack.Device.Hostname, + `ip`: pack.Device.WAN, + }, + }) } else { val, ok := common.Devices.Get(session.UUID) if ok { @@ -111,36 +119,84 @@ func CheckUpdate(ctx *gin.Context) { Commit string `form:"commit" binding:"required"` } if err := ctx.ShouldBind(&form); err != nil { - golog.Error(err) 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}) + common.Warn(ctx, `CLIENT_UPDATE`, `success`, `latest`, map[string]any{ + `client`: map[string]any{ + `os`: form.OS, + `arch`: form.Arch, + `commit`: form.Commit, + }, + `server`: config.COMMIT, + }) return } tpl, err := os.Open(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch)) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`}) + common.Warn(ctx, `CLIENT_UPDATE`, `fail`, `no prebuild asset`, map[string]any{ + `client`: map[string]any{ + `os`: form.OS, + `arch`: form.Arch, + `commit`: form.Commit, + }, + `server`: config.COMMIT, + }) return } const MaxBodySize = 384 // This is size of client config buffer. if ctx.Request.ContentLength > MaxBodySize { ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1}) + common.Warn(ctx, `CLIENT_UPDATE`, `fail`, `config too large`, map[string]any{ + `client`: map[string]any{ + `os`: form.OS, + `arch`: form.Arch, + `commit`: form.Commit, + }, + `server`: config.COMMIT, + }) return } body, err := ctx.GetRawData() if err != nil { ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: 1}) + common.Warn(ctx, `CLIENT_UPDATE`, `fail`, `read config fail`, map[string]any{ + `client`: map[string]any{ + `os`: form.OS, + `arch`: form.Arch, + `commit`: form.Commit, + }, + `server`: config.COMMIT, + }) return } session := common.CheckClientReq(ctx) if session == nil { ctx.AbortWithStatusJSON(http.StatusUnauthorized, modules.Packet{Code: 1}) + common.Warn(ctx, `CLIENT_UPDATE`, `fail`, `check config fail`, map[string]any{ + `client`: map[string]any{ + `os`: form.OS, + `arch`: form.Arch, + `commit`: form.Commit, + }, + `server`: config.COMMIT, + }) return } + common.Info(ctx, `CLIENT_UPDATE`, `success`, `updating`, map[string]any{ + `client`: map[string]any{ + `os`: form.OS, + `arch`: form.Arch, + `commit`: form.Commit, + }, + `server`: config.COMMIT, + }) + ctx.Header(`Spark-Commit`, config.COMMIT) ctx.Header(`Accept-Ranges`, `none`) ctx.Header(`Content-Transfer-Encoding`, `binary`) @@ -171,6 +227,46 @@ func CheckUpdate(ctx *gin.Context) { } } +// ExecDeviceCmd execute command on device. +func ExecDeviceCmd(ctx *gin.Context) { + var form struct { + Cmd string `json:"cmd" yaml:"cmd" form:"cmd" binding:"required"` + Args string `json:"args" yaml:"args" form:"args"` + } + target, ok := CheckForm(ctx, &form) + if !ok { + return + } + if len(form.Cmd) == 0 { + ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`}) + return + } + trigger := utils.GetStrUUID() + common.SendPackByUUID(modules.Packet{Code: 0, Act: `execCommand`, Data: gin.H{`cmd`: form.Cmd, `args`: form.Args}, Event: trigger}, target) + ok = common.AddEventOnce(func(p modules.Packet, _ *melody.Session) { + if p.Code != 0 { + common.Warn(ctx, `EXEC_COMMAND`, `fail`, p.Msg, map[string]any{ + `cmd`: form.Cmd, + `args`: form.Args, + }) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) + } else { + common.Info(ctx, `EXEC_COMMAND`, `success`, ``, map[string]any{ + `cmd`: form.Cmd, + `args`: form.Args, + }) + ctx.JSON(http.StatusOK, modules.Packet{Code: 0}) + } + }, target, trigger, 5*time.Second) + if !ok { + common.Warn(ctx, `EXEC_COMMAND`, `fail`, `timeout`, map[string]any{ + `cmd`: form.Cmd, + `args`: form.Args, + }) + ctx.AbortWithStatusJSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `${i18n|responseTimeout}`}) + } +} + // GetDevices will return all info about all clients. func GetDevices(ctx *gin.Context) { devices := map[string]any{} @@ -199,6 +295,9 @@ func CallDevice(ctx *gin.Context) { } } if !ok { + common.Warn(ctx, `CALL_DEVICE`, `fail`, `invalid act`, map[string]any{ + `act`: act, + }) ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`}) return } @@ -211,14 +310,23 @@ 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 { + common.Warn(ctx, `CALL_DEVICE`, `fail`, p.Msg, map[string]any{ + `act`: act, + }) ctx.AbortWithStatusJSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg}) } else { + common.Info(ctx, `CALL_DEVICE`, `success`, ``, map[string]any{ + `act`: act, + }) 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. + common.Info(ctx, `CALL_DEVICE`, `success`, ``, map[string]any{ + `act`: act, + }) ctx.JSON(http.StatusOK, modules.Packet{Code: 0}) } } diff --git a/server/main.go b/server/main.go index a1aa7ef..3f7c4fc 100644 --- a/server/main.go +++ b/server/main.go @@ -28,51 +28,24 @@ import ( "Spark/utils/melody" "net/http" - "github.com/gin-contrib/pprof" "github.com/gin-gonic/gin" "github.com/kataras/golog" ) +var blocked = cmap.New() var lastRequest = time.Now().Unix() func main() { - golog.SetTimeFormat(`2006/01/02 15:04:05`) - - data, err := os.ReadFile(`./Config.json`) - if err != nil { - golog.Fatal(`Failed to read config file: `, err) - return - } - err = utils.JSON.Unmarshal(data, &config.Config) - if err != nil { - golog.Fatal(`Failed to parse config file: `, err) - return - } - if len(config.Config.Salt) > 24 { - golog.Fatal(`Length of Salt should be less than 24.`) - return - } - config.Config.StdSalt = []byte(config.Config.Salt) - config.Config.StdSalt = append(config.Config.StdSalt, bytes.Repeat([]byte{25}, 24)...) - config.Config.StdSalt = config.Config.StdSalt[:24] - webFS, err := fs.NewWithNamespace(`web`) if err != nil { - golog.Fatal(`Failed to load static resources: `, err) + common.Fatal(nil, `LOAD_STATIC_RES`, `fail`, err.Error(), nil) return } - if config.Config.Debug.Gin { - gin.SetMode(gin.DebugMode) - } else { - gin.SetMode(gin.ReleaseMode) - } + gin.SetMode(gin.ReleaseMode) app := gin.New() app.Use(gin.Recovery()) - if config.Config.Debug.Pprof { - pprof.Register(app) - } { - handler.AuthHandler = authCheck() + handler.AuthHandler = checkAuth() handler.InitRouter(app.Group(`/api`)) app.Any(`/ws`, wsHandshake) app.NoRoute(handler.AuthHandler, func(ctx *gin.Context) { @@ -82,7 +55,7 @@ func main() { }) } - common.Melody.Config.MaxMessageSize = 32768 + 1024 + common.Melody.Config.MaxMessageSize = common.MaxMessageSize common.Melody.HandleConnect(wsOnConnect) common.Melody.HandleMessage(wsOnMessage) common.Melody.HandleMessageBinary(wsOnMessageBinary) @@ -93,33 +66,43 @@ func main() { Addr: config.Config.Listen, Handler: app, ConnContext: func(ctx context.Context, c net.Conn) context.Context { - return context.WithValue(ctx, `Conn`, c) + ctx = context.WithValue(ctx, `Conn`, c) + ctx = context.WithValue(ctx, `ClientIP`, common.GetAddrIP(c.RemoteAddr())) + return ctx }, } - go func() { - if err = srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - golog.Fatal(`Failed to bind address: `, err) + { + go func() { + err = srv.ListenAndServe() + }() + if err != nil { + common.Fatal(nil, `SERVICE_INIT`, `fail`, err.Error(), nil) + } else { + common.Info(nil, `SERVICE_INIT`, ``, ``, map[string]any{ + `listen`: config.Config.Listen, + }) } - }() + } quit := make(chan os.Signal, 3) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - golog.Warn(`Server is shutting down ...`) + common.Warn(nil, `SERVICE_EXITING`, ``, ``, nil) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { - golog.Fatal(`Server shutdown: `, err) + common.Warn(nil, `SERVICE_EXIT`, `error`, err.Error(), nil) } <-ctx.Done() - golog.Info(`Server exited.`) + common.Warn(nil, `SERVICE_EXIT`, `success`, ``, nil) + common.CloseLog() } func wsHandshake(ctx *gin.Context) { 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 + const MaxBodySize = 2 << 18 // 524288 512KB if ctx.Request.ContentLength > MaxBodySize { ctx.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1}) return @@ -145,7 +128,7 @@ func wsHandshake(ctx *gin.Context) { ctx.AbortWithStatus(http.StatusUnauthorized) return } - decrypted, err := common.DecAES(clientKey, config.Config.StdSalt) + decrypted, err := common.DecAES(clientKey, config.Config.SaltBytes) if err != nil || !bytes.Equal(decrypted, clientUUID) { ctx.AbortWithStatus(http.StatusUnauthorized) return @@ -155,7 +138,7 @@ func wsHandshake(ctx *gin.Context) { `Secret`: []string{hex.EncodeToString(secret)}, }, gin.H{ `Secret`: secret, - `LastPack`: common.Unix, + `LastPack`: utils.Unix, `Address`: common.GetRemoteAddr(ctx), }) if err != nil { @@ -169,7 +152,7 @@ func wsOnConnect(session *melody.Session) { pingDevice(session) } -func wsOnMessage(session *melody.Session, bytes []byte) { +func wsOnMessage(session *melody.Session, _ []byte) { session.Close() } @@ -200,7 +183,7 @@ func wsOnMessageBinary(session *melody.Session, data []byte) { return } if pack.Act == `report` || pack.Act == `setDevice` { - session.Set(`LastPack`, common.Unix) + session.Set(`LastPack`, utils.Unix) utility.OnDevicePack(data, session) return } @@ -209,7 +192,7 @@ func wsOnMessageBinary(session *melody.Session, data []byte) { return } common.CallEvent(pack, session) - session.Set(`LastPack`, common.Unix) + session.Set(`LastPack`, utils.Unix) } func wsOnDisconnect(session *melody.Session) { @@ -217,6 +200,18 @@ func wsOnDisconnect(session *melody.Session) { deviceInfo := val.(*modules.Device) terminal.CloseSessionsByDevice(deviceInfo.ID) desktop.CloseSessionsByDevice(deviceInfo.ID) + common.Info(nil, `CLIENT_OFFLINE`, ``, ``, map[string]any{ + `device`: map[string]any{ + `name`: deviceInfo.Hostname, + `ip`: deviceInfo.WAN, + }, + }) + } else { + common.Info(nil, `CLIENT_OFFLINE`, ``, ``, map[string]any{ + `device`: map[string]any{ + `ip`: common.GetAddrIP(session.GetWSConn().UnderlyingConn().RemoteAddr()), + }, + }) } common.Devices.Remove(session.UUID) } @@ -231,7 +226,7 @@ func wsHealthCheck(container *melody.Melody) { var pingInterval int64 = 3 for range time.NewTicker(3 * time.Second).C { tick += 3 - if tick >= common.Unix-lastRequest { + if tick >= utils.Unix-lastRequest { pingInterval = 3 } if tick >= 3 && (tick >= pingInterval || tick >= MaxPingInterval) { @@ -286,7 +281,7 @@ func pingDevice(s *melody.Session) { }, s.UUID, trigger, 3*time.Second) } -func authCheck() gin.HandlerFunc { +func checkAuth() gin.HandlerFunc { // Token as key and update timestamp as value. // Stores authenticated tokens. tokens := cmap.New() @@ -300,19 +295,30 @@ func authCheck() gin.HandlerFunc { return true }) tokens.Remove(queue...) + queue = nil + + blocked.IterCb(func(addr string, v any) bool { + if now.Unix() > v.(int64) { + queue = append(queue, addr) + } + return true + }) + blocked.Remove(queue...) } }() if config.Config.Auth == nil || len(config.Config.Auth) == 0 { return func(ctx *gin.Context) { - lastRequest = common.Unix + lastRequest = utils.Unix ctx.Next() } } + auth := gin.BasicAuth(config.Config.Auth) return func(ctx *gin.Context) { - now := common.Unix + now := utils.Unix passed := false + if token, err := ctx.Cookie(`Authorization`); err == nil { if tokens.Has(token) { lastRequest = now @@ -321,11 +327,32 @@ func authCheck() gin.HandlerFunc { return } } + if !passed { + addr := common.GetRealIP(ctx) + if expire, ok := blocked.Get(addr); ok { + if now < expire.(int64) { + ctx.AbortWithStatusJSON(http.StatusTooManyRequests, modules.Packet{Code: 1}) + return + } + blocked.Remove(addr) + } + auth(ctx) + user, _, _ := ctx.Request.BasicAuth() + if ctx.IsAborted() { + blocked.Set(addr, now+1) + user = utils.If(len(user) == 0, `EMPTY`, user) + common.Warn(ctx, `LOGIN_ATTEMPT`, `fail`, ``, map[string]any{ + `user`: user, + }) return } + + common.Warn(ctx, `LOGIN_ATTEMPT`, `success`, ``, map[string]any{ + `user`: user, + }) token := utils.GetStrUUID() tokens.Set(token, now) ctx.Header(`Set-Cookie`, fmt.Sprintf(`Authorization=%s; Path=/; HttpOnly`, token)) @@ -366,7 +393,7 @@ func serveGzip(ctx *gin.Context, statikFS http.FileSystem) bool { } ctx.Header(`Cache-Control`, `max-age=604800`) ctx.Header(`ETag`, etag) - ctx.Header(`Expires`, common.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`)) + ctx.Header(`Expires`, utils.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`)) ctx.Writer.Header().Del(`Content-Length`) ctx.Header(`Content-Encoding`, `gzip`) @@ -386,7 +413,7 @@ func serveGzip(ctx *gin.Context, statikFS http.FileSystem) bool { break } } - conn.SetWriteDeadline(common.Now.Add(10 * time.Second)) + conn.SetWriteDeadline(utils.Now.Add(10 * time.Second)) _, err = ctx.Writer.Write(buf[:n]) if eof || err != nil { break @@ -408,6 +435,6 @@ func checkCache(ctx *gin.Context, _ http.FileSystem) bool { } ctx.Header(`ETag`, etag) ctx.Header(`Cache-Control`, `max-age=604800`) - ctx.Header(`Expires`, common.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`)) + ctx.Header(`Expires`, utils.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`)) return false } diff --git a/client/common/time.go b/utils/time.go similarity index 94% rename from client/common/time.go rename to utils/time.go index 4615592..b3b0832 100644 --- a/client/common/time.go +++ b/utils/time.go @@ -1,4 +1,4 @@ -package common +package utils import "time" diff --git a/utils/utils.go b/utils/utils.go index 3295154..2a8607c 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -8,6 +8,7 @@ import ( "crypto/rand" "encoding/hex" "errors" + "fmt" jsoniter "github.com/json-iterator/go" "reflect" "unsafe" @@ -107,6 +108,57 @@ func Decrypt(data []byte, key []byte) ([]byte, error) { return decBuffer, nil } +func FormatSize(size int64) string { + sizes := []string{`B`, `KB`, `MB`, `GB`, `TB`, `PB`, `EB`, `ZB`, `YB`} + i := 0 + for size >= 1024 && i < len(sizes)-1 { + size /= 1024 + i++ + } + return fmt.Sprintf(`%d%s`, size, sizes[i]) +} + +func BytesToString(b []byte, r ...int) string { + sh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + bytesPtr := sh.Data + bytesLen := sh.Len + switch len(r) { + case 1: + r[0] = If(r[0] > bytesLen, bytesLen, r[0]) + bytesLen -= r[0] + bytesPtr += uintptr(r[0]) + case 2: + r[0] = If(r[0] > bytesLen, bytesLen, r[0]) + bytesLen = If(r[1] > bytesLen, bytesLen, r[1]) - r[0] + bytesPtr += uintptr(r[0]) + } + return *(*string)(unsafe.Pointer(&reflect.StringHeader{ + Data: bytesPtr, + Len: bytesLen, + })) +} + +func StringToBytes(s string, r ...int) []byte { + sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + strPtr := sh.Data + strLen := sh.Len + switch len(r) { + case 1: + r[0] = If(r[0] > strLen, strLen, r[0]) + strLen -= r[0] + strPtr += uintptr(r[0]) + case 2: + r[0] = If(r[0] > strLen, strLen, r[0]) + strLen = If(r[1] > strLen, strLen, r[1]) - r[0] + strPtr += uintptr(r[0]) + } + return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: strPtr, + Len: strLen, + Cap: strLen, + })) +} + func GetSlicePrefix[T any](data *[]T, n int) *[]T { sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(data)) return (*[]T)(unsafe.Pointer(&reflect.SliceHeader{ diff --git a/web/public/index.html b/web/public/index.html index 476109b..9dc5bfd 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -9,7 +9,7 @@ - -
+ +
diff --git a/web/src/components/explorer.js b/web/src/components/explorer.js index 8278d18..21a6cd7 100644 --- a/web/src/components/explorer.js +++ b/web/src/components/explorer.js @@ -14,7 +14,7 @@ import { Spin } from "antd"; import ProTable, {TableDropdown} from "@ant-design/pro-table"; -import {catchBlobReq, formatSize, orderCompare, post, preventClose, request, translate, waitTime} from "../utils/utils"; +import {catchBlobReq, formatSize, orderCompare, post, preventClose, request, waitTime} from "../utils/utils"; import dayjs from "dayjs"; import i18n from "../locale/locale"; import {VList} from "virtuallist-antd"; diff --git a/web/src/components/generate.js b/web/src/components/generate.js index 34566e6..e5b43ea 100644 --- a/web/src/components/generate.js +++ b/web/src/components/generate.js @@ -15,13 +15,11 @@ function Generate(props) { } form.secure = location.protocol === 'https:' ? 'true' : 'false'; let basePath = location.origin + location.pathname + 'api/client/'; - request(basePath + 'check', form) - .then((res) => { - if (res.data.code === 0) { - post(basePath += 'generate', form); - } - }) - .catch() + request(basePath + 'check', form).then(res => { + if (res.data.code === 0) { + post(basePath += 'generate', form); + } + }).catch(); } function getInitValues() { diff --git a/web/src/components/runner.js b/web/src/components/runner.js new file mode 100644 index 0000000..b6779eb --- /dev/null +++ b/web/src/components/runner.js @@ -0,0 +1,52 @@ +import React from 'react'; +import {ModalForm, ProFormText} from '@ant-design/pro-form'; +import {request} from "../utils/utils"; +import i18n from "../locale/locale"; +import {message} from "antd"; + +function Runner(props) { + async function onFinish(form) { + form.device = props.device.id; + let basePath = location.origin + location.pathname + 'api/device/'; + request(basePath + 'exec', form).then(res => { + if (res.data.code === 0) { + message.success(i18n.t('executionSuccess')); + } + }); + } + + return ( + { + if (!visible) props.onCancel(); + }} + submitter={{ + render: (_, elems) => elems.pop() + }} + {...props} + > + + + + ) +} + +export default Runner; \ No newline at end of file diff --git a/web/src/locale/en.json b/web/src/locale/en.json index 02356a0..4dc3bcc 100644 --- a/web/src/locale/en.json +++ b/web/src/locale/en.json @@ -25,6 +25,7 @@ "terminal": "Terminal", "procMgr": "ProcMgr", "fileMgr": "Explorer", + "run": "Run", "screenshot": "Screenshot", "desktop": "Desktop", "lock": "Lock", @@ -116,5 +117,9 @@ "screenshotObtainFailed": "Failed to obtain screenshot", "noDisplayFound": "No display found", + "executionSuccess": "Execution success", + "cmdPlaceholder": "Command", + "argsPlaceholder": "Arguments (separated by space)", + "colon": ": " } \ No newline at end of file diff --git a/web/src/locale/zh-CN.json b/web/src/locale/zh-CN.json index dd6d427..f18c2f5 100644 --- a/web/src/locale/zh-CN.json +++ b/web/src/locale/zh-CN.json @@ -25,6 +25,7 @@ "terminal": "终端", "procMgr": "进程", "fileMgr": "文件", + "run": "运行", "screenshot": "截屏", "desktop": "桌面", "lock": "锁屏", @@ -116,5 +117,9 @@ "screenshotObtainFailed": "截屏读取失败", "noDisplayFound": "设备未连接显示器", + "executionSuccess": "执行成功", + "cmdPlaceholder": "命令", + "argsPlaceholder": "参数(以空格分隔)", + "colon": ":" } \ No newline at end of file diff --git a/web/src/pages/overview.js b/web/src/pages/overview.js index 713cda5..49f8812 100644 --- a/web/src/pages/overview.js +++ b/web/src/pages/overview.js @@ -7,6 +7,7 @@ import Explorer from "../components/explorer"; import Terminal from "../components/terminal"; import ProcMgr from "../components/procmgr"; import Desktop from "../components/desktop"; +import Runner from "../components/runner"; import {QuestionCircleOutlined} from "@ant-design/icons"; import i18n from "../locale/locale"; @@ -38,6 +39,7 @@ function UsageBar(props) { } function overview(props) { + const [runner, setRunner] = useState(false); const [desktop, setDesktop] = useState(false); const [procMgr, setProcMgr] = useState(false); const [explorer, setExplorer] = useState(false); @@ -271,8 +273,9 @@ function overview(props) { } function renderOperation(device) { let menus = [ - {key: 'screenshot', name: i18n.t('screenshot')}, + {key: 'run', name: i18n.t('run')}, {key: 'desktop', name: i18n.t('desktop')}, + {key: 'screenshot', name: i18n.t('screenshot')}, {key: 'lock', name: i18n.t('lock')}, {key: 'logoff', name: i18n.t('logoff')}, {key: 'hibernate', name: i18n.t('hibernate')}, @@ -299,6 +302,14 @@ function overview(props) { } function callDevice(act, device) { + if (act === 'run') { + setRunner(device); + return; + } + if (act === 'desktop') { + setDesktop(device); + return; + } if (act === 'screenshot') { request('/api/device/screenshot/get', {device: device.id}, {}, { responseType: 'blob' @@ -312,10 +323,6 @@ function overview(props) { }).catch(catchBlobReq); return; } - if (act === 'desktop') { - setDesktop(device); - return; - } Modal.confirm({ title: i18n.t('operationConfirm').replace('{0}', i18n.t(act).toUpperCase()), icon: , @@ -409,6 +416,11 @@ function overview(props) { device={procMgr} onCancel={setProcMgr.bind(null, false)} /> +