426 lines
11 KiB
Go
426 lines
11 KiB
Go
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"
|
||
"time"
|
||
"wireguard-dashboard/client"
|
||
"wireguard-dashboard/http/param"
|
||
"wireguard-dashboard/model/entity"
|
||
"wireguard-dashboard/model/template_data"
|
||
"wireguard-dashboard/model/vo"
|
||
"wireguard-dashboard/queues"
|
||
"wireguard-dashboard/repository"
|
||
"wireguard-dashboard/utils"
|
||
)
|
||
|
||
type clients struct{}
|
||
|
||
func Client() clients {
|
||
return clients{}
|
||
}
|
||
|
||
// List
|
||
// @description: 客户端列表
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) List(c *gin.Context) {
|
||
var p param.ClientList
|
||
if err := c.ShouldBind(&p); err != nil {
|
||
utils.GinResponse(c).FailedWithErr("参数错误", err)
|
||
return
|
||
}
|
||
|
||
data, total, err := repository.Client().List(p)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("获取失败")
|
||
return
|
||
}
|
||
|
||
utils.GinResponse(c).OkWithPage(data, total, p.Current, p.Size)
|
||
}
|
||
|
||
// Save
|
||
// @description: 新增/更新客户端
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) Save(c *gin.Context) {
|
||
var p param.SaveClient
|
||
if err := c.ShouldBind(&p); err != nil {
|
||
utils.GinResponse(c).FailedWithErr("参数错误", err)
|
||
return
|
||
}
|
||
|
||
info, ok := c.Get("user")
|
||
if !ok {
|
||
utils.GinResponse(c).FailedWithMsg("获取信息失败")
|
||
return
|
||
}
|
||
|
||
_, err := repository.Client().Save(p, info.(*entity.User).Id)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("操作失败", err)
|
||
return
|
||
}
|
||
|
||
go func() {
|
||
if err = queues.PutAsyncWireguardConfigFile(p.ServerId); err != nil {
|
||
log.Errorf("[新增/编辑客户端]同步配置文件失败: %v", err.Error())
|
||
}
|
||
}()
|
||
|
||
utils.GinResponse(c).OK()
|
||
}
|
||
|
||
// AssignIPAndAllowedIP
|
||
// @description: 分配客户端IP和允许访问的IP段
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) AssignIPAndAllowedIP(c *gin.Context) {
|
||
var p param.AssignIPAndAllowedIP
|
||
if err := c.ShouldBind(&p); err != nil {
|
||
utils.GinResponse(c).FailedWithErr("参数错误", err)
|
||
return
|
||
}
|
||
|
||
// 获取一下服务端信息,因为IP分配需要根据服务端的IP制定
|
||
serverInfo, err := repository.Server().GetServer()
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("获取服务端信息失败", err)
|
||
return
|
||
}
|
||
|
||
var assignIPS []string
|
||
assignIPS = append(assignIPS, serverInfo.IpScope)
|
||
switch p.Rule {
|
||
case "AUTO":
|
||
// 只获取最新的一个
|
||
var clientInfo *entity.Client
|
||
if err = repository.Client().Order("created_at DESC").Take(&clientInfo).Error; err == nil {
|
||
if cast.ToInt64(utils.Wireguard().GetIPSuffix(clientInfo.IpAllocation)) >= 255 {
|
||
utils.GinResponse(c).FailedWithMsg("当前IP分配错误,请手动进行分配")
|
||
return
|
||
}
|
||
assignIPS = append(assignIPS, clientInfo.IpAllocation)
|
||
}
|
||
|
||
case "RANDOM":
|
||
// 查询全部客户端不管是禁用还是没禁用的
|
||
var clientsInfo []entity.Client
|
||
if err = repository.Client().Find(&clientsInfo).Error; err != nil {
|
||
utils.GinResponse(c).FailedWithErr("获取失败", err)
|
||
return
|
||
}
|
||
|
||
for _, v := range clientsInfo {
|
||
assignIPS = append(assignIPS, v.IpAllocation)
|
||
}
|
||
}
|
||
|
||
clientIP := utils.Wireguard().GenerateClientIP(serverInfo.IpScope, p.Rule, assignIPS...)
|
||
|
||
utils.GinResponse(c).OKWithData(map[string]any{
|
||
"clientIP": []string{fmt.Sprintf("%s/32", clientIP)},
|
||
"serverIP": []string{serverInfo.IpScope},
|
||
})
|
||
}
|
||
|
||
// GenerateKeys
|
||
// @description: 生成密钥对
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) GenerateKeys(c *gin.Context) {
|
||
// 为空,新增
|
||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("生成密钥对失败", err)
|
||
return
|
||
}
|
||
publicKey := privateKey.PublicKey().String()
|
||
presharedKey, err := wgtypes.GenerateKey()
|
||
if err != nil {
|
||
return
|
||
}
|
||
keys := template_data.Keys{
|
||
PrivateKey: privateKey.String(),
|
||
PublicKey: publicKey,
|
||
PresharedKey: presharedKey.String(),
|
||
}
|
||
|
||
utils.GinResponse(c).OKWithData(keys)
|
||
}
|
||
|
||
// Delete
|
||
// @description: 删除客户端
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) Delete(c *gin.Context) {
|
||
var id = c.Param("id")
|
||
if id == "" || id == "undefined" {
|
||
utils.GinResponse(c).FailedWithMsg("参数错误")
|
||
return
|
||
}
|
||
|
||
if err := repository.Client().Delete(id); err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("操作失败")
|
||
return
|
||
}
|
||
|
||
// 再同步一下配置文件
|
||
go func() {
|
||
if err := queues.PutAsyncWireguardConfigFile(""); err != nil {
|
||
log.Errorf("[下线客户端]同步配置文件失败: %v", err.Error())
|
||
}
|
||
}()
|
||
|
||
utils.GinResponse(c).OK()
|
||
}
|
||
|
||
// Download
|
||
// @description: 下载配置文件
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) Download(c *gin.Context) {
|
||
var id = c.Param("id")
|
||
if id == "" || id == "undefined" {
|
||
utils.GinResponse(c).FailedWithMsg("参数错误")
|
||
return
|
||
}
|
||
|
||
data, err := repository.Client().GetById(id)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("获取失败")
|
||
return
|
||
}
|
||
|
||
var keys template_data.Keys
|
||
_ = json.Unmarshal([]byte(data.Keys), &keys)
|
||
|
||
serverSetting, err := repository.System().GetServerSetting()
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("获取设置失败")
|
||
return
|
||
}
|
||
|
||
outPath, err := utils.Wireguard().GenerateClientFile(&data, serverSetting)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("生成失败", err)
|
||
return
|
||
}
|
||
|
||
// 输出文件流
|
||
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())
|
||
}
|
||
}
|
||
|
||
// GenerateQrCode
|
||
// @description: 生成客户端信息二维码
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) GenerateQrCode(c *gin.Context) {
|
||
var id = c.Param("id")
|
||
if id == "" || id == "undefined" {
|
||
utils.GinResponse(c).FailedWithMsg("参数错误")
|
||
return
|
||
}
|
||
|
||
data, err := repository.Client().GetById(id)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("获取失败")
|
||
return
|
||
}
|
||
|
||
var keys template_data.Keys
|
||
_ = json.Unmarshal([]byte(data.Keys), &keys)
|
||
|
||
serverSetting, err := repository.System().GetServerSetting()
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("获取设置失败")
|
||
return
|
||
}
|
||
|
||
outPath, err := utils.Wireguard().GenerateClientFile(&data, serverSetting)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("生成失败", err)
|
||
return
|
||
}
|
||
|
||
// 读取文件内容
|
||
fileContent, err := os.ReadFile(outPath)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("读取文件失败")
|
||
return
|
||
}
|
||
|
||
png, err := utils.QRCode().GenerateQrCodeBase64(fileContent, 256)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("生成二维码失败", err)
|
||
return
|
||
}
|
||
|
||
if err = os.Remove(outPath); err != nil {
|
||
log.Errorf("删除临时文件失败: %s", err.Error())
|
||
}
|
||
|
||
utils.GinResponse(c).OKWithData(map[string]interface{}{
|
||
"qrCode": png,
|
||
})
|
||
}
|
||
|
||
// SendEmail
|
||
// @description: 发送邮件
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) SendEmail(c *gin.Context) {
|
||
var id = c.Param("id")
|
||
if id == "" || id == "undefined" {
|
||
utils.GinResponse(c).FailedWithMsg("id不能为空")
|
||
return
|
||
}
|
||
|
||
// 先校验一下邮箱发送是否可用
|
||
if err := utils.Mail().VerifyConfig(); err != nil {
|
||
utils.GinResponse(c).FailedWithMsg(err.Error())
|
||
return
|
||
}
|
||
|
||
// 获取该客户端信息
|
||
clientInfo, err := repository.Client().GetById(id)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("获取失败", err)
|
||
return
|
||
}
|
||
|
||
if clientInfo.Email == "" {
|
||
utils.GinResponse(c).FailedWithMsg("当前客户端未配置联系邮箱!")
|
||
return
|
||
}
|
||
|
||
serverSetting, err := repository.System().GetServerSetting()
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("获取设置失败")
|
||
return
|
||
}
|
||
|
||
outPath, err := utils.Wireguard().GenerateClientFile(&clientInfo, serverSetting)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("生成失败", err)
|
||
return
|
||
}
|
||
|
||
err = utils.Mail().SendMail(clientInfo.Email, fmt.Sprintf("客户端: %s", clientInfo.Name), "请查收附件", outPath)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("发送邮件失败", err)
|
||
return
|
||
}
|
||
|
||
if err = os.Remove(outPath); err != nil {
|
||
log.Errorf("删除临时文件失败: %s", err.Error())
|
||
}
|
||
utils.GinResponse(c).OK()
|
||
}
|
||
|
||
// Status
|
||
// @description: 获取客户端状态信息,链接状态等
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) Status(c *gin.Context) {
|
||
// 使用sdk拉取一下客户端信息
|
||
devices, err := client.WireguardClient.Devices()
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithErr("获取客户端信息失败", err)
|
||
return
|
||
}
|
||
|
||
var data []vo.ClientStatus
|
||
// 遍历客户端数据,并渲染数据信息
|
||
for _, d := range devices {
|
||
for _, p := range d.Peers {
|
||
clientInfo, err := repository.Client().GetByPublicKey(p.PublicKey.String())
|
||
if err != nil {
|
||
log.Errorf("没有找到公钥匹配的客户端: %s", p.PublicKey.String())
|
||
continue
|
||
}
|
||
var ipAllocation string
|
||
for _, iaip := range p.AllowedIPs {
|
||
ipAllocation += iaip.String() + ","
|
||
}
|
||
ipAllocation = strings.TrimRight(ipAllocation, ",")
|
||
isOnline := time.Since(p.LastHandshakeTime).Minutes() < 3
|
||
data = append(data, vo.ClientStatus{
|
||
ID: clientInfo.Id,
|
||
Name: clientInfo.Name,
|
||
Email: clientInfo.Email,
|
||
IpAllocation: ipAllocation,
|
||
Endpoint: p.Endpoint.String(),
|
||
Received: utils.FlowCalculation().Parse(p.ReceiveBytes),
|
||
Transmitted: utils.FlowCalculation().Parse(p.TransmitBytes),
|
||
IsOnline: isOnline,
|
||
LastHandShake: p.LastHandshakeTime.Format("2006-01-02 15:04:05"),
|
||
})
|
||
}
|
||
}
|
||
|
||
utils.GinResponse(c).OKWithData(data)
|
||
}
|
||
|
||
// Offline
|
||
// @description: 强制下线指定客户端
|
||
// @receiver clients
|
||
// @param c
|
||
func (clients) Offline(c *gin.Context) {
|
||
id := c.Param("id")
|
||
if id == "" || id == "undefined" {
|
||
utils.GinResponse(c).FailedWithMsg("参数错误")
|
||
return
|
||
}
|
||
|
||
// 查询一下客户端信息
|
||
clientInfo, err := repository.Client().GetById(id)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("获取信息失败")
|
||
return
|
||
}
|
||
|
||
keys := template_data.Keys{}
|
||
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
|
||
|
||
connectInfo, err := utils.Wireguard().GetSpecClient(keys.PublicKey)
|
||
if err != nil {
|
||
utils.GinResponse(c).FailedWithMsg("获取客户端信息失败")
|
||
return
|
||
}
|
||
|
||
if connectInfo == nil {
|
||
utils.GinResponse(c).FailedWithMsg("未获取到该客户端链接信息")
|
||
return
|
||
}
|
||
|
||
// 获取到了,执行踢下线操作。此处踢下线就是禁用该客户端
|
||
if err = repository.Client().Disabled(clientInfo.Id); err != nil {
|
||
utils.GinResponse(c).FailedWithErr("客户端下线失败: %v", err)
|
||
return
|
||
}
|
||
|
||
// 再同步一下配置文件
|
||
go func() {
|
||
if err = queues.PutAsyncWireguardConfigFile(clientInfo.ServerId); err != nil {
|
||
log.Errorf("[下线客户端]同步配置文件失败: %v", err.Error())
|
||
}
|
||
}()
|
||
|
||
utils.GinResponse(c).OK()
|
||
}
|