This commit is contained in:
coward 2024-07-05 14:41:35 +08:00
commit 1f7f57ec9f
70 changed files with 4923 additions and 0 deletions

187
.gitignore vendored Normal file
View File

@ -0,0 +1,187 @@
### GoLand+all template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Go template
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# 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/
# Go workspace file
go.work
### GoLand template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
template/tmp/*
logs/*
app.yaml
*.db
.env
*.env

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
wireguard-ui

View File

@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
<functions>
<function importPath="github.com/golang-jwt/jwt/v5" name="ParseWithClaims" />
</functions>
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/wireguard-ui.iml" filepath="$PROJECT_DIR$/.idea/wireguard-ui.iml" />
</modules>
</component>
</project>

7
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/web" vcs="Git" />
</component>
</project>

8
.idea/watcherTasks.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<enabled-global>
<option value="go fmt" />
</enabled-global>
</component>
</project>

9
.idea/wireguard-ui.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

68
command/wireguard.go Normal file
View File

@ -0,0 +1,68 @@
package command
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"os/exec"
"strings"
"wireguard-ui/service"
)
// getConfigFileName
// @description: 获取服务端配置文件名称
// @return string
func getConfigFileName() string {
data, err := service.Setting().GetWGSetForConfig()
if err != nil {
log.Errorf("获取服务端配置失败: %v", err.Error())
return ""
}
if data.ConfigFilePath != "" {
data.ConfigFilePath = strings.Split(data.ConfigFilePath, "/")[len(strings.Split(data.ConfigFilePath, "/"))-1] // 这里取到的是wg0.conf
data.ConfigFilePath = strings.Split(data.ConfigFilePath, ".conf")[0] // 这里取到的就是wg0
}
return data.ConfigFilePath
}
// RestartWireguard
// @description: 是否重启
// @param isAsync // 是否异步执行
func RestartWireguard(isAsync bool) {
if isAsync {
go func() {
StopWireguard() // 停止
StartWireguard() // 启动
}()
} else {
StopWireguard() // 停止
StartWireguard() // 启动
}
return
}
// StopWireguard
// @description: 停止服务端
func StopWireguard() {
configFileName := getConfigFileName()
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("wg-quick down %s", configFileName))
if err := cmd.Run(); err == nil {
log.Infof("停止wireguard[%s]服务端成功", configFileName)
}
return
}
// StartWireguard
// @description: 启动服务端
func StartWireguard() {
configFileName := getConfigFileName()
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("wg-quick up %s", configFileName))
if err := cmd.Run(); err == nil {
log.Infof("启动wireguard[%s]服务端成功", configFileName)
}
return
}

57
component/captcha.go Normal file
View File

@ -0,0 +1,57 @@
package component
import (
"context"
"fmt"
"os"
"strings"
"time"
"wireguard-ui/global/client"
"wireguard-ui/global/constant"
)
type Captcha struct{}
// Set
// @description: 验证码放入指定存储
// @receiver Captcha
// @param id
// @param value
// @return error
func (Captcha) Set(id string, value string) error {
return client.Redis.Set(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id), value, 2*time.Minute).Err()
}
// Get
// @description: 获取验证码信息
// @receiver Captcha
// @param id
// @param clear
// @return string
func (Captcha) Get(id string, clear bool) string {
val, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id)).Result()
if err != nil {
return ""
}
if clear {
client.Redis.Del(context.Background(), fmt.Sprintf("%s:%s", constant.Captcha, id))
return val
}
return val
}
// Verify
// @description: 校验
// @receiver Captcha
// @param id
// @param answer
// @param clear
// @return bool
func (c Captcha) Verify(id, answer string, clear bool) bool {
if os.Getenv("GIN_MODE") != "release" {
return true
}
return strings.ToUpper(answer) == strings.ToUpper(c.Get(id, clear))
}

105
component/jwt.go Normal file
View File

@ -0,0 +1,105 @@
package component
import (
"context"
"errors"
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"strings"
"time"
"wireguard-ui/config"
"wireguard-ui/global/client"
"wireguard-ui/global/constant"
)
// jwt密钥
const secret = "JQo7L1RYa8ArFWuj0wC9PyM3VzmDIfXZ2d5tsTOBhNgviE64bnKqGpSckxUlHey6"
type JwtComponent struct {
ID string `json:"id"`
jwt.RegisteredClaims
}
// JWT
// @description: 初始化JWT组件
// @return JwtComponent
func JWT() JwtComponent {
return JwtComponent{}
}
// GenerateToken
// @description: 生成token
// @receiver JwtComponent
// @param userId
// @return token
// @return expireTime
// @return err
func (JwtComponent) GenerateToken(userId string) (token string, expireTime *jwt.NumericDate, err error) {
timeNow := time.Now().Local()
expireTime = jwt.NewNumericDate(timeNow.Add(7 * time.Hour))
notBefore := jwt.NewNumericDate(timeNow)
issuedAt := jwt.NewNumericDate(timeNow)
claims := JwtComponent{
ID: userId,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: config.Config.Http.Endpoint, // 颁发站点
Subject: "you can you up,no can no bb", // 发布主题
ExpiresAt: expireTime, // 过期时间
NotBefore: notBefore, // token不得早于该时间
IssuedAt: issuedAt, // token颁发时间
ID: strings.ReplaceAll(uuid.NewString(), "-", ""), // 该token的id
},
}
t := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
token, err = t.SignedString([]byte(secret))
if err != nil {
log.Errorf("生成token失败: %v", err.Error())
return "", nil, errors.New("生成token失败")
}
client.Redis.Set(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, userId), token, 7*time.Hour)
return
}
// ParseToken
// @description: 解析token
// @receiver JwtComponent
// @param token
// @return *JwtComponent
// @return error
func (JwtComponent) ParseToken(token string) (*JwtComponent, error) {
tokenStr := strings.Split(token, "Bearer ")[1]
t, err := jwt.ParseWithClaims(tokenStr, &JwtComponent{}, func(token *jwt.Token) (any, error) {
return []byte(secret), nil
})
if claims, ok := t.Claims.(*JwtComponent); ok && t.Valid {
userToken, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, claims.ID)).Result()
if err != nil {
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
return nil, errors.New("token不存在")
}
if userToken != tokenStr {
log.Errorf("token不一致")
return nil, errors.New("token错误")
}
return claims, nil
} else {
return nil, err
}
}
// Logout
// @description: 退出登陆
// @receiver JwtComponent
// @param userId
// @return error
func (JwtComponent) Logout(userId string) error {
return client.Redis.Del(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, userId)).Err()
}

76
component/template.go Normal file
View File

@ -0,0 +1,76 @@
package component
import (
"gitee.ltd/lxh/logger/log"
"html/template"
"os"
)
type TemplateComponent struct{}
func Template() TemplateComponent {
return TemplateComponent{}
}
// Execute
// @description: 渲染数据模板并生成对应文件
// @receiver t
// @param templateFilePath
// @param outFilePath
// @param data
// @return err
func (t TemplateComponent) Execute(templateFilePath, outFilePath string, data any) (err error) {
parseTemplate, err := t.ParseTemplate(templateFilePath)
if err != nil {
log.Errorf("解析模板信息失败:%v", err.Error())
return err
}
err = t.Render(parseTemplate, data, outFilePath)
if err != nil {
log.Errorf("渲染模板失败: %v", err.Error())
return err
}
return nil
}
// ParseTemplate
// @description: 解析模板
// @receiver t
// @param filepath
// @return t
// @return err
func (t TemplateComponent) ParseTemplate(filepath string) (tp *template.Template, err error) {
file, err := os.ReadFile(filepath)
if err != nil {
return
}
tp, err = template.New("wg.conf").Parse(string(file))
return
}
// Render
// @description: 渲染模板
// @receiver t
// @param tp
// @param data
// @param filepath
// @return err
func (t TemplateComponent) Render(tp *template.Template, data any, filepath string) (err error) {
wg0Conf, err := os.Create(filepath)
if err != nil {
log.Errorf("创建文件[%s]失败: %v", filepath, err.Error())
return
}
defer func() {
if err = wg0Conf.Close(); err != nil {
log.Errorf("关闭文件[%s]失败: %v", filepath, err.Error())
return
}
}()
return tp.Execute(wg0Conf, data)
}

92
component/validator.go Normal file
View File

@ -0,0 +1,92 @@
package component
import (
"errors"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
"reflect"
"strings"
)
var validatorTrans ut.Translator
func init() {
initValidatorTranslator()
}
func Error(err error) string {
var errs validator.ValidationErrors
ok := errors.As(err, &errs)
if !ok {
return err.Error()
}
errMap := errs.Translate(validatorTrans)
var errMsg string
for _, v := range errMap {
errMsg = v
}
return errMsg
}
// initValidatorTranslator
// @description: 初始化翻译机
// @receiver vli
func initValidatorTranslator() {
//修改gin框架中的Validator属性实现自定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册一个获取json tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("label"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New() //中文翻译器
enT := en.New() //英文翻译器
// 第一个参数是备用fallback的语言环境
// 后面的参数是应该支持的语言环境(支持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
// locale 通常取决于 http 请求头的 'Accept-Language'
var ok bool
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
validatorTrans, ok = uni.GetTranslator("zh")
if !ok {
log.Errorf("获取翻译机失败")
return
}
err := overrideTranslator(v, validatorTrans)
if err != nil {
log.Errorf("覆盖原有翻译失败: %v", err.Error())
return
}
}
}
// overrideTranslator
// @description: 覆盖原有翻译
// @param v
// @param translator
// @return error
func overrideTranslator(v *validator.Validate, translator ut.Translator) error {
err := zhTranslations.RegisterDefaultTranslations(v, translator)
if err != nil {
return err
}
return nil
}

168
component/wireguard.go Normal file
View File

@ -0,0 +1,168 @@
package component
import (
"encoding/json"
"errors"
"fmt"
"gitee.ltd/lxh/logger/log"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gopkg.in/fsnotify/fsnotify.v1"
"os"
"strings"
"time"
"wireguard-ui/command"
"wireguard-ui/config"
"wireguard-ui/global/client"
"wireguard-ui/model"
"wireguard-ui/template/render_data"
)
type WireguardComponent struct{}
func Wireguard() WireguardComponent {
return WireguardComponent{}
}
// GetClients
// @description: 获取所有链接的客户端信息
// @receiver w
// @return peers
// @return err
func (w WireguardComponent) GetClients() (peers []wgtypes.Peer, err error) {
device, err := client.WireguardClient.Devices()
if err != nil {
return
}
for _, v := range device {
return v.Peers, nil
}
return
}
// GetClientByPublicKey
// @description: 根据公钥获取指定客户端信息
// @receiver w
// @return peer
// @return err
func (w WireguardComponent) GetClientByPublicKey(pk string) (peer *wgtypes.Peer, err error) {
peers, err := w.GetClients()
if err != nil {
return
}
for _, v := range peers {
if v.PublicKey.String() == pk {
return &v, nil
}
}
return
}
// GenerateClientFile
// @description: 生成客户端文件
// @receiver w
// @param clientInfo
// @param server
// @param setting
// @return filePath
// @return err
func (w WireguardComponent) GenerateClientFile(clientInfo *model.Client, server *render_data.Server, setting *render_data.ServerSetting) (filePath string, err error) {
var keys render_data.Keys
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
var serverDNS []string
if clientInfo.UseServerDns == 1 {
serverDNS = setting.DnsServer
}
// 处理一下数据
execData := render_data.ClientConfig{
PrivateKey: keys.PrivateKey,
IpAllocation: clientInfo.IpAllocation,
MTU: setting.MTU,
DNS: strings.Join(serverDNS, ","),
PublicKey: server.PublicKey,
PresharedKey: keys.PresharedKey,
AllowedIPS: clientInfo.AllowedIps,
Endpoint: setting.EndpointAddress,
ListenPort: int(server.ListenPort),
PersistentKeepalive: setting.PersistentKeepalive,
}
// 不同环境下处理文件路径
var outPath = "/tmp/" + fmt.Sprintf("%s.conf", clientInfo.Name)
var templatePath = "./template/wg.client.conf"
if os.Getenv("GIN_MODE") != "release" {
outPath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\" + fmt.Sprintf("%s.conf", clientInfo.Name)
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.client.conf"
}
err = Template().Execute(templatePath, outPath, execData)
if err != nil {
return "", errors.New("文件渲染失败")
}
return outPath, nil
}
// ServerControl
// @description: 服务端控制
// @receiver w
// @return error
func (w WireguardComponent) ServerControl(filePath string) {
switch config.Config.Wireguard.RestartMode {
case "NOW": // 立即执行
w.watchConfigFile(filePath)
case "DELAY": // 延迟执行
time.Sleep(time.Duration(config.Config.Wireguard.DelayTime) * time.Second)
w.watchConfigFile(filePath)
}
}
// watchConfigFile
// @description: 监听并重新操作配置文件
// @receiver w
// @param filepath
func (w WireguardComponent) watchConfigFile(filepath string) {
go func() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Errorf("创建文件监控失败: %v", err.Error())
return
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op == fsnotify.Write {
command.RestartWireguard(true)
}
// 打印监听事件
log.Infof("监听事件是:%s", event.String())
case _, ok := <-watcher.Errors:
if !ok {
return
}
}
}
}()
if err = watcher.Add(filepath); err != nil {
log.Errorf("添加[%s]监听失败: %v", filepath, err.Error())
return
}
<-done
}()
}

12
config/config.go Normal file
View File

@ -0,0 +1,12 @@
package config
var Config *config
type config struct {
Http *http `yaml:"http"`
Database *database `yaml:"database"`
Redis *redis `yaml:"redis"`
File *file `yaml:"file"`
Mail *mail `yaml:"email"`
Wireguard *wireguard `yaml:"wireguard"`
}

25
config/databse.go Normal file
View File

@ -0,0 +1,25 @@
package config
import "fmt"
type database struct {
Driver string `yaml:"driver"`
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
Db string `yaml:"db"`
}
func (d database) GetDSN() string {
switch d.Driver {
case "mysql":
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", d.User, d.Password, d.Host, d.Port, d.Db)
case "pgsql":
return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", d.Host, d.User, d.Password, d.Db, d.Port)
case "sqlite":
return fmt.Sprintf("%s.db", d.Db)
}
return ""
}

10
config/file.go Normal file
View File

@ -0,0 +1,10 @@
package config
type file struct {
Type string `yaml:"type"`
Path string `yaml:"path"`
Endpoint string `yaml:"endpoint"`
AccessId string `yaml:"accessId"`
AccessSecret string `yaml:"accessSecret"`
BucketName string `yaml:"bucketName"`
}

6
config/http.go Normal file
View File

@ -0,0 +1,6 @@
package config
type http struct {
Port uint `yaml:"port"`
Endpoint string `yaml:"endpoint"`
}

9
config/mail.go Normal file
View File

@ -0,0 +1,9 @@
package config
type mail struct {
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
User string `json:"user" yaml:"user"`
Password string `json:"password" yaml:"password"`
SkipTls bool `json:"skipTls" yaml:"skipTls"`
}

8
config/redis.go Normal file
View File

@ -0,0 +1,8 @@
package config
type redis struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Password string `yaml:"password"`
Db int `yaml:"db"`
}

6
config/wireguard.go Normal file
View File

@ -0,0 +1,6 @@
package config
type wireguard struct {
RestartMode string `json:"restartMode"` // 重启模式 NOW - 立即重启 | DELAY - 延时 | HAND - 手动重启
DelayTime int64 `json:"delayTime"` // 延时重启的间隔(单位:秒)
}

17
global/client/client.go Normal file
View File

@ -0,0 +1,17 @@
package client
import (
"github.com/cowardmrx/go_aliyun_oss"
"github.com/go-resty/resty/v2"
"github.com/redis/go-redis/v9"
"golang.zx2c4.com/wireguard/wgctrl"
"gorm.io/gorm"
)
var (
WireguardClient *wgctrl.Client // wireguard客户端
DB *gorm.DB // 数据库客户端
Redis *redis.Client // redis客户端
HttpClient *resty.Client // http客户端
OSS *go_aliyun_oss.AliOssClient // oss客户端
)

View File

@ -0,0 +1,22 @@
package constant
// Status 启用禁用
type Status int
const (
Disabled Status = iota
Enabled
)
var StatusMap = map[Status]string{
Disabled: "禁用",
Enabled: "启用",
}
func (u Status) String() string {
if v, ok := StatusMap[u]; ok {
return v
}
return "未知类型"
}

View File

@ -0,0 +1,6 @@
package constant
const (
Captcha = "captcha:"
UserToken = "token:"
)

View File

@ -0,0 +1,22 @@
package constant
// UserType 用户类型
type UserType int
const (
NormalAdmin UserType = iota
SuperAdmin
)
var UserTypeMap = map[UserType]string{
NormalAdmin: "普通管理员",
SuperAdmin: "超级管理员",
}
func (u UserType) String() string {
if v, ok := UserTypeMap[u]; ok {
return v
}
return "未知类型"
}

View File

@ -0,0 +1,7 @@
package constant
const (
DefaultPostUpScript = "iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE"
DefaultPostDownScript = "iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE"
DefaultPreDownScript = ""
)

120
go.mod Normal file
View File

@ -0,0 +1,120 @@
module wireguard-ui
go 1.21
require (
gitee.ltd/lxh/logger v1.0.15
github.com/cowardmrx/go_aliyun_oss v1.0.7
github.com/fsnotify/fsnotify v1.7.0
github.com/gin-contrib/pprof v1.5.0
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0
github.com/go-resty/resty/v2 v2.13.1
github.com/redis/go-redis/v9 v9.5.3
github.com/spf13/viper v1.19.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.10
)
require (
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/caarlos0/env/v6 v6.10.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-kit/kit v0.12.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lixh00/loki-client-go v1.0.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mojocn/base64Captcha v1.3.6 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)

1387
go.sum Normal file

File diff suppressed because it is too large Load Diff

20
http/api/api.go Normal file
View File

@ -0,0 +1,20 @@
package api
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/response"
"wireguard-ui/http/vo"
)
// GetCurrentLoginUser
// @description: 获取当前登陆用户
// @param c
// @return *vo.User
func GetCurrentLoginUser(c *gin.Context) *vo.User {
if user, ok := c.Get("user"); ok {
return user.(*vo.User)
}
response.R(c).AuthorizationFailed("暂未登陆")
c.Abort()
return nil
}

239
http/api/client.go Normal file
View File

@ -0,0 +1,239 @@
package api
import (
"encoding/json"
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin"
"github.com/spf13/cast"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"os"
"strings"
"wireguard-ui/component"
"wireguard-ui/http/param"
"wireguard-ui/http/response"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type ClientApi struct{}
func Client() ClientApi {
return ClientApi{}
}
// Save
// @description: 新增/编辑客户端
// @param c
func (ClientApi) Save(c *gin.Context) {
var p param.SaveClient
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
var loginUser *vo.User
if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
return
}
if err := service.Client().SaveClient(p, loginUser); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// Delete
// @description: 删除客户端
// @receiver ClientApi
// @param c
func (ClientApi) Delete(c *gin.Context) {
id := c.Param("id")
if id == "" || id == "undefined" {
response.R(c).FailedWithError("id不能为空")
return
}
if err := service.Client().Delete(id); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// List
// @description: 客户端分页列表
// @receiver ClientApi
// @param c
func (ClientApi) List(c *gin.Context) {
var p param.ClientList
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
data, total, err := service.Client().List(p)
if err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).Paginate(data, total, p.Current, p.Size)
}
// GenerateKeys
// @description: 生成客户端密钥信息
// @receiver ClientApi
// @param c
func (ClientApi) GenerateKeys(c *gin.Context) {
// 为空,新增
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
response.R(c).FailedWithError(fmt.Errorf("生成密钥失败: %v", err.Error()))
return
}
publicKey := privateKey.PublicKey().String()
presharedKey, err := wgtypes.GenerateKey()
if err != nil {
response.R(c).FailedWithError(fmt.Errorf("生成密钥失败: %v", err.Error()))
return
}
keys := vo.Keys{
PrivateKey: privateKey.String(),
PublicKey: publicKey,
PresharedKey: presharedKey.String(),
}
response.R(c).OkWithData(keys)
}
// GenerateIP
// @description: 生成客户端IP
// @receiver ClientApi
// @param c
func (ClientApi) GenerateIP(c *gin.Context) {
// 获取一下服务端信息因为IP分配需要根据服务端的IP制定
serverInfo, err := service.Setting().GetWGServerForConfig()
if err != nil {
response.R(c).FailedWithError("获取服务端信息失败")
return
}
var assignIPS []string
// 只获取最新的一个
var clientInfo *model.Client
if err = service.Client().Order("created_at DESC").Take(&clientInfo).Error; err == nil {
// 遍历每一个ip是否可允许再分配
for _, ip := range strings.Split(clientInfo.IpAllocation, ",") {
if cast.ToInt64(utils.Network().GetIPSuffix(ip)) >= 255 {
log.Errorf("IP[%s]已无法分配新IP", ip)
continue
} else {
assignIPS = append(assignIPS, ip)
}
}
}
ips := utils.Network().GenerateIPByIPS(serverInfo.Address, assignIPS...)
response.R(c).OkWithData(ips)
}
// Download
// @description: 下载客户端配置文件
// @receiver ClientApi
// @param c
func (ClientApi) Download(c *gin.Context) {
var id = c.Param("id")
if id == "" || id == "undefined" {
response.R(c).FailedWithError("id不能为空")
return
}
var downloadType = c.Param("type")
if downloadType == "" {
response.R(c).FailedWithError("参数错误")
return
}
data, err := service.Client().GetByID(id)
if err != nil {
response.R(c).FailedWithError("获取客户端信息失败")
return
}
var keys vo.Keys
_ = json.Unmarshal([]byte(data.Keys), &keys)
globalSet, err := service.Setting().GetWGSetForConfig()
if err != nil {
response.R(c).FailedWithError("获取失败")
return
}
serverConf, err := service.Setting().GetWGServerForConfig()
if err != nil {
response.R(c).FailedWithError("获取失败")
return
}
outPath, err := component.Wireguard().GenerateClientFile(data, serverConf, globalSet)
if err != nil {
response.R(c).FailedWithError(fmt.Errorf("生成失败: %v", err.Error()))
return
}
// 根据不同下载类型执行不同逻辑
switch downloadType {
case "QRCODE": // 二维码
// 读取文件内容
fileContent, err := os.ReadFile(outPath)
if err != nil {
response.R(c).FailedWithError("读取文件失败")
return
}
png, err := utils.QRCode().GenerateQrCodeBase64(fileContent, 256)
if err != nil {
response.R(c).FailedWithError("生成二维码失败")
return
}
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
response.R(c).OkWithData(map[string]interface{}{
"qrCode": png,
})
case "FILE": // 文件
// 输出文件流
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename="+outPath)
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Connection", "keep-alive")
c.File(outPath)
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
case "EMAIL": // 邮件
if data.Email == "" {
response.R(c).FailedWithError("当前客户端并未配置通知邮箱!")
return
}
err = utils.Mail().SendMail(data.Email, fmt.Sprintf("客户端: %s", data.Name), "请查收附件", outPath)
if err != nil {
response.R(c).FailedWithError("发送邮件失败")
return
}
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
response.R(c).OK()
}
}

87
http/api/login.go Normal file
View File

@ -0,0 +1,87 @@
package api
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"wireguard-ui/component"
"wireguard-ui/http/param"
"wireguard-ui/http/response"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type LoginApi struct{}
func Login() LoginApi {
return LoginApi{}
}
// Captcha
// @description: 获取验证码
// @receiver login
// @param c
func (LoginApi) Captcha(c *gin.Context) {
math := base64Captcha.DriverMath{Height: 120, Width: 480, Fonts: []string{"ApothecaryFont.ttf", "3Dumb.ttf"}}
mathDriver := math.ConvertFonts()
capt := base64Captcha.NewCaptcha(mathDriver, component.Captcha{})
id, base64Str, _, err := capt.Generate()
if err != nil {
response.R(c).FailedWithError(fmt.Errorf("生成验证码失败: %s", err.Error()))
return
}
response.R(c).OkWithData(map[string]any{
"id": id,
"captcha": base64Str,
})
}
// Login
// @description: 登陆
// @receiver login
// @param c
func (LoginApi) Login(c *gin.Context) {
var p param.Login
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
// 验证验证码是否正确
ok := component.Captcha{}.Verify(p.CaptchaId, p.CaptchaCode, true)
if !ok {
response.R(c).FailedWithError("验证码错误")
return
}
// 验证码正确,查询用户信息
user, err := service.User().GetUserByAccount(p.Account)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
// 对比密码
if !utils.Password().ComparePassword(user.Password, p.Password) {
response.R(c).FailedWithError("密码错误")
return
}
// 生成token
token, expireAt, err := component.JWT().GenerateToken(user.Id)
if err != nil {
log.Errorf("用户[%s]生成token失败: %v", user.Account, err.Error())
response.R(c).FailedWithError("登陆失败!")
return
}
response.R(c).OkWithData(map[string]any{
"token": token,
"type": "Bearer",
"expireAt": expireAt,
})
}

252
http/api/user.go Normal file
View File

@ -0,0 +1,252 @@
package api
import (
"errors"
"github.com/gin-gonic/gin"
"wireguard-ui/global/constant"
"wireguard-ui/http/param"
"wireguard-ui/http/response"
"wireguard-ui/model"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type UserApi struct{}
func User() UserApi {
return UserApi{}
}
// GetLoginUser
// @description: 获取登陆用户信息
// @receiver UserApi
// @param c
func (UserApi) GetLoginUser(c *gin.Context) {
loginUser, ok := c.Get("user")
if !ok {
response.R(c).AuthorizationFailed("未登陆")
return
}
response.R(c).OkWithData(loginUser)
}
// SaveUser
// @description: 新增/编辑用户信息
// @receiver UserApi
// @param c
func (UserApi) SaveUser(c *gin.Context) {
var p param.SaveUser
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
// 如果是新增用户判断该用户是否已经存在
if p.Id == "" {
if len(p.Account) < 2 || len(p.Account) > 20 {
response.R(c).FailedWithError(errors.New("账号长度在2-20位"))
return
}
if len(p.Password) < 8 || len(p.Password) > 32 {
response.R(c).FailedWithError(errors.New("密码长度在8-32位"))
return
}
var count int64
if err := service.User().Model(&model.User{}).Where("account = ?", p.Account).Count(&count).Error; err != nil {
response.R(c).FailedWithError(err)
return
}
if count > 0 {
response.R(c).FailedWithError(errors.New("该账号已存在"))
return
}
}
userEnt := &model.User{
Base: model.Base{
Id: p.Id,
},
Account: p.Account,
Password: p.Password,
Nickname: p.Nickname,
Avatar: p.Avatar,
Contact: p.Contact,
IsAdmin: *p.IsAdmin,
Status: *p.Status,
}
if err := service.User().CreateUser(userEnt); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// List
// @description: 用户列表
// @receiver UserApi
// @param c
func (UserApi) List(c *gin.Context) {
var p param.Page
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
data, total, err := service.User().List(p)
if err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).Paginate(data, total, p.Current, p.Size)
}
// Delete
// @description: 删除用户
// @receiver UserApi
// @param c
func (UserApi) Delete(c *gin.Context) {
id := c.Param("id")
if id == "" || id == "undefined" {
response.R(c).FailedWithError("id不能为空")
return
}
// 是不是自己删除自己
if id == GetCurrentLoginUser(c).Id && c.IsAborted() {
response.R(c).FailedWithError("非法操作")
return
}
// 先查询一下
user, err := service.User().GetUserById(id)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
// admin用户不能被删除
if user.Account == "admin" {
response.R(c).FailedWithError("当前用户不能被删除")
return
}
if err = service.User().Delete(id); err != nil {
response.R(c).FailedWithError("删除用户失败")
return
}
response.R(c).OK()
}
// Status
// @description: 设置用户状态
// @receiver UserApi
// @param c
func (UserApi) Status(c *gin.Context) {
id := c.Param("id")
if id == "" || id == "undefined" {
response.R(c).FailedWithError("id不能为空")
return
}
// 是不是自己删除自己
if id == GetCurrentLoginUser(c).Id && c.IsAborted() {
response.R(c).FailedWithError("非法操作")
return
}
// 先查询一下
user, err := service.User().GetUserById(id)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
// admin用户不能被删除
if user.Account == "admin" {
response.R(c).FailedWithError("当前用户状态不可被变更")
return
}
var state = constant.Enabled
if user.Status == constant.Enabled {
state = constant.Disabled
}
if err := service.User().Status(id, state); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// ChangePassword
// @description: 修改密码
// @receiver UserApi
// @param c
func (UserApi) ChangePassword(c *gin.Context) {
var p param.ChangePassword
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
user := GetCurrentLoginUser(c)
if user == nil {
response.R(c).FailedWithError("用户信息错误")
return
}
// 判断原密码是否对
if !utils.Password().ComparePassword(user.Password, p.OriginalPassword) {
response.R(c).FailedWithError("原密码错误")
return
}
// 修改密码
if err := service.User().ChangePassword(user.Id, p.NewPassword); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}
// ResetPassword
// @description: 重置密码
// @receiver UserApi
// @param c
func (UserApi) ResetPassword(c *gin.Context) {
var id = c.Param("id")
if id == "" || id == "undefined" {
response.R(c).FailedWithError("id不能为空")
return
}
// 先查询一下
user, err := service.User().GetUserById(id)
if err != nil {
response.R(c).FailedWithError("获取用户信息失败")
return
}
if user.Status != constant.Enabled {
response.R(c).FailedWithError("当前用户不可重置密码")
return
}
// 修改密码
if err := service.User().ChangePassword(user.Id, "admin123"); err != nil {
response.R(c).FailedWithError(err)
return
}
response.R(c).OK()
}

View File

@ -0,0 +1,57 @@
package middleware
import (
"github.com/gin-gonic/gin"
"strings"
"wireguard-ui/component"
"wireguard-ui/global/constant"
"wireguard-ui/http/response"
"wireguard-ui/service"
"wireguard-ui/utils"
)
// Authorization
// @description: 授权中间件
// @return gin.HandlerFunc
func Authorization() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" || !strings.HasPrefix(token, "Bearer ") {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
userClaims, err := component.JWT().ParseToken(token)
if err != nil {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
// 如果token的颁发者与请求的站点不一致则直接给它狗日的丢出去
if userClaims.Issuer != utils.WebSite().GetHost(c.Request.Header.Get("Referer")) {
response.R(c).AuthorizationFailed("未登陆")
c.Abort()
return
}
// 查询用户
user, err := service.User().GetUserById(userClaims.ID)
if err != nil {
response.R(c).FailedWithError("用户不存在")
c.Abort()
return
}
if user.Status != constant.Enabled {
response.R(c).FailedWithError("用户状态异常,请联系管理员处理!")
c.Abort()
return
}
// 将用户信息放入上下文
c.Set("user", &user)
c.Next()
}
}

40
http/param/client.go Normal file
View File

@ -0,0 +1,40 @@
package param
import (
"wireguard-ui/global/constant"
)
// SaveClient
// @description: 新增/编辑客户端
type SaveClient struct {
Id string `json:"id" form:"id" label:"id" binding:"omitempty"` // id
Name string `json:"name" form:"name" label:"名称" binding:"required,min=1,max=64"` // 名称
Email string `json:"email" form:"email" label:"联系邮箱" binding:"omitempty"` // 联系邮箱
SubnetRange string `json:"subnetRange" form:"subnetRange" label:"子网范围" binding:"omitempty"` // 子网范围
IpAllocation []string `json:"ipAllocation" form:"ipAllocation" label:"客户端IP" binding:"required,dive"` // IP地址
AllowedIps []string `json:"allowedIps" form:"allowedIps" label:"allowedIps" binding:"omitempty,dive"` // 允许访问的IP段
ExtraAllowedIps []string `json:"extraAllowedIps" form:"extraAllowedIps" label:"extraAllowedIps" binding:"omitempty,dive"` // 其他允许访问的IP段
Endpoint string `json:"endpoint" form:"endpoint" label:"endpoint" binding:"omitempty"` // 服务端地址
UseServerDns *constant.Status `json:"useServerDns" form:"useServerDns" label:"useServerDns" binding:"required,oneof=0 1"` // 是否使用服务端DNS 1 - 是 | 0 - 否
Keys *Keys `json:"keys" form:"keys" label:"密钥信息" binding:"required"` // 密钥
Enabled *constant.Status `json:"enabled" form:"enabled" label:"状态" binding:"required,oneof=0 1"` // 状态 1 - 启用 | 0 - 禁用
OfflineMonitoring *constant.Status `json:"offlineMonitoring" form:"offlineMonitoring" label:"离线通知" binding:"required,oneof=0 1"` // 离线通知 1 - 启用 | 0 - 禁用
}
// Keys
// @description: 客户端密钥信息
type Keys struct {
PrivateKey string `json:"privateKey" form:"privateKey" label:"私钥" binding:"required"`
PublicKey string `json:"publicKey" form:"publicKey" label:"公钥" binding:"required"`
PresharedKey string `json:"presharedKey" form:"presharedKey" label:"共享密钥" binding:"required"`
}
// ClientList
// @description: 客户端列表
type ClientList struct {
Name string `json:"name" form:"name" label:"名称" binding:"omitempty"` // 客户端名称
Email string `json:"email" form:"email" label:"邮箱" binding:"omitempty,email"` // 联系邮箱
IpAllocation string `json:"ipAllocation" form:"ipAllocation" label:"IP范围段" binding:"omitempty"` // 客户端IP
Enabled *int `json:"enabled" form:"enabled" label:"状态" binding:"omitempty,oneof=0 1"` // 客户端状态
Page
}

10
http/param/login.go Normal file
View File

@ -0,0 +1,10 @@
package param
// Login
// @description: 登陆
type Login struct {
Account string `json:"account" form:"account" label:"账号" binding:"required,min=2,max=20"`
Password string `json:"password" form:"password" label:"密码" binding:"required,min=8,max=32"`
CaptchaId string `json:"captchaId" form:"captchaId" label:"验证码ID" binding:"required"`
CaptchaCode string `json:"captchaCode" form:"captchaCode" label:"验证码" binding:"required,len=4"`
}

6
http/param/request.go Normal file
View File

@ -0,0 +1,6 @@
package param
type Page struct {
Current int64 `json:"current" form:"current" label:"页码数" binding:"required"`
Size int64 `json:"size" form:"size" label:"每页数量" binging:"required"`
}

11
http/param/server.go Normal file
View File

@ -0,0 +1,11 @@
package param
type SaveServer struct {
IPScope []string `json:"ipScope" form:"IPScope" label:"IPScope" binding:"required"`
ListenPort uint64 `json:"listenPort" form:"listenPort" label:"listenPort" binding:"required"`
PrivateKey string `json:"privateKey" form:"privateKey" label:"privateKey" binding:"required"`
PublicKey string `json:"publicKey" form:"publicKey" label:"publicKey" binding:"required"`
PostUpScript string `json:"postUpScript,omitempty" form:"postUpScript" label:"postUpScript" binding:"omitempty"`
PreDownScript string `json:"preDownScript,omitempty" form:"preDownScript" label:"preDownScript" binding:"omitempty"`
PostDownScript string `json:"postDownScript,omitempty" form:"postDownScript" label:"postDownScript" binding:"omitempty"`
}

24
http/param/user.go Normal file
View File

@ -0,0 +1,24 @@
package param
import "wireguard-ui/global/constant"
// SaveUser
// @description: 新增/编辑用户信息
type SaveUser struct {
Id string `json:"id" form:"id" label:"id" binding:"omitempty"` // id
Account string `json:"account" form:"account" label:"账户号" binding:"required_without=Id"` // 账户号
Password string `json:"password" form:"password" label:"密码" binding:"required_without=Id"` // 密码
Nickname string `json:"nickname" form:"nickname" label:"昵称" binding:"required,min=2"` // 昵称
Avatar string `json:"avatar" form:"avatar" label:"头像" binding:"omitempty"` // 头像
Contact string `json:"contact" form:"contact" label:"联系方式" binding:"omitempty"` // 联系方式
IsAdmin *constant.UserType `json:"isAdmin" form:"isAdmin" label:"是否为管理员" binding:"required,oneof=0 1"` // 是否为管理员 0 - 否 | 1 - 是
Status *constant.Status `json:"status" form:"status" label:"状态" binding:"required,oneof=0 1"` // 用户状态 0 - 禁用 | 1 - 启用
}
// ChangePassword
// @description: 修改密码
type ChangePassword struct {
OriginalPassword string `json:"originalPassword" form:"originalPassword" label:"原密码" binding:"required,min=8,max=32"` // 原密码
NewPassword string `json:"newPassword" form:"newPassword" label:"新密码" binding:"required,min=8,max=32"` // 新密码
ConfirmPassword string `json:"confirmPassword" form:"confirmPassword" label:"确认密码" binding:"eqfield=NewPassword"` // 确认密码
}

109
http/response/response.go Normal file
View File

@ -0,0 +1,109 @@
package response
import (
"github.com/gin-gonic/gin"
"net/http"
"wireguard-ui/component"
"wireguard-ui/utils"
)
type PageData[T any] struct {
Current int `json:"current"` // 当前页码
Size int `json:"size"` // 每页数量
Total int64 `json:"total"` // 总数
TotalPage int `json:"totalPage"` // 总页数
Records T `json:"records"` // 返回数据
}
type response struct {
c *gin.Context
}
func R(c *gin.Context) response {
return response{c}
}
func (r response) OK() {
r.c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"message": "success",
})
return
}
func (r response) OkWithData(data any) {
r.c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"message": "success",
"data": data,
})
}
// Paginate
// @description: 页码数
// @receiver r
// @param data
// @param total
// @param current
// @param size
func (r response) Paginate(data any, total int64, current, size int64) {
// 处理一下页码、页数量
if current == -1 {
current = 1
size = total
}
// 计算总页码
totalPage := utils.Paginate().Generate(total, int(size))
// 返回结果
r.c.JSON(http.StatusOK, map[string]any{
"code": http.StatusOK,
"data": &PageData[any]{Current: int(current), Size: int(size), Total: total, TotalPage: totalPage, Records: data},
"message": "success",
})
}
func (r response) AuthorizationFailed(msg string) {
if msg == "" {
msg = "authorized failed"
}
r.c.JSON(http.StatusUnauthorized, gin.H{
"code": http.StatusUnauthorized,
"message": msg,
})
}
func (r response) Failed() {
r.c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": "failed",
})
}
func (r response) FailedWithError(err any) {
var errStr string
switch err.(type) {
case error:
errStr = err.(error).Error()
case string:
errStr = err.(string)
}
r.c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": errStr,
})
}
func (r response) Validator(err error) {
r.c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"message": component.Error(err),
})
}
func (r response) Internal() {
r.c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"message": "server error",
})
}

23
http/router/client.go Normal file
View File

@ -0,0 +1,23 @@
package router
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/api"
"wireguard-ui/http/middleware"
)
// ClientApi
// @description: 登陆相关API
// @param r
func ClientApi(r *gin.RouterGroup) {
client := r.Group("client", middleware.Authorization())
{
client.POST("", api.Client().Save) // 新增/编辑客户端
client.DELETE("/:id", api.Client().Delete) // 删除客户端
client.GET("/list", api.Client().List) // 客户端列表
client.POST("/generate-keys", api.Client().GenerateKeys) // 生成客户端密钥
client.POST("/generate-ip", api.Client().GenerateIP) // 生成客户端IP
client.GET("/download/:id/:type", api.Client().Download) // 下载客户端配置文件
}
}

18
http/router/login.go Normal file
View File

@ -0,0 +1,18 @@
package router
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/api"
)
// LoginApi
// @description: 登陆相关API
// @param r
func LoginApi(r *gin.RouterGroup) {
login := r.Group("/login")
{
login.GET("/captcha", api.Login().Captcha) // 获取登陆验证码
login.POST("", api.Login().Login) // 登陆
}
}

35
http/router/root.go Normal file
View File

@ -0,0 +1,35 @@
package router
import (
"github.com/gin-gonic/gin"
)
type Option func(engine *gin.RouterGroup)
var options []Option
func includeRouters(opts ...Option) {
options = append(options, opts...)
}
func InitRouter() *gin.Engine {
r := gin.New()
// 开启IP 追踪
r.ForwardedByClientIP = true
// 将请求打印至控制台
r.Use(gin.Logger())
for _, opt := range options {
opt(r.Group("api"))
}
return r
}
func Rooters() {
includeRouters(
LoginApi,
UserApi,
ClientApi,
)
}

23
http/router/user.go Normal file
View File

@ -0,0 +1,23 @@
package router
import (
"github.com/gin-gonic/gin"
"wireguard-ui/http/api"
"wireguard-ui/http/middleware"
)
// UserApi
// @description: 用户相关API
// @param r
func UserApi(r *gin.RouterGroup) {
userApi := r.Group("user", middleware.Authorization())
{
userApi.GET("/info", api.User().GetLoginUser) // 获取当前登陆用户信息
userApi.POST("", api.User().SaveUser) // 新增/编辑用户
userApi.DELETE("/:id", api.User().Delete) // 删除用户
userApi.GET("/list", api.User().List) // 分页列表
userApi.PUT("/status/:id", api.User().Status) // 修改用户状态
userApi.PUT("/change-password", api.User().ChangePassword) // 修改用户密码
userApi.PUT("/reset-password/:id", api.User().ResetPassword) // 重置用户密码
}
}

33
http/vo/client.go Normal file
View File

@ -0,0 +1,33 @@
package vo
import "wireguard-ui/model"
// ClientItem
// @description: 客户端信息
type ClientItem struct {
Id string `json:"id"` // id
Name string `json:"name"` // 名称
Email string `json:"email"` // 通知邮箱
//SubnetRange string `json:"subnetRange"` // 子网范围段
IpAllocation []string `json:"ipAllocation" gorm:"-"` // 分配的IP
IpAllocationStr string `json:"-" gorm:"ipAllocationStr"`
AllowedIps []string `json:"allowedIps" gorm:"-"` // 允许访问的IP
AllowedIpsStr string `json:"-" gorm:"allowedIpsStr"`
ExtraAllowedIps []string `json:"extraAllowedIps" gorm:"-"` // 其他允许访问的IP
ExtraAllowedIpsStr string `json:"-" gorm:"extraAllowedIpsStr"`
Endpoint string `json:"endpoint"` // 服务端点
UseServerDns int `json:"useServerDns"` // 是否使用服务端DNS
Keys *Keys `json:"keys" gorm:"-"` // 密钥等
KeysStr string `json:"-" gorm:"keys_str"`
CreateUser string `json:"createUser"` // 创建人
Enabled int `json:"enabled"` // 是否启用
OfflineMonitoring int `json:"offlineMonitoring"` // 离线通知
CreatedAt model.JsonTime `json:"createdAt"` // 创建时间
UpdatedAt model.JsonTime `json:"updatedAt"` // 更新时间
}
type Keys struct {
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
PresharedKey string `json:"presharedKey"`
}

28
http/vo/user.go Normal file
View File

@ -0,0 +1,28 @@
package vo
import "wireguard-ui/global/constant"
// UserItem
// @description: 用户列表的数据
type UserItem struct {
Id string `json:"id"`
Account string `json:"account"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Contact string `json:"contact"`
IsAdmin constant.UserType `json:"isAdmin"`
Status constant.Status `json:"status"`
}
// User
// @description: 用户信息
type User struct {
Id string `json:"id"`
Account string `json:"account"`
Password string `json:"-"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Contact string `json:"contact"`
IsAdmin constant.UserType `json:"isAdmin"`
Status constant.Status `json:"status"`
}

147
initialize/init.go Normal file
View File

@ -0,0 +1,147 @@
package initialize
import (
"fmt"
"gitee.ltd/lxh/logger"
"gitee.ltd/lxh/logger/log"
"github.com/cowardmrx/go_aliyun_oss"
"github.com/fsnotify/fsnotify"
"github.com/glebarez/sqlite"
"github.com/go-resty/resty/v2"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
"golang.zx2c4.com/wireguard/wgctrl"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
gl "gorm.io/gorm/logger"
"time"
"wireguard-ui/config"
"wireguard-ui/global/client"
)
// Init
// @description: 初始化
func Init() {
initLogger() // 初始化日志
initConfig() // 读取配置文件
initWireguard() // 初始化wireguard客户端
initDatabase() // 初始化数据库
initRedis() // 初始化redis
initHttpClient() // 初始化http客户端
initOSS() // 初始化oss客户端链接
}
// initConfig
// @description: 初始化配置
func initConfig() {
vp := viper.New()
vp.SetConfigFile("app.yaml")
if err := vp.ReadInConfig(); err != nil {
log.Panicf("读取配置文件失败: %v", err.Error())
}
if err := vp.Unmarshal(&config.Config); err != nil {
log.Panicf("解析配置文件失败: %v", err.Error())
}
vp.OnConfigChange(func(in fsnotify.Event) {
if err := vp.Unmarshal(&config.Config); err != nil {
log.Errorf("配置文件变动,读取失败: %v", err.Error())
} else {
initDatabase()
initRedis()
initOSS()
}
})
vp.WatchConfig()
}
// InitWireguard
// @description: 初始化wireguard客户端
func initWireguard() {
c, err := wgctrl.New()
if err != nil {
log.Panicf("初始化wireguard客户端失败: %v", err.Error())
}
client.WireguardClient = c
}
// initDatabase
// @description: 初始化数据库
func initDatabase() {
// 不同驱动提供
var dbDialector gorm.Dialector
switch config.Config.Database.Driver {
case "mysql":
dbDialector = mysql.Open(config.Config.Database.GetDSN())
case "pgsql":
dbDialector = postgres.Open(config.Config.Database.GetDSN())
case "sqlite":
dbDialector = sqlite.Open(config.Config.Database.GetDSN())
}
logLevel := gl.Info
db, err := gorm.Open(dbDialector, &gorm.Config{
Logger: logger.NewGormLoggerWithConfig(gl.Config{
SlowThreshold: time.Second, // Slow SQL threshold
IgnoreRecordNotFoundError: false, // 忽略没找到结果的错误
LogLevel: logLevel, // Log level
Colorful: false, // Disable color
}),
})
if err != nil {
log.Panicf("链接数据库[%s]失败:%v", config.Config.Database.Driver, err.Error())
}
client.DB = db
}
// initRedis
// @description: 初始化redis
func initRedis() {
c := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", config.Config.Redis.Host, config.Config.Redis.Port),
Password: config.Config.Redis.Password,
DB: config.Config.Redis.Db,
})
client.Redis = c
}
// initHttpClient
// @description: 初始化http客户端
func initHttpClient() {
client.HttpClient = resty.New()
}
// initOSS
// @description: 初始化oss客户端
func initOSS() {
if config.Config.File.Type != "oss" {
return
}
ossConfig := &go_aliyun_oss.AliOssConfig{
EndPoint: config.Config.File.Endpoint,
AccessKeyId: config.Config.File.AccessId,
AccessKeySecret: config.Config.File.AccessSecret,
BucketName: config.Config.File.BucketName,
OriginalFileName: false,
}
ossClient := ossConfig.CreateOssConnect()
client.OSS = ossClient
}
// initLogger
// @description: 初始化日志
func initLogger() {
logger.InitLogger(logger.LogConfig{
Mode: logger.Dev,
FileEnable: true,
})
}

42
main.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-contrib/pprof"
"github.com/spf13/cast"
"math/rand"
"net/http"
"os"
"time"
"wireguard-ui/config"
"wireguard-ui/http/router"
"wireguard-ui/initialize"
"wireguard-ui/script"
)
func init() {
initialize.Init()
if err := script.New().Do(); err != nil {
log.Errorf("执行脚本失败: %v", err.Error())
}
}
func main() {
rand.New(rand.NewSource(time.Now().Local().UnixNano()))
router.Rooters()
handler := router.InitRouter()
if cast.ToBool(os.Getenv("ENABLED_PPROF")) {
pprof.Register(handler, "/monitoring")
}
httpServe := http.Server{
Addr: fmt.Sprintf(":%d", config.Config.Http.Port),
Handler: handler,
}
if err := httpServe.ListenAndServe(); err != nil {
log.Panicf("启动http服务端失败: %v", err.Error())
}
}

67
model/base.go Normal file
View File

@ -0,0 +1,67 @@
package model
import (
"database/sql/driver"
"fmt"
"github.com/google/uuid"
"gorm.io/gorm"
"strings"
"time"
)
// Base
// @description: 数据模型基类
type Base struct {
Id string `json:"id" gorm:"primaryKey;type:varchar(36);not null;comment:'主键'"`
Timestamp
}
func (b *Base) BeforeCreate(*gorm.DB) (err error) {
if b.Id == "" {
b.Id = strings.ReplaceAll(uuid.NewString(), "-", "")
}
return
}
type JsonTime struct {
time.Time
}
type Timestamp struct {
CreatedAt JsonTime
UpdatedAt JsonTime
}
func (jt JsonTime) MarshalJSON() ([]byte, error) {
if jt.IsZero() {
return []byte(`""`), nil
}
output := fmt.Sprintf("\"%s\"", jt.Format("2006-01-02 15:04:05"))
return []byte(output), nil
}
func (jt JsonTime) Value() (driver.Value, error) {
var zeroTime time.Time
if jt.Time.UnixNano() == zeroTime.UnixNano() {
return nil, nil
}
return jt.Time.Format("2006-01-02 15:04:05"), nil
}
func (jt *JsonTime) Scan(v any) error {
value, ok := v.(time.Time)
if ok {
*jt = JsonTime{Time: value}
return nil
}
return fmt.Errorf("can not convert %v to timestamp", v)
}
func (jt JsonTime) String() string {
if jt.IsZero() {
return ""
}
output := fmt.Sprintf("%s", jt.Format("2006-01-02 15:04:05"))
return output
}

18
model/other.go Normal file
View File

@ -0,0 +1,18 @@
package model
type RequestLog struct {
Base
UserId string `json:"userId" gorm:"type:char(40);comment:'用户id'"`
ClientIP string `json:"clientIP" gorm:"type:varchar(60);not null;comment:'客户端IP'"`
Host string `json:"host" gorm:"type:varchar(255);not null;comment:'请求域名'"`
Method string `json:"method" gorm:"type:varchar(20);not null;comment:'请求方法[GET POST DELETE PUT PATCH]'"`
Uri string `json:"uri" gorm:"type:varchar(255);not null;comment:'请求path'"`
Header string `json:"header" gorm:"type:text;comment:'请求头'"`
Body string `json:"body" gorm:"type:text;comment:'请求体'"`
Form string `json:"form" gorm:"type:text;comment:'请求表单'"`
Query string `json:"query" gorm:"type:text;comment:'请求query'"`
UserAgent string `json:"userAgent" gorm:"type:text;comment:'ua信息'"`
Cost int64 `json:"cost" gorm:"type:int(10);comment:'请求耗时'"`
StatusCode int `json:"statusCode" gorm:"type:int(10);comment:'响应状态码'"`
Response string `json:"response" gorm:"type:text;comment:'返回数据'"`
}

12
model/setting.go Normal file
View File

@ -0,0 +1,12 @@
package model
type Setting struct {
Base
Code string `json:"code" gorm:"type:char(20);not null;index:idx_code; comment:'设定code'"`
Data string `json:"render_data" gorm:"type:text;not null; comment:'值'"`
Describe string `json:"describe" gorm:"type:text;default null;comment:'配置说明'"`
}
func (Setting) TableName() string {
return "t_setting"
}

18
model/user.go Normal file
View File

@ -0,0 +1,18 @@
package model
import "wireguard-ui/global/constant"
type User struct {
Base
Account string `json:"account" gorm:"type:varchar(50);not null;index:idx_account;comment: '登陆账号'"`
Password string `json:"password" gorm:"type:varchar(255);not null;comment: '密码'"`
Nickname string `json:"nickname" gorm:"type:varchar(50);not null;comment: '昵称'"`
Avatar string `json:"avatar" gorm:"type:varchar(255);not null;comment: '头像'"`
Contact string `json:"contact" gorm:"type:varchar(255);default null;comment: '联系方式(邮箱|电话)'"`
IsAdmin constant.UserType `json:"isAdmin" gorm:"type:tinyint(1);not null;comment: '是否为管理员0 - 否 | 1 - 是)'"`
Status constant.Status `json:"status" gorm:"type:tinyint(1);not null;comment: '用户状态0 - 否 | 1 - 是)'"`
}
func (User) TableName() string {
return "t_user"
}

24
model/wireguard.go Normal file
View File

@ -0,0 +1,24 @@
package model
import "wireguard-ui/global/constant"
type Client struct {
Base
Name string `json:"name" gorm:"type:varchar(100);not null;comment:'客户端名称'"`
Email string `json:"email" gorm:"type:varchar(100);default null;comment:'联系邮箱'"`
SubnetRange string `json:"subnetRange" gorm:"type:varchar(255);default null;comment:'子网范围'"`
IpAllocation string `json:"ipAllocation" gorm:"type:varchar(255);not null;comment:'客户端ip'"`
AllowedIps string `json:"allowedIps" gorm:"type:varchar(255);not null;comment:'允许访问的ip'"`
ExtraAllowedIps string `json:"extraAllowedIps" gorm:"type:varchar(255);default null;comment:'额外允许的ip范围'"`
Endpoint string `json:"endpoint" gorm:"type:varchar(255);default null;comment:'端点'"`
UseServerDns constant.Status `json:"useServerDns" gorm:"type:tinyint(1);default 1;comment:'是否使用服务端dns'"`
Keys string `json:"keys" gorm:"type:text;default null;comment:'公钥和密钥的json串'"`
UserId string `json:"userId" gorm:"type:char(36);not null;comment:'创建人id'"`
Enabled constant.Status `json:"enabled" gorm:"type:tinyint(1);default 1;comment:'状态0 - 禁用 | 1 - 正常)'"`
OfflineMonitoring constant.Status `json:"offlineMonitoring" gorm:"tinyint(1);default 0;comment:'是否启用离线监听0 - 禁用 | 1 - 启用)"`
User *User `json:"user" gorm:"foreignKey:UserId"`
}
func (Client) TableName() string {
return "t_client"
}

190
script/script.go Normal file
View File

@ -0,0 +1,190 @@
package script
import (
"encoding/json"
"gitee.ltd/lxh/logger/log"
"github.com/spf13/cast"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"os"
"wireguard-ui/component"
"wireguard-ui/global/client"
"wireguard-ui/global/constant"
"wireguard-ui/http/param"
"wireguard-ui/model"
"wireguard-ui/service"
"wireguard-ui/template/render_data"
"wireguard-ui/utils"
)
type script struct{}
func New() script {
return script{}
}
func (s script) Do() error {
if err := s.migrate(); err != nil {
return err
}
if err := s.createSuperAdmin(); err != nil {
return err
}
if err := s.initServer(); err != nil {
log.Errorf("初始化wg服务端失败: %v", err.Error())
}
return nil
}
// migrate
// @description: 生成数据库
// @receiver s
// @return error
func (s script) migrate() error {
var ent = []any{
new(model.User),
new(model.Client),
new(model.Setting),
new(model.RequestLog),
}
return client.DB.AutoMigrate(ent...)
}
// createSuperAdmin
// @description: 创建超级管理员
// @receiver s
// @return error
func (s script) createSuperAdmin() error {
var count int64
if err := client.DB.Model(&model.User{}).
Where("account = ?", "admin").
Where("is_admin = ?", 1).Count(&count).Error; err != nil {
return err
}
// 没有超管就创建一个
if count > 0 {
return nil
}
// 生成一下头像
avatarPath, err := utils.Avatar().GenerateAvatar(true)
if err != nil {
log.Errorf("生成头像失败: %v", err.Error())
return err
}
return service.User().CreateUser(&model.User{
Avatar: avatarPath,
Nickname: "超级管理员",
Account: "admin",
Contact: "",
Password: "admin123",
IsAdmin: constant.SuperAdmin,
Status: constant.Enabled,
})
}
// initServer
// @description: 初始化wg的一些配置
// @receiver s
// @return error
func (s script) initServer() error {
var count int64
if err := client.DB.Model(&model.Setting{}).Where("code = ?", "WG_SERVER").Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
// 初始化服务端的全局配置
var data = map[string]any{
"endpointAddress": utils.Network().GetHostPublicIP(),
"dnsServer": []string{"10.25.8.1"},
"MTU": 1450,
"persistentKeepalive": 15,
"firewallMark": "0xca6c",
"table": "",
"configFilePath": "/etc/wireguard/wg0.conf",
}
dataJ, _ := json.Marshal(data)
globalSet := &model.Setting{
Code: "WG_SETTING",
Data: string(dataJ),
Describe: "服务端全局配置",
}
if err := service.Setting().SetData(globalSet); err != nil {
return err
}
// 生成密钥
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
log.Errorf("生成密钥失败: %v", err.Error())
return err
}
// 根据密钥生成公钥
publicKey := privateKey.PublicKey()
serverEnt := &param.SaveServer{
IPScope: []string{"10.25.8.1/24"},
ListenPort: 51820,
PrivateKey: privateKey.String(),
PublicKey: publicKey.String(),
PostUpScript: constant.DefaultPostUpScript,
PreDownScript: constant.DefaultPreDownScript,
PostDownScript: constant.DefaultPostDownScript,
}
serverJ, _ := json.Marshal(serverEnt)
serverSet := &model.Setting{
Code: "WG_SERVER",
Data: string(serverJ),
Describe: "服务端配置",
}
// 没有服务端,开始初始化
if err = service.Setting().SetData(serverSet); err != nil {
return err
}
// 处理一下要渲染到配置文件上的数据
serverConfig := render_data.Server{
Address: serverEnt.IPScope,
ListenPort: serverEnt.ListenPort,
PrivateKey: serverEnt.PrivateKey,
MTU: cast.ToInt(data["MTU"]),
PostUp: serverEnt.PostUpScript,
PreDown: serverEnt.PreDownScript,
PostDown: serverEnt.PostDownScript,
Table: cast.ToString(data["table"]),
}
execData := map[string]any{
"Server": serverConfig,
}
var templatePath, outFilePath string
if os.Getenv("GIN_MODE") != "release" {
templatePath = "E:\\Workspace\\Go\\wireguard-ui\\template\\conf\\wg.conf"
outFilePath = "E:\\Workspace\\Go\\wireguard-ui\\template\\tmp\\wg0.conf"
} else {
templatePath = "./template/wg.conf"
outFilePath = cast.ToString(data["configFilePath"])
}
// 先渲染模板
if err = component.Template().Execute(templatePath, outFilePath, execData); err != nil {
return nil
}
// 模板渲染成功,开始执行服务端控制
component.Wireguard().ServerControl(outFilePath)
return nil
}

25
service/base.go Normal file
View File

@ -0,0 +1,25 @@
package service
import "gorm.io/gorm"
func Paginate(current, size int64) func(db *gorm.DB) *gorm.DB {
// 如果页码是-1就不分页
if current == -1 {
return func(db *gorm.DB) *gorm.DB {
return db
}
}
// 分页
return func(db *gorm.DB) *gorm.DB {
if current == 0 {
current = 1
}
if size < 1 {
size = 10
}
// 计算偏移量
offset := (current - 1) * size
// 返回组装结果
return db.Offset(int(offset)).Limit(int(size))
}
}

171
service/client.go Normal file
View File

@ -0,0 +1,171 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
"strings"
gdb "wireguard-ui/global/client"
"wireguard-ui/http/param"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/utils"
)
type client struct {
*gorm.DB
}
func Client() client {
return client{
gdb.DB,
}
}
// SaveClient
// @description: 新增/编辑客户端
// @receiver s
// @param p
// @param loginUser
// @return error
func (s client) SaveClient(p param.SaveClient, loginUser *vo.User) error {
serverConf, err := Setting().GetWGServerForConfig()
if err != nil {
return err
}
// 对客户端IP做格式校验
for _, cip := range p.IpAllocation {
if !utils.Network().IPContains(serverConf.Address, cip) {
return fmt.Errorf("客户端IP[%s]不符合定义", cip)
}
}
// 处理一下endpoint
if p.Endpoint == "" {
globalConf, err := Setting().GetWGSetForConfig()
if err != nil {
return err
}
p.Endpoint = fmt.Sprintf("%s:%d", globalConf.EndpointAddress, serverConf.ListenPort)
}
keys, _ := json.Marshal(p.Keys)
ent := &model.Client{
Base: model.Base{
Id: p.Id,
},
Name: p.Name,
Email: p.Email,
SubnetRange: p.SubnetRange,
IpAllocation: strings.Join(p.IpAllocation, ","),
AllowedIps: strings.Join(p.AllowedIps, ","),
ExtraAllowedIps: strings.Join(p.ExtraAllowedIps, ","),
Endpoint: p.Endpoint,
UseServerDns: *p.UseServerDns,
Keys: string(keys),
UserId: loginUser.Id,
Enabled: *p.Enabled,
OfflineMonitoring: *p.OfflineMonitoring,
}
// 编辑
if p.Id != "" {
return s.Model(&model.Client{}).Select("id", "name", "email", "subnet_range",
"ip_allocation", "allowed_ips",
"extra_allowed_ips", "endpoint",
"use_server_dns", "keys", "user_id", "enabled",
"offline_monitoring").
Where("id = ?", ent.Id).Updates(&ent).Error
}
// 如果是新增判断这个客户端IP是否已经存在过了
var count int64
if err := s.Model(&model.Client{}).Where("ip_allocation = ?", strings.Join(p.IpAllocation, ",")).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("当前IP客户端已经存在")
}
// 新增
return s.Model(&model.Client{}).Create(&ent).Error
}
// Delete
// @description: 删除
// @receiver s
// @param id
// @return error
func (s client) Delete(id string) error {
return s.Model(&model.Client{}).Where("id = ?", id).Delete(&model.Client{}).Error
}
// List
// @description: 客户端分页列表
// @receiver s
// @param p
// @return data
// @return total
// @return err
func (s client) List(p param.ClientList) (data []vo.ClientItem, total int64, err error) {
sel := s.Table("t_client as tc").Scopes(Paginate(p.Current, p.Size)).
Joins("LEFT JOIN t_user as tu ON tu.id = tc.user_id").
Select("tc.id,tc.name,tc.email,tc.ip_allocation as ip_allocation_str,"+
"tc.allowed_ips as allowed_ips_str,tc.extra_allowed_ips as extra_allowed_ips_str,"+
"tc.endpoint,tc.use_server_dns,tc.keys as keys_str,tu.nickname as create_user,"+
"tc.enabled,tc.offline_monitoring,"+
"tc.created_at", "tc.updated_at")
if p.Enabled != nil {
sel.Where("tc.enabled = ?", *p.Enabled)
}
if p.Name != "" {
sel.Where("tc.name like ?", "%"+p.Name+"%")
}
if p.Email != "" {
sel.Where("tc.email like ?", "%"+p.Email+"%")
}
if p.IpAllocation != "" {
sel.Where("tc.ip_allocation like ?", "%"+p.IpAllocation+"%")
}
err = sel.Order("tc.created_at DESC").Find(&data).Limit(-1).Offset(-1).Count(&total).Error
if err != nil {
return
}
for i, v := range data {
if v.KeysStr != "" {
_ = json.Unmarshal([]byte(v.KeysStr), &data[i].Keys)
}
if v.IpAllocationStr != "" {
data[i].IpAllocation = strings.Split(v.IpAllocationStr, ",")
}
if v.AllowedIpsStr != "" {
data[i].AllowedIps = strings.Split(v.AllowedIpsStr, ",")
} else {
data[i].AllowedIps = []string{}
}
if v.ExtraAllowedIpsStr != "" {
data[i].ExtraAllowedIps = strings.Split(v.ExtraAllowedIpsStr, ",")
} else {
data[i].ExtraAllowedIps = []string{}
}
}
return
}
// GetByID
// @description: 通过ID获取客户端
// @receiver s
// @param id
// @return data
// @return err
func (s client) GetByID(id string) (data *model.Client, err error) {
err = s.Model(&model.Client{}).Where("id = ?", id).Take(&data).Error
return
}

74
service/setting.go Normal file
View File

@ -0,0 +1,74 @@
package service
import (
"encoding/json"
"gorm.io/gorm"
gdb "wireguard-ui/global/client"
"wireguard-ui/model"
"wireguard-ui/template/render_data"
)
type setting struct{ *gorm.DB }
func Setting() setting {
return setting{gdb.DB}
}
func (s setting) SetData(data *model.Setting) error {
// 判断code是否已经存在
var count int64
if err := s.Model(&model.Setting{}).Where("code = ?", data.Code).Count(&count).Error; err != nil {
return err
}
// 存在就更新,反之新增
if count > 0 {
return s.Save(&data).Error
}
return s.Create(&data).Error
}
// GetWGSetForConfig
// @description: 获取全局配置
// @receiver s
// @return data
// @return err
func (s setting) GetWGSetForConfig() (data *render_data.ServerSetting, err error) {
var rs *model.Setting
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SETTING").Take(&rs).Error; err != nil {
return
}
if err = json.Unmarshal([]byte(rs.Data), &data); err != nil {
return
}
return
}
// GetWGServerForConfig
// @description: 获取wireguard服务端配置为了渲染数据
// @receiver s
// @return render_data
// @return err
func (s setting) GetWGServerForConfig() (data *render_data.Server, err error) {
var rs *model.Setting
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SERVER").Take(&rs).Error; err != nil {
return
}
if err = json.Unmarshal([]byte(rs.Data), &data); err != nil {
return
}
// 获取一下全局的服务配置
gs, err := s.GetWGSetForConfig()
if err != nil {
return
}
data.MTU = gs.MTU
data.Table = gs.Table
return
}

126
service/user.go Normal file
View File

@ -0,0 +1,126 @@
package service
import (
"gorm.io/gorm"
gdb "wireguard-ui/global/client"
"wireguard-ui/global/constant"
"wireguard-ui/http/param"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/utils"
)
type user struct {
*gorm.DB
}
func User() user {
return user{
gdb.DB,
}
}
// CreateUser
// @description: 创建用户
// @receiver s
// @param user
// @return err
func (s user) CreateUser(user *model.User) (err error) {
// 更新
if user.Id != "" {
updates := map[string]any{
"nickname": user.Nickname,
"avatar": user.Avatar,
"contact": user.Contact,
"is_admin": user.IsAdmin,
"status": user.Status,
}
return s.Model(&model.User{}).Where("id = ?", user.Id).Updates(&updates).Error
}
defaultPassword := utils.Password().GenerateHashPassword("admin123")
if user.Password == "" { // 没有密码给一个默认密码
user.Password = defaultPassword
} else {
user.Password = utils.Password().GenerateHashPassword(user.Password)
}
// 没有头像就生成一个头像
if user.Avatar == "" {
user.Avatar, _ = utils.Avatar().GenerateAvatar(true)
}
// 创建
return s.Create(&user).Error
}
// Delete
// @description: 删除用户
// @receiver s
// @param id
// @return error
func (s user) Delete(id string) error {
return s.Model(&model.User{}).Where("id = ?", id).Delete(&model.User{}).Error
}
// List
// @description: 用户列表
// @receiver s
// @param p
// @return data
// @return count
// @return err
func (s user) List(p param.Page) (data []vo.UserItem, count int64, err error) {
if err = s.Model(&model.User{}).
Scopes(Paginate(p.Current, p.Size)).
Order("created_at DESC").
Find(&data).
Offset(-1).Limit(-1).
Count(&count).Error; err != nil {
return
}
return
}
// GetUserByAccount
// @description: 根据账户获取用户信息
// @receiver s
// @param id
// @return data
// @return err
func (s user) GetUserByAccount(account string) (data vo.User, err error) {
err = s.Model(&model.User{}).Where("account = ?", account).Take(&data).Error
return
}
// GetUserById
// @description: 根据id获取用户信息
// @receiver s
// @param id
// @return data
// @return err
func (s user) GetUserById(id string) (data vo.User, err error) {
err = s.Model(&model.User{}).Where("id = ?", id).Take(&data).Error
return
}
// Status
// @description: 修改用户状态
// @receiver s
// @param id
// @return error
func (s user) Status(id string, state constant.Status) error {
return s.Model(&model.User{}).Where("id = ?", id).Update("status", state).Error
}
// ChangePassword
// @description: 修改密码
// @receiver s
// @param id
// @param password
// @return error
func (s user) ChangePassword(id, password string) error {
return s.Model(&model.User{}).Where("id = ?", id).Update("password", utils.Password().GenerateHashPassword(password)).Error
}

View File

@ -0,0 +1,13 @@
[Interface]
PrivateKey = {{ .PrivateKey|html }}
Address = {{ .IpAllocation|html }}
{{ if .DNS }}DNS = {{ .DNS|html }}
MTU = {{ .MTU }}
{{ else }}MTU = {{ .MTU }}{{ end }}
[Peer]
PublicKey = {{ .PublicKey|html }}
PresharedKey = {{ .PresharedKey|html }}
AllowedIPs = {{ .AllowedIPS|html }}
Endpoint = {{ .Endpoint|html }}:{{ .ListenPort|html }}
PersistentKeepalive = {{ .PersistentKeepalive|html }}

24
template/conf/wg.conf Normal file
View File

@ -0,0 +1,24 @@
[Interface]
Address = {{ .Server.Address|html }}
ListenPort = {{ .Server.ListenPort|html }}
PrivateKey = {{ .Server.PrivateKey|html }}
MTU = {{ .Server.MTU|html }}
PostUp = {{ .Server.PostUp|html }}
PreDown = {{ .Server.PreDown|html }}
PostDown = {{ .Server.PostDown|html }}
Table = {{ .Server.Table|html }}
{{ range .Clients }}{{ if eq .Enabled true }}
# ID: {{ .ID|html }}
# Name: {{ .Name|html }}
# Emil: {{ .Email|html }}
# CreatedAt: {{ .CreatedAt|html }}
# UpdatedAt: {{ .UpdatedAt|html }}
# CreateUser: {{ .CreateUser|html }}
[Peer]
PublicKey = {{ .PublicKey|html }}
PresharedKey = {{ .PresharedKey|html }}
AllowedIPs = {{ .AllowedIPS|html }}
PersistentKeepalive = {{ .PersistentKeepalive|html }}
{{ if .Endpoint }}Endpoint = {{ .Endpoint|html }}{{ end }}
{{ end }}{{ end }}

View File

@ -0,0 +1,58 @@
package render_data
type ServerSetting struct {
EndpointAddress string `json:"endpointAddress"`
DnsServer []string `json:"dnsServer"`
MTU int `json:"MTU"`
PersistentKeepalive int `json:"persistentKeepalive"`
FirewallMark string `json:"firewallMark"`
Table string `json:"table"`
ConfigFilePath string `json:"configFilePath"`
}
type Server struct {
Address []string `json:"ipScope"`
ListenPort uint64 `json:"listenPort"`
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey" `
MTU int `json:"MTU"`
PostUp string `json:"postUpScript"`
PreDown string `json:"preDownScript"`
PostDown string `json:"postDownScript"`
Table string `json:"table"`
Clients []Client `json:"clients"`
}
type Client struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
PublicKey string `json:"publicKey"`
PresharedKey string `json:"presharedKey"`
AllowedIPS string `json:"allowedIps"`
PersistentKeepalive string `json:"persistentKeepalive"`
Endpoint string `json:"endpoint"`
CreateUser string `json:"createUser"`
Enabled bool `json:"enabled"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type Keys struct {
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
PresharedKey string `json:"presharedKey"`
}
type ClientConfig struct {
PrivateKey string `json:"privateKey"`
IpAllocation string `json:"ipAllocation"`
MTU int `json:"MTU"`
DNS string `json:"DNS"`
PublicKey string `json:"publicKey"`
PresharedKey string `json:"presharedKey"`
AllowedIPS string `json:"allowedIPS"`
Endpoint string `json:"endpoint"`
ListenPort int `json:"listenPort"`
PersistentKeepalive int `json:"persistentKeepalive"`
}

42
utils/avatar.go Normal file
View File

@ -0,0 +1,42 @@
package utils
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"math/rand"
"time"
"wireguard-ui/global/client"
)
type avatar struct{}
func Avatar() avatar {
return avatar{}
}
// GenerateAvatar
// @description: 生成随机头像 - 默认头像
// @receiver avatar
// @return path
// @return err
func (avatar) GenerateAvatar(isUpload bool) (path string, err error) {
rand.New(rand.NewSource(time.Now().UnixNano()))
r := client.HttpClient.R()
result, err := r.Get(fmt.Sprintf("https://api.dicebear.com/7.x/croodles/png?seed=%d&scale=120&size=200&clip=true&randomizeIds=true&beard=variant01,variant02,variant03&"+
"eyes=variant01,variant02,variant03,variant04,variant05,variant06,variant07,variant08,variant09,variant10,variant11,variant12&mustache=variant01,variant02,variant03&"+
"topColor=000000,0fa958,699bf7", rand.Uint32()))
if err != nil {
log.Errorf("请求头像API失败: %v", err.Error())
return "", err
}
if isUpload {
path, err = FileSystem().UploadFile(result.Body(), ".png")
if err != nil {
return "", err
}
return
}
return string(result.Body()), nil
}

47
utils/file_system.go Normal file
View File

@ -0,0 +1,47 @@
package utils
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"os"
"time"
"wireguard-ui/config"
"wireguard-ui/global/client"
)
type fileSystem struct{}
func FileSystem() fileSystem {
return fileSystem{}
}
// UploadFile
// @description: 上传文件
// @receiver fileSystem
// @param file
// @return filePath
// @return err
func (fileSystem) UploadFile(file []byte, suffix string) (filePath string, err error) {
switch config.Config.File.Type {
case "oss":
ossObj, err := client.OSS.Put(config.Config.File.Path, file, suffix)
if err != nil {
return "", err
}
return ossObj.LongPath, nil
case "local":
filePath = fmt.Sprintf("%v/%d-avatar%s", config.Config.File.Path, time.Now().Unix(), suffix)
// 创建目录
if err = os.MkdirAll(filePath, os.FileMode(0777)); err != nil {
log.Errorf("本地存储目录创建失败: %v", err)
return "", err
}
if err = os.WriteFile(filePath, file, os.FileMode(0777)); err != nil {
return "", err
}
return filePath, nil
}
return "", nil
}

82
utils/mail.go Normal file
View File

@ -0,0 +1,82 @@
package utils
import (
"crypto/tls"
"errors"
"fmt"
"github.com/jordan-wright/email"
"mime"
"net/smtp"
"net/textproto"
"path/filepath"
"wireguard-ui/config"
)
type mail struct {
*email.Email
addr string
auth smtp.Auth
}
func Mail() *mail {
var m mail
em := email.NewEmail()
m.Email = em
m.auth = smtp.PlainAuth("", config.Config.Mail.User, config.Config.Mail.Password, config.Config.Mail.Host)
m.addr = fmt.Sprintf("%s:%d", config.Config.Mail.Host, config.Config.Mail.Port)
return &m
}
func (m *mail) VerifyConfig() (err error) {
if m == nil {
return errors.New("邮件客户端初始化失败")
}
if m.auth == nil || m.addr == "" {
return errors.New("邮件客户端未完成初始化")
}
return nil
}
// SendMail
// @description: 发送附件
// @receiver m
// @param to
// @param subject
// @param attacheFilePath
// @return err
func (m *mail) SendMail(to, subject, content, attacheFilePath string) (err error) {
m.From = fmt.Sprintf("wg-dashboard <%s>", config.Config.Mail.User)
m.To = []string{to}
m.Subject = subject
m.Text = []byte(content)
if attacheFilePath != "" {
atch, err := m.AttachFile(attacheFilePath)
if err != nil {
return fmt.Errorf("读取附件文件失败: %v", err.Error())
}
emHeader := textproto.MIMEHeader{}
emHeader.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, mime.BEncoding.Encode("UTF-8", m.getFileName(attacheFilePath))))
atch.Header = emHeader
}
if config.Config.Mail.SkipTls {
return m.Send(m.addr, m.auth)
}
tlsConfig := &tls.Config{}
tlsConfig.InsecureSkipVerify = config.Config.Mail.SkipTls
tlsConfig.ServerName = config.Config.Mail.Host
return m.SendWithTLS(m.addr, m.auth, tlsConfig)
}
// getFileName
// @description: 获取文件名
// @receiver m
// @param filePath
// @return string
func (m *mail) getFileName(filePath string) string {
return filepath.Base(filePath)
}

128
utils/network.go Normal file
View File

@ -0,0 +1,128 @@
package utils
import (
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/spf13/cast"
"net"
"strings"
"wireguard-ui/global/client"
)
type network struct{}
func Network() network {
return network{}
}
// GetHostPublicIP
// @description: 获取本机公网地址
// @receiver network
// @return string
func (network) GetHostPublicIP() string {
var response = map[string]string{}
_, err := client.HttpClient.R().SetResult(&response).Get("https://httpbin.org/ip")
if err != nil {
log.Errorf("获取本机公网IP失败: %v", err.Error())
return ""
}
return response["origin"]
}
// IPContains
// @description: 校验ip是否在指定IP段中
// @receiver network
// @param specIp
// @param checkIP
func (network) IPContains(specIp []string, checkIP string) bool {
var isContains bool
for _, sip := range specIp {
ip, _, _ := net.ParseCIDR(checkIP)
sipNet, _, _ := net.ParseCIDR(sip)
ipNet := net.IPNet{IP: sipNet, Mask: net.CIDRMask(24, 32)}
if ipNet.Contains(ip) {
isContains = true
}
}
return isContains
}
// IPContainsByIP
// @description: 校验ip是否在指定IP段中
// @receiver network
// @param specIp
// @param checkIP
func (network) IPContainsByIP(specIp string, checkIP string) bool {
ip, _, _ := net.ParseCIDR(checkIP)
sipNet, _, _ := net.ParseCIDR(specIp)
ipNet := net.IPNet{IP: sipNet, Mask: net.CIDRMask(24, 32)}
if ipNet.Contains(ip) {
return true
}
return false
}
// ParseIP
// @description: 将带有mark的ip解析
// @receiver network
// @param ip
// @return string
func (network) ParseIP(ip string) string {
ipn, _, _ := net.ParseCIDR(ip)
return ipn.String()
}
// GetIPSuffix
// @description: 获取IP后缀 [10.10.10.23] 这里只获取23
// @receiver network
// @param ip
// @return string
func (network) GetIPSuffix(ip string) string {
if strings.Contains(ip, "/") {
ip = strings.Split(ip, "/")[0]
}
// 再次拆分,只取最后一段
return strings.Split(ip, ".")[3]
}
// GetIPPrefix
// @description: 获取IP前缀[10.10.10.23] 这里只获取 10.10.10
// @receiver network
// @param ip
// @return string
func (network) GetIPPrefix(ip string) string {
if strings.Contains(ip, "/") {
ip = strings.Split(ip, "/")[0]
}
return strings.Join(strings.Split(ip, ".")[:3], ".")
}
// GenerateIPByIPS
// @description: 根据指定IP段分配IP
// @receiver n
// @param ips
// @param assignedIPS
// @return string
func (n network) GenerateIPByIPS(ips []string, assignedIPS ...string) []string {
var oips []string
for _, sip := range ips {
// 再次拆分,只取最后一段
suffix := n.GetIPSuffix(sip)
prefix := n.GetIPPrefix(sip)
for _, cip := range assignedIPS {
if n.IPContainsByIP(sip, cip) {
suffix = n.GetIPSuffix(cip)
}
}
suffix = cast.ToString(cast.ToInt64(suffix) + 1)
oips = append(oips, fmt.Sprintf("%s.%s", prefix, suffix))
}
return oips
}

19
utils/paginate.go Normal file
View File

@ -0,0 +1,19 @@
package utils
type paginate struct{}
func Paginate() paginate {
return paginate{}
}
func (paginate) Generate(count int64, size int) int {
totalPage := 0
if count > 0 {
upPage := 0
if int(count)%size > 0 {
upPage = 1
}
totalPage = (int(count) / size) + upPage
}
return totalPage
}

38
utils/password.go Normal file
View File

@ -0,0 +1,38 @@
package utils
import (
"gitee.ltd/lxh/logger/log"
"golang.org/x/crypto/bcrypt"
)
type password struct{}
func Password() password {
return password{}
}
// GenerateHashPassword
// @description: 生成hash密码
// @receiver password
// @param pass
// @return string
func (password) GenerateHashPassword(pass string) string {
bytePass := []byte(pass)
hPass, _ := bcrypt.GenerateFromPassword(bytePass, bcrypt.DefaultCost)
return string(hPass)
}
// ComparePassword
// @description: 密码比对
// @receiver password
// @param dbPass
// @param pass
// @return bool
func (password) ComparePassword(dbPass, pass string) bool {
if err := bcrypt.CompareHashAndPassword([]byte(dbPass), []byte(pass)); err != nil {
log.Errorf("密码错误: %v", err.Error())
return false
}
return true
}

38
utils/qr_code.go Normal file
View File

@ -0,0 +1,38 @@
package utils
import (
"encoding/base64"
"gitee.ltd/lxh/logger/log"
"github.com/skip2/go-qrcode"
)
type qrCode struct{}
func QRCode() qrCode {
return qrCode{}
}
// GenerateQrCodeBase64
// @description: 生成二维码
// @receiver qr
// @param content
// @param size
// @return imgStr
// @return err
func (qr qrCode) GenerateQrCodeBase64(content []byte, size int) (imgStr string, err error) {
q, err := qrcode.New(string(content), qrcode.Highest)
if err != nil {
log.Errorf("初始化二维码对象失败: %v", err.Error())
return
}
q.DisableBorder = false
png, err := q.PNG(size)
if err != nil {
log.Errorf("生成二维码失败: %v", err.Error())
return "", err
}
imgStr = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
return
}

26
utils/rand.go Normal file
View File

@ -0,0 +1,26 @@
package utils
import (
"bytes"
"crypto/rand"
"math/big"
)
type random struct{}
func Random() random {
return random{}
}
func (random) RandStr(len int) string {
var container string
var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
b := bytes.NewBufferString(str)
length := b.Len()
bigInt := big.NewInt(int64(length))
for i := 0; i < len; i++ {
randomInt, _ := rand.Int(rand.Reader, bigInt)
container += string(str[randomInt.Int64()])
}
return container
}

17
utils/website.go Normal file
View File

@ -0,0 +1,17 @@
package utils
import "net/url"
type website struct{}
func WebSite() website {
return website{}
}
func (website) GetHost(addr string) string {
uu, err := url.Parse(addr)
if err != nil {
return ""
}
return uu.Host
}

1
web Submodule

@ -0,0 +1 @@
Subproject commit c6b0aa2467633861b82e1483a6a3f2daf868d686