🆕新增tui管理模式[能用就行]

This commit is contained in:
coward
2024-12-24 16:35:57 +08:00
parent 014c97f877
commit b245c26515
17 changed files with 1475 additions and 20 deletions

165
cli/tui/app.go Normal file
View File

@@ -0,0 +1,165 @@
package tui
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"time"
"wireguard-ui/component"
"wireguard-ui/global/client"
"wireguard-ui/global/constant"
"wireguard-ui/http/vo"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type App struct {
TokenSecret string // token密钥
User *vo.User // 登陆用户
Client *ClientComponent // 客户端组件
Server *ServerComponent // 服务端组件
Setting *SettingComponent // 设置组件
}
func NewApp() *App {
app := &App{}
if _, err := app.Login(); err != nil {
fmt.Println("登陆失败: ", err)
return nil
}
err := app.AuthLogin()
if err != nil {
fmt.Println("登陆失败: ", err)
return nil
}
fmt.Println("\n=============== 登陆成功 ==============================================================================")
app.Client = NewClientComponent(app.User)
app.Server = NewServerComponent(app.User)
app.Setting = NewSettingComponent(app.User)
fmt.Println("=============== 欢迎使用wireguard-tui =================================================================")
fmt.Println("=============== 当前用户: ", app.User.Nickname, " ================================================================")
fmt.Println("=============== 当前时间: ", time.Now().Format("2006-01-02 15:04:05"), " =======================================================")
fmt.Println("=============== 注意事项如下: =========================================================================")
fmt.Println("=============== 1. 请确保服务端已经安装wireguard ======================================================")
fmt.Println("=============== 2. 请确保服务端和客户端配置文件路径正确 ===============================================")
fmt.Println("=============== 3. 请确保服务端和客户端配置文件权限正确 ===============================================")
fmt.Println("=============== 4. 请确保服务端和客户端配置文件内容正确 ===============================================")
fmt.Println("=============== 5. 请勿泄露配置文件内容 ===============================================================")
fmt.Println("=============== 6. 每次修改客户端、服务端配置或者全局配置过后,请使用重启功能重启服务端,以保证生效 ===")
fmt.Println("=============== 7. 当使用重启无效时,请手动执行对应命令 ===============================================")
fmt.Println("=============== 8. 请勿随意删除客户端,删除后无法恢复 =================================================")
fmt.Println("=============== 9. 手动命令 ===========================================================================")
fmt.Println("=============== 10. 启动wireguard服务端: wg-quick up wg0 ==============================================")
fmt.Println("=============== 11. 停止wireguard服务端: wg-quick down wg0 ============================================")
return app
}
func (a *App) Run() {
if a == nil {
return
}
for {
PrintMenu()
chooseMenu := readInput("请选择菜单: ")
switch chooseMenu {
case "1":
a.Client.ConnectList()
case "2":
a.Client.Menus()
case "3":
a.Server.Menus()
case "4":
a.Setting.Menus()
case "q":
return
}
}
}
// AuthLogin
// @description: 登陆认证
// @receiver a
// @return string
func (a *App) AuthLogin() error {
// 先判断token是否存在
tokenStr, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.TUIUserToken, a.User.Id)).Result()
if err != nil {
// 不存在,去登陆
tokenStr, err = a.Login()
if err != nil {
return err
}
}
// 存在,不必要再次登陆,解析token
claims, err := component.JWT().ParseToken("Bearer "+tokenStr, a.TokenSecret, "tui")
if err != nil {
return err
}
user, err := service.User().GetUserById(claims.ID)
if err != nil {
return err
}
if user.Status != constant.Enabled {
return errors.New("用户状态异常,请联系管理员处理")
}
a.User = &vo.User{
Id: user.Id,
Account: user.Account,
Nickname: user.Nickname,
Avatar: user.Avatar,
Contact: user.Contact,
IsAdmin: user.IsAdmin,
Status: user.Status,
}
return nil
}
// Login
// @description: 登陆
// @receiver a
// @return string
func (a *App) Login() (tokenStr string, err error) {
fmt.Println("============== 登陆 ==============")
username := readInput("请输入用户名: ")
password := readInput("请输入密码: ")
// 验证码正确,查询用户信息
user, err := service.User().GetUserByAccount(username)
if err != nil {
return "", fmt.Errorf("用户不存在: %v", err)
}
// 对比密码
if !utils.Password().ComparePassword(user.Password, password) {
return "", errors.New("密码错误")
}
secret := component.JWT().GenerateSecret(password, uuid.NewString(), time.Now().Local().String())
// 生成token
token, _, err := component.JWT().GenerateToken(user.Id, secret, "tui")
if err != nil {
return "", fmt.Errorf("登陆失败: %v", err)
}
a.User = &vo.User{
Id: user.Id,
Account: user.Account,
Nickname: user.Nickname,
Avatar: user.Avatar,
Contact: user.Contact,
IsAdmin: user.IsAdmin,
Status: user.Status,
}
a.TokenSecret = secret
return "Bearer " + token, nil
}

