mirror of
https://github.com/bjdgyc/anylink.git
synced 2025-09-29 00:19:36 +08:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dd1eae5d32 | ||
|
879b9114ac | ||
|
e5c4a47a37 | ||
|
a0669e1e32 | ||
|
ea7d27b4f0 | ||
|
9f2e9de49a | ||
|
8709dbaba1 | ||
|
4928ad5f62 | ||
|
0f91c779e3 | ||
|
3464d1d10e | ||
|
1579e92ba1 | ||
|
48327fe8d3 | ||
|
ef7723b03b | ||
|
0baab68bb2 | ||
|
665732fc03 | ||
|
edb0fe2dc9 | ||
|
1c6572f5e3 | ||
|
103329c3d0 | ||
|
d40b753871 | ||
|
fa5a58e98d | ||
|
62f30c05ff | ||
|
c02ffc27c0 | ||
|
a4e09e7719 | ||
|
631e49bd41 | ||
|
ef95b1f927 |
5
.codecov.yml
Normal file
5
.codecov.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
ignore:
|
||||
- "screenshot"
|
||||
- "web"
|
||||
- "server/conf"
|
||||
- "server/files"
|
21
.github/workflows/go.yml
vendored
21
.github/workflows/go.yml
vendored
@@ -2,9 +2,9 @@ name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -24,10 +24,21 @@ jobs:
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
cd server
|
||||
go get -v -t -d ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
run: |
|
||||
cd server
|
||||
go build -v -o anylink -ldflags "-X main.COMMIT_ID=`git rev-parse HEAD`"
|
||||
./anylink -rev
|
||||
|
||||
- name: Test
|
||||
run: go test -v .
|
||||
- name: Test coverage
|
||||
run: |
|
||||
cd server
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic -v ./...
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
run: |
|
||||
cd server
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
|
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,19 +1,3 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
|
||||
ui/
|
||||
.idea/
|
||||
anylink
|
||||
anylink-deploy
|
||||
|
46
README.md
46
README.md
@@ -1,6 +1,9 @@
|
||||
# AnyLink
|
||||
|
||||
[](https://github.com/bjdgyc/anylink/actions)
|
||||
[](https://pkg.go.dev/github.com/bjdgyc/anylink)
|
||||
[](https://goreportcard.com/report/github.com/bjdgyc/anylink)
|
||||
[](https://codecov.io/gh/bjdgyc/anylink)
|
||||
|
||||
AnyLink 是一个企业级远程办公ssl vpn软件,可以支持多人同时在线使用。
|
||||
|
||||
@@ -17,11 +20,11 @@ AnyLink 基于 [ietf-openconnect](https://tools.ietf.org/html/draft-mavrogiannop
|
||||
|
||||
AnyLink 使用TLS/DTLS进行数据加密,因此需要RSA或ECC证书,可以通过 Let's Encrypt 和 TrustAsia 申请免费的SSL证书。
|
||||
|
||||
AnyLink 服务端仅在CentOS7测试通过,如需要安装在其他系统,需要服务端支持tun/tap功能、ip设置命令。
|
||||
AnyLink 服务端仅在CentOS 7、Ubuntu 18.04测试通过,如需要安装在其他系统,需要服务端支持tun/tap功能、ip设置命令。
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
@@ -31,11 +34,14 @@ AnyLink 服务端仅在CentOS7测试通过,如需要安装在其他系统,
|
||||
git clone https://github.com/bjdgyc/anylink.git
|
||||
|
||||
cd anylink
|
||||
sh deploy.sh
|
||||
sh build.sh
|
||||
|
||||
#注意使用root权限运行
|
||||
# 注意使用root权限运行
|
||||
cd anylink-deploy
|
||||
sudo ./anylink -conf="conf/server.toml"
|
||||
|
||||
# 默认管理后台访问地址
|
||||
# http://host:8800
|
||||
```
|
||||
|
||||
## Feature
|
||||
@@ -49,10 +55,11 @@ sudo ./anylink -conf="conf/server.toml"
|
||||
- [x] 用户组支持
|
||||
- [x] 多用户支持
|
||||
- [x] TOTP令牌支持
|
||||
- [x] TOTP令牌开关
|
||||
- [x] 流量控制
|
||||
- [x] 后台管理界面
|
||||
- [x] 访问权限管理
|
||||
|
||||
- [ ] 访问权限管理
|
||||
- [ ] DTLS-UDP通道
|
||||
|
||||
## Config
|
||||
@@ -67,7 +74,7 @@ sudo ./anylink -conf="conf/server.toml"
|
||||
./anylink -secret
|
||||
```
|
||||
|
||||
[conf/server.toml](https://github.com/bjdgyc/anylink/blob/master/conf/server.toml)
|
||||
[conf/server.toml](server/conf/server.toml)
|
||||
|
||||
## Setting
|
||||
|
||||
@@ -122,11 +129,11 @@ sh bridge-init.sh
|
||||
|
||||
## Soft
|
||||
|
||||
相关软件下载: https://gitee.com/bjdgyc/anylink-soft
|
||||
相关软件下载: QQ群共享文件: 567510628
|
||||
|
||||
## Discussion
|
||||
|
||||

|
||||

|
||||
|
||||
添加QQ群: 567510628
|
||||
|
||||
@@ -136,19 +143,26 @@ sh bridge-init.sh
|
||||
|
||||
## Other Screenshot
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
<details>
|
||||
<summary>展开查看</summary>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## License
|
||||
|
||||
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 LICENSE 文件中。
|
||||
|
||||
|
||||
|
||||
|
||||
## Thank
|
||||
|
||||
<a href="https://www.jetbrains.com">
|
||||
<img src="screenshot/jetbrains.png" width="200" height="200" alt="jetbrains.png" />
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
90
base/log.go
90
base/log.go
@@ -1,90 +0,0 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
_Debug = iota
|
||||
_Info
|
||||
_Warn
|
||||
_Error
|
||||
_Fatal
|
||||
)
|
||||
|
||||
var (
|
||||
baseLog *log.Logger
|
||||
baseLevel int
|
||||
levels map[int]string
|
||||
)
|
||||
|
||||
func initLog() {
|
||||
baseLog = log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile)
|
||||
baseLevel = logLevel2Int(Cfg.LogLevel)
|
||||
}
|
||||
|
||||
func logLevel2Int(l string) int {
|
||||
levels = map[int]string{
|
||||
_Debug: "Debug",
|
||||
_Info: "Info",
|
||||
_Warn: "Warn",
|
||||
_Error: "Error",
|
||||
_Fatal: "Fatal",
|
||||
}
|
||||
lvl := _Info
|
||||
for k, v := range levels {
|
||||
if strings.ToLower(l) == strings.ToLower(v) {
|
||||
lvl = k
|
||||
}
|
||||
}
|
||||
return lvl
|
||||
}
|
||||
|
||||
func output(l int, s ...interface{}) {
|
||||
lvl := fmt.Sprintf("[%s] ", levels[l])
|
||||
baseLog.Output(3, lvl+fmt.Sprintln(s...))
|
||||
}
|
||||
|
||||
func Debug(v ...interface{}) {
|
||||
l := _Debug
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
}
|
||||
|
||||
func Info(v ...interface{}) {
|
||||
l := _Info
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
}
|
||||
|
||||
func Warn(v ...interface{}) {
|
||||
l := _Warn
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
}
|
||||
|
||||
func Error(v ...interface{}) {
|
||||
l := _Error
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
}
|
||||
|
||||
func Fatal(v ...interface{}) {
|
||||
l := _Fatal
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
os.Exit(1)
|
||||
}
|
35
build.sh
Normal file
35
build.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#当前目录
|
||||
cpath=$(pwd)
|
||||
|
||||
echo "编译二进制文件"
|
||||
cd $cpath/server
|
||||
go build -o anylink -ldflags "-X main.COMMIT_ID=$(git rev-parse HEAD)"
|
||||
|
||||
echo "编译前端项目"
|
||||
cd $cpath/web
|
||||
#国内可替换源加快速度
|
||||
npm install --registry=https://registry.npm.taobao.org
|
||||
npm run build --registry=https://registry.npm.taobao.org
|
||||
#npm install
|
||||
#npm run build
|
||||
|
||||
cd $cpath
|
||||
|
||||
echo "整理部署文件"
|
||||
deploy="anylink-deploy"
|
||||
rm -rf $deploy
|
||||
mkdir $deploy
|
||||
mkdir $deploy/log
|
||||
|
||||
cp -r server/anylink $deploy
|
||||
cp -r server/conf $deploy
|
||||
cp -r server/files $deploy
|
||||
cp -r server/bridge-init.sh $deploy
|
||||
|
||||
cp -r web/ui $deploy
|
||||
|
||||
#注意使用root权限运行
|
||||
#cd anylink-deploy
|
||||
#sudo ./anylink -conf="conf/server.toml"
|
28
deploy.sh
28
deploy.sh
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#编译二进制文件
|
||||
go build -o anylink -ldflags "-X main.COMMIT_ID=`git rev-parse HEAD`"
|
||||
|
||||
#编译前端项目
|
||||
git clone https://github.com/bjdgyc/anylink-web.git
|
||||
cd anylink-web
|
||||
#国内可替换源加快速度
|
||||
#npm install --registry=https://registry.npm.taobao.org
|
||||
#npm run build --registry=https://registry.npm.taobao.org
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
cd ../
|
||||
|
||||
#整理部署文件
|
||||
mkdir anylink-deploy
|
||||
|
||||
cp -r anylink anylink-deploy
|
||||
cp -r anylink-web/ui anylink-deploy
|
||||
cp -r conf anylink-deploy
|
||||
cp -r down_files anylink-deploy
|
||||
|
||||
#注意使用root权限运行
|
||||
#cd anylink-deploy
|
||||
#sudo ./anylink -conf="conf/server.toml"
|
||||
|
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -1,47 +0,0 @@
|
||||
package handler
|
||||
|
||||
import "github.com/bjdgyc/anylink/sessdata"
|
||||
|
||||
func payloadIn(cSess *sessdata.ConnSession, lType sessdata.LType, pType byte, data []byte) bool {
|
||||
payload := &sessdata.Payload{
|
||||
LType: lType,
|
||||
PType: pType,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
return payloadInData(cSess, payload)
|
||||
}
|
||||
|
||||
func payloadInData(cSess *sessdata.ConnSession, payload *sessdata.Payload) bool {
|
||||
closed := false
|
||||
|
||||
select {
|
||||
case cSess.PayloadIn <- payload:
|
||||
case <-cSess.CloseChan:
|
||||
closed = true
|
||||
}
|
||||
|
||||
return closed
|
||||
}
|
||||
|
||||
func payloadOut(cSess *sessdata.ConnSession, lType sessdata.LType, pType byte, data []byte) bool {
|
||||
payload := &sessdata.Payload{
|
||||
LType: lType,
|
||||
PType: pType,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
return payloadOutData(cSess, payload)
|
||||
}
|
||||
|
||||
func payloadOutData(cSess *sessdata.ConnSession, payload *sessdata.Payload) bool {
|
||||
closed := false
|
||||
|
||||
select {
|
||||
case cSess.PayloadOut <- payload:
|
||||
case <-cSess.CloseChan:
|
||||
closed = true
|
||||
}
|
||||
|
||||
return closed
|
||||
}
|
BIN
screenshot/jetbrains.png
Normal file
BIN
screenshot/jetbrains.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 164 KiB |
5
server/.codecov.yml
Normal file
5
server/.codecov.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
ignore:
|
||||
- "screenshot"
|
||||
- "web"
|
||||
- "server/conf"
|
||||
- "server/files"
|
19
server/.gitignore
vendored
Normal file
19
server/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
|
||||
ui/
|
||||
.idea/
|
||||
anylink
|
@@ -10,26 +10,26 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// 登陆接口
|
||||
// Login 登陆接口
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO 调试信息输出
|
||||
// hd, _ := httputil.DumpRequest(r, true)
|
||||
// fmt.Println("DumpRequest: ", string(hd))
|
||||
|
||||
r.ParseForm()
|
||||
admin_user := r.PostFormValue("admin_user")
|
||||
admin_pass := r.PostFormValue("admin_pass")
|
||||
_ = r.ParseForm()
|
||||
adminUser := r.PostFormValue("admin_user")
|
||||
adminPass := r.PostFormValue("admin_pass")
|
||||
|
||||
// 认证错误
|
||||
if !(admin_user == base.Cfg.AdminUser &&
|
||||
utils.PasswordVerify(admin_pass, base.Cfg.AdminPass)) {
|
||||
if !(adminUser == base.Cfg.AdminUser &&
|
||||
utils.PasswordVerify(adminPass, base.Cfg.AdminPass)) {
|
||||
RespError(w, RespUserOrPassErr)
|
||||
return
|
||||
}
|
||||
|
||||
// token有效期
|
||||
expiresAt := time.Now().Unix() + 3600*3
|
||||
jwtData := map[string]interface{}{"admin_user": admin_user}
|
||||
jwtData := map[string]interface{}{"admin_user": adminUser}
|
||||
tokenString, err := SetJwtData(jwtData, expiresAt)
|
||||
if err != nil {
|
||||
RespError(w, 1, err)
|
||||
@@ -38,7 +38,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
data := make(map[string]interface{})
|
||||
data["token"] = tokenString
|
||||
data["admin_user"] = admin_user
|
||||
data["admin_user"] = adminUser
|
||||
data["expires_at"] = expiresAt
|
||||
|
||||
RespSucess(w, data)
|
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func GroupList(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
pageS := r.FormValue("page")
|
||||
page, _ := strconv.Atoi(pageS)
|
||||
if page < 1 {
|
||||
@@ -48,7 +48,7 @@ func GroupNames(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func GroupDetail(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
idS := r.FormValue("id")
|
||||
id, _ := strconv.Atoi(idS)
|
||||
if id < 1 {
|
||||
@@ -90,7 +90,7 @@ func GroupSet(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func GroupDel(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
idS := r.FormValue("id")
|
||||
id, _ := strconv.Atoi(idS)
|
||||
if id < 1 {
|
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func UserIpMapList(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
pageS := r.FormValue("page")
|
||||
page, _ := strconv.Atoi(pageS)
|
||||
if page < 1 {
|
||||
@@ -39,7 +39,7 @@ func UserIpMapList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func UserIpMapDetail(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
idS := r.FormValue("id")
|
||||
id, _ := strconv.Atoi(idS)
|
||||
if id < 1 {
|
||||
@@ -58,7 +58,7 @@ func UserIpMapDetail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func UserIpMapSet(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -92,7 +92,7 @@ func UserIpMapSet(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func UserIpMapDel(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
idS := r.FormValue("id")
|
||||
id, _ := strconv.Atoi(idS)
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
@@ -84,9 +83,8 @@ func SetSystem(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func SetSoft(w http.ResponseWriter, r *http.Request) {
|
||||
datas := base.ServerCfg2Slice()
|
||||
b, _ := json.Marshal(datas)
|
||||
w.Write(b)
|
||||
data := base.ServerCfg2Slice()
|
||||
RespSucess(w, data)
|
||||
}
|
||||
|
||||
func decimal(f float64) float64 {
|
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func UserList(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
prefix := r.FormValue("prefix")
|
||||
pageS := r.FormValue("page")
|
||||
page, _ := strconv.Atoi(pageS)
|
||||
@@ -58,7 +58,7 @@ func UserList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func UserDetail(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
idS := r.FormValue("id")
|
||||
id, _ := strconv.Atoi(idS)
|
||||
if id < 1 {
|
||||
@@ -77,7 +77,7 @@ func UserDetail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func UserSet(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -111,7 +111,7 @@ func UserSet(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func UserDel(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
idS := r.FormValue("id")
|
||||
id, _ := strconv.Atoi(idS)
|
||||
|
||||
@@ -130,7 +130,7 @@ func UserDel(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func UserOtpQr(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
b64 := r.FormValue("b64")
|
||||
idS := r.FormValue("id")
|
||||
id, _ := strconv.Atoi(idS)
|
||||
@@ -148,11 +148,16 @@ func UserOtpQr(w http.ResponseWriter, r *http.Request) {
|
||||
if b64 == "1" {
|
||||
data, _ := qr.PNG(300)
|
||||
s := base64.StdEncoding.EncodeToString(data)
|
||||
fmt.Fprint(w, s)
|
||||
} else {
|
||||
qr.Write(300, w)
|
||||
_, err = fmt.Fprint(w, s)
|
||||
if err != nil {
|
||||
base.Error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = qr.Write(300, w)
|
||||
if err != nil {
|
||||
base.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 在线用户
|
||||
@@ -169,14 +174,14 @@ func UserOnline(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func UserOffline(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
token := r.FormValue("token")
|
||||
sessdata.CloseSess(token)
|
||||
RespSucess(w, nil)
|
||||
}
|
||||
|
||||
func UserReline(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
token := r.FormValue("token")
|
||||
sessdata.CloseCSess(token)
|
||||
RespSucess(w, nil)
|
||||
@@ -231,7 +236,10 @@ func userAccountMail(user *dbdata.User) error {
|
||||
}
|
||||
w := bytes.NewBufferString("")
|
||||
t, _ := template.New("auth_complete").Parse(htmlBody)
|
||||
t.Execute(w, data)
|
||||
err = t.Execute(w, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// fmt.Println(w.String())
|
||||
return SendMail(base.Cfg.Issuer+"平台通知", user.Email, w.String())
|
||||
}
|
@@ -8,8 +8,8 @@ import (
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
"github.com/bjdgyc/anylink/dbdata"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
// "github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
func SetJwtData(data map[string]interface{}, expiresAt int64) (string, error) {
|
||||
@@ -43,15 +43,6 @@ func GetJwtData(jwtToken string) (map[string]interface{}, error) {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func createCaptcha() {
|
||||
var store = base64Captcha.DefaultMemStore
|
||||
var driver base64Captcha.Driver
|
||||
driverString := &base64Captcha.DriverString{}
|
||||
driver = driverString.ConvertFonts()
|
||||
c := base64Captcha.NewCaptcha(driver, store)
|
||||
_ = c
|
||||
}
|
||||
|
||||
func SendMail(subject, to, htmlBody string) error {
|
||||
|
||||
dataSmtp := &dbdata.SettingSmtp{}
|
23
server/admin/common_test.go
Normal file
23
server/admin/common_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJwtData(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
base.Cfg.JwtSecret = "dsfasfdfsadfasdfasd3sdaf"
|
||||
data := map[string]interface{}{
|
||||
"key": "value",
|
||||
}
|
||||
expiresAt := time.Now().Add(time.Minute).Unix()
|
||||
token, err := SetJwtData(data, expiresAt)
|
||||
assert.Nil(err)
|
||||
dataN, err := GetJwtData(token)
|
||||
assert.Nil(err)
|
||||
assert.Equal(dataN["key"], "value")
|
||||
}
|
@@ -43,8 +43,10 @@ func respHttp(w http.ResponseWriter, respCode int, data interface{}, errS ...int
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
|
||||
_, err = w.Write(b)
|
||||
if err != nil {
|
||||
base.Error(err)
|
||||
}
|
||||
// 记录返回数据
|
||||
// logger.Category("response").Debug(string(b))
|
||||
}
|
39
server/admin/resp_test.go
Normal file
39
server/admin/resp_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRespSucess(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
w := httptest.NewRecorder()
|
||||
RespSucess(w, "data")
|
||||
// fmt.Println(w)
|
||||
assert.Equal(w.Code, 200)
|
||||
body, _ := ioutil.ReadAll(w.Body)
|
||||
res := Resp{}
|
||||
err := json.Unmarshal(body, &res)
|
||||
assert.Nil(err)
|
||||
assert.Equal(res.Code, 0)
|
||||
assert.Equal(res.Data, "data")
|
||||
|
||||
}
|
||||
|
||||
func TestRespError(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
w := httptest.NewRecorder()
|
||||
RespError(w, 10, "err-msg")
|
||||
// fmt.Println(w)
|
||||
assert.Equal(w.Code, 200)
|
||||
body, _ := ioutil.ReadAll(w.Body)
|
||||
res := Resp{}
|
||||
err := json.Unmarshal(body, &res)
|
||||
assert.Nil(err)
|
||||
assert.Equal(res.Code, 10)
|
||||
assert.Equal(res.Msg, "err-msg")
|
||||
}
|
@@ -2,5 +2,5 @@ package base
|
||||
|
||||
const (
|
||||
APP_NAME = "AnyLink"
|
||||
APP_VER = "0.1.0"
|
||||
APP_VER = "0.1.6"
|
||||
)
|
@@ -40,7 +40,8 @@ type ServerConfig struct {
|
||||
CertFile string `toml:"cert_file" info:"证书文件"`
|
||||
CertKey string `toml:"cert_key" info:"证书密钥"`
|
||||
UiPath string `toml:"ui_path" info:"ui文件路径"`
|
||||
DownFilesPath string `toml:"down_files_path" info:"外部下载文件路径"`
|
||||
FilesPath string `toml:"files_path" info:"外部下载文件路径"`
|
||||
LogPath string `toml:"log_path" info:"日志文件路径"`
|
||||
LogLevel string `toml:"log_level" info:"日志等级"`
|
||||
Issuer string `toml:"issuer" info:"系统名称"`
|
||||
AdminUser string `toml:"admin_user" info:"管理用户名"`
|
||||
@@ -48,8 +49,7 @@ type ServerConfig struct {
|
||||
JwtSecret string `toml:"jwt_secret" info:"JWT密钥"`
|
||||
|
||||
LinkMode string `toml:"link_mode" info:"虚拟网络类型"` // tun tap
|
||||
Ipv4Network string `toml:"ipv4_network" info:"ipv4_network"` // 192.168.1.0
|
||||
Ipv4Netmask string `toml:"ipv4_netmask" info:"ipv4_netmask"` // 255.255.255.0
|
||||
Ipv4CIDR string `toml:"ipv4_cidr" info:"ip地址网段"` // 192.168.1.0/24
|
||||
Ipv4Gateway string `toml:"ipv4_gateway" info:"ipv4_gateway"`
|
||||
Ipv4Pool []string `toml:"ipv4_pool" info:"IPV4起止地址池"` // Pool[0]=192.168.1.100 Pool[1]=192.168.1.200
|
||||
IpLease int `toml:"ip_lease" info:"IP租期(秒)"`
|
||||
@@ -84,7 +84,8 @@ func initServerCfg() {
|
||||
Cfg.CertFile = getAbsPath(base, Cfg.CertFile)
|
||||
Cfg.CertKey = getAbsPath(base, Cfg.CertKey)
|
||||
Cfg.UiPath = getAbsPath(base, Cfg.UiPath)
|
||||
Cfg.DownFilesPath = getAbsPath(base, Cfg.DownFilesPath)
|
||||
Cfg.FilesPath = getAbsPath(base, Cfg.FilesPath)
|
||||
Cfg.LogPath = getAbsPath(base, Cfg.LogPath)
|
||||
|
||||
if len(Cfg.JwtSecret) < 20 {
|
||||
fmt.Println("请设置 jwt_secret 长度20位以上")
|
||||
@@ -95,6 +96,10 @@ func initServerCfg() {
|
||||
}
|
||||
|
||||
func getAbsPath(base, cfile string) string {
|
||||
if cfile == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
abs := filepath.IsAbs(cfile)
|
||||
if abs {
|
||||
return cfile
|
||||
@@ -102,16 +107,17 @@ func getAbsPath(base, cfile string) string {
|
||||
return filepath.Join(base, cfile)
|
||||
}
|
||||
|
||||
func ServerCfg2Slice() interface{} {
|
||||
ref := reflect.ValueOf(Cfg)
|
||||
s := ref.Elem()
|
||||
|
||||
type cfg struct {
|
||||
type SCfg struct {
|
||||
Name string `json:"name"`
|
||||
Info string `json:"info"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
var datas []cfg
|
||||
}
|
||||
|
||||
func ServerCfg2Slice() []SCfg {
|
||||
ref := reflect.ValueOf(Cfg)
|
||||
s := ref.Elem()
|
||||
|
||||
var datas []SCfg
|
||||
|
||||
typ := s.Type()
|
||||
numFields := s.NumField()
|
||||
@@ -122,7 +128,7 @@ func ServerCfg2Slice() interface{} {
|
||||
tags := strings.Split(tag, ",")
|
||||
info := field.Tag.Get("info")
|
||||
|
||||
datas = append(datas, cfg{Name: tags[0], Info: info, Data: value.Interface()})
|
||||
datas = append(datas, SCfg{Name: tags[0], Info: info, Data: value.Interface()})
|
||||
}
|
||||
|
||||
return datas
|
@@ -26,7 +26,7 @@ var (
|
||||
)
|
||||
|
||||
func initFlag() {
|
||||
flag.StringVar(&serverFile, "conf", "./conf/server.toml", "server config file path")
|
||||
flag.StringVar(&serverFile, "conf", "./conf/server.toml", "server config files path")
|
||||
flag.StringVar(&passwd, "passwd", "", "convert the password plaintext")
|
||||
flag.BoolVar(&secret, "secret", false, "generate a random jwt secret")
|
||||
flag.BoolVar(&rev, "rev", false, "display version info")
|
146
server/base/log.go
Normal file
146
server/base/log.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
_Debug = iota
|
||||
_Info
|
||||
_Warn
|
||||
_Error
|
||||
_Fatal
|
||||
)
|
||||
|
||||
var (
|
||||
baseLog *log.Logger
|
||||
baseLevel int
|
||||
levels map[int]string
|
||||
|
||||
dateFormat = "2006-01-02"
|
||||
logName = "anylink.log"
|
||||
)
|
||||
|
||||
// 实现 os.Writer 接口
|
||||
type logWriter struct {
|
||||
UseStdout bool
|
||||
FileName string
|
||||
File *os.File
|
||||
NowDate string
|
||||
}
|
||||
|
||||
// 实现日志文件的切割
|
||||
func (lw *logWriter) Write(p []byte) (n int, err error) {
|
||||
if !lw.UseStdout {
|
||||
return lw.File.Write(p)
|
||||
}
|
||||
|
||||
date := time.Now().Format(dateFormat)
|
||||
if lw.NowDate != date {
|
||||
_ = lw.File.Close()
|
||||
_ = os.Rename(lw.FileName, lw.FileName+"."+lw.NowDate)
|
||||
lw.NowDate = date
|
||||
lw.newFile()
|
||||
}
|
||||
return lw.File.Write(p)
|
||||
}
|
||||
|
||||
// 创建新文件
|
||||
func (lw *logWriter) newFile() {
|
||||
if lw.UseStdout {
|
||||
lw.File = os.Stdout
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(lw.FileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
lw.File = f
|
||||
}
|
||||
|
||||
func initLog() {
|
||||
// 初始化 baseLog
|
||||
baseLw := &logWriter{
|
||||
UseStdout: Cfg.LogPath == "",
|
||||
FileName: path.Join(Cfg.LogPath, logName),
|
||||
NowDate: time.Now().Format(dateFormat),
|
||||
}
|
||||
|
||||
baseLw.newFile()
|
||||
baseLevel = logLevel2Int(Cfg.LogLevel)
|
||||
baseLog = log.New(baseLw, "", log.LstdFlags|log.Lshortfile)
|
||||
}
|
||||
|
||||
// 获取 log.Logger
|
||||
func GetBaseLog() *log.Logger {
|
||||
return baseLog
|
||||
}
|
||||
|
||||
func logLevel2Int(l string) int {
|
||||
levels = map[int]string{
|
||||
_Debug: "Debug",
|
||||
_Info: "Info",
|
||||
_Warn: "Warn",
|
||||
_Error: "Error",
|
||||
_Fatal: "Fatal",
|
||||
}
|
||||
lvl := _Info
|
||||
for k, v := range levels {
|
||||
if strings.EqualFold(strings.ToLower(l), strings.ToLower(v)) {
|
||||
lvl = k
|
||||
}
|
||||
}
|
||||
return lvl
|
||||
}
|
||||
|
||||
func output(l int, s ...interface{}) {
|
||||
lvl := fmt.Sprintf("[%s] ", levels[l])
|
||||
_ = baseLog.Output(3, lvl+fmt.Sprintln(s...))
|
||||
}
|
||||
|
||||
func Debug(v ...interface{}) {
|
||||
l := _Debug
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
}
|
||||
|
||||
func Info(v ...interface{}) {
|
||||
l := _Info
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
}
|
||||
|
||||
func Warn(v ...interface{}) {
|
||||
l := _Warn
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
}
|
||||
|
||||
func Error(v ...interface{}) {
|
||||
l := _Error
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
}
|
||||
|
||||
func Fatal(v ...interface{}) {
|
||||
l := _Fatal
|
||||
if baseLevel > l {
|
||||
return
|
||||
}
|
||||
output(l, v...)
|
||||
os.Exit(1)
|
||||
}
|
@@ -5,3 +5,7 @@ func Start() {
|
||||
initServerCfg()
|
||||
initLog()
|
||||
}
|
||||
|
||||
func Test() {
|
||||
initLog()
|
||||
}
|
@@ -8,10 +8,6 @@
|
||||
# Define Bridge Interface
|
||||
br="anylink0"
|
||||
|
||||
# Define list of TAP interfaces to be bridged,
|
||||
# for example tap="tap0 tap1 tap2".
|
||||
tap="tap0"
|
||||
|
||||
# Define physical ethernet interface to be bridged
|
||||
# with TAP interface(s) above.
|
||||
|
@@ -9,8 +9,9 @@ db_file = "./data.db"
|
||||
cert_file = "./vpn_cert.pem"
|
||||
cert_key = "./vpn_cert.key"
|
||||
ui_path = "../ui"
|
||||
down_files_path = "../down_files"
|
||||
|
||||
files_path = "../files"
|
||||
#日志目录,为空写入标准输出
|
||||
log_path = "../log"
|
||||
log_level = "info"
|
||||
|
||||
#系统名称
|
||||
@@ -35,8 +36,7 @@ proxy_protocol = false
|
||||
link_mode = "tun"
|
||||
|
||||
#客户端分配的ip地址池
|
||||
ipv4_network = "192.168.10.0"
|
||||
ipv4_netmask = "255.255.255.0"
|
||||
ipv4_cidr = "192.168.10.0/24"
|
||||
ipv4_gateway = "192.168.10.1"
|
||||
ipv4_pool = ["192.168.10.100", "192.168.10.200"]
|
||||
|
@@ -43,28 +43,27 @@ func initData() {
|
||||
return
|
||||
}
|
||||
|
||||
defer Set(SettingBucket, Installed, true)
|
||||
defer func() {
|
||||
_ = Set(SettingBucket, Installed, true)
|
||||
}()
|
||||
|
||||
smtp := &SettingSmtp{
|
||||
Host: "127.0.0.1",
|
||||
Port: 25,
|
||||
From: "vpn@xx.com",
|
||||
}
|
||||
SettingSet(smtp)
|
||||
_ = SettingSet(smtp)
|
||||
|
||||
other := &SettingOther{
|
||||
Banner: "您已接入公司网络,请按照公司规定使用。\n请勿进行非工作下载及视频行为!",
|
||||
AccountMail: accountMail,
|
||||
}
|
||||
SettingSet(other)
|
||||
_ = SettingSet(other)
|
||||
|
||||
}
|
||||
|
||||
func CheckErrNotFound(err error) bool {
|
||||
if err == storm.ErrNotFound {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return err == storm.ErrNotFound
|
||||
}
|
||||
|
||||
const accountMail = `<p>您好:</p>
|
@@ -27,7 +27,7 @@ func TestDb(t *testing.T) {
|
||||
defer closeIpdata()
|
||||
|
||||
u := User{Username: "a"}
|
||||
Save(&u)
|
||||
_ = Save(&u)
|
||||
|
||||
assert.Equal(u.Id, 1)
|
||||
}
|
@@ -83,8 +83,9 @@ func SetGroup(g *Group) error {
|
||||
if err != nil {
|
||||
return errors.New("RouteInclude 错误" + err.Error())
|
||||
}
|
||||
vn := ValData{Val: v.Val, IpMask: ipMask}
|
||||
routeInclude = append(routeInclude, vn)
|
||||
|
||||
v.IpMask = ipMask
|
||||
routeInclude = append(routeInclude, v)
|
||||
}
|
||||
}
|
||||
g.RouteInclude = routeInclude
|
||||
@@ -95,8 +96,8 @@ func SetGroup(g *Group) error {
|
||||
if err != nil {
|
||||
return errors.New("RouteExclude 错误" + err.Error())
|
||||
}
|
||||
vn := ValData{Val: v.Val, IpMask: ipMask}
|
||||
routeExclude = append(routeExclude, vn)
|
||||
v.IpMask = ipMask
|
||||
routeExclude = append(routeExclude, v)
|
||||
}
|
||||
}
|
||||
g.RouteExclude = routeExclude
|
||||
@@ -108,9 +109,8 @@ func SetGroup(g *Group) error {
|
||||
if err != nil {
|
||||
return errors.New("GroupLinkAcl 错误" + err.Error())
|
||||
}
|
||||
vn := v
|
||||
vn.IpNet = ipNet
|
||||
linkAcl = append(linkAcl, vn)
|
||||
v.IpNet = ipNet
|
||||
linkAcl = append(linkAcl, v)
|
||||
}
|
||||
}
|
||||
g.LinkAcl = linkAcl
|
@@ -18,6 +18,7 @@ type User struct {
|
||||
// Password string `json:"password"`
|
||||
PinCode string `json:"pin_code"`
|
||||
OtpSecret string `json:"otp_secret"`
|
||||
DisableOtp bool `json:"disable_otp"` // 禁用otp
|
||||
Groups []string `json:"groups"`
|
||||
Status int8 `json:"status"` // 1正常
|
||||
SendEmail bool `json:"send_email"`
|
||||
@@ -39,7 +40,7 @@ func SetUser(v *User) error {
|
||||
v.PinCode = planPass
|
||||
|
||||
if v.OtpSecret == "" {
|
||||
v.OtpSecret = gotp.RandomSecret(24)
|
||||
v.OtpSecret = gotp.RandomSecret(32)
|
||||
}
|
||||
|
||||
// 判断组是否有效
|
||||
@@ -85,19 +86,16 @@ func CheckUser(name, pwd, group string) error {
|
||||
}
|
||||
|
||||
// 判断otp信息
|
||||
if !v.DisableOtp {
|
||||
pwd = pwd[:pl-6]
|
||||
otp := pwd[pl-6:]
|
||||
if !checkOtp(name, otp) {
|
||||
if !checkOtp(name, otp, v.OtpSecret) {
|
||||
return fmt.Errorf("%s %s", name, "动态码错误")
|
||||
}
|
||||
totp := gotp.NewDefaultTOTP(v.OtpSecret)
|
||||
unix := time.Now().Unix()
|
||||
verify := totp.Verify(otp, int(unix))
|
||||
if !verify {
|
||||
return fmt.Errorf("%s %s", name, "动态码错误")
|
||||
}
|
||||
|
||||
pinCode := pwd[:pl-6]
|
||||
if pinCode != v.PinCode {
|
||||
// 判断用户密码
|
||||
if pwd != v.PinCode {
|
||||
return fmt.Errorf("%s %s", name, "密码错误")
|
||||
}
|
||||
|
||||
@@ -126,18 +124,23 @@ func init() {
|
||||
}()
|
||||
}
|
||||
|
||||
// 令牌只能使用一次
|
||||
func checkOtp(username, otp string) bool {
|
||||
key := fmt.Sprintf("%s:%s", username, otp)
|
||||
// 判断令牌信息
|
||||
func checkOtp(name, otp, secret string) bool {
|
||||
key := fmt.Sprintf("%s:%s", name, otp)
|
||||
|
||||
userOtpMux.Lock()
|
||||
defer userOtpMux.Unlock()
|
||||
|
||||
// 令牌只能使用一次
|
||||
if _, ok := userOtp[key]; ok {
|
||||
// 已经存在
|
||||
return false
|
||||
}
|
||||
|
||||
userOtp[key] = time.Now()
|
||||
return true
|
||||
|
||||
totp := gotp.NewDefaultTOTP(secret)
|
||||
unix := time.Now().Unix()
|
||||
verify := totp.Verify(otp, int(unix))
|
||||
|
||||
return verify
|
||||
}
|
0
server/files/index.html
Normal file
0
server/files/index.html
Normal file
@@ -6,21 +6,19 @@ require (
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
||||
github.com/asdine/storm/v3 v3.2.1
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/mojocn/base64Captcha v1.3.1
|
||||
github.com/pelletier/go-toml v1.8.1
|
||||
github.com/shirou/gopsutil v3.20.11+incompatible
|
||||
github.com/shirou/gopsutil v3.21.1+incompatible
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/xhit/go-simple-mail/v2 v2.6.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/xhit/go-simple-mail/v2 v2.8.0
|
||||
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
)
|
@@ -11,10 +11,8 @@ 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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -29,14 +27,12 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mojocn/base64Captcha v1.3.1 h1:2Wbkt8Oc8qjmNJ5GyOfSo4tgVQPsbKMftqASnq8GlT0=
|
||||
github.com/mojocn/base64Captcha v1.3.1/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY=
|
||||
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
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/shirou/gopsutil v3.20.11+incompatible h1:LJr4ZQK4mPpIV5gOa4jCOKOGb4ty4DZO54I4FGqIpto=
|
||||
github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/gopsutil v3.21.1+incompatible h1:2LwXWdbjXwyDgq26Yy/OT4xozlpmssQfy/rtfhWb0bY=
|
||||
github.com/shirou/gopsutil v3.21.1+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w=
|
||||
@@ -45,12 +41,12 @@ github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/xhit/go-simple-mail/v2 v2.6.0 h1:pvPmpDUUWy07cnTgwxwEe5fjdyYtETnxcvdGPQxtv/k=
|
||||
github.com/xhit/go-simple-mail/v2 v2.6.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4=
|
||||
github.com/xhit/go-simple-mail/v2 v2.8.0 h1:w6ZDXvRk0EO+r78LRlQl14ngP2tiRDRRHhr9UaVJ0p4=
|
||||
github.com/xhit/go-simple-mail/v2 v2.8.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4=
|
||||
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119 h1:YyPWX3jLOtYKulBR6AScGIs74lLrJcgeKRwcbAuQOG4=
|
||||
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119/go.mod h1:/nuTSlK+okRfR/vnIPqR89fFKonnWPiZymN5ydRJkX8=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
@@ -58,21 +54,20 @@ go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604=
|
||||
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 h1:TbGuee8sSq15Iguxu4deQ7+Bqq/d2rsQejGcEtADAMQ=
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
@@ -82,13 +77,11 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
|
||||
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
@@ -2,4 +2,5 @@ package handler
|
||||
|
||||
// 暂时没有实现
|
||||
func startDtls() {
|
||||
|
||||
}
|
@@ -17,10 +17,10 @@ import (
|
||||
func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||
// 判断anyconnect客户端
|
||||
userAgent := strings.ToLower(r.UserAgent())
|
||||
x_Aggregate_Auth := r.Header.Get("X-Aggregate-Auth")
|
||||
x_Transcend_Version := r.Header.Get("X-Transcend-Version")
|
||||
xAggregateAuth := r.Header.Get("X-Aggregate-Auth")
|
||||
xTranscendVersion := r.Header.Get("X-Transcend-Version")
|
||||
if !(strings.Contains(userAgent, "anyconnect") &&
|
||||
x_Aggregate_Auth == "1" && x_Transcend_Version == "1") {
|
||||
xAggregateAuth == "1" && xTranscendVersion == "1") {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
fmt.Fprintf(w, "error request")
|
||||
return
|
||||
@@ -67,7 +67,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO 用户密码校验
|
||||
err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect)
|
||||
if err != nil {
|
||||
base.Info(err)
|
||||
base.Warn(err)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNames(), Error: "用户名或密码错误"}
|
||||
tplRequest(tpl_request, w, data)
|
||||
@@ -87,11 +87,12 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||
sess.MacAddr = strings.ToLower(cr.MacAddressList.MacAddress)
|
||||
sess.UniqueIdGlobal = cr.DeviceId.UniqueIdGlobal
|
||||
other := &dbdata.SettingOther{}
|
||||
dbdata.SettingGet(other)
|
||||
_ = dbdata.SettingGet(other)
|
||||
rd := RequestData{SessionId: sess.Sid, SessionToken: sess.Sid + "@" + sess.Token,
|
||||
Banner: other.Banner}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
tplRequest(tpl_complete, w, rd)
|
||||
base.Debug("login", cr.Auth.Username)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -102,7 +103,7 @@ const (
|
||||
func tplRequest(typ int, w io.Writer, data RequestData) {
|
||||
if typ == tpl_request {
|
||||
t, _ := template.New("auth_request").Parse(auth_request)
|
||||
t.Execute(w, data)
|
||||
_ = t.Execute(w, data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ func tplRequest(typ int, w io.Writer, data RequestData) {
|
||||
data.Banner = strings.ReplaceAll(data.Banner, "\n", "
")
|
||||
}
|
||||
t, _ := template.New("auth_complete").Parse(auth_complete)
|
||||
t.Execute(w, data)
|
||||
_ = t.Execute(w, data)
|
||||
}
|
||||
|
||||
// 设置输出信息
|
@@ -2,11 +2,9 @@ package handler
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const BufferSize = 2048
|
||||
@@ -43,27 +41,6 @@ type macAddressList struct {
|
||||
MacAddress string `xml:"mac-address"`
|
||||
}
|
||||
|
||||
// 判断anyconnect客户端
|
||||
func checkLinkClient(h http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO 调试信息输出
|
||||
// hd, _ := httputil.DumpRequest(r, true)
|
||||
// fmt.Println("DumpRequest: ", string(hd))
|
||||
// fmt.Println(r.RemoteAddr)
|
||||
|
||||
userAgent := strings.ToLower(r.UserAgent())
|
||||
x_Aggregate_Auth := r.Header.Get("X-Aggregate-Auth")
|
||||
x_Transcend_Version := r.Header.Get("X-Transcend-Version")
|
||||
if strings.Contains(userAgent, "anyconnect") &&
|
||||
x_Aggregate_Auth == "1" && x_Transcend_Version == "1" {
|
||||
h(w, r)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
fmt.Fprintf(w, "error request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setCommonHeader(w http.ResponseWriter) {
|
||||
// Content-Length Date 默认已经存在
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
func LinkCstp(conn net.Conn, cSess *sessdata.ConnSession) {
|
||||
defer func() {
|
||||
// log.Println("LinkCstp return")
|
||||
conn.Close()
|
||||
base.Debug("LinkCstp return", cSess.IpAddr)
|
||||
_ = conn.Close()
|
||||
cSess.Close()
|
||||
}()
|
||||
|
||||
@@ -72,8 +72,8 @@ func LinkCstp(conn net.Conn, cSess *sessdata.ConnSession) {
|
||||
|
||||
func cstpWrite(conn net.Conn, cSess *sessdata.ConnSession) {
|
||||
defer func() {
|
||||
// log.Println("cstpWrite return")
|
||||
conn.Close()
|
||||
base.Debug("cstpWrite return", cSess.IpAddr)
|
||||
_ = conn.Close()
|
||||
cSess.Close()
|
||||
}()
|
||||
|
@@ -26,7 +26,7 @@ func LinkHome(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func LinkOtpQr(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
idS := r.FormValue("id")
|
||||
jwtToken := r.FormValue("jwt")
|
||||
data, err := admin.GetJwtData(jwtToken)
|
@@ -29,6 +29,9 @@ func checkTap() {
|
||||
bridgeHw = brFace.HardwareAddr
|
||||
|
||||
addrs, err := brFace.Addrs()
|
||||
if err != nil {
|
||||
base.Fatal("testTap err: ", err)
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ip, _, err := net.ParseCIDR(addr.String())
|
||||
if err != nil || ip.To4() == nil {
|
||||
@@ -67,7 +70,7 @@ func LinkTap(cSess *sessdata.ConnSession) error {
|
||||
err = execCmd(cmdStrs)
|
||||
if err != nil {
|
||||
base.Error(err)
|
||||
ifce.Close()
|
||||
_ = ifce.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -78,9 +81,9 @@ func LinkTap(cSess *sessdata.ConnSession) error {
|
||||
|
||||
func tapWrite(ifce *water.Interface, cSess *sessdata.ConnSession) {
|
||||
defer func() {
|
||||
// log.Println("LinkTap return")
|
||||
base.Debug("LinkTap return", cSess.IpAddr)
|
||||
cSess.Close()
|
||||
ifce.Close()
|
||||
_ = ifce.Close()
|
||||
}()
|
||||
|
||||
var (
|
||||
@@ -150,8 +153,8 @@ func tapWrite(ifce *water.Interface, cSess *sessdata.ConnSession) {
|
||||
|
||||
func tapRead(ifce *water.Interface, cSess *sessdata.ConnSession) {
|
||||
defer func() {
|
||||
// log.Println("tapRead return")
|
||||
ifce.Close()
|
||||
base.Debug("tapRead return", cSess.IpAddr)
|
||||
_ = ifce.Close()
|
||||
}()
|
||||
|
||||
var (
|
@@ -51,7 +51,7 @@ func LinkTun(cSess *sessdata.ConnSession) error {
|
||||
err = execCmd(cmdStrs)
|
||||
if err != nil {
|
||||
base.Error(err)
|
||||
ifce.Close()
|
||||
_ = ifce.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -62,9 +62,9 @@ func LinkTun(cSess *sessdata.ConnSession) error {
|
||||
|
||||
func tunWrite(ifce *water.Interface, cSess *sessdata.ConnSession) {
|
||||
defer func() {
|
||||
// log.Println("LinkTun return")
|
||||
base.Debug("LinkTun return", cSess.IpAddr)
|
||||
cSess.Close()
|
||||
ifce.Close()
|
||||
_ = ifce.Close()
|
||||
}()
|
||||
|
||||
var (
|
||||
@@ -89,8 +89,8 @@ func tunWrite(ifce *water.Interface, cSess *sessdata.ConnSession) {
|
||||
|
||||
func tunRead(ifce *water.Interface, cSess *sessdata.ConnSession) {
|
||||
defer func() {
|
||||
// log.Println("tunRead return")
|
||||
ifce.Close()
|
||||
base.Debug("tunRead return", cSess.IpAddr)
|
||||
_ = ifce.Close()
|
||||
}()
|
||||
var (
|
||||
err error
|
@@ -74,7 +74,7 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-CSTP-Version", "1")
|
||||
w.Header().Set("X-CSTP-Protocol", "Copyright (c) 2004 Cisco Systems, Inc.")
|
||||
w.Header().Set("X-CSTP-Address", cSess.IpAddr.String()) // 分配的ip地址
|
||||
w.Header().Set("X-CSTP-Netmask", base.Cfg.Ipv4Netmask) // 子网掩码
|
||||
w.Header().Set("X-CSTP-Netmask", sessdata.IpPool.Ipv4Mask.String()) // 子网掩码
|
||||
w.Header().Set("X-CSTP-Hostname", hn) // 机器名称
|
||||
|
||||
// 允许本地LAN访问vpn网络,必须放在路由的第一个
|
||||
@@ -131,15 +131,16 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
||||
// w.Header().Set("X-CSTP-Post-Auth-XML", ``)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
h := w.Header().Clone()
|
||||
hClone := w.Header().Clone()
|
||||
headers := make([]byte, 0)
|
||||
buf := bytes.NewBuffer(headers)
|
||||
h.Write(buf)
|
||||
base.Debug(string(buf.Bytes()))
|
||||
_ = hClone.Write(buf)
|
||||
base.Debug(buf.String())
|
||||
|
||||
hj := w.(http.Hijacker)
|
||||
conn, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
base.Error(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
91
server/handler/payload.go
Normal file
91
server/handler/payload.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/bjdgyc/anylink/dbdata"
|
||||
"github.com/bjdgyc/anylink/sessdata"
|
||||
"github.com/songgao/water/waterutil"
|
||||
)
|
||||
|
||||
func payloadIn(cSess *sessdata.ConnSession, lType sessdata.LType, pType byte, data []byte) bool {
|
||||
payload := &sessdata.Payload{
|
||||
LType: lType,
|
||||
PType: pType,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
return payloadInData(cSess, payload)
|
||||
}
|
||||
|
||||
func payloadInData(cSess *sessdata.ConnSession, payload *sessdata.Payload) bool {
|
||||
// 进行Acl规则判断
|
||||
check := checkLinkAcl(cSess.Group, payload)
|
||||
if !check {
|
||||
// 校验不通过直接丢弃
|
||||
return false
|
||||
}
|
||||
|
||||
closed := false
|
||||
select {
|
||||
case cSess.PayloadIn <- payload:
|
||||
case <-cSess.CloseChan:
|
||||
closed = true
|
||||
}
|
||||
|
||||
return closed
|
||||
}
|
||||
|
||||
func payloadOut(cSess *sessdata.ConnSession, lType sessdata.LType, pType byte, data []byte) bool {
|
||||
payload := &sessdata.Payload{
|
||||
LType: lType,
|
||||
PType: pType,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
return payloadOutData(cSess, payload)
|
||||
}
|
||||
|
||||
func payloadOutData(cSess *sessdata.ConnSession, payload *sessdata.Payload) bool {
|
||||
closed := false
|
||||
|
||||
select {
|
||||
case cSess.PayloadOut <- payload:
|
||||
case <-cSess.CloseChan:
|
||||
closed = true
|
||||
}
|
||||
|
||||
return closed
|
||||
}
|
||||
|
||||
// Acl规则校验
|
||||
func checkLinkAcl(group *dbdata.Group, payload *sessdata.Payload) bool {
|
||||
if payload.LType == sessdata.LTypeIPData && payload.PType == 0x00 && len(group.LinkAcl) > 0 {
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
|
||||
ip_dst := waterutil.IPv4Destination(payload.Data)
|
||||
ip_port := waterutil.IPv4DestinationPort(payload.Data)
|
||||
// fmt.Println("sent:", ip_dst, ip_port)
|
||||
|
||||
// 优先放行dns端口
|
||||
for _, v := range group.ClientDns {
|
||||
if v.Val == ip_dst.String() && ip_port == 53 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range group.LinkAcl {
|
||||
// 循环判断ip和端口
|
||||
if v.IpNet.Contains(ip_dst) {
|
||||
if v.Port == ip_port || v.Port == 0 {
|
||||
if v.Action == dbdata.Allow {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@@ -6,7 +6,6 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
@@ -14,22 +13,28 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := tls.LoadX509KeyPair(base.Cfg.CertFile, base.Cfg.CertKey)
|
||||
return &cert, err
|
||||
}
|
||||
|
||||
func startTls() {
|
||||
addr := base.Cfg.ServerAddr
|
||||
certFile := base.Cfg.CertFile
|
||||
keyFile := base.Cfg.CertKey
|
||||
|
||||
logger := log.New(os.Stdout, "[SERVER]", log.Lshortfile|log.Ldate)
|
||||
// 设置tls信息
|
||||
tlsConfig := &tls.Config{
|
||||
NextProtos: []string{"http/1.1"},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: true,
|
||||
GetCertificate: GetCertificate,
|
||||
}
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: initRoute(),
|
||||
TLSConfig: tlsConfig,
|
||||
ErrorLog: logger,
|
||||
ErrorLog: base.GetBaseLog(),
|
||||
}
|
||||
|
||||
var ln net.Listener
|
||||
@@ -57,9 +62,9 @@ func initRoute() http.Handler {
|
||||
r.HandleFunc("/", LinkAuth).Methods(http.MethodPost)
|
||||
r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect)
|
||||
r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet)
|
||||
r.PathPrefix("/down_files/").Handler(
|
||||
http.StripPrefix("/down_files/",
|
||||
http.FileServer(http.Dir(base.Cfg.DownFilesPath)),
|
||||
r.PathPrefix("/files/").Handler(
|
||||
http.StripPrefix("/files/",
|
||||
http.FileServer(http.Dir(base.Cfg.FilesPath)),
|
||||
),
|
||||
)
|
||||
r.NotFoundHandler = http.HandlerFunc(notFound)
|
@@ -21,5 +21,5 @@ func Start() {
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
dbdata.Stop()
|
||||
_ = dbdata.Stop()
|
||||
}
|
@@ -48,7 +48,7 @@ func tableLookup(ip net.IP) *Addr {
|
||||
}
|
||||
|
||||
// 判断老化过期时间
|
||||
tsub := time.Now().Sub(addr.disTime)
|
||||
tsub := time.Since(addr.disTime)
|
||||
switch addr.Type {
|
||||
case TypeNormal:
|
||||
if tsub > StaleTimeNormal {
|
@@ -48,7 +48,7 @@ func doPing(ip string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second * 2))
|
||||
_ = conn.SetReadDeadline(time.Now().Add(time.Second * 2))
|
||||
|
||||
for {
|
||||
buf := make([]byte, 512)
|
@@ -198,8 +198,10 @@ func (p *Conn) checkPrefixOnce() {
|
||||
func (p *Conn) checkPrefix() error {
|
||||
if p.proxyHeaderTimeout != 0 {
|
||||
readDeadLine := time.Now().Add(p.proxyHeaderTimeout)
|
||||
p.conn.SetReadDeadline(readDeadLine)
|
||||
defer p.conn.SetReadDeadline(time.Time{})
|
||||
_ = p.conn.SetReadDeadline(readDeadLine)
|
||||
defer func() {
|
||||
_ = p.conn.SetReadDeadline(time.Time{})
|
||||
}()
|
||||
}
|
||||
|
||||
// Incrementally check each byte of the prefix
|
@@ -45,8 +45,6 @@ func CopyStruct(a interface{}, b interface{}, fields ...string) (err error) {
|
||||
// a中有同名的字段并且类型一致才复制
|
||||
if f.IsValid() && f.Kind() == bValue.Kind() {
|
||||
f.Set(bValue)
|
||||
} else {
|
||||
// fmt.Printf("no such field or different kind, fieldName: %s\n", name)
|
||||
}
|
||||
}
|
||||
return
|
@@ -19,7 +19,8 @@ type ipPoolConfig struct {
|
||||
mux sync.Mutex
|
||||
// 计算动态ip
|
||||
Ipv4Gateway net.IP
|
||||
Ipv4IPNet net.IPNet
|
||||
Ipv4Mask net.IP
|
||||
Ipv4IPNet *net.IPNet
|
||||
IpLongMin uint32
|
||||
IpLongMax uint32
|
||||
}
|
||||
@@ -27,11 +28,12 @@ type ipPoolConfig struct {
|
||||
func initIpPool() {
|
||||
|
||||
// 地址处理
|
||||
// ip地址
|
||||
ip := net.ParseIP(base.Cfg.Ipv4Network)
|
||||
// 子网掩码
|
||||
maskIp := net.ParseIP(base.Cfg.Ipv4Netmask).To4()
|
||||
IpPool.Ipv4IPNet = net.IPNet{IP: ip, Mask: net.IPMask(maskIp)}
|
||||
_, ipNet, err := net.ParseCIDR(base.Cfg.Ipv4CIDR)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
IpPool.Ipv4IPNet = ipNet
|
||||
IpPool.Ipv4Mask = net.IP(ipNet.Mask)
|
||||
IpPool.Ipv4Gateway = net.ParseIP(base.Cfg.Ipv4Gateway)
|
||||
|
||||
// 网络地址零值
|
||||
@@ -74,11 +76,11 @@ func AcquireIp(username, macAddr string) net.IP {
|
||||
mi.Username = username
|
||||
mi.LastLogin = tNow
|
||||
// 回写db数据
|
||||
dbdata.Save(mi)
|
||||
_ = dbdata.Save(mi)
|
||||
ipActive[ipStr] = true
|
||||
return ip
|
||||
} else {
|
||||
dbdata.Del(mi)
|
||||
_ = dbdata.Del(mi)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +94,7 @@ func AcquireIp(username, macAddr string) net.IP {
|
||||
if err != nil && dbdata.CheckErrNotFound(err) {
|
||||
// 该ip没有被使用
|
||||
mi := &dbdata.IpMap{IpAddr: ip, MacAddr: macAddr, Username: username, LastLogin: tNow}
|
||||
dbdata.Save(mi)
|
||||
_ = dbdata.Save(mi)
|
||||
ipActive[ipStr] = true
|
||||
return ip
|
||||
}
|
||||
@@ -121,10 +123,10 @@ func AcquireIp(username, macAddr string) net.IP {
|
||||
|
||||
// 已经超过租期
|
||||
if tNow.Sub(v.LastLogin) > time.Duration(base.Cfg.IpLease)*time.Second {
|
||||
dbdata.Del(v)
|
||||
_ = dbdata.Del(v)
|
||||
mi := &dbdata.IpMap{IpAddr: ip, MacAddr: macAddr, Username: username, LastLogin: tNow}
|
||||
// 重写db数据
|
||||
dbdata.Save(mi)
|
||||
_ = dbdata.Save(mi)
|
||||
ipActive[ipStr] = true
|
||||
return ip
|
||||
}
|
||||
@@ -145,7 +147,7 @@ func AcquireIp(username, macAddr string) net.IP {
|
||||
ipStr := ip.String()
|
||||
mi = &dbdata.IpMap{IpAddr: ip, MacAddr: macAddr, Username: username, LastLogin: tNow}
|
||||
// 回写db数据
|
||||
dbdata.Save(mi)
|
||||
_ = dbdata.Save(mi)
|
||||
ipActive[ipStr] = true
|
||||
return ip
|
||||
}
|
||||
@@ -160,6 +162,6 @@ func ReleaseIp(ip net.IP, macAddr string) {
|
||||
err := dbdata.One("IpAddr", ip, mi)
|
||||
if err == nil {
|
||||
mi.LastLogin = time.Now()
|
||||
dbdata.Save(mi)
|
||||
_ = dbdata.Save(mi)
|
||||
}
|
||||
}
|
@@ -12,45 +12,53 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func preIpData() {
|
||||
base.Cfg.Ipv4Network = "192.168.3.0"
|
||||
base.Cfg.Ipv4Netmask = "255.255.255.0"
|
||||
base.Cfg.Ipv4Pool = []string{"192.168.3.1", "192.168.3.199"}
|
||||
tmpDb := path.Join(os.TempDir(), "anylink_test.db")
|
||||
func preData(tmpDir string) {
|
||||
base.Test()
|
||||
tmpDb := path.Join(tmpDir, "test.db")
|
||||
base.Cfg.DbFile = tmpDb
|
||||
base.Cfg.Ipv4CIDR = "192.168.3.0/24"
|
||||
base.Cfg.Ipv4Pool = []string{"192.168.3.1", "192.168.3.199"}
|
||||
base.Cfg.MaxClient = 100
|
||||
base.Cfg.MaxUserClient = 3
|
||||
|
||||
dbdata.Start()
|
||||
group := dbdata.Group{
|
||||
Name: "group1",
|
||||
Bandwidth: 1000,
|
||||
}
|
||||
_ = dbdata.Save(&group)
|
||||
initIpPool()
|
||||
}
|
||||
|
||||
func closeIpdata() {
|
||||
dbdata.Stop()
|
||||
tmpDb := path.Join(os.TempDir(), "anylink_test.db")
|
||||
func cleardata(tmpDir string) {
|
||||
_ = dbdata.Stop()
|
||||
tmpDb := path.Join(tmpDir, "test.db")
|
||||
os.Remove(tmpDb)
|
||||
}
|
||||
|
||||
func TestIpPool(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
preIpData()
|
||||
defer closeIpdata()
|
||||
|
||||
initIpPool()
|
||||
tmp := t.TempDir()
|
||||
preData(tmp)
|
||||
defer cleardata(tmp)
|
||||
|
||||
var ip net.IP
|
||||
|
||||
for i := 1; i <= 100; i++ {
|
||||
ip = AcquireIp("user", fmt.Sprintf("mac-%d", i))
|
||||
_ = AcquireIp("user", fmt.Sprintf("mac-%d", i))
|
||||
}
|
||||
ip = AcquireIp("user", fmt.Sprintf("mac-new"))
|
||||
ip = AcquireIp("user", "mac-new")
|
||||
assert.True(net.IPv4(192, 168, 3, 101).Equal(ip))
|
||||
for i := 102; i <= 199; i++ {
|
||||
ip = AcquireIp("user", fmt.Sprintf("mac-%d", i))
|
||||
}
|
||||
assert.True(net.IPv4(192, 168, 3, 199).Equal(ip))
|
||||
ip = AcquireIp("user", fmt.Sprintf("mac-nil"))
|
||||
ip = AcquireIp("user", "mac-nil")
|
||||
assert.Nil(ip)
|
||||
|
||||
ReleaseIp(net.IPv4(192, 168, 3, 88), "mac-88")
|
||||
ReleaseIp(net.IPv4(192, 168, 3, 77), "mac-77")
|
||||
// 最早过期的ip
|
||||
// 从头循环获取可用ip
|
||||
ip = AcquireIp("user", "mac-release-new")
|
||||
assert.True(net.IPv4(192, 168, 3, 88).Equal(ip))
|
||||
assert.True(net.IPv4(192, 168, 3, 77).Equal(ip))
|
||||
}
|
@@ -43,10 +43,11 @@ func TestLimitClient(t *testing.T) {
|
||||
func TestLimitWait(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
limit := NewLimitRater(1, 2)
|
||||
limit.Wait(2)
|
||||
start := time.Now()
|
||||
err := limit.Wait(2)
|
||||
assert.Nil(err)
|
||||
start := time.Now()
|
||||
err = limit.Wait(2)
|
||||
assert.Nil(err)
|
||||
err = limit.Wait(1)
|
||||
assert.Nil(err)
|
||||
end := time.Now()
|
@@ -34,10 +34,7 @@ func (o Onlines) Len() int {
|
||||
}
|
||||
|
||||
func (o Onlines) Less(i, j int) bool {
|
||||
if bytes.Compare(o[i].Ip, o[j].Ip) < 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return bytes.Compare(o[i].Ip, o[j].Ip) < 0
|
||||
}
|
||||
|
||||
func (o Onlines) Swap(i, j int) {
|
@@ -3,7 +3,6 @@ package sessdata
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
@@ -78,13 +77,13 @@ func checkSession() {
|
||||
return
|
||||
}
|
||||
timeout := time.Duration(base.Cfg.SessionTimeout) * time.Second
|
||||
tick := time.Tick(time.Second * 60)
|
||||
for range tick {
|
||||
tick := time.NewTicker(time.Second * 60)
|
||||
for range tick.C {
|
||||
sessMux.Lock()
|
||||
t := time.Now()
|
||||
for k, v := range sessions {
|
||||
v.mux.Lock()
|
||||
if v.IsActive != true {
|
||||
if !v.IsActive {
|
||||
if t.Sub(v.LastLogin) > timeout {
|
||||
delete(sessions, k)
|
||||
}
|
||||
@@ -133,12 +132,12 @@ func (s *Session) NewConn() *ConnSession {
|
||||
macAddr := s.MacAddr
|
||||
username := s.Username
|
||||
s.mux.Unlock()
|
||||
if active == true {
|
||||
if active {
|
||||
s.CSess.Close()
|
||||
}
|
||||
|
||||
limit := LimitClient(username, false)
|
||||
if limit == false {
|
||||
if !limit {
|
||||
return nil
|
||||
}
|
||||
// 获取客户端mac地址
|
||||
@@ -191,7 +190,7 @@ func (s *Session) NewConn() *ConnSession {
|
||||
|
||||
func (cs *ConnSession) Close() {
|
||||
cs.closeOnce.Do(func() {
|
||||
log.Println("closeOnce:", cs.IpAddr)
|
||||
base.Info("closeOnce:", cs.IpAddr)
|
||||
cs.Sess.mux.Lock()
|
||||
defer cs.Sess.mux.Unlock()
|
||||
|
||||
@@ -208,8 +207,10 @@ func (cs *ConnSession) Close() {
|
||||
const BandwidthPeriodSec = 2 // 流量速率统计周期(秒)
|
||||
|
||||
func (cs *ConnSession) ratePeriod() {
|
||||
tick := time.Tick(time.Second * BandwidthPeriodSec)
|
||||
for range tick {
|
||||
tick := time.NewTicker(time.Second * BandwidthPeriodSec)
|
||||
defer tick.Stop()
|
||||
|
||||
for range tick.C {
|
||||
select {
|
||||
case <-cs.CloseChan:
|
||||
return
|
38
server/sessdata/session_test.go
Normal file
38
server/sessdata/session_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package sessdata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewSession(t *testing.T) {
|
||||
ast := assert.New(t)
|
||||
sessions = make(map[string]*Session)
|
||||
sess := NewSession("")
|
||||
token := sess.Token
|
||||
v, ok := sessions[token]
|
||||
ast.True(ok)
|
||||
ast.Equal(sess, v)
|
||||
}
|
||||
|
||||
func TestConnSession(t *testing.T) {
|
||||
ast := assert.New(t)
|
||||
tmp := t.TempDir()
|
||||
preData(tmp)
|
||||
defer cleardata(tmp)
|
||||
|
||||
sess := NewSession("")
|
||||
sess.Group = "group1"
|
||||
sess.MacAddr = "00:15:5d:50:14:43"
|
||||
|
||||
cSess := sess.NewConn()
|
||||
|
||||
err := cSess.RateLimit(100, true)
|
||||
ast.Nil(err)
|
||||
ast.Equal(cSess.BandwidthUp, uint32(100))
|
||||
err = cSess.RateLimit(200, false)
|
||||
ast.Nil(err)
|
||||
ast.Equal(cSess.BandwidthDown, uint32(200))
|
||||
cSess.Close()
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
package sessdata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewSession(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
sessions = make(map[string]*Session)
|
||||
sess := NewSession("")
|
||||
token := sess.Token
|
||||
v, ok := sessions[token]
|
||||
assert.True(ok)
|
||||
assert.Equal(sess, v)
|
||||
}
|
||||
|
||||
func TestConnSession(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
preIpData()
|
||||
defer closeIpdata()
|
||||
sess := NewSession("")
|
||||
cSess := sess.NewConn()
|
||||
cSess.RateLimit(100, true)
|
||||
assert.Equal(cSess.BandwidthUp, uint32(100))
|
||||
cSess.RateLimit(200, false)
|
||||
assert.Equal(cSess.BandwidthDown, uint32(200))
|
||||
cSess.Close()
|
||||
}
|
13
systemd/anylink.service
Normal file
13
systemd/anylink.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=VPN Server Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory= /usr/local/anylink-deploy
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
ExecStart=/usr/local/anylink-deploy/anylink -conf="conf/server.toml"
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
3
web/.browserslistrc
Normal file
3
web/.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
17
web/.eslintrc.js
Normal file
17
web/.eslintrc.js
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/essential',
|
||||
'eslint:recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
}
|
||||
}
|
24
web/.gitignore
vendored
Normal file
24
web/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
/ui
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
21
web/LICENSE
Normal file
21
web/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 bjdgyc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
30
web/README.md
Normal file
30
web/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# anylink-web
|
||||
|
||||
## Repo
|
||||
|
||||
> github: https://github.com/bjdgyc/anylink-web
|
||||
|
||||
> gitee: https://gitee.com/bjdgyc/anylink-web
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
web/babel.config.js
Normal file
5
web/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
26204
web/package-lock.json
generated
Normal file
26204
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
web/package.json
Normal file
29
web/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "anylink-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.20.0",
|
||||
"core-js": "^3.6.5",
|
||||
"echarts": "^4.9.0",
|
||||
"element-ui": "^2.4.5",
|
||||
"vue": "^2.6.11",
|
||||
"vue-count-to": "^1.0.13",
|
||||
"vue-router": "^3.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"vue-cli-plugin-element": "~1.0.1",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
}
|
||||
}
|
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
17
web/public/index.html
Normal file
17
web/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>AnyLink</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but AnyLink doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
84
web/src/App.vue
Normal file
84
web/src/App.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {},
|
||||
created() {
|
||||
const token = sessionStorage.getItem('jwtToken');
|
||||
console.log("App created", token)
|
||||
if (token) {
|
||||
this.$root.isLogin = true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log("App mounted")
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
currentComponent: function () {
|
||||
var isLogin = this.$root.isLogin
|
||||
console.log("App isLogin", isLogin)
|
||||
if (isLogin) {
|
||||
return "layout";
|
||||
}
|
||||
return "login";
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/*color: #2c3e50;*/
|
||||
/*border: 1px solid red;*/
|
||||
height: 100%;
|
||||
/*width:100%;*/
|
||||
/*box-sizing: border-box;*/
|
||||
/*padding: 4px;*/
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*space vertical*/
|
||||
.sh-10 {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.sh-20 {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/*space horizontal*/
|
||||
.sw-10 {
|
||||
height: 1px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.sw-20 {
|
||||
height: 1px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.m-left-10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
</style>
|
BIN
web/src/assets/logo.png
Normal file
BIN
web/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
37
web/src/components/Cell.vue
Normal file
37
web/src/components/Cell.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="monitor">
|
||||
<div class="monitor-left">{{ left }}</div>
|
||||
<div class="monitor-right">{{ right }}</div>
|
||||
</div>
|
||||
<el-divider v-if="divider"></el-divider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Cell",
|
||||
props: {
|
||||
left: {},
|
||||
right: {},
|
||||
divider: {type: Boolean}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.monitor {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monitor-left {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.monitor-right {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
82
web/src/components/LineChart.vue
Normal file
82
web/src/components/LineChart.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div id="line-chart" :style="{height:height,width:width}"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'LineChart',
|
||||
props: {
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '350px'
|
||||
},
|
||||
// title,xAxis,series
|
||||
chartData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
},
|
||||
beforeDestroy() {
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
let chart = echarts.init(this.$el)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: this.chartData.title || '折线图'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
// toolbox: {
|
||||
// feature: {
|
||||
// saveAsImage: {}
|
||||
// }
|
||||
// },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: this.chartData.xname
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [],
|
||||
};
|
||||
|
||||
let xdata = this.chartData.xdata
|
||||
for (let key in xdata) {
|
||||
// window.console.log(key);
|
||||
let a = {
|
||||
name: key,
|
||||
type: 'line',
|
||||
data: xdata[key]
|
||||
};
|
||||
option.series.push(a)
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
63
web/src/layout/Layout.vue
Normal file
63
web/src/layout/Layout.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<el-container style="height: 100%;">
|
||||
<!--侧边栏菜单-->
|
||||
<el-aside :width="is_active?'200':'64'">
|
||||
<LayoutAside :is_active="is_active" :route_path="route_path"/>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<!--正文头部内容-->
|
||||
<el-header>
|
||||
<!--监听子组件的变量事件-->
|
||||
<LayoutHeader :is_active.sync="is_active" :route_name="route_name"/>
|
||||
</el-header>
|
||||
<!--正文内容-->
|
||||
<!--style="background-color: rgb(240, 242, 245);"-->
|
||||
<el-main style="background-color: #fbfbfb">
|
||||
<!-- 对应的组件内容渲染到router-view中 -->
|
||||
<!--子组件上报route信息-->
|
||||
<router-view :route_path.sync="route_path" :route_name.sync="route_name"></router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LayoutAside from "@/layout/LayoutAside";
|
||||
import LayoutHeader from "@/layout/LayoutHeader";
|
||||
|
||||
export default {
|
||||
name: "Layout",
|
||||
components: {LayoutHeader, LayoutAside},
|
||||
data() {
|
||||
return {
|
||||
is_active: true,
|
||||
route_path: '/index',
|
||||
route_name: ['首页'],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
route_path: function (val) {
|
||||
// var w = document.getElementById('layout-menu').clientWidth;
|
||||
window.console.log('is_active', val)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
window.console.log('layout-route', this.$route)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.el-header {
|
||||
background-color: #fff;
|
||||
/*box-shadow: 0 1px 4px rgba(0, 21, 41, .08);*/
|
||||
color: #333;
|
||||
line-height: 60px;
|
||||
/*width: 100%;*/
|
||||
|
||||
border-bottom: 1px solid #d8dce5;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||
}
|
||||
|
||||
</style>
|
78
web/src/layout/LayoutAside.vue
Normal file
78
web/src/layout/LayoutAside.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
|
||||
<!--background-color="#304156"-->
|
||||
<!--text-color="#bfcbd9"-->
|
||||
<!--active-text-color="#409EFF"-->
|
||||
<!--:unique-opened="false"-->
|
||||
|
||||
<!--<div class="layout-aside" :style="aside_style">-->
|
||||
|
||||
<el-menu :collapse="!is_active"
|
||||
:default-active="route_path"
|
||||
:style="is_active?'width:200px':''"
|
||||
router
|
||||
class="layout-menu"
|
||||
:collapse-transition="false"
|
||||
|
||||
background-color="#545c64"
|
||||
text-color="#fff"
|
||||
active-text-color="#ffd04b"
|
||||
>
|
||||
<el-menu-item index="/admin/home">
|
||||
<i class="el-icon-s-home"></i>
|
||||
<span slot="title">首页</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-submenu index="1">
|
||||
<template slot="title">
|
||||
<i class="el-icon-menu"></i>
|
||||
<span slot="title">基础信息</span>
|
||||
</template>
|
||||
|
||||
<el-menu-item index="/admin/set/system">系统信息</el-menu-item>
|
||||
<el-menu-item index="/admin/set/soft">软件配置</el-menu-item>
|
||||
<el-menu-item index="/admin/set/other">其他设置</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-submenu index="2">
|
||||
<template slot="title">
|
||||
<i class="el-icon-s-custom"></i>
|
||||
<span slot="title">用户信息</span>
|
||||
</template>
|
||||
|
||||
<el-menu-item index="/admin/user/list">用户列表</el-menu-item>
|
||||
<el-menu-item index="/admin/user/online">在线用户</el-menu-item>
|
||||
<el-menu-item index="/admin/user/ip_map">IP映射</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-submenu index="3">
|
||||
<template slot="title">
|
||||
<i class="el-icon-s-order"></i>
|
||||
<span slot="title">用户组信息</span>
|
||||
</template>
|
||||
|
||||
<el-menu-item index="/admin/group/list">用户组列表</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
|
||||
</el-menu>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "LayoutAside",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
props: ['is_active', 'route_path'],
|
||||
mounted() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-menu {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
82
web/src/layout/LayoutHeader.vue
Normal file
82
web/src/layout/LayoutHeader.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
|
||||
<div class="layout-header">
|
||||
<div>
|
||||
<i @click="toggleClick" :class="is_active ? 'el-icon-s-fold' : 'el-icon-s-unfold'" class="toggle-icon"
|
||||
style="font-size: 26px;"></i>
|
||||
|
||||
<el-breadcrumb separator="/" class="app-breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in route_name" :key="index">{{ item }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
|
||||
<el-dropdown trigger="click" @command="handleCommand">
|
||||
<i class="el-icon-setting" style="margin-right: 15px"></i>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="logout">退出</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
<span style="font-size: 12px;">{{ admin_user }}</span>
|
||||
</el-dropdown>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {getUser, removeToken} from "@/plugins/token";
|
||||
|
||||
export default {
|
||||
name: "Layoutheader",
|
||||
props: ['route_name'],
|
||||
data() {
|
||||
return {
|
||||
is_active: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
admin_user() {
|
||||
return getUser();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 菜单栏开关按钮
|
||||
toggleClick() {
|
||||
this.is_active = !this.is_active
|
||||
// 触发事件,抛出到上层
|
||||
this.$emit('update:is_active', this.is_active)
|
||||
},
|
||||
handleCommand() {
|
||||
console.log("handleCommand")
|
||||
// 退出 删除登录信息
|
||||
removeToken()
|
||||
this.$router.push("/login");
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.toggle-icon:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
|
||||
.app-breadcrumb {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
/*line-height: 20;*/
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
</style>
|
22
web/src/main.js
Normal file
22
web/src/main.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import './plugins/element'
|
||||
import "./plugins/mixin";
|
||||
import request from './plugins/request'
|
||||
import router from "./plugins/router";
|
||||
|
||||
|
||||
//TODO
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
||||
const vm = new Vue({
|
||||
data: {
|
||||
// 判断是否登录
|
||||
isLogin: false,
|
||||
},
|
||||
router,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
|
||||
request(vm)
|
160
web/src/pages/Home.vue
Normal file
160
web/src/pages/Home.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<el-row :gutter="40" class="panel-group">
|
||||
<el-col :span="6" class="card-panel-col">
|
||||
<div class="card-panel">
|
||||
<i class="el-icon-user-solid" style="font-size:50px;color: #f4516c;"></i>
|
||||
<div class="card-panel-description">
|
||||
<div class="card-panel-text">在线数</div>
|
||||
<countTo :startVal='0' :endVal='counts.online' :duration='2000' class="panel-num"></countTo>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6" class="card-panel-col">
|
||||
<div class="card-panel">
|
||||
<i class="el-icon-user-solid" style="font-size:50px;color: #36a3f7"></i>
|
||||
<div class="card-panel-description">
|
||||
<div class="card-panel-text">用户数</div>
|
||||
<countTo :startVal='0' :endVal='counts.user' :duration='2000' class="panel-num"></countTo>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6" class="card-panel-col">
|
||||
<div class="card-panel">
|
||||
<i class="el-icon-wallet" style="font-size:50px;color:#34bfa3"></i>
|
||||
<div class="card-panel-description">
|
||||
<div class="card-panel-text">用户组数</div>
|
||||
<countTo :startVal='0' :endVal='counts.group' :duration='2000' class="panel-num"></countTo>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6" class="card-panel-col">
|
||||
<div class="card-panel">
|
||||
<i class="el-icon-s-order" style="font-size:50px;color:#40c9c6"></i>
|
||||
<div class="card-panel-description">
|
||||
<div class="card-panel-text">IP映射数</div>
|
||||
<countTo :startVal='0' :endVal='counts.ip_map' :duration='2000' class="panel-num"></countTo>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
|
||||
<el-row class="line-chart">
|
||||
<LineChart :chart-data="lineChartUser"/>
|
||||
</el-row>
|
||||
|
||||
<el-row class="line-chart">
|
||||
<LineChart :chart-data="lineChartOrder"/>
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import countTo from 'vue-count-to';
|
||||
import LineChart from "@/components/LineChart";
|
||||
import axios from "axios";
|
||||
|
||||
const lineChartUser = {
|
||||
title: '每日在线统计',
|
||||
xname: ['2019-12-13', '2019-12-14', '2019-12-15', '2019-12-16', '2019-12-17', '2019-12-18', '2019-12-19'],
|
||||
xdata: {
|
||||
'test1': [10, 120, 11, 134, 105, 10, 15],
|
||||
'test2': [10, 82, 91, 14, 162, 10, 15]
|
||||
}
|
||||
}
|
||||
|
||||
const lineChartOrder = {
|
||||
title: '每日流量统计',
|
||||
xname: ['2019-12-13', '2019-12-14', '2019-12-15', '2019-12-16', '2019-12-17', '2019-12-18', '2019-12-19'],
|
||||
xdata: {
|
||||
'test1': [100, 120, 161, 134, 105, 160, 165],
|
||||
'test2': [120, 82, 91, 154, 162, 140, 145]
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "Home",
|
||||
components: {
|
||||
LineChart,
|
||||
countTo,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
counts: {
|
||||
online: 0,
|
||||
user: 0,
|
||||
group: 0,
|
||||
ip_map: 0,
|
||||
},
|
||||
lineChartUser: lineChartUser,
|
||||
lineChartOrder: lineChartOrder,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$emit('update:route_path', this.$route.path)
|
||||
this.$emit('update:route_name', ['首页'])
|
||||
},
|
||||
mounted() {
|
||||
this.getData()
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
axios.get('/set/home').then(resp => {
|
||||
var data = resp.data.data
|
||||
console.log(data);
|
||||
this.counts = data.counts
|
||||
}).catch(error => {
|
||||
this.$message.error('哦,请求出错');
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-panel {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border: 1px solid red;
|
||||
padding: 30px 0;
|
||||
|
||||
color: #666;
|
||||
background: #fff;
|
||||
/*box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);*/
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||
border-color: rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
.card-panel-description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-panel-text {
|
||||
line-height: 18px;
|
||||
color: rgba(0, 0, 0, .45);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.panel-num {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.line-chart {
|
||||
background: #fff;
|
||||
padding: 0 16px;
|
||||
margin-top: 40px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||
border-color: rgba(0, 0, 0, .05);
|
||||
}
|
||||
</style>
|
118
web/src/pages/Login.vue
Normal file
118
web/src/pages/Login.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
|
||||
<div class="login">
|
||||
<el-card style="width: 550px;">
|
||||
|
||||
<div class="issuer">AnyLink SSL VPN管理后台</div>
|
||||
|
||||
<el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
|
||||
<el-form-item label="管理用户名" prop="admin_user">
|
||||
<el-input v-model="ruleForm.admin_user"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理密码" prop="admin_pass">
|
||||
<el-input type="password" v-model="ruleForm.admin_pass" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="isLoading" @click="submitForm('ruleForm')">登录</el-button>
|
||||
<el-button @click="resetForm('ruleForm')">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
import {setToken, setUser} from "@/plugins/token";
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
mounted() {
|
||||
// 进入login,删除登录信息
|
||||
console.log("login created")
|
||||
//绑定事件
|
||||
window.addEventListener('keydown', this.keyDown);
|
||||
},
|
||||
destroyed(){
|
||||
window.removeEventListener('keydown',this.keyDown,false);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ruleForm: {},
|
||||
rules: {
|
||||
admin_user: [
|
||||
{required: true, message: '请输入用户名', trigger: 'blur'},
|
||||
{max: 50, message: '长度小于 50 个字符', trigger: 'blur'}
|
||||
],
|
||||
admin_pass: [
|
||||
{required: true, message: '请输入密码', trigger: 'blur'},
|
||||
{min: 6, message: '长度大于 6 个字符', trigger: 'blur'}
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
keyDown(e) {
|
||||
//如果是回车则执行登录方法
|
||||
if (e.keyCode === 13) {
|
||||
this.submitForm('ruleForm');
|
||||
}
|
||||
},
|
||||
submitForm(formName) {
|
||||
this.$refs[formName].validate((valid) => {
|
||||
if (!valid) {
|
||||
console.log('error submit!!');
|
||||
return false;
|
||||
}
|
||||
this.isLoading = true
|
||||
|
||||
// alert('submit!');
|
||||
axios.post('/base/login', qs.stringify(this.ruleForm)).then(resp => {
|
||||
var rdata = resp.data
|
||||
if (rdata.code === 0) {
|
||||
this.$message.success(rdata.msg);
|
||||
setToken(rdata.data.token)
|
||||
setUser(rdata.data.admin_user)
|
||||
this.$router.push("/home");
|
||||
} else {
|
||||
this.$message.error(rdata.msg);
|
||||
}
|
||||
console.log(rdata);
|
||||
}).catch(error => {
|
||||
this.$message.error('哦,请求出错');
|
||||
console.log(error);
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
},
|
||||
resetForm(formName) {
|
||||
this.$refs[formName].resetFields();
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login {
|
||||
/*border: 1px solid red;*/
|
||||
height: 100%;
|
||||
/*margin: 0 auto;*/
|
||||
text-align: center;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.issuer {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
</style>
|
447
web/src/pages/group/List.vue
Normal file
447
web/src/pages/group/List.vue
Normal file
@@ -0,0 +1,447 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
|
||||
<el-form :inline="true">
|
||||
<el-form-item>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
@click="handleEdit('')">添加
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
:data="tableData"
|
||||
border>
|
||||
|
||||
<el-table-column
|
||||
sortable="true"
|
||||
prop="id"
|
||||
label="ID"
|
||||
width="60">
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="组名">
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="note"
|
||||
label="备注">
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="allow_lan"
|
||||
label="本地网络">
|
||||
<template slot-scope="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.allow_lan"
|
||||
disabled>
|
||||
</el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="bandwidth"
|
||||
label="带宽限制">
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="client_dns"
|
||||
label="客户端DNS"
|
||||
width="160">
|
||||
<template slot-scope="scope">
|
||||
<el-row v-for="(item,inx) in scope.row.client_dns" :key="inx">{{ item.val }}</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="route_include"
|
||||
label="路由包含"
|
||||
width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-row v-for="(item,inx) in scope.row.route_include" :key="inx">{{ item.val }}</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="route_exclude"
|
||||
label="路由排除"
|
||||
width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-row v-for="(item,inx) in scope.row.route_exclude" :key="inx">{{ item.val }}</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="link_acl"
|
||||
label="LINK-ACL"
|
||||
min-width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-row v-for="(item,inx) in scope.row.link_acl" :key="inx">
|
||||
{{ item.action }} => {{ item.val }} : {{ item.port }}
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="状态"
|
||||
width="70">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 1" type="success">可用</el-tag>
|
||||
<el-tag v-else type="danger">停用</el-tag>
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="updated_at"
|
||||
label="更新时间"
|
||||
:formatter="tableDateFormat">
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="handleEdit(scope.row)">编辑
|
||||
</el-button>
|
||||
|
||||
<el-popconfirm
|
||||
style="margin-left: 10px"
|
||||
@onConfirm="handleDel(scope.row)"
|
||||
title="确定要删除用户吗?">
|
||||
<el-button
|
||||
slot="reference"
|
||||
size="mini"
|
||||
type="danger">删除
|
||||
</el-button>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="sh-20"></div>
|
||||
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:pager-count="11"
|
||||
@current-change="pageChange"
|
||||
:current-page="page"
|
||||
:total="count">
|
||||
</el-pagination>
|
||||
|
||||
</el-card>
|
||||
|
||||
<!--新增、修改弹出框-->
|
||||
<el-dialog
|
||||
:close-on-click-modal="false"
|
||||
title="用户组"
|
||||
:visible.sync="user_edit_dialog"
|
||||
width="750px"
|
||||
center>
|
||||
|
||||
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
|
||||
<el-form-item label="用户组ID" prop="id">
|
||||
<el-input v-model="ruleForm.id" disabled></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="组名" prop="name">
|
||||
<el-input v-model="ruleForm.name" :disabled="ruleForm.id > 0"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="note">
|
||||
<el-input v-model="ruleForm.note"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="带宽限制" prop="bandwidth">
|
||||
<el-input v-model.number="ruleForm.bandwidth">
|
||||
<template slot="append">BYTE</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="本地网络" prop="allow_lan">
|
||||
<el-switch
|
||||
v-model="ruleForm.allow_lan">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客户端DNS" prop="client_dns">
|
||||
<el-row class="msg-info">
|
||||
<el-col :span="20">输入IP格式如: 192.168.0.10</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
||||
@click.prevent="addDomain(ruleForm.client_dns)"></el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||
@click.prevent="removeDomain(ruleForm.client_dns)"></el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-for="(item,index) in ruleForm.client_dns"
|
||||
:key="index" style="margin-bottom: 5px" gutter="10">
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.val"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="包含路由" prop="route_include">
|
||||
<el-row class="msg-info">
|
||||
<el-col :span="20">输入CIDR格式如: 192.168.1.0/24</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
||||
@click.prevent="addDomain(ruleForm.route_include)"></el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||
@click.prevent="removeDomain(ruleForm.route_include)"></el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-for="(item,index) in ruleForm.route_include"
|
||||
:key="index" style="margin-bottom: 5px" gutter="10">
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.val"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排除路由" prop="route_exclude">
|
||||
<el-row class="msg-info">
|
||||
<el-col :span="20">输入CIDR格式如: 192.168.2.0/24</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
||||
@click.prevent="addDomain(ruleForm.route_exclude)"></el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||
@click.prevent="removeDomain(ruleForm.route_exclude)"></el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-for="(item,index) in ruleForm.route_exclude"
|
||||
:key="index" style="margin-bottom: 5px" gutter="10">
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.val"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="权限控制" prop="link_acl">
|
||||
<el-row class="msg-info">
|
||||
<el-col :span="20">输入CIDR格式如: 192.168.3.0/24 端口0表示所有端口</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button size="mini" type="success" icon="el-icon-plus" circle
|
||||
@click.prevent="addDomain(ruleForm.link_acl)"></el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-minus" circle
|
||||
@click.prevent="removeDomain(ruleForm.link_acl)"></el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row v-for="(item,index) in ruleForm.link_acl"
|
||||
:key="index" style="margin-bottom: 5px" gutter="5">
|
||||
<el-col :span="11">
|
||||
<el-input placeholder="请输入CIDR地址" v-model="item.val">
|
||||
<el-select v-model="item.action" slot="prepend">
|
||||
<el-option label="允许" value="allow"></el-option>
|
||||
<el-option label="禁止" value="deny"></el-option>
|
||||
</el-select>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-input v-model.number="item.port" placeholder="端口"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.note" placeholder="备注"></el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="ruleForm.status">
|
||||
<el-radio :label="1" border>启用</el-radio>
|
||||
<el-radio :label="0" border>停用</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
|
||||
<el-button @click="disVisible">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
name: "List",
|
||||
components: {},
|
||||
mixins: [],
|
||||
created() {
|
||||
this.$emit('update:route_path', this.$route.path)
|
||||
this.$emit('update:route_name', ['用户组信息', '用户组列表'])
|
||||
},
|
||||
mounted() {
|
||||
this.getData(1)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
tableData: [],
|
||||
count: 10,
|
||||
|
||||
ruleForm: {
|
||||
bandwidth: 0,
|
||||
status: 1,
|
||||
allow_lan: true,
|
||||
client_dns: [{val: '114.114.114.114'}],
|
||||
route_include: [],
|
||||
route_exclude: [],
|
||||
link_acl: [],
|
||||
},
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: '请输入用户名', trigger: 'blur'},
|
||||
{max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
|
||||
],
|
||||
bandwidth: [
|
||||
{required: true, message: '请输入用户姓名', trigger: 'blur'},
|
||||
{type: 'number', message: '年龄必须为数字值'}
|
||||
],
|
||||
email: [
|
||||
{required: true, message: '请输入用户邮箱', trigger: 'blur'},
|
||||
{type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change']}
|
||||
],
|
||||
|
||||
status: [
|
||||
{required: true}
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleDel(row) {
|
||||
axios.post('/group/del?id=' + row.id).then(resp => {
|
||||
const rdata = resp.data;
|
||||
if (rdata.code === 0) {
|
||||
this.$message.success(rdata.msg);
|
||||
this.getData(1);
|
||||
} else {
|
||||
this.$message.error(rdata.msg);
|
||||
}
|
||||
console.log(rdata);
|
||||
}).catch(error => {
|
||||
this.$message.error('哦,请求出错');
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
handleEdit(row) {
|
||||
!this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
|
||||
console.log(row)
|
||||
this.user_edit_dialog = true
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/group/detail', {
|
||||
params: {
|
||||
id: row.id,
|
||||
}
|
||||
}).then(resp => {
|
||||
this.ruleForm = resp.data.data
|
||||
}).catch(error => {
|
||||
this.$message.error('哦,请求出错');
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
pageChange(p) {
|
||||
this.getData(p)
|
||||
},
|
||||
getData(page) {
|
||||
this.page = page
|
||||
axios.get('/group/list', {
|
||||
params: {
|
||||
page: page,
|
||||
}
|
||||
}).then(resp => {
|
||||
const rdata = resp.data.data;
|
||||
console.log(rdata);
|
||||
this.tableData = rdata.datas;
|
||||
this.count = rdata.count
|
||||
}).catch(error => {
|
||||
this.$message.error('哦,请求出错');
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
removeDomain(arr, item) {
|
||||
console.log(item)
|
||||
// let index = arr.indexOf(item);
|
||||
// if (index !== -1 && arr.length > 1) {
|
||||
// arr.splice(index, 1)
|
||||
// }
|
||||
arr.pop()
|
||||
},
|
||||
addDomain(arr) {
|
||||
arr.push({val: "", action: "allow", port: 0});
|
||||
},
|
||||
submitForm(formName) {
|
||||
this.$refs[formName].validate((valid) => {
|
||||
if (!valid) {
|
||||
console.log('error submit!!');
|
||||
return false;
|
||||
}
|
||||
|
||||
axios.post('/group/set', this.ruleForm).then(resp => {
|
||||
const rdata = resp.data;
|
||||
if (rdata.code === 0) {
|
||||
this.$message.success(rdata.msg);
|
||||
this.getData(1);
|
||||
} else {
|
||||
this.$message.error(rdata.msg);
|
||||
}
|
||||
console.log(rdata);
|
||||
}).catch(error => {
|
||||
this.$message.error('哦,请求出错');
|
||||
console.log(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
resetForm(formName) {
|
||||
this.$refs[formName].resetFields();
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.msg-info {
|
||||
background-color: #f4f4f5;
|
||||
color: #909399;
|
||||
padding: 0 5px;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 80px;
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user