31 Commits

Author SHA1 Message Date
bjdgyc
7342f1f1a9 修改版本 v0.1.8 2021-03-17 14:14:45 +08:00
bjdgyc
7a2d8a3ad0 修改日志默认为标准输出 2021-03-17 14:04:07 +08:00
bjdgyc
b5927a11d3 Merge pull request #8 from xbclub/main
fix systemd
2021-03-17 13:06:57 +08:00
yii
273b3ee1eb fix systemd 2021-03-17 11:37:34 +08:00
bjdgyc
016a43b792 完善测试文件 2021-03-10 17:00:16 +08:00
bjdgyc
ddba116fbf 修复登陆密码判断bug 2021-03-02 15:28:08 +08:00
bjdgyc
dd1eae5d32 Merge pull request #7 from xbclub/master
add systemd service
2021-03-01 17:31:38 +08:00
yii
879b9114ac add systemd service 2021-03-01 17:12:54 +08:00
bjdgyc
e5c4a47a37 修改分支为main,添加otp开关 2021-03-01 16:28:50 +08:00
bjdgyc
a0669e1e32 Update go.yml 2021-03-01 16:19:35 +08:00
bjdgyc
ea7d27b4f0 Update go.yml 2021-03-01 16:15:57 +08:00
bjdgyc
9f2e9de49a Update go.yml 2021-03-01 16:10:48 +08:00
bjdgyc
8709dbaba1 Update go.yml 2021-03-01 16:08:14 +08:00
bjdgyc
4928ad5f62 修改分支为main,添加otp开关 2021-03-01 16:02:00 +08:00
bjdgyc
0f91c779e3 更改目录结构 2021-03-01 15:46:08 +08:00
bjdgyc
3464d1d10e 修改图片地址 2021-02-25 14:17:09 +08:00
bjdgyc
1579e92ba1 修改图片地址 2021-02-25 14:15:53 +08:00
bjdgyc
48327fe8d3 修改jetbrains图片尺寸 2021-02-25 14:11:44 +08:00
bjdgyc
ef7723b03b 修复测试bug 2021-02-22 16:39:58 +08:00
bjdgyc
0baab68bb2 修改日志写入文件内 2021-02-22 14:35:29 +08:00
bjd
665732fc03 添加codecov配置文件 2021-02-04 15:19:51 +08:00
bjd
edb0fe2dc9 修改客户端分配的ip为CIDR格式,请注意原来network格式 2021-02-04 13:32:10 +08:00
bjd
1c6572f5e3 折叠截图文档 2021-02-03 15:20:19 +08:00
bjd
103329c3d0 增加测试覆盖率 2021-02-03 11:44:25 +08:00
bjdgyc
d40b753871 Update go.yml 2021-02-02 20:51:49 +08:00
bjdgyc
fa5a58e98d Update go.yml 2021-02-02 20:42:08 +08:00
bjdgyc
62f30c05ff Update .travis.yml 2021-02-02 20:34:22 +08:00
bjdgyc
c02ffc27c0 Update .travis.yml 2021-02-02 20:28:12 +08:00
bjdgyc
a4e09e7719 Create .travis.yml 2021-02-02 20:27:02 +08:00
bjd
631e49bd41 增加LinkAcl功能,可以限制访问端口 2021-02-01 17:36:59 +08:00
bjd
ef95b1f927 增加LinkAcl功能,可以限制访问端口 2021-02-01 17:34:56 +08:00
114 changed files with 29804 additions and 505 deletions

5
.codecov.yml Normal file
View File

@@ -0,0 +1,5 @@
ignore:
- "screenshot"
- "web"
- "server/conf"
- "server/files"

View File

@@ -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)

20
.gitignore vendored
View File

@@ -1,19 +1,5 @@
# 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
ui

View File

