mirror of
https://github.com/bjdgyc/anylink.git
synced 2025-09-29 00:19:36 +08:00
Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7342f1f1a9 | ||
|
7a2d8a3ad0 | ||
|
b5927a11d3 | ||
|
273b3ee1eb | ||
|
016a43b792 | ||
|
ddba116fbf | ||
|
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 | ||
|
9e0da33c6a | ||
|
3bb771971c | ||
|
dd83b330eb | ||
|
73d1edd62f | ||
|
3ebb669558 | ||
|
a72fc63c06 | ||
|
206cfebb9a |
5
.codecov.yml
Normal file
5
.codecov.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
ignore:
|
||||
- "screenshot"
|
||||
- "web"
|
||||
- "server/conf"
|
||||
- "server/files"
|
23
.github/workflows/go.yml
vendored
23
.github/workflows/go.yml
vendored
@@ -2,9 +2,9 @@ name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
@@ -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
20
.gitignore
vendored
@@ -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
|
||||
|
||||
|
83
README.md
83
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,25 +20,30 @@ 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
|
||||
|
||||
> 升级 go version = 1.16
|
||||
> 升级 go version = 1.15
|
||||
|
||||
```shell
|
||||
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
|
||||
|
||||
@@ -101,13 +126,12 @@ iptables -t nat -A POSTROUTING -s 192.168.10.0/255.255.255.0 -o eth0 -j MASQUERA
|
||||
1. 创建桥接网卡
|
||||
|
||||
```
|
||||
注意 server.toml 的ip参数,需要与 bridge.sh 的配置参数一致
|
||||
注意 server.toml 的ip参数,需要与 bridge-init.sh 的配置参数一致
|
||||
```
|
||||
|
||||
2. 修改 bridge.sh 内的参数
|
||||
2. 修改 bridge-init.sh 内的参数
|
||||
|
||||
```
|
||||
# file: ./bridge.sh
|
||||
eth="eth0"
|
||||
eth_ip="192.168.1.4"
|
||||
eth_netmask="255.255.255.0"
|
||||
@@ -115,31 +139,48 @@ eth_broadcast="192.168.1.255"
|
||||
eth_gateway="192.168.1.1"
|
||||
```
|
||||
|
||||
3. 执行 bridge.sh 文件
|
||||
3. 执行 bridge-init.sh 文件
|
||||
|
||||
```
|
||||
sh bridge.sh
|
||||
sh bridge-init.sh
|
||||
```
|
||||
|
||||
## Soft
|
||||
|
||||
相关软件下载: https://gitee.com/bjdgyc/anylink-soft
|
||||
相关软件下载: QQ群共享文件: 567510628
|
||||
|
||||
## Discussion
|
||||
|
||||

|
||||
|
||||
添加QQ群: 567510628
|
||||
|
||||
## Contribution
|
||||
|
||||
欢迎提交 PR、Issues,感谢为AnyLink做出贡献
|
||||
|
||||
## 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"
|
23
deploy.sh
23
deploy.sh
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
git clone https://github.com/bjdgyc/anylink-web.git
|
||||
|
||||
cd anylink-web
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
cd ../
|
||||
cp -r anylink-web/ui .
|
||||
go build -o anylink -ldflags "-X main.COMMIT_ID=`git rev-parse HEAD`"
|
||||
|
||||
#整理部署文件
|
||||
mkdir anylink-deploy
|
||||
cd anylink-deploy
|
||||
|
||||
cp -r ../anylink .
|
||||
cp -r ../conf .
|
||||
cp -r ../down_files .
|
||||
|
||||
#注意使用root权限运行
|
||||
#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 |
BIN
screenshot/qq.png
Normal file
BIN
screenshot/qq.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 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)
|
||||
@@ -56,7 +56,7 @@ func authMiddleware(next http.Handler) http.Handler {
|
||||
route := mux.CurrentRoute(r)
|
||||
name := route.GetName()
|
||||
// fmt.Println("bb", r.URL.Path, name)
|
||||
if name == "login" || name == "static" {
|
||||
if utils.InArrStr([]string{"login", "index", "static"}, name) {
|
||||
// 不进行鉴权
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
@@ -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 {
|
||||
@@ -100,14 +100,18 @@ func UserSet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 发送邮件
|
||||
if data.SendEmail {
|
||||
userAccountMail(data)
|
||||
err = userAccountMail(data)
|
||||
if err != nil {
|
||||
RespError(w, RespInternalErr, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
RespSucess(w, nil)
|
||||
}
|
||||
|
||||
func UserDel(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
_ = r.ParseForm()
|
||||
idS := r.FormValue("id")
|
||||
id, _ := strconv.Atoi(idS)
|
||||
|
||||
@@ -126,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)
|
||||
@@ -144,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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 在线用户
|
||||
@@ -165,12 +174,19 @@ 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()
|
||||
token := r.FormValue("token")
|
||||
sessdata.CloseCSess(token)
|
||||
RespSucess(w, nil)
|
||||
}
|
||||
|
||||
type userAccountMailData struct {
|
||||
Issuer string
|
||||
LinkAddr string
|
||||
@@ -220,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{}
|
||||
@@ -68,7 +59,9 @@ func SendMail(subject, to, htmlBody string) error {
|
||||
server.Port = dataSmtp.Port
|
||||
server.Username = dataSmtp.Username
|
||||
server.Password = dataSmtp.Password
|
||||
// server.Encryption = mail.EncryptionTLS
|
||||
if dataSmtp.UseSSl {
|
||||
server.Encryption = mail.EncryptionSSL
|
||||
}
|
||||
|
||||
// Since v2.3.0 you can specified authentication type:
|
||||
// - PLAIN (default)
|
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,7 +2,6 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
@@ -10,21 +9,18 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var UiPath embed.FS
|
||||
|
||||
// 开启服务
|
||||
func StartAdmin() {
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Use(authMiddleware)
|
||||
|
||||
r.Handle("/", http.RedirectHandler("/ui/", http.StatusFound)).
|
||||
Name("static")
|
||||
r.PathPrefix("/ui/").Handler(http.FileServer(
|
||||
http.FS(UiPath),
|
||||
)).Name("static")
|
||||
|
||||
r.Handle("/", http.RedirectHandler("/ui/", http.StatusFound)).Name("index")
|
||||
r.PathPrefix("/ui/").Handler(
|
||||
http.StripPrefix("/ui/", http.FileServer(http.Dir(base.Cfg.UiPath))),
|
||||
).Name("static")
|
||||
r.HandleFunc("/base/login", Login).Name("login")
|
||||
|
||||
r.HandleFunc("/set/home", SetHome)
|
||||
r.HandleFunc("/set/system", SetSystem)
|
||||
r.HandleFunc("/set/soft", SetSoft)
|
||||
@@ -39,6 +35,7 @@ func StartAdmin() {
|
||||
r.HandleFunc("/user/del", UserDel)
|
||||
r.HandleFunc("/user/online", UserOnline)
|
||||
r.HandleFunc("/user/offline", UserOffline)
|
||||
r.HandleFunc("/user/reline", UserReline)
|
||||
r.HandleFunc("/user/otp_qr", UserOtpQr)
|
||||
r.HandleFunc("/user/ip_map/list", UserIpMapList)
|
||||
r.HandleFunc("/user/ip_map/detail", UserIpMapDetail)
|
@@ -2,5 +2,5 @@ package base
|
||||
|
||||
const (
|
||||
APP_NAME = "AnyLink"
|
||||
APP_VER = "0.0.6"
|
||||
APP_VER = "0.1.8"
|
||||
)
|
@@ -39,7 +39,9 @@ type ServerConfig struct {
|
||||
DbFile string `toml:"db_file" info:"数据库地址"`
|
||||
CertFile string `toml:"cert_file" info:"证书文件"`
|
||||
CertKey string `toml:"cert_key" info:"证书密钥"`
|
||||
DownFilesPath string `json:"down_files_path" info:"外部下载文件路径"`
|
||||
UiPath string `toml:"ui_path" info:"ui文件路径"`
|
||||
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:"管理用户名"`
|
||||
@@ -47,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租期(秒)"`
|
||||
@@ -82,7 +83,9 @@ func initServerCfg() {
|
||||
Cfg.DbFile = getAbsPath(base, Cfg.DbFile)
|
||||
Cfg.CertFile = getAbsPath(base, Cfg.CertFile)
|
||||
Cfg.CertKey = getAbsPath(base, Cfg.CertKey)
|
||||
Cfg.DownFilesPath = getAbsPath(base, Cfg.DownFilesPath)
|
||||
Cfg.UiPath = getAbsPath(base, Cfg.UiPath)
|
||||
Cfg.FilesPath = getAbsPath(base, Cfg.FilesPath)
|
||||
Cfg.LogPath = getAbsPath(base, Cfg.LogPath)
|
||||
|
||||
if len(Cfg.JwtSecret) < 20 {
|
||||
fmt.Println("请设置 jwt_secret 长度20位以上")
|
||||
@@ -93,6 +96,10 @@ func initServerCfg() {
|
||||
}
|
||||
|
||||
func getAbsPath(base, cfile string) string {
|
||||
if cfile == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
abs := filepath.IsAbs(cfile)
|
||||
if abs {
|
||||
return cfile
|
||||
@@ -100,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()
|
||||
@@ -120,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.
|
||||
|
@@ -8,8 +8,11 @@ db_file = "./data.db"
|
||||
#证书文件
|
||||
cert_file = "./vpn_cert.pem"
|
||||
cert_key = "./vpn_cert.key"
|
||||
down_files_path = "../down_files"
|
||||
|
||||
ui_path = "../ui"
|
||||
files_path = "../files"
|
||||
#日志目录,为空写入标准输出
|
||||
#log_path = "../log"
|
||||
log_path = ""
|
||||
log_level = "info"
|
||||
|
||||
#系统名称
|
||||
@@ -21,7 +24,7 @@ admin_pass = "$2a$10$UQ7C.EoPifDeJh6d8.31TeSPQU7hM/NOM2nixmBucJpAuXDQNqNke"
|
||||
jwt_secret = ""
|
||||
|
||||
|
||||
#vpn服务对外地址
|
||||
#vpn服务对外地址,影响开通邮件二维码
|
||||
link_addr = "vpn.xx.com"
|
||||
|
||||
#前台服务监听地址
|
||||
@@ -34,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"]
|
||||
|
@@ -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>
|
@@ -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)
|
||||
}
|
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
@@ -19,12 +18,15 @@ type GroupLinkAcl struct {
|
||||
// 自上而下匹配 默认 allow * *
|
||||
Action string `json:"action"` // allow、deny
|
||||
Val string `json:"val"`
|
||||
Port uint8 `json:"port"`
|
||||
IpNet *net.IPNet `json:"-"`
|
||||
Port uint16 `json:"port"`
|
||||
IpNet *net.IPNet `json:"ip_net"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
type ValData struct {
|
||||
Val string `json:"val"`
|
||||
IpMask string `json:"ip_mask"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
@@ -70,25 +72,32 @@ func SetGroup(g *Group) error {
|
||||
}
|
||||
}
|
||||
if len(clientDns) == 0 {
|
||||
return errors.New("DNS错误")
|
||||
return errors.New("DNS 错误")
|
||||
}
|
||||
g.ClientDns = clientDns
|
||||
|
||||
routeInclude := []ValData{}
|
||||
for _, v := range g.RouteInclude {
|
||||
if v.Val != "" {
|
||||
v1, _ := parseIpNet(v.Val)
|
||||
vn := ValData{Val: v1}
|
||||
routeInclude = append(routeInclude, vn)
|
||||
ipMask, _, err := parseIpNet(v.Val)
|
||||
if err != nil {
|
||||
return errors.New("RouteInclude 错误" + err.Error())
|
||||
}
|
||||
|
||||
v.IpMask = ipMask
|
||||
routeInclude = append(routeInclude, v)
|
||||
}
|
||||
}
|
||||
g.RouteInclude = routeInclude
|
||||
routeExclude := []ValData{}
|
||||
for _, v := range g.RouteExclude {
|
||||
if v.Val != "" {
|
||||
v1, _ := parseIpNet(v.Val)
|
||||
vn := ValData{Val: v1}
|
||||
routeExclude = append(routeExclude, vn)
|
||||
ipMask, _, err := parseIpNet(v.Val)
|
||||
if err != nil {
|
||||
return errors.New("RouteExclude 错误" + err.Error())
|
||||
}
|
||||
v.IpMask = ipMask
|
||||
routeExclude = append(routeExclude, v)
|
||||
}
|
||||
}
|
||||
g.RouteExclude = routeExclude
|
||||
@@ -96,13 +105,12 @@ func SetGroup(g *Group) error {
|
||||
linkAcl := []GroupLinkAcl{}
|
||||
for _, v := range g.LinkAcl {
|
||||
if v.Val != "" {
|
||||
v1, v2 := parseIpNet(v.Val)
|
||||
if v2 != nil {
|
||||
vn := v
|
||||
vn.Val = v1
|
||||
vn.IpNet = v2
|
||||
linkAcl = append(linkAcl, vn)
|
||||
_, ipNet, err := parseIpNet(v.Val)
|
||||
if err != nil {
|
||||
return errors.New("GroupLinkAcl 错误" + err.Error())
|
||||
}
|
||||
v.IpNet = ipNet
|
||||
linkAcl = append(linkAcl, v)
|
||||
}
|
||||
}
|
||||
g.LinkAcl = linkAcl
|
||||
@@ -113,24 +121,14 @@ func SetGroup(g *Group) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func parseIpNet(s string) (string, *net.IPNet) {
|
||||
ips := strings.Split(s, "/")
|
||||
if len(ips) != 2 {
|
||||
return "", nil
|
||||
}
|
||||
ip := net.ParseIP(ips[0])
|
||||
mask := net.ParseIP(ips[1])
|
||||
|
||||
if strings.Contains(ips[0], ".") {
|
||||
ip = ip.To4()
|
||||
mask = mask.To4()
|
||||
func parseIpNet(s string) (string, *net.IPNet, error) {
|
||||
ip, ipNet, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
ipmask := net.IPMask(mask)
|
||||
ip0 := ip.Mask(ipmask)
|
||||
mask := net.IP(ipNet.Mask)
|
||||
ipMask := fmt.Sprintf("%s/%s", ip, mask)
|
||||
|
||||
ipNetS := fmt.Sprintf("%s/%s", ip0, mask)
|
||||
ipNet := &net.IPNet{IP: ip0, Mask: ipmask}
|
||||
|
||||
return ipNetS, ipNet
|
||||
return ipMask, ipNet, nil
|
||||
}
|
33
server/dbdata/group_test.go
Normal file
33
server/dbdata/group_test.go
Normal 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))
|
||||
}
|
||||
}
|
@@ -38,6 +38,7 @@ type SettingSmtp struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
From string `json:"from"`
|
||||
UseSSl bool `json:"use_ssl"`
|
||||
}
|
||||
|
||||
type SettingOther struct {
|
@@ -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
|
||||
}
|
43
server/dbdata/user_test.go
Normal file
43
server/dbdata/user_test.go
Normal 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
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()
|
||||
}()
|
||||
|
||||
@@ -49,12 +49,12 @@ func LinkCstp(conn net.Conn, cSess *sessdata.ConnSession) {
|
||||
switch hdata[6] {
|
||||
case 0x07: // KEEPALIVE
|
||||
// do nothing
|
||||
base.Debug("recv keepalive", cSess.IpAddr)
|
||||
// base.Debug("recv keepalive", cSess.IpAddr)
|
||||
case 0x05: // DISCONNECT
|
||||
base.Debug("DISCONNECT", cSess.IpAddr)
|
||||
return
|
||||
case 0x03: // DPD-REQ
|
||||
base.Debug("recv DPD-REQ", cSess.IpAddr)
|
||||
// base.Debug("recv DPD-REQ", cSess.IpAddr)
|
||||
if payloadOut(cSess, sessdata.LTypeIPData, 0x04, nil) {
|
||||
return
|
||||
}
|
||||
@@ -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
|
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -73,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网络,必须放在路由的第一个
|
||||
@@ -86,11 +87,11 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// 允许的路由
|
||||
for _, v := range cSess.Group.RouteInclude {
|
||||
w.Header().Add("X-CSTP-Split-Include", v.Val)
|
||||
w.Header().Add("X-CSTP-Split-Include", v.IpMask)
|
||||
}
|
||||
// 不允许的路由
|
||||
for _, v := range cSess.Group.RouteExclude {
|
||||
w.Header().Add("X-CSTP-Split-Exclude", v.Val)
|
||||
w.Header().Add("X-CSTP-Split-Exclude", v.IpMask)
|
||||
}
|
||||
|
||||
w.Header().Set("X-CSTP-Lease-Duration", fmt.Sprintf("%d", base.Cfg.IpLease)) // ip地址租期
|
||||
@@ -130,12 +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()
|
||||
// h.Write(os.Stdout)
|
||||
hClone := w.Header().Clone()
|
||||
headers := make([]byte, 0)
|
||||
buf := bytes.NewBuffer(headers)
|
||||
_ = 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()
|
||||
}
|
@@ -1,26 +1,23 @@
|
||||
// AnyLink 是一个企业级远程办公vpn软件,可以支持多人同时在线使用。
|
||||
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/bjdgyc/anylink/admin"
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
"github.com/bjdgyc/anylink/handler"
|
||||
)
|
||||
|
||||
//go:embed ui/*
|
||||
var UiPath embed.FS
|
||||
|
||||
// 程序版本
|
||||
var COMMIT_ID string
|
||||
|
||||
func main() {
|
||||
base.CommitId = COMMIT_ID
|
||||
admin.UiPath = UiPath
|
||||
|
||||
base.Start()
|
||||
handler.Start()
|
@@ -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
|
@@ -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)
|
@@ -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
|
||||
@@ -296,6 +297,17 @@ func CloseSess(token string) {
|
||||
sess.CSess.Close()
|
||||
}
|
||||
|
||||
func CloseCSess(token string) {
|
||||
sessMux.Lock()
|
||||
defer sessMux.Unlock()
|
||||
sess, ok := sessions[token]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
sess.CSess.Close()
|
||||
}
|
||||
|
||||
func DelSessByStoken(stoken string) {
|
||||
stoken = strings.TrimSpace(stoken)
|
||||
sarr := strings.Split(stoken, "@")
|
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)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user