package controller
import (
"bytes"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"golang.org/x/net/idna"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/service/singleton"
)
type memberAPI struct {
r gin.IRouter
}
func (ma *memberAPI) serve() {
mr := ma.r.Group("")
// mr.Use(mygin.Authorize(mygin.AuthorizeOption{
// MemberOnly: true,
// IsPage: false,
// Msg: "访问此接口需要登录",
// Btn: "点此登录",
// Redirect: "/login",
// }))
mr.POST("/cron", ma.addOrEditCron)
mr.GET("/cron/:id/manual", ma.manualTrigger)
mr.POST("/force-update", ma.forceUpdate)
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/ddns", ma.addOrEditDDNS)
mr.POST("/nat", ma.addOrEditNAT)
mr.POST("/alert-rule", ma.addOrEditAlertRule)
mr.POST("/setting", ma.updateSetting)
mr.DELETE("/:model/:id", ma.delete)
mr.POST("/logout", ma.logout)
}
func (ma *memberAPI) delete(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if id < 1 {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: "错误的 Server ID",
})
return
}
var err error
switch c.Param("model") {
case "nat":
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
if err == nil {
singleton.OnNATUpdate()
}
case "cron":
err = singleton.DB.Unscoped().Delete(&model.Cron{}, "id = ?", id).Error
if err == nil {
singleton.CronLock.RLock()
defer singleton.CronLock.RUnlock()
cr := singleton.Crons[id]
if cr != nil && cr.CronJobID != 0 {
singleton.Cron.Remove(cr.CronJobID)
}
delete(singleton.Crons, id)
}
case "alert-rule":
err = singleton.DB.Unscoped().Delete(&model.AlertRule{}, "id = ?", id).Error
if err == nil {
singleton.OnDeleteAlert(id)
}
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("数据库错误:%s", err),
})
return
}
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type cronForm struct {
ID uint64
TaskType uint8 // 0:计划任务 1:触发任务
Name string
Scheduler string
Command string
ServersRaw string
Cover uint8
PushSuccessful string
NotificationTag string
}
func (ma *memberAPI) addOrEditCron(c *gin.Context) {
var cf cronForm
var cr model.Cron
err := c.ShouldBindJSON(&cf)
if err == nil {
cr.TaskType = cf.TaskType
cr.Name = cf.Name
cr.Scheduler = cf.Scheduler
cr.Command = cf.Command
cr.ServersRaw = cf.ServersRaw
cr.PushSuccessful = cf.PushSuccessful == "on"
//cr.NotificationTag = cf.NotificationTag
cr.ID = cf.ID
cr.Cover = cf.Cover
err = utils.Json.Unmarshal([]byte(cf.ServersRaw), &cr.Servers)
}
// 计划任务类型不得使用触发服务器执行方式
if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger {
err = errors.New("计划任务类型不得使用触发服务器执行方式")
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
tx := singleton.DB.Begin()
if err == nil {
// 保证NotificationTag不为空
//if cr.NotificationTag == "" {
// cr.NotificationTag = "default"
//}
if cf.ID == 0 {
err = tx.Create(&cr).Error
} else {
err = tx.Save(&cr).Error
}
}
if err == nil {
// 对于计划任务类型,需要更新CronJob
if cf.TaskType == model.CronTypeCronTask {
cr.CronJobID, err = singleton.Cron.AddFunc(cr.Scheduler, singleton.CronTrigger(cr))
}
}
if err == nil {
err = tx.Commit().Error
} else {
tx.Rollback()
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
singleton.CronLock.Lock()
defer singleton.CronLock.Unlock()
crOld := singleton.Crons[cr.ID]
if crOld != nil && crOld.CronJobID != 0 {
singleton.Cron.Remove(crOld.CronJobID)
}
delete(singleton.Crons, cr.ID)
singleton.Crons[cr.ID] = &cr
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
func (ma *memberAPI) manualTrigger(c *gin.Context) {
var cr model.Cron
if err := singleton.DB.First(&cr, "id = ?", c.Param("id")).Error; err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: err.Error(),
})
return
}
singleton.ManualTrigger(cr)
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type BatchUpdateServerGroupRequest struct {
Servers []uint64
Group string
}
func (ma *memberAPI) batchUpdateServerGroup(c *gin.Context) {
var req BatchUpdateServerGroupRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: err.Error(),
})
return
}
if err := singleton.DB.Model(&model.Server{}).Where("id in (?)", req.Servers).Update("tag", req.Group).Error; err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: err.Error(),
})
return
}
singleton.ServerLock.Lock()
for i := 0; i < len(req.Servers); i++ {
serverId := req.Servers[i]
var s model.Server
copier.Copy(&s, singleton.ServerList[serverId])
// s.Tag = req.Group
// // 如果修改了Ta
// oldTag := singleton.ServerList[serverId].Tag
// newTag := s.Tag
// if newTag != oldTag {
// index := -1
// for i := 0; i < len(singleton.ServerTagToIDList[oldTag]); i++ {
// if singleton.ServerTagToIDList[oldTag][i] == s.ID {
// index = i
// break
// }
// }
// if index > -1 {
// // 删除旧 Tag-ID 绑定关系
// singleton.ServerTagToIDList[oldTag] = append(singleton.ServerTagToIDList[oldTag][:index], singleton.ServerTagToIDList[oldTag][index+1:]...)
// if len(singleton.ServerTagToIDList[oldTag]) == 0 {
// delete(singleton.ServerTagToIDList, oldTag)
// }
// }
// // 设置新的 Tag-ID 绑定关系
// singleton.ServerTagToIDList[newTag] = append(singleton.ServerTagToIDList[newTag], s.ID)
// }
singleton.ServerList[s.ID] = &s
}
singleton.ServerLock.Unlock()
singleton.ReSortServer()
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
func (ma *memberAPI) forceUpdate(c *gin.Context) {
var forceUpdateServers []uint64
if err := c.ShouldBindJSON(&forceUpdateServers); err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: err.Error(),
})
return
}
var executeResult bytes.Buffer
for i := 0; i < len(forceUpdateServers); i++ {
singleton.ServerLock.RLock()
server := singleton.ServerList[forceUpdateServers[i]]
singleton.ServerLock.RUnlock()
if server != nil && server.TaskStream != nil {
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeUpgrade,
}); err != nil {
executeResult.WriteString(fmt.Sprintf("%d 下发指令失败 %+v
", forceUpdateServers[i], err))
} else {
executeResult.WriteString(fmt.Sprintf("%d 下发指令成功
", forceUpdateServers[i]))
}
} else {
executeResult.WriteString(fmt.Sprintf("%d 离线
", forceUpdateServers[i]))
}
}
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
Message: executeResult.String(),
})
}
type notificationForm struct {
ID uint64
Name string
URL string
RequestMethod int
RequestType int
RequestHeader string
RequestBody string
VerifySSL string
SkipCheck string
}
func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
var nf notificationForm
var n model.Notification
err := c.ShouldBindJSON(&nf)
if err == nil {
n.Name = nf.Name
n.RequestMethod = nf.RequestMethod
n.RequestType = nf.RequestType
n.RequestHeader = nf.RequestHeader
n.RequestBody = nf.RequestBody
n.URL = nf.URL
verifySSL := nf.VerifySSL == "on"
n.VerifySSL = &verifySSL
n.ID = nf.ID
ns := model.NotificationServerBundle{
Notification: &n,
Server: nil,
Loc: singleton.Loc,
}
// 勾选了跳过检查
if nf.SkipCheck != "on" {
err = ns.Send("这是测试消息")
}
}
if err == nil {
if n.ID == 0 {
err = singleton.DB.Create(&n).Error
} else {
err = singleton.DB.Save(&n).Error
}
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
singleton.OnRefreshOrAddNotification(&n)
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type ddnsForm struct {
ID uint64
MaxRetries uint64
EnableIPv4 string
EnableIPv6 string
Name string
Provider string
DomainsRaw string
AccessID string
AccessSecret string
WebhookURL string
WebhookMethod uint8
WebhookRequestType uint8
WebhookRequestBody string
WebhookHeaders string
}
func (ma *memberAPI) addOrEditDDNS(c *gin.Context) {
var df ddnsForm
var p model.DDNSProfile
err := c.ShouldBindJSON(&df)
if err == nil {
if df.MaxRetries < 1 || df.MaxRetries > 10 {
err = errors.New("重试次数必须为大于 1 且不超过 10 的整数")
}
}
if err == nil {
p.Name = df.Name
p.ID = df.ID
enableIPv4 := df.EnableIPv4 == "on"
enableIPv6 := df.EnableIPv6 == "on"
p.EnableIPv4 = &enableIPv4
p.EnableIPv6 = &enableIPv6
p.MaxRetries = df.MaxRetries
p.Provider = df.Provider
p.DomainsRaw = df.DomainsRaw
p.Domains = strings.Split(p.DomainsRaw, ",")
p.AccessID = df.AccessID
p.AccessSecret = df.AccessSecret
p.WebhookURL = df.WebhookURL
p.WebhookMethod = df.WebhookMethod
p.WebhookRequestType = df.WebhookRequestType
p.WebhookRequestBody = df.WebhookRequestBody
p.WebhookHeaders = df.WebhookHeaders
for n, domain := range p.Domains {
// IDN to ASCII
domainValid, domainErr := idna.Lookup.ToASCII(domain)
if domainErr != nil {
err = fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr)
break
}
p.Domains[n] = domainValid
}
}
if err == nil {
if p.ID == 0 {
err = singleton.DB.Create(&p).Error
} else {
err = singleton.DB.Save(&p).Error
}
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
//singleton.OnDDNSUpdate()
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type natForm struct {
ID uint64
Name string
ServerID uint64
Host string
Domain string
}
func (ma *memberAPI) addOrEditNAT(c *gin.Context) {
var nf natForm
var n model.NAT
err := c.ShouldBindJSON(&nf)
if err == nil {
n.Name = nf.Name
n.ID = nf.ID
n.Domain = nf.Domain
n.Host = nf.Host
n.ServerID = nf.ServerID
}
if err == nil {
if n.ID == 0 {
err = singleton.DB.Create(&n).Error
} else {
err = singleton.DB.Save(&n).Error
}
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
singleton.OnNATUpdate()
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type alertRuleForm struct {
ID uint64
Name string
RulesRaw string
FailTriggerTasksRaw string // 失败时触发的任务id
RecoverTriggerTasksRaw string // 恢复时触发的任务id
NotificationTag string
TriggerMode int
Enable string
}
func (ma *memberAPI) addOrEditAlertRule(c *gin.Context) {
var arf alertRuleForm
var r model.AlertRule
err := c.ShouldBindJSON(&arf)
if err == nil {
err = utils.Json.Unmarshal([]byte(arf.RulesRaw), &r.Rules)
}
if err == nil {
if len(r.Rules) == 0 {
err = errors.New("至少定义一条规则")
} else {
for i := 0; i < len(r.Rules); i++ {
if !r.Rules[i].IsTransferDurationRule() {
if r.Rules[i].Duration < 3 {
err = errors.New("错误:Duration 至少为 3")
break
}
} else {
if r.Rules[i].CycleInterval < 1 {
err = errors.New("错误: cycle_interval 至少为 1")
break
}
if r.Rules[i].CycleStart == nil {
err = errors.New("错误: cycle_start 未设置")
break
}
if r.Rules[i].CycleStart.After(time.Now()) {
err = errors.New("错误: cycle_start 是个未来值")
break
}
}
}
}
}
if err == nil {
r.Name = arf.Name
r.RulesRaw = arf.RulesRaw
r.FailTriggerTasksRaw = arf.FailTriggerTasksRaw
r.RecoverTriggerTasksRaw = arf.RecoverTriggerTasksRaw
//r.NotificationTag = arf.NotificationTag
enable := arf.Enable == "on"
r.TriggerMode = arf.TriggerMode
r.Enable = &enable
r.ID = arf.ID
}
if err == nil {
err = utils.Json.Unmarshal([]byte(arf.FailTriggerTasksRaw), &r.FailTriggerTasks)
}
if err == nil {
err = utils.Json.Unmarshal([]byte(arf.RecoverTriggerTasksRaw), &r.RecoverTriggerTasks)
}
//保证NotificationTag不为空
if err == nil {
//if r.NotificationTag == "" {
// r.NotificationTag = "default"
//}
if r.ID == 0 {
err = singleton.DB.Create(&r).Error
} else {
err = singleton.DB.Save(&r).Error
}
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
singleton.OnRefreshOrAddAlert(r)
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type logoutForm struct {
ID uint64
}
func (ma *memberAPI) logout(c *gin.Context) {
admin := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User)
var lf logoutForm
if err := c.ShouldBindJSON(&lf); err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
if lf.ID != admin.ID {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", "用户ID不匹配"),
})
return
}
singleton.DB.Model(admin).UpdateColumns(model.User{
// Token: "",
// TokenExpired: time.Now(),
})
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
// if oidcLogoutUrl := singleton.Conf.Oauth2.OidcLogoutURL; oidcLogoutUrl != "" {
// // 重定向到 OIDC 退出登录地址。不知道为什么,这里的重定向不生效
// c.Redirect(http.StatusOK, oidcLogoutUrl)
// }
}
type settingForm struct {
SiteName string
Language string
CustomNameservers string
IgnoredIPNotification string
IPChangeNotificationTag string // IP变更提醒的通知组
InstallHost string
Cover uint8
EnableIPChangeNotification string
EnablePlainIPInNotification string
}
func (ma *memberAPI) updateSetting(c *gin.Context) {
var sf settingForm
if err := c.ShouldBind(&sf); err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
// if _, yes := model.Themes[sf.Theme]; !yes {
// c.JSON(http.StatusOK, model.Response{
// Code: http.StatusBadRequest,
// Message: fmt.Sprintf("前台主题不存在:%s", sf.Theme),
// })
// return
// }
// if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes {
// c.JSON(http.StatusOK, model.Response{
// Code: http.StatusBadRequest,
// Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
// })
// return
// }
singleton.Conf.Language = sf.Language
singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification == "on"
singleton.Conf.EnablePlainIPInNotification = sf.EnablePlainIPInNotification == "on"
singleton.Conf.Cover = sf.Cover
singleton.Conf.InstallHost = sf.InstallHost
singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification
singleton.Conf.IPChangeNotificationTag = sf.IPChangeNotificationTag
singleton.Conf.SiteName = sf.SiteName
singleton.Conf.DNSServers = sf.CustomNameservers
// 保证NotificationTag不为空
if singleton.Conf.IPChangeNotificationTag == "" {
singleton.Conf.IPChangeNotificationTag = "default"
}
if err := singleton.Conf.Save(); err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
// 更新DNS服务器
singleton.OnNameserverUpdate()
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}