mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-05-06 13:48:52 +00:00
Compare commits
7 Commits
bc32f8fce4
...
b4a0641177
| Author | SHA1 | Date | |
|---|---|---|---|
| b4a0641177 | |||
| a3e2b2edba | |||
| 3d19be657d | |||
| 5dfc049121 | |||
| 5493f5c32f | |||
| dc58e84946 | |||
| 3d849b4b43 |
@@ -155,6 +155,7 @@ func routers(r *gin.Engine, frontendDist fs.FS) {
|
|||||||
|
|
||||||
auth.POST("/domains", commonHandler(AddDomain))
|
auth.POST("/domains", commonHandler(AddDomain))
|
||||||
auth.POST("/domains/:id/verify", commonHandler(VerifyDomain))
|
auth.POST("/domains/:id/verify", commonHandler(VerifyDomain))
|
||||||
|
auth.POST("/domains/:id/sync", commonHandler(SyncDomainWHOIS))
|
||||||
auth.PUT("/domains/:id", commonHandler(UpdateDomain))
|
auth.PUT("/domains/:id", commonHandler(UpdateDomain))
|
||||||
auth.DELETE("/domains/:id", commonHandler(DeleteDomain))
|
auth.DELETE("/domains/:id", commonHandler(DeleteDomain))
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -118,3 +119,21 @@ func UpdateDomainInfo(c *gin.Context) (any, error) {
|
|||||||
|
|
||||||
return singleton.UpdateDomain(domainID, req)
|
return singleton.UpdateDomain(domainID, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SyncDomainWHOIS(c *gin.Context) (any, error) {
|
||||||
|
domainID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, newGormError("无效的域名ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := singleton.GetDomainByID(domainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, newGormError("未找到域名: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := singleton.SyncDomainWHOIS(domain); err != nil {
|
||||||
|
return nil, fmt.Errorf("Whois 同步失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ func updateConfig(c *gin.Context) (any, error) {
|
|||||||
singleton.Conf.InstallHost = sf.InstallHost
|
singleton.Conf.InstallHost = sf.InstallHost
|
||||||
singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification
|
singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification
|
||||||
singleton.Conf.IPChangeNotificationGroupID = sf.IPChangeNotificationGroupID
|
singleton.Conf.IPChangeNotificationGroupID = sf.IPChangeNotificationGroupID
|
||||||
|
singleton.Conf.ExpiryNotificationGroupID = sf.ExpiryNotificationGroupID
|
||||||
singleton.Conf.SiteName = sf.SiteName
|
singleton.Conf.SiteName = sf.SiteName
|
||||||
singleton.Conf.DNSServers = sf.DNSServers
|
singleton.Conf.DNSServers = sf.DNSServers
|
||||||
singleton.Conf.CustomCode = sf.CustomCode
|
singleton.Conf.CustomCode = sf.CustomCode
|
||||||
|
|||||||
@@ -75,6 +75,15 @@ func initSystem(bus chan<- *model.Service) error {
|
|||||||
if _, err := singleton.CronShared.AddFunc("0 0 * * * *", func() { singleton.RecordTransferHourlyUsage() }); err != nil {
|
if _, err := singleton.CronShared.AddFunc("0 0 * * * *", func() { singleton.RecordTransferHourlyUsage() }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 每天 12:00 检查域名与服务器到期
|
||||||
|
if _, err := singleton.CronShared.AddFunc("0 0 12 * * *", func() {
|
||||||
|
singleton.CronJobForDomainStatus()
|
||||||
|
singleton.CronJobForServerStatus()
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ require (
|
|||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/likexian/gokit v0.25.16 // indirect
|
||||||
|
github.com/likexian/whois v1.15.7 // indirect
|
||||||
|
github.com/likexian/whois-parser v1.24.21 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.37 // indirect
|
github.com/mattn/go-sqlite3 v1.14.37 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
|||||||
@@ -155,6 +155,12 @@ github.com/libdns/he v1.2.1 h1:cjTZlxM5wv2lBPmtxsQCqMgmXMqTnmR4eLqUVwEkqis=
|
|||||||
github.com/libdns/he v1.2.1/go.mod h1:SWTm80gn+7sUASGsQbRHayenoW4QIw/iGmsrkDzFghM=
|
github.com/libdns/he v1.2.1/go.mod h1:SWTm80gn+7sUASGsQbRHayenoW4QIw/iGmsrkDzFghM=
|
||||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
|
github.com/likexian/gokit v0.25.16 h1:wwBeUIN/OdoPp6t00xTnZE8Di/+s969Bl5N2Kw6bzP8=
|
||||||
|
github.com/likexian/gokit v0.25.16/go.mod h1:Wqd4f+iifV0qxA1N3MqePJTUsmRy/lpst9/yXriDx/4=
|
||||||
|
github.com/likexian/whois v1.15.7 h1:sajjDhi2bVD71AHJhjV7jLYxN92H4AWhTwxM8hmj7c0=
|
||||||
|
github.com/likexian/whois v1.15.7/go.mod h1:kdPQtYb+7SQVftBEbCblDadUkycN7Mg1k1/Li/rwvmc=
|
||||||
|
github.com/likexian/whois-parser v1.24.21 h1:MxsrGRxDOiZIVp7q7N/yAIbKuN4QAkGjCpOtTDA5OsM=
|
||||||
|
github.com/likexian/whois-parser v1.24.21/go.mod h1:o3DUruO65Pb8WXCJCTlSVkTbwuYVrBCeoMTw2q0mxY4=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
|
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
|
||||||
|
|||||||
+2
-1
@@ -42,7 +42,8 @@ type ConfigDashboard struct {
|
|||||||
UserTemplate string `koanf:"user_template" json:"user_template,omitempty"`
|
UserTemplate string `koanf:"user_template" json:"user_template,omitempty"`
|
||||||
AdminTemplate string `koanf:"admin_template" json:"admin_template,omitempty"`
|
AdminTemplate string `koanf:"admin_template" json:"admin_template,omitempty"`
|
||||||
|
|
||||||
EnablePlainIPInNotification bool `koanf:"enable_plain_ip_in_notification" json:"enable_plain_ip_in_notification,omitempty"` // 通知信息IP不打码
|
EnablePlainIPInNotification bool `koanf:"enable_plain_ip_in_notification" json:"enable_plain_ip_in_notification,omitempty"` // 通知信息IP不打码
|
||||||
|
ExpiryNotificationGroupID uint64 `koanf:"expiry_notification_group_id" json:"expiry_notification_group_id"`
|
||||||
|
|
||||||
// IP变更提醒
|
// IP变更提醒
|
||||||
EnableIPChangeNotification bool `koanf:"enable_ip_change_notification" json:"enable_ip_change_notification,omitempty"`
|
EnableIPChangeNotification bool `koanf:"enable_ip_change_notification" json:"enable_ip_change_notification,omitempty"`
|
||||||
|
|||||||
+82
-4
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/smtp"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -31,14 +32,22 @@ type NotificationServerBundle struct {
|
|||||||
Loc *time.Location
|
Loc *time.Location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = iota
|
||||||
|
NotificationTypeWebhook
|
||||||
|
NotificationTypeSMTP
|
||||||
|
NotificationTypeTelegram
|
||||||
|
)
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
Common
|
Common
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
URL string `json:"url"`
|
Type uint8 `json:"type"` // 1: Webhook, 2: SMTP, 3: Telegram
|
||||||
|
URL string `json:"url"` // SMTP: host:port, Webhook: url, Telegram: bot_token
|
||||||
RequestMethod uint8 `json:"request_method"`
|
RequestMethod uint8 `json:"request_method"`
|
||||||
RequestType uint8 `json:"request_type"`
|
RequestType uint8 `json:"request_type"`
|
||||||
RequestHeader string `json:"request_header" gorm:"type:longtext"`
|
RequestHeader string `json:"request_header" gorm:"type:longtext"` // SMTP: user:pass, Webhook: header, Telegram: chat_id
|
||||||
RequestBody string `json:"request_body" gorm:"type:longtext"`
|
RequestBody string `json:"request_body" gorm:"type:longtext"` // SMTP: recipient, Webhook: body, Telegram: (ignored)
|
||||||
VerifyTLS *bool `json:"verify_tls,omitempty"`
|
VerifyTLS *bool `json:"verify_tls,omitempty"`
|
||||||
FormatMetricUnits *bool `json:"format_metric_units,omitempty"`
|
FormatMetricUnits *bool `json:"format_metric_units,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -111,8 +120,15 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ns *NotificationServerBundle) Send(message string) error {
|
func (ns *NotificationServerBundle) Send(message string) error {
|
||||||
var client *http.Client
|
|
||||||
n := ns.Notification
|
n := ns.Notification
|
||||||
|
if n.Type == NotificationTypeSMTP {
|
||||||
|
return ns.sendSMTP(message)
|
||||||
|
}
|
||||||
|
if n.Type == NotificationTypeTelegram {
|
||||||
|
return ns.sendTelegram(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
var client *http.Client
|
||||||
if n.VerifyTLS != nil && *n.VerifyTLS {
|
if n.VerifyTLS != nil && *n.VerifyTLS {
|
||||||
client = utils.HttpClient
|
client = utils.HttpClient
|
||||||
} else {
|
} else {
|
||||||
@@ -158,6 +174,68 @@ func (ns *NotificationServerBundle) Send(message string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ns *NotificationServerBundle) sendSMTP(message string) error {
|
||||||
|
n := ns.Notification
|
||||||
|
// RequestHeader: user:pass
|
||||||
|
// RequestBody: to_email
|
||||||
|
// URL: host:port
|
||||||
|
authInfo := strings.SplitN(n.RequestHeader, ":", 2)
|
||||||
|
if len(authInfo) < 2 {
|
||||||
|
return errors.New("SMTP认证信息格式错误 (user:pass)")
|
||||||
|
}
|
||||||
|
user := authInfo[0]
|
||||||
|
pass := authInfo[1]
|
||||||
|
to := n.RequestBody
|
||||||
|
|
||||||
|
hp := strings.SplitN(n.URL, ":", 2)
|
||||||
|
if len(hp) < 2 {
|
||||||
|
return errors.New("SMTP服务器地址格式错误 (host:port)")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := smtp.PlainAuth("", user, pass, hp[0])
|
||||||
|
|
||||||
|
subject := "Nezha Monitoring Alert"
|
||||||
|
if ns.Server != nil {
|
||||||
|
subject = fmt.Sprintf("Nezha Alert: %s", ns.Server.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := fmt.Sprintf("To: %s\r\nSubject: %s\r\n\r\n%s", to, subject, message)
|
||||||
|
|
||||||
|
err := smtp.SendMail(n.URL, auth, user, []string{to}, []byte(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NotificationServerBundle) sendTelegram(message string) error {
|
||||||
|
n := ns.Notification
|
||||||
|
// URL: bot_token
|
||||||
|
// RequestHeader: chat_id
|
||||||
|
token := n.URL
|
||||||
|
chatID := n.RequestHeader
|
||||||
|
|
||||||
|
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
|
||||||
|
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("chat_id", chatID)
|
||||||
|
params.Add("text", message)
|
||||||
|
params.Add("parse_mode", "HTML")
|
||||||
|
|
||||||
|
resp, err := http.PostForm(apiURL, params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("Telegram API Error (%d): %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// replaceParamInString 替换字符串中的占位符
|
// replaceParamInString 替换字符串中的占位符
|
||||||
func (ns *NotificationServerBundle) replaceParamsInString(str string, message string, mod func(string) string) string {
|
func (ns *NotificationServerBundle) replaceParamsInString(str string, message string, mod func(string) string) string {
|
||||||
if mod == nil {
|
if mod == nil {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
|
"gorm.io/datatypes"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
pb "github.com/nezhahq/nezha/proto"
|
pb "github.com/nezhahq/nezha/proto"
|
||||||
@@ -21,6 +22,7 @@ type Server struct {
|
|||||||
DisplayIndex int `json:"display_index"` // 展示排序,越大越靠前
|
DisplayIndex int `json:"display_index"` // 展示排序,越大越靠前
|
||||||
HideForGuest bool `json:"hide_for_guest,omitempty"` // 对游客隐藏
|
HideForGuest bool `json:"hide_for_guest,omitempty"` // 对游客隐藏
|
||||||
EnableDDNS bool `json:"enable_ddns,omitempty"` // 启用DDNS
|
EnableDDNS bool `json:"enable_ddns,omitempty"` // 启用DDNS
|
||||||
|
BillingData datatypes.JSON `gorm:"type:json" json:"billing_data,omitempty"`
|
||||||
DDNSProfilesRaw string `gorm:"default:'[]';column:ddns_profiles_raw" json:"-"`
|
DDNSProfilesRaw string `gorm:"default:'[]';column:ddns_profiles_raw" json:"-"`
|
||||||
OverrideDDNSDomainsRaw string `gorm:"default:'{}';column:override_ddns_domains_raw" json:"-"`
|
OverrideDDNSDomainsRaw string `gorm:"default:'{}';column:override_ddns_domains_raw" json:"-"`
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type SettingForm struct {
|
|||||||
AgentTLS bool `json:"tls,omitempty" validate:"optional"`
|
AgentTLS bool `json:"tls,omitempty" validate:"optional"`
|
||||||
EnableIPChangeNotification bool `json:"enable_ip_change_notification,omitempty" validate:"optional"`
|
EnableIPChangeNotification bool `json:"enable_ip_change_notification,omitempty" validate:"optional"`
|
||||||
EnablePlainIPInNotification bool `json:"enable_plain_ip_in_notification,omitempty" validate:"optional"`
|
EnablePlainIPInNotification bool `json:"enable_plain_ip_in_notification,omitempty" validate:"optional"`
|
||||||
|
ExpiryNotificationGroupID uint64 `json:"expiry_notification_group_id,omitempty" validate:"optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
|
|||||||
+255
-1
@@ -9,13 +9,192 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nezhahq/nezha/model"
|
"github.com/nezhahq/nezha/model"
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
|
|
||||||
|
whois "github.com/likexian/whois"
|
||||||
|
whoisparser "github.com/likexian/whois-parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// SyncDomainPrice 从 哪煮米(nazhumi.com) 获取域名续费价格
|
||||||
|
func SyncDomainPrice(billing *model.BillingDataMod, domainName string) {
|
||||||
|
// 获取 TLD
|
||||||
|
parts := strings.Split(domainName, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tld := parts[len(parts)-1]
|
||||||
|
|
||||||
|
// 匹配注册商代码 (简单启示式匹配)
|
||||||
|
registrarCode := ""
|
||||||
|
regNameLower := strings.ToLower(billing.Registrar)
|
||||||
|
|
||||||
|
// 这里可以扩展更多的映射关系
|
||||||
|
mapping := map[string]string{
|
||||||
|
"aliyun": "aliyun", "tencent": "tencent", "cloudflare": "cloudflare",
|
||||||
|
"namesilo": "namesilo", "porkbun": "porkbun", "dynadot": "dynadot",
|
||||||
|
"google": "google", "namecheap": "namecheap", "godaddy": "godaddy",
|
||||||
|
"spaceship": "spaceship", "huawei": "huawei", "baidu": "baidu",
|
||||||
|
"volcengine": "volcengine", "juming": "juming", "quyu": "quyu",
|
||||||
|
"west": "west", "xinnet": "xinnet", "ename": "ename",
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, code := range mapping {
|
||||||
|
if strings.Contains(regNameLower, key) {
|
||||||
|
registrarCode = code
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if registrarCode == "" {
|
||||||
|
// 备选方案:去除常用后缀和空格
|
||||||
|
registrarCode = strings.ReplaceAll(regNameLower, " ", "")
|
||||||
|
registrarCode = strings.ReplaceAll(registrarCode, "inc", "")
|
||||||
|
registrarCode = strings.ReplaceAll(registrarCode, "llc", "")
|
||||||
|
registrarCode = strings.ReplaceAll(registrarCode, ".", "")
|
||||||
|
registrarCode = strings.ReplaceAll(registrarCode, ",", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := fmt.Sprintf("https://www.nazhumi.com/api/v1?registrar=%s&domain=%s", registrarCode, tld)
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Get(apiURL)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var results []struct {
|
||||||
|
Renew interface{} `json:"renew"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil || len(results) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换价格
|
||||||
|
res := results[0]
|
||||||
|
priceStr := ""
|
||||||
|
switch v := res.Renew.(type) {
|
||||||
|
case float64:
|
||||||
|
priceStr = fmt.Sprintf("%.2f", v)
|
||||||
|
case string:
|
||||||
|
if v != "n/a" {
|
||||||
|
priceStr = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if priceStr != "" {
|
||||||
|
billing.RenewalPrice = fmt.Sprintf("%s %s", priceStr, res.Currency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDAPResponse 简化的 RDAP 响应结构
|
||||||
|
type RDAPResponse struct {
|
||||||
|
Events []struct {
|
||||||
|
EventAction string `json:"eventAction"`
|
||||||
|
EventDate string `json:"eventDate"`
|
||||||
|
} `json:"events"`
|
||||||
|
Entities []struct {
|
||||||
|
Roles []string `json:"roles"`
|
||||||
|
VcardArray []interface{} `json:"vcardArray"`
|
||||||
|
} `json:"entities"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncDomainWHOIS 使用 RDAP (主要) 和 Whois (备用) 同步域名信息
|
||||||
|
func SyncDomainWHOIS(d *model.Domain) error {
|
||||||
|
var billing model.BillingDataMod
|
||||||
|
if d.BillingData != nil && len(d.BillingData) > 0 {
|
||||||
|
json.Unmarshal(d.BillingData, &billing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 尝试使用官方 RDAP 协议 (JSON格式,更可靠,无需解析正则)
|
||||||
|
rdapSuccess := false
|
||||||
|
apiURL := fmt.Sprintf("https://rdap.org/domain/%s", d.Domain)
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Get(apiURL)
|
||||||
|
if err == nil && resp.StatusCode == http.StatusOK {
|
||||||
|
var rdap RDAPResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&rdap); err == nil {
|
||||||
|
rdapSuccess = true
|
||||||
|
for _, event := range rdap.Events {
|
||||||
|
switch event.EventAction {
|
||||||
|
case "expiration":
|
||||||
|
billing.EndDate = event.EventDate
|
||||||
|
case "registration":
|
||||||
|
billing.RegisteredDate = event.EventDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 提取注册商
|
||||||
|
for _, entity := range rdap.Entities {
|
||||||
|
isRegistrar := false
|
||||||
|
for _, role := range entity.Roles {
|
||||||
|
if role == "registrar" {
|
||||||
|
isRegistrar = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isRegistrar && len(entity.VcardArray) > 1 {
|
||||||
|
if vcard, ok := entity.VcardArray[1].([]interface{}); ok {
|
||||||
|
for _, field := range vcard {
|
||||||
|
if f, ok := field.([]interface{}); ok && len(f) > 3 {
|
||||||
|
if f[0] == "fn" {
|
||||||
|
billing.Registrar = fmt.Sprint(f[3])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 如果 RDAP 失败,回退到传统的 Whois 查询
|
||||||
|
if !rdapSuccess {
|
||||||
|
raw, err := whois.Whois(d.Domain)
|
||||||
|
if err == nil {
|
||||||
|
result, err := whoisparser.Parse(raw)
|
||||||
|
if err == nil {
|
||||||
|
if result.Registrar.Name != "" {
|
||||||
|
billing.Registrar = result.Registrar.Name
|
||||||
|
}
|
||||||
|
if result.Domain.ExpirationDate != "" {
|
||||||
|
billing.EndDate = result.Domain.ExpirationDate
|
||||||
|
}
|
||||||
|
if result.Domain.CreatedDate != "" {
|
||||||
|
billing.RegisteredDate = result.Domain.CreatedDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 补充价格同步
|
||||||
|
SyncDomainPrice(&billing, d.Domain)
|
||||||
|
|
||||||
|
newBillingData, err := json.Marshal(billing)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.BillingData = newBillingData
|
||||||
|
saveErr := DB.Save(d).Error
|
||||||
|
if saveErr != nil {
|
||||||
|
return fmt.Errorf("数据库保存失败: %w", saveErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rdapSuccess && billing.EndDate == "" {
|
||||||
|
return fmt.Errorf("RDAP 和 Whois 同步均失败,请检查网络或手动输入")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetDomains 获取所有域名记录
|
// GetDomains 获取所有域名记录
|
||||||
func GetDomains(scope string) ([]model.Domain, error) {
|
func GetDomains(scope string) ([]model.Domain, error) {
|
||||||
var domains []model.Domain
|
var domains []model.Domain
|
||||||
@@ -81,13 +260,23 @@ func VerifyDomain(id uint64) (bool, error) {
|
|||||||
return false, fmt.Errorf("DNS查询失败: %w", err)
|
return false, fmt.Errorf("DNS查询失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
for _, record := range txtRecords {
|
for _, record := range txtRecords {
|
||||||
if record == domain.VerifyToken {
|
if record == domain.VerifyToken {
|
||||||
domain.Status = "verified"
|
domain.Status = "verified"
|
||||||
return true, DB.Save(domain).Error
|
found = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
// 自动同步 Whois 信息
|
||||||
|
if err := SyncDomainWHOIS(domain); err != nil {
|
||||||
|
log.Printf("NEZHA>> 域名 %s 验证成功但 Whois 同步失败: %v", domain.Domain, err)
|
||||||
|
}
|
||||||
|
return true, DB.Save(domain).Error
|
||||||
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +347,22 @@ func CronJobForDomainStatus() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
daysLeft := int(endDate.Sub(now).Hours() / 24)
|
||||||
|
|
||||||
|
// 只有在到期前一定天数通知,且避开重复通知 (简单逻辑:每天通知一次)
|
||||||
|
if Conf.ExpiryNotificationGroupID != 0 {
|
||||||
|
msg := ""
|
||||||
|
switch daysLeft + 1 {
|
||||||
|
case 60, 30, 15, 7, 3, 1:
|
||||||
|
msg = fmt.Sprintf("域名 [%s] 即将到期,剩余 %d 天。到期时间: %s", d.Domain, daysLeft+1, endDate.Format("2006-01-02"))
|
||||||
|
case 0:
|
||||||
|
msg = fmt.Sprintf("域名 [%s] 已到期!到期时间: %s", d.Domain, endDate.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
if msg != "" {
|
||||||
|
NotificationShared.SendNotification(Conf.ExpiryNotificationGroupID, msg, fmt.Sprintf("expiry-domain-%d-%d", d.ID, daysLeft))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if now.After(endDate) {
|
if now.After(endDate) {
|
||||||
if billing.AutoRenewal == "1" {
|
if billing.AutoRenewal == "1" {
|
||||||
var newEndDate time.Time
|
var newEndDate time.Time
|
||||||
@@ -192,3 +397,52 @@ func CronJobForDomainStatus() {
|
|||||||
}
|
}
|
||||||
log.Println("NEZHA>> Cron::域名状态检查任务执行完毕")
|
log.Println("NEZHA>> Cron::域名状态检查任务执行完毕")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CronJobForServerStatus 检查服务器/VPS 到期通知
|
||||||
|
func CronJobForServerStatus() {
|
||||||
|
log.Println("NEZHA>> Cron::开始执行服务器到期检查任务")
|
||||||
|
var servers []model.Server
|
||||||
|
if err := DB.Find(&servers).Error; err != nil {
|
||||||
|
log.Printf("NEZHA>> Cron::Error fetching servers: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for i := range servers {
|
||||||
|
s := servers[i]
|
||||||
|
if s.BillingData == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var billing model.BillingDataMod
|
||||||
|
if err := json.Unmarshal(s.BillingData, &billing); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if billing.EndDate == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
endDate, err := time.Parse(time.RFC3339, billing.EndDate)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
daysLeft := int(endDate.Sub(now).Hours() / 24)
|
||||||
|
|
||||||
|
if Conf.ExpiryNotificationGroupID != 0 {
|
||||||
|
msg := ""
|
||||||
|
switch daysLeft + 1 {
|
||||||
|
case 30, 15, 7, 3, 1:
|
||||||
|
msg = fmt.Sprintf("VPS [%s] 即将到期,剩余 %d 天。到期时间: %s", s.Name, daysLeft+1, endDate.Format("2006-01-02"))
|
||||||
|
case 0:
|
||||||
|
msg = fmt.Sprintf("VPS [%s] 已到期!到期时间: %s", s.Name, endDate.Format("2006-01-02"))
|
||||||
|
}
|
||||||
|
if msg != "" {
|
||||||
|
NotificationShared.SendNotification(Conf.ExpiryNotificationGroupID, msg, fmt.Sprintf("expiry-server-%d-%d", s.ID, daysLeft))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Println("NEZHA>> Cron::服务器到期检查任务执行完毕")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user