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