520
cli/tui/client.go Normal file
View File

@@ -0,0 +1,520 @@
package tui
import (
"encoding/json"
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/charmbracelet/bubbles/table"
"github.com/spf13/cast"
"os"
"strconv"
"strings"
"time"
"wireguard-ui/component"
"wireguard-ui/global/constant"
"wireguard-ui/http/param"
"wireguard-ui/http/vo"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type ClientComponent struct {
LoginUser *vo.User
Menu []string
Clients [][]string
}
func NewClientComponent(loginUser *vo.User) *ClientComponent {
ccp := &ClientComponent{
LoginUser: loginUser,
Menu: []string{"[1] 客户端列表", "[2] 查看客户端配置", "[3] 添加客户端", "[4] 编辑客户端", "[5] 删除客户端", "[q] 返回上一级菜单"},
}
return ccp
}
// Menus
// @description: 客户端菜单
// @receiver c
func (c *ClientComponent) Menus() {
fmt.Println("")
for _, r := range c.Menu {
fmt.Println(" -> " + r)
}
chooseMenu := readInput("\n请选择: ")
switch chooseMenu {
case "1":
c.List(true)
case "2":
c.ShowConfig()
case "3":
c.Add()
case "4":
c.Edit()
case "5":
c.Delete()
case "q":
return
}
}
// ConnectList
// @description: 客户端链接列表
// @receiver c
// @return string
func (c *ClientComponent) ConnectList() {
fmt.Println("\n客户端链接列表")
connectList, err := component.Wireguard().GetClients()
if err != nil {
fmt.Println("获取客户端链接列表失败: ", err.Error())
return
}
var data [][]string
for _, peer := range connectList {
// 获取客户端链接信息
clientInfo, err := service.Client().GetByPublicKey(peer.PublicKey.String())
if err != nil {
continue
}
var ipAllocation string
for _, iaip := range peer.AllowedIPs {
ipAllocation += iaip.String() + ","
}
// 去除一下最右边的逗号
if len(ipAllocation) > 0 {
ipAllocation = strings.TrimRight(ipAllocation, ",")
}
var isOnline = "否"
if time.Since(peer.LastHandshakeTime).Minutes() < 3 {
isOnline = "是"
}
data = append(data, []string{clientInfo.Name,
clientInfo.Email,
ipAllocation,
isOnline,
utils.FlowCalculation().Parse(peer.TransmitBytes),
utils.FlowCalculation().Parse(peer.ReceiveBytes),
peer.Endpoint.String(),
peer.LastHandshakeTime.Format("2006-01-02 15:04:05")})
}
//if len(data) <= 0 {
// //data = append(data, []string{"暂无数据"})
// // data = append(data, []string{"名称1", "12345678910@qq.com", "192.168.100.1", "是", "10G", "20G", "1.14.30.133:51280", "2024-12-20 15:07:36"}, []string{"名称2", "12345678910@qq.com", "192.168.100.2", "否", "20G", "40G", "1.14.30.133:51280", "2024-12-22 15:07:36"})
//}
title := []table.Column{
{
Title: "客户端名称",
Width: 20,
}, {
Title: "联系邮箱",
Width: 20,
}, {
Title: "分配的IP",
Width: 30,
}, {
Title: "是否在线",
Width: 10,
}, {
Title: "接收流量",
Width: 10,
}, {
Title: "传输流量",
Width: 10,
}, {
Title: "链接端点",
Width: 30,
}, {
Title: "最后握手时间",
Width: 30,
}}
Show(GenerateTable(title, data))
return
}
// List
// @description: 客户端列表
// @receiver c
func (c *ClientComponent) List(showMenu bool) {
title := []table.Column{
{
Title: "序号",
Width: 5,
},
{
Title: "ID",
Width: 35,
},
{
Title: "名称",
Width: 25,
},
{
Title: "联系邮箱",
Width: 30,
},
{
Title: "客户端IP",
Width: 20,
},
{
Title: "状态",
Width: 10,
},
{
Title: "离线通知",
Width: 10,
},
}
records, _, err := service.Client().List(param.ClientList{
Page: param.Page{
Current: -1,
},
})
if err != nil {
fmt.Println("获取客户端列表失败: ", err.Error())
return
}
fmt.Println("\n客户端列表")
var data [][]string
for i, client := range records {
var status, offlineNotify string
if client.Enabled == 1 {
status = "启用"
} else {
status = "禁用"
}
if client.OfflineMonitoring == 1 {
offlineNotify = "开启"
} else {
offlineNotify = "关闭"
}
data = append(data, []string{
cast.ToString(i + 1),
client.Id,
client.Name,
client.Email,
client.IpAllocationStr,
status,
offlineNotify,
})
}
Show(GenerateTable(title, data))
c.Clients = data
if showMenu {
c.Menus()
}
return
}
// ShowConfig
// @description: 显示配置
// @receiver c
func (c *ClientComponent) ShowConfig() {
c.List(false)
clientIdx := readInput("请输入客户端序号: ")
downloadType := readInput("请输入下载类型(FILE - 文件 | EMAIL - 邮件): ")
idx, err := strconv.Atoi(clientIdx)
if err != nil {
fmt.Println("输入有误: ", err.Error())
return
}
if idx < 1 || idx > len(c.Clients) {
fmt.Println("输入有误")
return
}
client := c.Clients[idx-1]
// 取到id
clientID := client[1]
// 查询客户端信息
clientInfo, err := service.Client().GetByID(clientID)
if err != nil {
fmt.Println("获取客户端信息失败: ", err.Error())
return
}
// 渲染配置
var keys vo.Keys
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
globalSet, err := service.Setting().GetWGSetForConfig()
if err != nil {
fmt.Println("获取全局配置失败: ", err.Error())
return
}
serverConf, err := service.Setting().GetWGServerForConfig()
if err != nil {
fmt.Println("获取服务器配置失败: ", err.Error())
return
}
outPath, err := component.Wireguard().GenerateClientFile(clientInfo, serverConf, globalSet)
if err != nil {
fmt.Println("生成客户端配置失败: ", err.Error())
return
}
// 根据不同下载类型执行不同逻辑
switch downloadType {
case "FILE": // 二维码
// 读取文件内容
fileContent, err := os.ReadFile(outPath)
if err != nil {
fmt.Println("读取文件失败: ", err.Error())
return
}
fmt.Println("\n#请将以下内容复制到客户端配置文件中【不包含本行提示语】")
fmt.Println("\n" + string(fileContent))
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
case "EMAIL": // 邮件
if clientInfo.Email == "" {
fmt.Println("当前客户端并未配置通知邮箱")
return
}
// 获取邮箱配置
emailConf, err := service.Setting().GetByCode("EMAIL_SMTP")
if err != nil {
fmt.Println("获取邮箱配置失败请先到设置页面的【其他】里面添加code为【EMAIL_SMTP】的具体配置")
return
}
err = utils.Mail(emailConf).SendMail(clientInfo.Email, fmt.Sprintf("客户端: %s", clientInfo.Name), "请查收附件", outPath)
if err != nil {
fmt.Println("发送邮件失败")
return
}
if err = os.Remove(outPath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
fmt.Println("发送邮件成功,请注意查收!")
}
c.Menus()
}
// Add
// @description: 添加客户端
// @receiver c
func (c *ClientComponent) Add() {
fmt.Println("\n添加客户端")
clientIP, serverIP, err := GenerateIP()
if err != nil {
fmt.Println("生成客户端IP失败: ", err.Error())
return
}
keys, err := GenerateKeys()
if err != nil {
fmt.Println("生成密钥对失败: ", err.Error())
return
}
var p param.SaveClient
p.Name = readInput("请输入客户端名称: ")
p.Email = readInput("请输入联系邮箱: ")
clientIPIn := readInput("请输入客户端IP(默认自动生成,多个采用 ',' 分割,例如 10.10.0.1/32,10.10.0.2/32): ")
if clientIPIn == "" {
p.IpAllocation = clientIP
} else {
p.IpAllocation = strings.Split(clientIPIn, ",")
}
p.AllowedIps = serverIP
p.Keys = &param.Keys{
PrivateKey: keys.PrivateKey,
PublicKey: keys.PublicKey,
PresharedKey: keys.PresharedKey,
}
var useServerDNS, enabled, offlineNotify constant.Status
useServerDNSIn := readInput("是否使用服务器DNS(默认不使用 1 - 是 | 0 - 否 ): ")
switch useServerDNSIn {
case "1":
useServerDNS = constant.Enabled
case "0":
useServerDNS = constant.Disabled
default:
useServerDNS = constant.Disabled
}
enabledIn := readInput("是否启用(默认启用 1 - 是 | 0 - 否 ): ")
switch enabledIn {
case "1":
enabled = constant.Enabled
case "0":
enabled = constant.Disabled
default:
enabled = constant.Enabled
}
offlineNotifyIn := readInput("是否开启离线通知(默认关闭 1 - 是 | 0 - 否 ): ")
switch offlineNotifyIn {
case "1":
offlineNotify = constant.Enabled
case "0":
offlineNotify = constant.Disabled
default:
offlineNotify = constant.Disabled
}
p.UseServerDns = &useServerDNS
p.Enabled = &enabled
p.OfflineMonitoring = &offlineNotify
err = service.Client().SaveClient(p, c.LoginUser)
if err != nil {
fmt.Println("添加客户端失败: ", err.Error())
return
}
fmt.Println("添加客户端成功")
c.List(true)
return
}
// Edit
// @description: 编辑客户端
// @receiver c
func (c *ClientComponent) Edit() {
fmt.Println("\n编辑客户端")
c.List(false)
clientIdx := readInput("请输入客户端序号: ")
idx, err := strconv.Atoi(clientIdx)
if err != nil {
fmt.Println("输入有误: ", err.Error())
return
}
if idx < 1 || idx > len(c.Clients) {
fmt.Println("输入有误")
return
}
client := c.Clients[idx-1]
// 取到id
clientID := client[1]
// 查询客户端信息
clientInfo, err := service.Client().GetByID(clientID)
if err != nil {
fmt.Println("获取客户端信息失败: ", err.Error())
return
}
var p param.SaveClient
p.Id = clientID
p.Name = readInput("请输入客户端名称[无需改变请回车跳过,下同]: ")
p.Email = readInput("请输入联系邮箱: ")
clientIPIn := readInput("请输入客户端IP(默认自动生成,多个采用 ',' 分割,例如 10.10.0.1/32,10.10.0.2/32): ")
if clientIPIn == "" {
p.IpAllocation = strings.Split(clientInfo.IpAllocation, ",")
} else {
p.IpAllocation = strings.Split(clientIPIn, ",")
}
p.AllowedIps = strings.Split(clientInfo.AllowedIps, ",")
var keys *param.Keys
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
p.Keys = keys
var useServerDNS, enabled, offlineNotify constant.Status
useServerDNSIn := readInput("是否使用服务器DNS(默认不使用 1 - 是 | 0 - 否 ): ")
switch useServerDNSIn {
case "1":
useServerDNS = constant.Enabled
case "0":
useServerDNS = constant.Disabled
default:
useServerDNS = clientInfo.UseServerDns
}
enabledIn := readInput("是否启用(默认启用 1 - 是 | 0 - 否 ): ")
switch enabledIn {
case "1":
enabled = constant.Enabled
case "0":
enabled = constant.Disabled
default:
enabled = clientInfo.Enabled
}
offlineNotifyIn := readInput("是否开启离线通知(默认关闭 1 - 是 | 0 - 否 ): ")
switch offlineNotifyIn {
case "1":
offlineNotify = constant.Enabled
case "0":
offlineNotify = constant.Disabled
default:
offlineNotify = clientInfo.OfflineMonitoring
}
p.UseServerDns = &useServerDNS
p.Enabled = &enabled
p.OfflineMonitoring = &offlineNotify
err = service.Client().SaveClient(p, c.LoginUser)
if err != nil {
fmt.Println("编辑客户端失败: ", err.Error())
return
}
fmt.Println("编辑客户端成功")
c.List(true)
return
}
// Delete
// @description: 删除客户端
// @receiver c
func (c *ClientComponent) Delete() {
fmt.Println("\n删除客户端")
c.List(false)
clientIdx := readInput("请输入客户端序号: ")
idx, err := strconv.Atoi(clientIdx)
if err != nil {
fmt.Println("输入有误: ", err.Error())
return
}
if idx < 1 || idx > len(c.Clients) {
fmt.Println("输入有误")
return
}
client := c.Clients[idx-1]
// 取到id
clientID := client[1]
if err := service.Client().Delete(clientID); err != nil {
fmt.Println("删除客户端失败: ", err.Error())
return
}
c.List(true)
fmt.Println("删除客户端成功")
return
}

62
cli/tui/server.go Normal file
View File

@@ -0,0 +1,62 @@
package tui
import (
"fmt"
"wireguard-ui/command"
"wireguard-ui/http/vo"
)
type ServerComponent struct {
LoginUser *vo.User
Menu []string
}
func NewServerComponent(loginUser *vo.User) *ServerComponent {
return &ServerComponent{
LoginUser: loginUser,
Menu: []string{"[1] 启动服务", "[2] 关闭服务", "[3] 重启服务", "[q] 返回上一级菜单"},
}
}
// Menus
// @description: 服务端菜单
// @receiver s
func (s *ServerComponent) Menus() {
fmt.Println("")
for _, r := range s.Menu {
fmt.Println(" -> " + r)
}
chooseMenu := readInput("\n请选择: ")
switch chooseMenu {
case "1":
s.Start()
case "2":
s.Stop()
case "3":
s.Restart()
case "q":
return
}
}
// Start
// @description: 启动服务
// @receiver s
func (s *ServerComponent) Start() {
command.StartWireguard("")
}
// Stop
// @description: 停止服务
// @receiver s
func (s *ServerComponent) Stop() {
command.StopWireguard("")
}
// Restart
// @description: 重启服务
// @receiver s
func (s *ServerComponent) Restart() {
command.RestartWireguard(false, "")
}

440
cli/tui/setting.go Normal file
View File

@@ -0,0 +1,440 @@
package tui
import (
"encoding/json"
"fmt"
"github.com/charmbracelet/bubbles/table"
"github.com/eiannone/keyboard"
"github.com/spf13/cast"
"strings"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/service"
"wireguard-ui/utils"
)
type SettingComponent struct {
LoginUser *vo.User
Menu []string
Other *OtherSettingComponent
}
func NewSettingComponent(loginUser *vo.User) *SettingComponent {
return &SettingComponent{
LoginUser: loginUser,
Menu: []string{"[1] 服务端配置", "[2] 全局设置", "[3] 其他配置", "[q] 返回上一级菜单"},
Other: NewOtherSettingComponent(),
}
}
// Menus
// @description: 设置菜单
// @receiver s
func (s *SettingComponent) Menus() {
fmt.Println("")
for _, r := range s.Menu {
fmt.Println(" -> " + r)
}
chooseMenu := readInput("\n请选择: ")
switch chooseMenu {
case "1":
s.ServerSetting()
case "2":
s.GlobalSetting()
case "3":
s.Other.Menus(s)
case "q":
return
}
}
// ServerSetting
// @description: 服务端配置
// @receiver s
func (s *SettingComponent) ServerSetting() {
fmt.Println("\n服务端配置")
// 先读取一下服务端配置
servConf, err := service.Setting().GetByCode("WG_SERVER")
if err != nil {
fmt.Println("获取服务端配置失败: ", err)
return
}
type serverConf struct {
IpScope []string `json:"ipScope"`
ListenPort int `json:"listenPort"`
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
PostUpScript string `json:"postUpScript"`
PostDownScript string `json:"postDownScript"`
}
// 解析出来好渲染
var conf serverConf
if err = json.Unmarshal([]byte(servConf.Data), &conf); err != nil {
fmt.Println("解析服务端配置失败: ", err)
return
}
ipScopeIn := readInput(fmt.Sprintf("请输入IP段多个采用 ',[英文逗号]' 分割,不填写默认当前值,下同,当前值[%s] ", strings.Replace(strings.Join(conf.IpScope, ","), " ", "", -1)))
listenPortIn := readInput(fmt.Sprintf("请输入监听端口,当前值[%d] ", conf.ListenPort))
privateKeyIn := readInput(fmt.Sprintf("请输入私钥,当前值[%s] ", conf.PrivateKey))
publicKeyIn := readInput(fmt.Sprintf("请输入公钥,当前值[%s] ", conf.PublicKey))
postUpScriptIn := readInput(fmt.Sprintf("请输入PostUp脚本当前值[%s] ", conf.PostUpScript))
postDownScriptIn := readInput(fmt.Sprintf("请输入PostDown脚本当前值[%s] ", conf.PostDownScript))
if ipScopeIn != "" {
conf.IpScope = strings.Split(ipScopeIn, ",")
}
if listenPortIn != "" {
conf.ListenPort = cast.ToInt(listenPortIn)
}
if privateKeyIn != "" {
conf.PrivateKey = privateKeyIn
}
if publicKeyIn != "" {
conf.PublicKey = publicKeyIn
}
if postUpScriptIn != "" {
conf.PostUpScript = postUpScriptIn
}
if postDownScriptIn != "" {
conf.PostDownScript = postDownScriptIn
}
data, _ := json.Marshal(conf)
if err := service.Setting().SetData(&model.Setting{
Code: "WG_SERVER",
Data: string(data),
}); err != nil {
fmt.Println("保存服务端配置失败: ", err)
return
}
fmt.Println("修改服务端配置成功")
return
}
// GlobalSetting
// @description: 全局设置
// @receiver s
func (s *SettingComponent) GlobalSetting() {
fmt.Println("\n服务端配置")
// 先读取一下服务端配置
globalConf, err := service.Setting().GetByCode("WG_SETTING")
if err != nil {
fmt.Println("获取服务端配置失败: ", err)
return
}
type gConf struct {
MTU int `json:"MTU"`
ConfigFilePath string `json:"configFilePath"`
DnsServer []string `json:"dnsServer"`
EndpointAddress string `json:"endpointAddress"`
FirewallMark string `json:"firewallMark"`
PersistentKeepalive int `json:"persistentKeepalive"`
Table string `json:"table"`
}
// 解析出来好渲染
var conf gConf
if err = json.Unmarshal([]byte(globalConf.Data), &conf); err != nil {
fmt.Println("解析全局配置失败: ", err)
return
}
mtu := readInput(fmt.Sprintf("请输入mtu不填写默认当前值下同当前值[%d] ", conf.MTU))
configFilePath := readInput(fmt.Sprintf("请输入配置文件地址,当前值[%s] ", conf.ConfigFilePath))
dnsServer := readInput(fmt.Sprintf("请输入dns多个采用 ',[英文逗号]' 分割,当前值[%s] ", strings.Replace(strings.Join(conf.DnsServer, ","), " ", "", -1)))
endpointAddress := readInput(fmt.Sprintf("请输入公网IP默认系统自动获取当前值[%s] ", conf.EndpointAddress))
firewallMark := readInput(fmt.Sprintf("请输入FirewallMark当前值[%s] ", conf.FirewallMark))
persistentKeepalive := readInput(fmt.Sprintf("请输入PersistentKeepalive当前值[%d] ", conf.PersistentKeepalive))
tableRule := readInput(fmt.Sprintf("请输入Table当前值[%s] ", conf.Table))
if mtu != "" {
conf.MTU = cast.ToInt(mtu)
}
if configFilePath != "" {
conf.ConfigFilePath = configFilePath
}
if dnsServer != "" {
conf.DnsServer = strings.Split(dnsServer, ",")
}
if endpointAddress != "" {
conf.EndpointAddress = endpointAddress
} else {
conf.EndpointAddress = utils.Network().GetHostPublicIP()
}
if firewallMark != "" {
conf.FirewallMark = firewallMark
}
if persistentKeepalive != "" {
conf.PersistentKeepalive = cast.ToInt(persistentKeepalive)
}
if tableRule != "" {
conf.Table = tableRule
}
data, _ := json.Marshal(conf)
if err := service.Setting().SetData(&model.Setting{
Code: "WG_SETTING",
Data: string(data),
}); err != nil {
fmt.Println("保存全局配置失败: ", err)
return
}
fmt.Println("修改全局配置成功")
return
}
// OtherSettingComponent
// @description: 其他配置杂项
type OtherSettingComponent struct {
Setting *SettingComponent
Menu []string
}
func NewOtherSettingComponent() *OtherSettingComponent {
return &OtherSettingComponent{
Menu: []string{"[1] 列表", "[2] 添加", "[3] 编辑", "[4] 删除", "[q] 返回上一级菜单"},
}
}
func (s *OtherSettingComponent) Menus(setting *SettingComponent) {
if s.Setting == nil {
s.Setting = setting
}
fmt.Println("")
for _, r := range s.Menu {
fmt.Println(" -> " + r)
}
chooseMenu := readInput("\n请选择: ")
switch chooseMenu {
case "1":
s.List(true)
case "2":
s.Add()
case "3":
s.Edit()
case "4":
s.Delete()
case "q":
s.Setting.Menus()
}
}
// List
// @description: 其他配置
// @receiver s
// @param showMenu
func (s *OtherSettingComponent) List(showMenu bool) {
fmt.Println("\n其他配置列表")
// 不查询的配置
var blackList = []string{"WG_SETTING", "WG_SERVER"}
data, err := service.Setting().GetAllSetting(blackList)
if err != nil {
fmt.Println("获取配置失败")
return
}
title := []table.Column{
{
Title: "序号",
Width: 10,
},
{
Title: "编码",
Width: 40,
},
{
Title: "描述",
Width: 50,
},
{
Title: "创建时间",
Width: 30,
},
{
Title: "更新时间",
Width: 30,
},
}
var result [][]string
for i, v := range data {
result = append(result, []string{
cast.ToString(i + 1),
v.Code,
v.Describe,
v.CreatedAt.Format("2006-01-02 15:04:05"),
v.UpdatedAt.Format("2006-01-02 15:04:05"),
})
}
Show(GenerateTable(title, result))
if showMenu {
s.Menus(nil)
}
}
// Add
// @description: 新增其他配置
// @receiver s
func (s *OtherSettingComponent) Add() {
fmt.Println("\n新增其他配置")
code := readInput("请输入配置编码,此编码是唯一编码不可重复:")
desc := readInput("请输入配置描述:")
// 监听键盘事件,只监听 + 和 - 和 enter
if err := keyboard.Open(); err != nil {
return
}
defer func() {
_ = keyboard.Close()
}()
// + <=> 43 | - <=> 45 | enter <=> 0
fmt.Println("请按下 + 或者 - 进行配置项的新增和删除")
fmt.Println("每一项配置如此: key=val ")
fmt.Println("确认输入完成后 enter[按一次代表当前配置项输入完成,两次代表新增完成]")
fmt.Println("首先进入时请输入 + 进行第一个配置项填写")
var breakCycle bool
var keyVal []string
for {
char, _, err := keyboard.GetKey()
if err != nil {
break
}
if breakCycle {
break
}
switch char {
case 0:
// 收到enter事件触发执行后面的
var dm = make(map[string]any)
for _, kv := range keyVal {
kvs := strings.Split(kv, "=")
key := kvs[0]
val := kvs[1]
dm[key] = val
}
dms, err := json.Marshal(dm)
if err != nil {
breakCycle = true
continue
}
if err = service.Setting().SetData(&model.Setting{
Code: code,
Data: string(dms),
Describe: desc,
}); err != nil {
breakCycle = true
fmt.Println("保存配置失败: ", err.Error())
continue
}
fmt.Println("保存配置成功")
s.List(true)
breakCycle = true
case 43:
keyVal = append(keyVal, readInput("请输入配置项:"))
case 45:
keyVal = keyVal[:len(keyVal)-1]
fmt.Println("已删除最后一个配置项,当前配置项为:", keyVal)
}
}
}
// Edit
// @description: 编辑
// @receiver s
func (s *OtherSettingComponent) Edit() {
fmt.Println("\n编辑其他配置")
s.List(false)
code := readInput("请输入需要编辑的配置编码:")
// 通过编码查询配置
setting, err := service.Setting().GetByCode(code)
if err != nil {
fmt.Println("查找["+code+"]配置失败:", err.Error())
return
}
desc := readInput("请输入需要编辑的配置描述:")
if desc != "" {
setting.Describe = desc
}
var kvm = make(map[string]any)
if err = json.Unmarshal([]byte(setting.Data), &kvm); err != nil {
fmt.Println("配置解析失败: ", err.Error())
return
}
for k, v := range kvm {
valIn := readInput(fmt.Sprintf("请输入配置项值,仅能修改值,当前键值对:%s=%v ", k, v))
if valIn != "" {
kvm[k] = valIn
}
}
// 处理以下数据
kvmStr, err := json.Marshal(kvm)
if err != nil {
fmt.Println("序列化数据失败: ", err.Error())
return
}
setting.Data = string(kvmStr)
if err = service.Setting().SetData(setting); err != nil {
fmt.Println("修改配置失败: ", err.Error())
return
}
fmt.Println("修改配置成功")
}
// Delete
// @description: 删除
// @receiver s
func (s *OtherSettingComponent) Delete() {
fmt.Println("\n 删除指定配置")
s.List(false)
code := readInput("请输入要删除的配置项编码:")
// 查询配置是否存在
if err := service.Setting().Model(&model.Setting{}).
Where("code NOT IN (?)", []string{"WG_SETTING", "WG_SERVER"}).
Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil {
fmt.Println("删除[" + code + "]配置失败")
return
}
fmt.Println("删除成功")
s.List(true)
}

136
cli/tui/utils.go Normal file
View File

@@ -0,0 +1,136 @@
package tui
import (
"bufio"
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cast"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"os"
"strings"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/service"
"wireguard-ui/utils"
)
var BaseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240"))
var menus = []string{"[1] 客户端链接列表", "[2] 客户端", "[3] 服务端", "[4] 设置", "[q] 退出"}
// PrintMenu
// @description: 打印一下菜单
func PrintMenu() {
fmt.Println("\n菜单:")
for _, menu := range menus {
fmt.Println(menu)
}
}
// GenerateTable
// @description: 生成数据表
// @param column
// @param rows
// @return string
func GenerateTable(column []table.Column, rows [][]string) string {
var data []table.Row
for _, v := range rows {
data = append(data, v)
}
tl := table.New(
table.WithColumns(column),
table.WithRows(data),
table.WithHeight(len(data)+1),
)
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = lipgloss.NewStyle()
tl.SetStyles(s)
return BaseStyle.Render(tl.View()+"\n") + "\n一共有 【" + fmt.Sprintf("%d", len(data)) + "】 条数据"
}
// GenerateIP
// @description: 生成IP
// @return clientIPS
// @return serverIPS
// @return err
func GenerateIP() (clientIPS, serverIPS []string, err error) {
// 获取一下服务端信息因为IP分配需要根据服务端的IP制定
serverInfo, err := service.Setting().GetWGServerForConfig()
if err != nil {
return nil, nil, err
}
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...)
return ips, serverInfo.Address, nil
}
// GenerateKeys
// @description: 生成密钥对
// @return keys
// @return err
func GenerateKeys() (keys *vo.Keys, err error) {
// 为空,新增
privateKey, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
}
publicKey := privateKey.PublicKey().String()
presharedKey, err := wgtypes.GenerateKey()
if err != nil {
return nil, fmt.Errorf("生成预共享密钥失败: %s", err.Error())
}
keys = &vo.Keys{
PrivateKey: privateKey.String(),
PublicKey: publicKey,
PresharedKey: presharedKey.String(),
}
return keys, nil
}
// Show
// @description: 展示
// @param data
func Show(data any) {
fmt.Println(data)
}
// readInput
// @description: 读取输入
// @param prompt
// @return string
func readInput(prompt string) string {
fmt.Print(prompt)
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
return scanner.Text()
}