@@ -1,6 +1,9 @@
# AnyLink
[![Go](https://github.com/bjdgyc/anylink/workflows/Go/badge.svg?branch=master)](https://github.com/bjdgyc/anylink/actions)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/bjdgyc/anylink)](https://pkg.go.dev/github.com/bjdgyc/anylink)
[![Go Report Card](https://goreportcard.com/badge/github.com/bjdgyc/anylink)](https://goreportcard.com/report/github.com/bjdgyc/anylink)
[![codecov](https://codecov.io/gh/bjdgyc/anylink/branch/master/graph/badge.svg?token=JTFLIIIBQ0)](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
![online](https://gitee.com/bjdgyc/anylink/raw/master/screenshot/online.jpg)
![online](screenshot/online.jpg)
## Installation
@@ -31,11 +34,16 @@ AnyLink 服务端仅在CentOS7测试通过如需要安装在其他系统
git clone https://github.com/bjdgyc/anylink.git
cd anylink
sh deploy.sh
sh -x build.sh
#注意使用root权限运行
# 注意使用root权限运行
cd anylink-deploy
sudo ./anylink -conf="conf/server.toml"
# 默认管理后台访问地址
# http://host:8800
# 默认日志文件
# log/anylink.log
```
## Feature
@@ -49,10 +57,11 @@ sudo ./anylink -conf="conf/server.toml"
- [x] 用户组支持
- [x] 多用户支持
- [x] TOTP令牌支持
- [x] TOTP令牌开关
- [x] 流量控制
- [x] 后台管理界面
- [x] 访问权限管理
- [ ] 访问权限管理
- [ ] DTLS-UDP通道
## Config
@@ -67,7 +76,23 @@ 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)
## systemd
添加 systemd脚本
* anylink 程序目录放入 `/usr/local/anylink-deploy`
systemd 脚本放入:
* centos: `/usr/lib/systemd/system/`
* ubuntu: `/lib/systemd/system/`
操作命令:
* 启动: `systemctl start anylink`
* 停止: `systemctl stop anylink`
* 开机自启: `systemctl enable anylink`
## Setting
@@ -122,11 +147,11 @@ sh bridge-init.sh
## Soft
相关软件下载: https://gitee.com/bjdgyc/anylink-soft
相关软件下载: QQ群共享文件: 567510628
## Discussion
![qq.png](https://gitee.com/bjdgyc/anylink/raw/master/screenshot/qq.png)
![qq.png](screenshot/qq.png)
添加QQ群: 567510628
@@ -136,19 +161,26 @@ sh bridge-init.sh
## Other Screenshot
![system.jpg](https://gitee.com/bjdgyc/anylink/raw/master/screenshot/system.jpg)
![setting.jpg](https://gitee.com/bjdgyc/anylink/raw/master/screenshot/setting.jpg)
![users.jpg](https://gitee.com/bjdgyc/anylink/raw/master/screenshot/users.jpg)
![ip_map.jpg](https://gitee.com/bjdgyc/anylink/raw/master/screenshot/ip_map.jpg)
![group.jpg](https://gitee.com/bjdgyc/anylink/raw/master/screenshot/group.jpg)
<details>
<summary>展开查看</summary>
![system.jpg](screenshot/system.jpg)
![setting.jpg](screenshot/setting.jpg)
![users.jpg](screenshot/users.jpg)
![ip_map.jpg](screenshot/ip_map.jpg)
![group.jpg](screenshot/group.jpg)
</details>
## License
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 LICENSE 文件中。
## Thank
<a href="https://www.jetbrains.com">
<img src="screenshot/jetbrains.png" width="200" height="200" alt="jetbrains.png" />
</a>

View File

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

View File

@@ -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"

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

5
server/.codecov.yml Normal file
View File

@@ -0,0 +1,5 @@
ignore:
- "screenshot"
- "web"
- "server/conf"
- "server/files"

19
server/.gitignore vendored Normal file
View 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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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())
}

View File

@@ -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{}

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

View File

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

View File

@@ -2,5 +2,5 @@ package base
const (
APP_NAME = "AnyLink"
APP_VER = "0.1.0"
APP_VER = "0.1.8"
)

View File

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

View File

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

View File

@@ -5,3 +5,7 @@ func Start() {
initServerCfg()
initLog()
}
func Test() {
initLog()
}

View File

@@ -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.

View File

@@ -9,8 +9,10 @@ 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_path = ""
log_level = "info"
#系统名称
@@ -35,8 +37,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"]

View File

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

View File

@@ -22,12 +22,13 @@ func closeIpdata() {
}
func TestDb(t *testing.T) {
assert := assert.New(t)
ast := assert.New(t)
preIpData()
defer closeIpdata()
u := User{Username: "a"}
Save(&u)
err := Save(&u)
ast.Nil(err)
assert.Equal(u.Id, 1)
ast.Equal(u.Id, 1)
}

View File

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

View File

@@ -0,0 +1,33 @@
package dbdata
import (
"testing"
"github.com/bjdgyc/anylink/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestGetGroupNames(t *testing.T) {
ast := assert.New(t)
preIpData()
defer closeIpdata()
// 添加 group
g1 := Group{Name: "g1", ClientDns: []ValData{{Val: "114.114.114.114"}}}
err := SetGroup(&g1)
ast.Nil(err)
g2 := Group{Name: "g2", ClientDns: []ValData{{Val: "114.114.114.114"}}}
err = SetGroup(&g2)
ast.Nil(err)
g3 := Group{Name: "g3", ClientDns: []ValData{{Val: "114.114.114.114"}}}
err = SetGroup(&g3)
ast.Nil(err)
// 判断所有数据
gAll := []string{"g1", "g2", "g3"}
gs := GetGroupNames()
for _, v := range gs {
ast.Equal(true, utils.InArrStr(gAll, v))
}
}

View File

@@ -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"`
@@ -34,12 +35,12 @@ func SetUser(v *User) error {
planPass := v.PinCode
// 自动生成密码
if len(planPass) < 6 {
planPass = utils.RandomNum(8)
planPass = utils.RandomRunes(8)
}
v.PinCode = planPass
if v.OtpSecret == "" {
v.OtpSecret = gotp.RandomSecret(24)
v.OtpSecret = gotp.RandomSecret(32)
}
// 判断组是否有效
@@ -81,22 +82,20 @@ func CheckUser(name, pwd, group string) error {
groupData := &Group{}
err = One("Name", group, groupData)
if err != nil || groupData.Status != 1 {
return fmt.Errorf("%s %s", name, "用户组错误")
return fmt.Errorf("%s - %s", name, "用户组错误")
}
// 判断otp信息
pinCode := pwd
if !v.DisableOtp {
pinCode = 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 {
return fmt.Errorf("%s %s", name, "密码错误")
}
@@ -126,18 +125,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
}

View File

@@ -0,0 +1,43 @@
package dbdata
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/xlzd/gotp"
)
func TestCheckUser(t *testing.T) {
ast := assert.New(t)
preIpData()
defer closeIpdata()
group := "group1"
// 添加一个组
dns := []ValData{{Val: "114.114.114.114"}}
route := []ValData{{Val: "192.168.1.1/24"}}
g := Group{Name: group, Status: 1, ClientDns: dns, RouteInclude: route}
err := SetGroup(&g)
ast.Nil(err)
// 判断 IpMask
ast.Equal(g.RouteInclude[0].IpMask, "192.168.1.1/255.255.255.0")
// 添加一个用户
u := User{Username: "aaa", Groups: []string{group}, Status: 1}
err = SetUser(&u)
ast.Nil(err)
// 验证 PinCode + OtpSecret
totp := gotp.NewDefaultTOTP(u.OtpSecret)
secret := totp.Now()
err = CheckUser("aaa", u.PinCode+secret, group)
ast.Nil(err)
// 单独验证密码
u.DisableOtp = true
_ = SetUser(&u)
err = CheckUser("aaa", u.PinCode, group)
ast.Nil(err)
}

0
server/files/index.html Normal file
View File

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

View File

@@ -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=

View File

@@ -2,4 +2,5 @@ package handler
// 暂时没有实现
func startDtls() {
}

View File

@@ -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", "&#x0A;")
}
t, _ := template.New("auth_complete").Parse(auth_complete)
t.Execute(w, data)
_ = t.Execute(w, data)
}
// 设置输出信息

View File

@@ -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")

View File

@@ -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()
}()

View File

@@ -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)

View File

@@ -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 (

View File

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

View File

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

View File

@@ -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)

View File

@@ -21,5 +21,5 @@ func Start() {
}
func Stop() {
dbdata.Stop()
_ = dbdata.Stop()
}

View File

@@ -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 {

View File

@@ -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)

View File

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

View File

@@ -61,7 +61,7 @@ func HumanByte(bf interface{}) string {
return hb
}
func RandomNum(length int) string {
func RandomRunes(length int) string {
letterRunes := []rune("abcdefghijklmnpqrstuvwxy1234567890")
bytes := make([]rune, length)

View File

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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) {

View File

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

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

View File

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

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

17
web/.eslintrc.js Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

26204
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

17
web/public/index.html Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More