Compare commits

...
11 Commits
Author SHA1 Message Date
UUBulb a474b8484b ddns: replace tencentcloud implemention (#462) 2024-10-29 21:39:45 +08:00
UUBulb 67b003a27d installer: fix download service scripts (#449) 2024-10-22 13:02:54 +08:00
UUBulb 99aeda7939 installer: fix format (#448) 2024-10-22 10:25:31 +08:00
UUBulb d5744d0c73 fix installer (#442)
* fix installer

* version
2024-10-20 00:33:28 +08:00
UUBulb e7837d8eaf installer: fix some style issues (#441)
* installer: fix some style issues

* translate

* translate

* replace cf-ns
2024-10-19 22:03:00 +08:00
UUBulb aa81fc02ab fix: add custom theme key (#440) 2024-10-19 11:59:31 +08:00
UUBulb f6531a52bd ddns: remove ipv6 nameservers, support custom nameservers (#439) 2024-10-18 22:06:01 +08:00
UUBulb c58c4f866a do not check description file for theme-custom (#437)
* do not check description file for theme-custom

* deprecation note
2024-10-18 12:26:59 +08:00
UUBulb fc9f1b6bcc fix(ddns): handle second-level domain correctly (#438) 2024-10-18 12:26:43 +08:00
UUBulb be7b6e9c5e fix(ddns): add missing field WebhookRequestType (#436)
* fix(ddns): add missing field WebhookRequestType

* add missing placeholders
2024-10-17 23:35:28 +08:00
UUBulbandnap0o a503f0cf40 ddns: store configuation in database (#435)
* ddns: store configuation in database

Co-authored-by: nap0o <[email protected]>

* feat: split domain with soa lookup

* switch to libdns interface

* ddns: add unit test

* ddns: skip TestSplitDomainSOA on ci

network is not steady

* fix error handling

* fix error handling

---------

Co-authored-by: nap0o <[email protected]>
2024-10-17 21:03:03 +08:00
44 changed files with 1573 additions and 1033 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
name: Build artifacts name: Build artifacts
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: goreleaser/goreleaser-cross:v1.21 image: goreleaser/goreleaser-cross:v1.23
env: env:
GOOS: ${{ matrix.goos }} GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
@@ -43,7 +43,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.21.x" go-version: "1.23.x"
- name: Build - name: Build
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "1.21.x" go-version: "1.23.x"
- name: Unit test - name: Unit test
run: | run: |
+1 -1
View File
@@ -4,7 +4,7 @@
<br> <br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small> <small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br> <br><br>
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.19.2-brightgreen?style=for-the-badge&logo=linux"> <img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.20.2-brightgreen?style=for-the-badge&logo=linux">
<br> <br>
<br> <br>
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p> <p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
+7
View File
@@ -96,6 +96,13 @@ func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
} }
themeDir := theme.Name() themeDir := theme.Name()
if themeDir == "theme-custom" {
// for backward compatibility
// note: will remove this in future versions
ret = loadTemplates(ret, themeDir)
continue
}
if strings.HasPrefix(themeDir, "dashboard-") { if strings.HasPrefix(themeDir, "dashboard-") {
// load dashboard templates, ignore desc file // load dashboard templates, ignore desc file
ret = loadTemplates(ret, themeDir) ret = loadTemplates(ret, themeDir)
+129 -24
View File
@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"golang.org/x/net/idna"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
@@ -38,6 +39,7 @@ func (ma *memberAPI) serve() {
mr.GET("/search-server", ma.searchServer) mr.GET("/search-server", ma.searchServer)
mr.GET("/search-tasks", ma.searchTask) mr.GET("/search-tasks", ma.searchTask)
mr.GET("/search-ddns", ma.searchDDNS)
mr.POST("/server", ma.addOrEditServer) mr.POST("/server", ma.addOrEditServer)
mr.POST("/monitor", ma.addOrEditMonitor) mr.POST("/monitor", ma.addOrEditMonitor)
mr.POST("/cron", ma.addOrEditCron) mr.POST("/cron", ma.addOrEditCron)
@@ -46,6 +48,7 @@ func (ma *memberAPI) serve() {
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup) mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
mr.POST("/batch-delete-server", ma.batchDeleteServer) mr.POST("/batch-delete-server", ma.batchDeleteServer)
mr.POST("/notification", ma.addOrEditNotification) mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/ddns", ma.addOrEditDDNS)
mr.POST("/nat", ma.addOrEditNAT) mr.POST("/nat", ma.addOrEditNAT)
mr.POST("/alert-rule", ma.addOrEditAlertRule) mr.POST("/alert-rule", ma.addOrEditAlertRule)
mr.POST("/setting", ma.updateSetting) mr.POST("/setting", ma.updateSetting)
@@ -211,6 +214,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
if err == nil { if err == nil {
singleton.OnDeleteNotification(id) singleton.OnDeleteNotification(id)
} }
case "ddns":
err = singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id = ?", id).Error
if err == nil {
singleton.OnDDNSUpdate()
}
case "nat": case "nat":
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
if err == nil { if err == nil {
@@ -299,20 +307,38 @@ func (ma *memberAPI) searchTask(c *gin.Context) {
}) })
} }
func (ma *memberAPI) searchDDNS(c *gin.Context) {
var ddns []model.DDNSProfile
likeWord := "%" + c.Query("word") + "%"
singleton.DB.Select("id,name").Where("id = ? OR name LIKE ?",
c.Query("word"), likeWord).Find(&ddns)
var resp []searchResult
for i := 0; i < len(ddns); i++ {
resp = append(resp, searchResult{
Value: ddns[i].ID,
Name: ddns[i].Name,
Text: ddns[i].Name,
})
}
c.JSON(http.StatusOK, map[string]interface{}{
"success": true,
"results": resp,
})
}
type serverForm struct { type serverForm struct {
ID uint64 ID uint64
Name string `binding:"required"` Name string `binding:"required"`
DisplayIndex int DisplayIndex int
Secret string Secret string
Tag string Tag string
Note string Note string
PublicNote string PublicNote string
HideForGuest string HideForGuest string
EnableDDNS string EnableDDNS string
EnableIPv4 string DDNSProfilesRaw string
EnableIpv6 string
DDNSDomain string
DDNSProfile string
} }
func (ma *memberAPI) addOrEditServer(c *gin.Context) { func (ma *memberAPI) addOrEditServer(c *gin.Context) {
@@ -330,18 +356,18 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
s.PublicNote = sf.PublicNote s.PublicNote = sf.PublicNote
s.HideForGuest = sf.HideForGuest == "on" s.HideForGuest = sf.HideForGuest == "on"
s.EnableDDNS = sf.EnableDDNS == "on" s.EnableDDNS = sf.EnableDDNS == "on"
s.EnableIPv4 = sf.EnableIPv4 == "on" s.DDNSProfilesRaw = sf.DDNSProfilesRaw
s.EnableIpv6 = sf.EnableIpv6 == "on" err = utils.Json.Unmarshal([]byte(sf.DDNSProfilesRaw), &s.DDNSProfiles)
s.DDNSDomain = sf.DDNSDomain if err == nil {
s.DDNSProfile = sf.DDNSProfile if s.ID == 0 {
if s.ID == 0 { s.Secret, err = utils.GenerateRandomString(18)
s.Secret, err = utils.GenerateRandomString(18) if err == nil {
if err == nil { err = singleton.DB.Create(&s).Error
err = singleton.DB.Create(&s).Error }
} else {
isEdit = true
err = singleton.DB.Save(&s).Error
} }
} else {
isEdit = true
err = singleton.DB.Save(&s).Error
} }
} }
if err != nil { if err != nil {
@@ -743,6 +769,81 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
}) })
} }
type ddnsForm struct {
ID uint64
MaxRetries uint64
EnableIPv4 string
EnableIPv6 string
Name string
Provider uint8
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 { type natForm struct {
ID uint64 ID uint64
Name string Name string
@@ -911,6 +1012,7 @@ type settingForm struct {
DashboardTheme string DashboardTheme string
CustomCode string CustomCode string
CustomCodeDashboard string CustomCodeDashboard string
CustomNameservers string
ViewPassword string ViewPassword string
IgnoredIPNotification string IgnoredIPNotification string
IPChangeNotificationTag string // IP变更提醒的通知组 IPChangeNotificationTag string // IP变更提醒的通知组
@@ -977,6 +1079,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
singleton.Conf.Site.DashboardTheme = sf.DashboardTheme singleton.Conf.Site.DashboardTheme = sf.DashboardTheme
singleton.Conf.Site.CustomCode = sf.CustomCode singleton.Conf.Site.CustomCode = sf.CustomCode
singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard
singleton.Conf.DNSServers = sf.CustomNameservers
singleton.Conf.Site.ViewPassword = sf.ViewPassword singleton.Conf.Site.ViewPassword = sf.ViewPassword
singleton.Conf.Oauth2.Admin = sf.Admin singleton.Conf.Oauth2.Admin = sf.Admin
// 保证NotificationTag不为空 // 保证NotificationTag不为空
@@ -992,6 +1095,8 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
} }
// 更新系统语言 // 更新系统语言
singleton.InitLocalizer() singleton.InitLocalizer()
// 更新DNS服务器
singleton.OnNameserverUpdate()
c.JSON(http.StatusOK, model.Response{ c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK, Code: http.StatusOK,
}) })
+12
View File
@@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
mr.GET("/monitor", mp.monitor) mr.GET("/monitor", mp.monitor)
mr.GET("/cron", mp.cron) mr.GET("/cron", mp.cron)
mr.GET("/notification", mp.notification) mr.GET("/notification", mp.notification)
mr.GET("/ddns", mp.ddns)
mr.GET("/nat", mp.nat) mr.GET("/nat", mp.nat)
mr.GET("/setting", mp.setting) mr.GET("/setting", mp.setting)
mr.GET("/api", mp.api) mr.GET("/api", mp.api)
@@ -78,6 +79,17 @@ func (mp *memberPage) notification(c *gin.Context) {
})) }))
} }
func (mp *memberPage) ddns(c *gin.Context) {
var data []model.DDNSProfile
singleton.DB.Find(&data)
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/ddns", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
"DDNS": data,
"ProviderMap": model.ProviderMap,
"ProviderList": model.ProviderList,
}))
}
func (mp *memberPage) nat(c *gin.Context) { func (mp *memberPage) nat(c *gin.Context) {
var data []model.NAT var data []model.NAT
singleton.DB.Find(&data) singleton.DB.Find(&data)
+7
View File
@@ -14,6 +14,10 @@ require (
github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-uuid v1.0.3
github.com/jinzhu/copier v0.4.0 github.com/jinzhu/copier v0.4.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/libdns/cloudflare v0.1.1
github.com/libdns/libdns v0.2.2
github.com/miekg/dns v1.1.62
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff
github.com/nicksnyder/go-i18n/v2 v2.4.0 github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/ory/graceful v0.1.3 github.com/ory/graceful v0.1.3
github.com/oschwald/maxminddb-golang v1.13.1 github.com/oschwald/maxminddb-golang v1.13.1
@@ -73,14 +77,17 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.22.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
+15 -2
View File
@@ -107,6 +107,10 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -116,6 +120,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
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.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -123,6 +129,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff h1:3WDsbsg3dmsRENYLUPGPTkEcWWmTvk41i+rM91AIIbY=
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff/go.mod h1:k+cDtTbUY+UV56Aqv4Ahmc9bWmhpga9JJXZlAIPKBEc=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
@@ -180,12 +188,15 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
@@ -209,6 +220,8 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
@@ -238,8 +251,8 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
+2 -26
View File
@@ -23,6 +23,7 @@ var Themes = map[string]string{
"hotaru": "Hotaru", "hotaru": "Hotaru",
"angel-kanade": "AngelKanade", "angel-kanade": "AngelKanade",
"server-status": "ServerStatus", "server-status": "ServerStatus",
"custom": "Custom(local)",
} }
var DashboardThemes = map[string]string{ var DashboardThemes = map[string]string{
@@ -126,29 +127,7 @@ type Config struct {
MaxTCPPingValue int32 MaxTCPPingValue int32
AvgPingCount int AvgPingCount int
// 动态域名解析更新 DNSServers string
DDNS struct {
Enable bool
Provider string
AccessID string
AccessSecret string
WebhookURL string
WebhookMethod string
WebhookRequestBody string
WebhookHeaders string
MaxRetries uint32
Profiles map[string]DDNSProfile
}
}
type DDNSProfile struct {
Provider string
AccessID string
AccessSecret string
WebhookURL string
WebhookMethod string
WebhookRequestBody string
WebhookHeaders string
} }
// Read 读取配置文件并应用 // Read 读取配置文件并应用
@@ -189,9 +168,6 @@ func (c *Config) Read(path string) error {
if c.AvgPingCount == 0 { if c.AvgPingCount == 0 {
c.AvgPingCount = 2 c.AvgPingCount = 2
} }
if c.DDNS.MaxRetries == 0 {
c.DDNS.MaxRetries = 3
}
if c.Oauth2.OidcScopes == "" { if c.Oauth2.OidcScopes == "" {
c.Oauth2.OidcScopes = "openid,profile,email" c.Oauth2.OidcScopes = "openid,profile,email"
} }
+100
View File
@@ -0,0 +1,100 @@
package model
import (
"strings"
"gorm.io/gorm"
)
const (
ProviderDummy = iota
ProviderWebHook
ProviderCloudflare
ProviderTencentCloud
)
const (
_Dummy = "dummy"
_WebHook = "webhook"
_Cloudflare = "cloudflare"
_TencentCloud = "tencentcloud"
)
var ProviderMap = map[uint8]string{
ProviderDummy: _Dummy,
ProviderWebHook: _WebHook,
ProviderCloudflare: _Cloudflare,
ProviderTencentCloud: _TencentCloud,
}
var ProviderList = []DDNSProvider{
{
Name: _Dummy,
ID: ProviderDummy,
},
{
Name: _Cloudflare,
ID: ProviderCloudflare,
AccessSecret: true,
},
{
Name: _TencentCloud,
ID: ProviderTencentCloud,
AccessID: true,
AccessSecret: true,
},
// Least frequently used, always place this at the end
{
Name: _WebHook,
ID: ProviderWebHook,
AccessID: true,
AccessSecret: true,
WebhookURL: true,
WebhookMethod: true,
WebhookRequestType: true,
WebhookRequestBody: true,
WebhookHeaders: true,
},
}
type DDNSProfile struct {
Common
EnableIPv4 *bool
EnableIPv6 *bool
MaxRetries uint64
Name string
Provider uint8
AccessID string
AccessSecret string
WebhookURL string
WebhookMethod uint8
WebhookRequestType uint8
WebhookRequestBody string
WebhookHeaders string
Domains []string `gorm:"-"`
DomainsRaw string
}
func (d DDNSProfile) TableName() string {
return "ddns"
}
func (d *DDNSProfile) AfterFind(tx *gorm.DB) error {
if d.DomainsRaw != "" {
d.Domains = strings.Split(d.DomainsRaw, ",")
}
return nil
}
type DDNSProvider struct {
Name string
ID uint8
AccessID bool
AccessSecret bool
WebhookURL bool
WebhookMethod bool
WebhookRequestType bool
WebhookRequestBody bool
WebhookHeaders bool
}
+24 -14
View File
@@ -3,27 +3,28 @@ package model
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"log"
"sync" "sync"
"time" "time"
"github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/pkg/utils"
pb "github.com/naiba/nezha/proto" pb "github.com/naiba/nezha/proto"
"gorm.io/gorm"
) )
type Server struct { type Server struct {
Common Common
Name string Name string
Tag string // 分组名 Tag string // 分组名
Secret string `gorm:"uniqueIndex" json:"-"` Secret string `gorm:"uniqueIndex" json:"-"`
Note string `json:"-"` // 管理员可见备注 Note string `json:"-"` // 管理员可见备注
PublicNote string `json:"PublicNote,omitempty"` // 公开备注 PublicNote string `json:"PublicNote,omitempty"` // 公开备注
DisplayIndex int // 展示排序,越大越靠前 DisplayIndex int // 展示排序,越大越靠前
HideForGuest bool // 对游客隐藏 HideForGuest bool // 对游客隐藏
EnableDDNS bool `json:"-"` // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效 EnableDDNS bool // 启用DDNS
EnableIPv4 bool `json:"-"` // 是否启用DDNS IPv4 DDNSProfiles []uint64 `gorm:"-" json:"-"` // DDNS配置
EnableIpv6 bool `json:"-"` // 是否启用DDNS IPv6
DDNSDomain string `json:"-"` // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用 DDNSProfilesRaw string `gorm:"default:'[]';column:ddns_profiles_raw" json:"-"`
DDNSProfile string `json:"-"` // DDNS配置
Host *Host `gorm:"-"` Host *Host `gorm:"-"`
State *HostState `gorm:"-"` State *HostState `gorm:"-"`
@@ -48,6 +49,16 @@ func (s *Server) CopyFromRunningServer(old *Server) {
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot
} }
func (s *Server) AfterFind(tx *gorm.DB) error {
if s.DDNSProfilesRaw != "" {
if err := utils.Json.Unmarshal([]byte(s.DDNSProfilesRaw), &s.DDNSProfiles); err != nil {
log.Println("NEZHA>> Server.AfterFind:", err)
return nil
}
}
return nil
}
func boolToString(b bool) string { func boolToString(b bool) string {
if b { if b {
return "true" return "true"
@@ -60,8 +71,7 @@ func (s Server) MarshalForDashboard() template.JS {
tag, _ := utils.Json.Marshal(s.Tag) tag, _ := utils.Json.Marshal(s.Tag)
note, _ := utils.Json.Marshal(s.Note) note, _ := utils.Json.Marshal(s.Note)
secret, _ := utils.Json.Marshal(s.Secret) secret, _ := utils.Json.Marshal(s.Secret)
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain) ddnsProfilesRaw, _ := utils.Json.Marshal(s.DDNSProfilesRaw)
ddnsProfile, _ := utils.Json.Marshal(s.DDNSProfile)
publicNote, _ := utils.Json.Marshal(s.PublicNote) publicNote, _ := utils.Json.Marshal(s.PublicNote)
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"EnableIPv4": %s,"EnableIpv6": %s,"DDNSDomain": %s,"DDNSProfile": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), boolToString(s.EnableIPv4), boolToString(s.EnableIpv6), ddnsDomain, ddnsProfile, publicNote)) return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"DDNSProfilesRaw": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), ddnsProfilesRaw, publicNote))
} }
-190
View File
@@ -1,190 +0,0 @@
package ddns
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"github.com/naiba/nezha/pkg/utils"
)
const baseEndpoint = "https://api.cloudflare.com/client/v4/zones"
type ProviderCloudflare struct {
isIpv4 bool
domainConfig *DomainConfig
secret string
zoneId string
ipAddr string
recordId string
recordType string
}
type cfReq struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL uint32 `json:"ttl"`
Proxied bool `json:"proxied"`
}
func NewProviderCloudflare(s string) *ProviderCloudflare {
return &ProviderCloudflare{
secret: s,
}
}
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
err := provider.getZoneID()
if err != nil {
return fmt.Errorf("无法获取 zone ID: %s", err)
}
// 当IPv4和IPv6同时成功才算作成功
if provider.domainConfig.EnableIPv4 {
provider.isIpv4 = true
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv4Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
if provider.domainConfig.EnableIpv6 {
provider.isIpv4 = false
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv6Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
return nil
}
func (provider *ProviderCloudflare) addDomainRecord() error {
err := provider.findDNSRecord()
if err != nil {
if errors.Is(err, utils.ErrGjsonNotFound) {
// 添加 DNS 记录
return provider.createDNSRecord()
}
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
// 更新 DNS 记录
return provider.updateDNSRecord()
}
func (provider *ProviderCloudflare) getZoneID() error {
_, realDomain := splitDomain(provider.domainConfig.FullDomain)
zu, _ := url.Parse(baseEndpoint)
q := zu.Query()
q.Set("name", realDomain)
zu.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", zu.String(), nil)
if err != nil {
return err
}
result, err := utils.GjsonGet(body, "result.0.id")
if err != nil {
return err
}
provider.zoneId = result.String()
return nil
}
func (provider *ProviderCloudflare) findDNSRecord() error {
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
du, _ := url.Parse(de)
q := du.Query()
q.Set("name", provider.domainConfig.FullDomain)
q.Set("type", provider.recordType)
du.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", du.String(), nil)
if err != nil {
return err
}
result, err := utils.GjsonGet(body, "result.0.id")
if err != nil {
return err
}
provider.recordId = result.String()
return nil
}
func (provider *ProviderCloudflare) createDNSRecord() error {
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: provider.recordType,
Content: provider.ipAddr,
TTL: 60,
Proxied: false,
}
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("POST", de, jsonData)
return err
}
func (provider *ProviderCloudflare) updateDNSRecord() error {
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records", provider.recordId)
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: provider.recordType,
Content: provider.ipAddr,
TTL: 60,
Proxied: false,
}
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("PATCH", de, jsonData)
return err
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider *ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.secret))
req.Header.Add("Content-Type", "application/json")
resp, err := utils.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s", err.Error())
}
}(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
+126 -14
View File
@@ -1,24 +1,136 @@
package ddns package ddns
import "golang.org/x/net/publicsuffix" import (
"context"
"fmt"
"log"
"strings"
"time"
type DomainConfig struct { "github.com/libdns/libdns"
EnableIPv4 bool "github.com/miekg/dns"
EnableIpv6 bool
FullDomain string "github.com/naiba/nezha/model"
Ipv4Addr string "github.com/naiba/nezha/pkg/utils"
Ipv6Addr string )
var (
dnsTimeOut = 10 * time.Second
customDNSServers []string
)
type IP struct {
Ipv4Addr string
Ipv6Addr string
} }
type Provider interface { type Provider struct {
// UpdateDomain Return is updated ctx context.Context
UpdateDomain(*DomainConfig) error ipAddr string
recordType string
domain string
prefix string
zone string
DDNSProfile *model.DDNSProfile
IPAddrs *IP
Setter libdns.RecordSetter
} }
func splitDomain(domain string) (prefix string, realDomain string) { func InitDNSServers(s string) {
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain) if s != "" {
prefix = domain[:len(domain)-len(realDomain)-1] customDNSServers = strings.Split(s, ",")
return prefix, realDomain }
}
func (provider *Provider) UpdateDomain(ctx context.Context) {
provider.ctx = ctx
for _, domain := range provider.DDNSProfile.Domains {
for retries := 0; retries < int(provider.DDNSProfile.MaxRetries); retries++ {
provider.domain = domain
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", provider.domain, retries+1, provider.DDNSProfile.MaxRetries)
if err := provider.updateDomain(); err != nil {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", provider.domain, err)
} else {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", provider.domain)
break
}
}
}
}
func (provider *Provider) updateDomain() error {
var err error
provider.prefix, provider.zone, err = splitDomainSOA(provider.domain)
if err != nil {
return err
}
// 当IPv4和IPv6同时成功才算作成功
if *provider.DDNSProfile.EnableIPv4 {
provider.recordType = getRecordString(true)
provider.ipAddr = provider.IPAddrs.Ipv4Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
if *provider.DDNSProfile.EnableIPv6 {
provider.recordType = getRecordString(false)
provider.ipAddr = provider.IPAddrs.Ipv6Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
return nil
}
func (provider *Provider) addDomainRecord() error {
_, err := provider.Setter.SetRecords(provider.ctx, provider.zone,
[]libdns.Record{
{
Type: provider.recordType,
Name: provider.prefix,
Value: provider.ipAddr,
TTL: time.Minute,
},
})
return err
}
func splitDomainSOA(domain string) (prefix string, zone string, err error) {
c := &dns.Client{Timeout: dnsTimeOut}
domain += "."
indexes := dns.Split(domain)
servers := utils.DNSServers
if len(customDNSServers) > 0 {
servers = customDNSServers
}
var r *dns.Msg
for _, idx := range indexes {
m := new(dns.Msg)
m.SetQuestion(domain[idx:], dns.TypeSOA)
for _, server := range servers {
r, _, err = c.Exchange(m, server)
if err != nil {
return
}
if len(r.Answer) > 0 {
if soa, ok := r.Answer[0].(*dns.SOA); ok {
zone = soa.Hdr.Name
prefix = libdns.RelativeName(domain, zone)
return
}
}
}
}
return "", "", fmt.Errorf("SOA record not found for domain: %s", domain)
} }
func getRecordString(isIpv4 bool) string { func getRecordString(isIpv4 bool) string {
+49
View File
@@ -0,0 +1,49 @@
package ddns
import (
"os"
"testing"
)
type testSt struct {
domain string
zone string
prefix string
}
func TestSplitDomainSOA(t *testing.T) {
if ci := os.Getenv("CI"); ci != "" { // skip if test on CI
return
}
cases := []testSt{
{
domain: "www.example.co.uk",
zone: "example.co.uk.",
prefix: "www",
},
{
domain: "abc.example.com",
zone: "example.com.",
prefix: "abc",
},
{
domain: "example.com",
zone: "example.com.",
prefix: "",
},
}
for _, c := range cases {
prefix, zone, err := splitDomainSOA(c.domain)
if err != nil {
t.Fatalf("Error: %s", err)
}
if prefix != c.prefix {
t.Fatalf("Expected prefix %s, but got %s", c.prefix, prefix)
}
if zone != c.zone {
t.Fatalf("Expected zone %s, but got %s", c.zone, zone)
}
}
}
-7
View File
@@ -1,7 +0,0 @@
package ddns
type ProviderDummy struct{}
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) error {
return nil
}
+16
View File
@@ -0,0 +1,16 @@
package dummy
import (
"context"
"github.com/libdns/libdns"
)
// Internal use
type Provider struct {
}
func (provider *Provider) SetRecords(ctx context.Context, zone string,
recs []libdns.Record) ([]libdns.Record, error) {
return recs, nil
}
-243
View File
@@ -1,243 +0,0 @@
package ddns
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/naiba/nezha/pkg/utils"
)
const te = "https://dnspod.tencentcloudapi.com"
type ProviderTencentCloud struct {
isIpv4 bool
domainConfig *DomainConfig
recordID uint64
recordType string
secretID string
secretKey string
errCode string
ipAddr string
}
type tcReq struct {
RecordType string `json:"RecordType"`
Domain string `json:"Domain"`
RecordLine string `json:"RecordLine"`
Subdomain string `json:"Subdomain,omitempty"`
SubDomain string `json:"SubDomain,omitempty"` // As is
Value string `json:"Value,omitempty"`
TTL uint32 `json:"TTL,omitempty"`
RecordId uint64 `json:"RecordId,omitempty"`
}
func NewProviderTencentCloud(id, key string) *ProviderTencentCloud {
return &ProviderTencentCloud{
secretID: id,
secretKey: key,
}
}
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
// 当IPv4和IPv6同时成功才算作成功
var err error
if provider.domainConfig.EnableIPv4 {
provider.isIpv4 = true
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv4Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
if provider.domainConfig.EnableIpv6 {
provider.isIpv4 = false
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv6Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
return err
}
func (provider *ProviderTencentCloud) addDomainRecord() error {
err := provider.findDNSRecord()
if err != nil {
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
if provider.errCode == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
return provider.createDNSRecord()
} else if provider.errCode != "" {
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.errCode)
}
// 默认情况下更新 DNS 记录
return provider.updateDNSRecord()
}
func (provider *ProviderTencentCloud) findDNSRecord() error {
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: provider.recordType,
Domain: realDomain,
RecordLine: "默认",
Subdomain: prefix,
}
jsonData, _ := utils.Json.Marshal(data)
body, err := provider.sendRequest("DescribeRecordList", jsonData)
if err != nil {
return err
}
result, err := utils.GjsonGet(body, "Response.RecordList.0.RecordId")
if err != nil {
if errors.Is(err, utils.ErrGjsonNotFound) {
if errCode, err := utils.GjsonGet(body, "Response.Error.Code"); err == nil {
provider.errCode = errCode.String()
return nil
}
}
return err
}
provider.recordID = result.Uint()
return nil
}
func (provider *ProviderTencentCloud) createDNSRecord() error {
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: provider.recordType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: provider.ipAddr,
TTL: 600,
}
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("CreateRecord", jsonData)
return err
}
func (provider *ProviderTencentCloud) updateDNSRecord() error {
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: provider.recordType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: provider.ipAddr,
TTL: 600,
RecordId: provider.recordID,
}
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("ModifyRecord", jsonData)
return err
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
req, err := http.NewRequest("POST", te, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-TC-Version", "2021-03-23")
provider.signRequest(provider.secretID, provider.secretKey, req, action, string(data))
resp, err := utils.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
}
}(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
func (provider *ProviderTencentCloud) sha256hex(s string) string {
b := sha256.Sum256([]byte(s))
return hex.EncodeToString(b[:])
}
func (provider *ProviderTencentCloud) hmacsha256(s, key string) string {
hashed := hmac.New(sha256.New, []byte(key))
hashed.Write([]byte(s))
return string(hashed.Sum(nil))
}
func (provider *ProviderTencentCloud) WriteString(strs ...string) string {
var b strings.Builder
for _, str := range strs {
b.WriteString(str)
}
return b.String()
}
func (provider *ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
algorithm := "TC3-HMAC-SHA256"
service := "dnspod"
host := provider.WriteString(service, ".tencentcloudapi.com")
timestamp := time.Now().Unix()
timestampStr := strconv.FormatInt(timestamp, 10)
// 步骤 1:拼接规范请求串
canonicalHeaders := provider.WriteString("content-type:application/json\nhost:", host, "\nx-tc-action:", strings.ToLower(action), "\n")
signedHeaders := "content-type;host;x-tc-action"
hashedRequestPayload := provider.sha256hex(payload)
canonicalRequest := provider.WriteString("POST\n/\n\n", canonicalHeaders, "\n", signedHeaders, "\n", hashedRequestPayload)
// 步骤 2:拼接待签名字符串
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
credentialScope := provider.WriteString(date, "/", service, "/tc3_request")
hashedCanonicalRequest := provider.sha256hex(canonicalRequest)
string2sign := provider.WriteString(algorithm, "\n", timestampStr, "\n", credentialScope, "\n", hashedCanonicalRequest)
// 步骤 3:计算签名
secretDate := provider.hmacsha256(date, provider.WriteString("TC3", secretKey))
secretService := provider.hmacsha256(service, secretDate)
secretSigning := provider.hmacsha256("tc3_request", secretService)
signature := hex.EncodeToString([]byte(provider.hmacsha256(string2sign, secretSigning)))
// 步骤 4:拼接 Authorization
authorization := provider.WriteString(algorithm, " Credential=", secretId, "/", credentialScope, ", SignedHeaders=", signedHeaders, ", Signature=", signature)
r.Header.Add("Authorization", authorization)
r.Header.Set("Host", host)
r.Header.Set("X-TC-Action", action)
r.Header.Add("X-TC-Timestamp", timestampStr)
}
-110
View File
@@ -1,110 +0,0 @@
package ddns
import (
"bytes"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/naiba/nezha/pkg/utils"
)
type ProviderWebHook struct {
url string
requestMethod string
requestBody string
requestHeader string
domainConfig *DomainConfig
}
func NewProviderWebHook(s, rm, rb, rh string) *ProviderWebHook {
return &ProviderWebHook{
url: s,
requestMethod: rm,
requestBody: rb,
requestHeader: rh,
}
}
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
if provider.domainConfig.FullDomain == "" {
return fmt.Errorf("failed to update an empty domain")
}
if provider.domainConfig.EnableIPv4 && provider.domainConfig.Ipv4Addr != "" {
req, err := provider.prepareRequest(true)
if err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
}
if provider.domainConfig.EnableIpv6 && provider.domainConfig.Ipv6Addr != "" {
req, err := provider.prepareRequest(false)
if err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
}
return nil
}
func (provider *ProviderWebHook) prepareRequest(isIPv4 bool) (*http.Request, error) {
u, err := url.Parse(provider.url)
if err != nil {
return nil, fmt.Errorf("failed parsing url: %v", err)
}
// Only handle queries here
q := u.Query()
for p, vals := range q {
for n, v := range vals {
vals[n] = provider.formatWebhookString(v, isIPv4)
}
q[p] = vals
}
u.RawQuery = q.Encode()
body := provider.formatWebhookString(provider.requestBody, isIPv4)
header := provider.formatWebhookString(provider.requestHeader, isIPv4)
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.requestMethod, u.String(), bytes.NewBufferString(body))
if err != nil {
return nil, fmt.Errorf("failed creating new request: %v", err)
}
utils.SetStringHeadersToRequest(req, headers)
return req, nil
}
func (provider *ProviderWebHook) formatWebhookString(s string, isIPv4 bool) string {
var ipAddr, ipType string
if isIPv4 {
ipAddr = provider.domainConfig.Ipv4Addr
ipType = "ipv4"
} else {
ipAddr = provider.domainConfig.Ipv6Addr
ipType = "ipv6"
}
r := strings.NewReplacer(
"{ip}", ipAddr,
"{domain}", provider.domainConfig.FullDomain,
"{type}", ipType,
"\r", "",
)
result := r.Replace(strings.TrimSpace(s))
return result
}
+180
View File
@@ -0,0 +1,180 @@
package webhook
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/libdns/libdns"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/utils"
)
const (
_ = iota
methodGET
methodPOST
methodPATCH
methodDELETE
methodPUT
)
const (
_ = iota
requestTypeJSON
requestTypeForm
)
var requestTypes = map[uint8]string{
methodGET: "GET",
methodPOST: "POST",
methodPATCH: "PATCH",
methodDELETE: "DELETE",
methodPUT: "PUT",
}
// Internal use
type Provider struct {
ipAddr string
ipType string
recordType string
domain string
DDNSProfile *model.DDNSProfile
}
func (provider *Provider) SetRecords(ctx context.Context, zone string,
recs []libdns.Record) ([]libdns.Record, error) {
for _, rec := range recs {
provider.recordType = rec.Type
provider.ipType = recordToIPType(provider.recordType)
provider.ipAddr = rec.Value
provider.domain = fmt.Sprintf("%s.%s", rec.Name, strings.TrimSuffix(zone, "."))
req, err := provider.prepareRequest(ctx)
if err != nil {
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
}
}
return recs, nil
}
func (provider *Provider) prepareRequest(ctx context.Context) (*http.Request, error) {
u, err := provider.reqUrl()
if err != nil {
return nil, err
}
body, err := provider.reqBody()
if err != nil {
return nil, err
}
headers, err := utils.GjsonParseStringMap(
provider.formatWebhookString(provider.DDNSProfile.WebhookHeaders))
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, requestTypes[provider.DDNSProfile.WebhookMethod], u.String(), strings.NewReader(body))
if err != nil {
return nil, err
}
provider.setContentType(req)
for k, v := range headers {
req.Header.Set(k, v)
}
return req, nil
}
func (provider *Provider) setContentType(req *http.Request) {
if provider.DDNSProfile.WebhookMethod == methodGET {
return
}
if provider.DDNSProfile.WebhookRequestType == requestTypeForm {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
} else {
req.Header.Set("Content-Type", "application/json")
}
}
func (provider *Provider) reqUrl() (*url.URL, error) {
formattedUrl := strings.ReplaceAll(provider.DDNSProfile.WebhookURL, "#", "%23")
u, err := url.Parse(formattedUrl)
if err != nil {
return nil, err
}
// Only handle queries here
q := u.Query()
for p, vals := range q {
for n, v := range vals {
vals[n] = provider.formatWebhookString(v)
}
q[p] = vals
}
u.RawQuery = q.Encode()
return u, nil
}
func (provider *Provider) reqBody() (string, error) {
if provider.DDNSProfile.WebhookMethod == methodGET ||
provider.DDNSProfile.WebhookMethod == methodDELETE {
return "", nil
}
switch provider.DDNSProfile.WebhookRequestType {
case requestTypeJSON:
return provider.formatWebhookString(provider.DDNSProfile.WebhookRequestBody), nil
case requestTypeForm:
data, err := utils.GjsonParseStringMap(provider.DDNSProfile.WebhookRequestBody)
if err != nil {
return "", err
}
params := url.Values{}
for k, v := range data {
params.Add(k, provider.formatWebhookString(v))
}
return params.Encode(), nil
default:
return "", errors.New("request type not supported")
}
}
func (provider *Provider) formatWebhookString(s string) string {
r := strings.NewReplacer(
"#ip#", provider.ipAddr,
"#domain#", provider.domain,
"#type#", provider.ipType,
"#record#", provider.recordType,
"#access_id#", provider.DDNSProfile.AccessID,
"#access_secret#", provider.DDNSProfile.AccessSecret,
"\r", "",
)
result := r.Replace(strings.TrimSpace(s))
return result
}
func recordToIPType(record string) string {
switch record {
case "A":
return "ipv4"
case "AAAA":
return "ipv6"
default:
return ""
}
}
+116
View File
@@ -0,0 +1,116 @@
package webhook
import (
"context"
"testing"
"github.com/naiba/nezha/model"
)
var (
reqTypeForm = "application/x-www-form-urlencoded"
reqTypeJSON = "application/json"
)
type testSt struct {
profile model.DDNSProfile
expectURL string
expectBody string
expectContentType string
expectHeader map[string]string
}
func execCase(t *testing.T, item testSt) {
pw := Provider{DDNSProfile: &item.profile}
pw.ipAddr = "1.1.1.1"
pw.domain = item.profile.Domains[0]
pw.ipType = "ipv4"
pw.recordType = "A"
pw.DDNSProfile = &item.profile
reqUrl, err := pw.reqUrl()
if err != nil {
t.Fatalf("Error: %s", err)
}
if item.expectURL != reqUrl.String() {
t.Fatalf("Expected %s, but got %s", item.expectURL, reqUrl.String())
}
reqBody, err := pw.reqBody()
if err != nil {
t.Fatalf("Error: %s", err)
}
if item.expectBody != reqBody {
t.Fatalf("Expected %s, but got %s", item.expectBody, reqBody)
}
req, err := pw.prepareRequest(context.Background())
if err != nil {
t.Fatalf("Error: %s", err)
}
if item.expectContentType != req.Header.Get("Content-Type") {
t.Fatalf("Expected %s, but got %s", item.expectContentType, req.Header.Get("Content-Type"))
}
for k, v := range item.expectHeader {
if v != req.Header.Get(k) {
t.Fatalf("Expected %s, but got %s", v, req.Header.Get(k))
}
}
}
func TestWebhookRequest(t *testing.T) {
ipv4 := true
cases := []testSt{
{
profile: model.DDNSProfile{
Domains: []string{"www.example.com"},
MaxRetries: 1,
EnableIPv4: &ipv4,
WebhookURL: "http://ddns.example.com/?ip=#ip#",
WebhookMethod: methodGET,
WebhookHeaders: `{"ip":"#ip#","record":"#record#"}`,
},
expectURL: "http://ddns.example.com/?ip=1.1.1.1",
expectContentType: "",
expectHeader: map[string]string{
"ip": "1.1.1.1",
"record": "A",
},
},
{
profile: model.DDNSProfile{
Domains: []string{"www.example.com"},
MaxRetries: 1,
EnableIPv4: &ipv4,
WebhookURL: "http://ddns.example.com/api",
WebhookMethod: methodPOST,
WebhookRequestType: requestTypeJSON,
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
},
expectURL: "http://ddns.example.com/api",
expectContentType: reqTypeJSON,
expectBody: `{"ip":"1.1.1.1","record":"A"}`,
},
{
profile: model.DDNSProfile{
Domains: []string{"www.example.com"},
MaxRetries: 1,
EnableIPv4: &ipv4,
WebhookURL: "http://ddns.example.com/api",
WebhookMethod: methodPOST,
WebhookRequestType: requestTypeForm,
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
},
expectURL: "http://ddns.example.com/api",
expectContentType: reqTypeForm,
expectBody: "ip=1.1.1.1&record=A",
},
}
for _, c := range cases {
execCase(t, c)
}
}
+1
View File
@@ -16,6 +16,7 @@ var adminPage = map[string]bool{
"/monitor": true, "/monitor": true,
"/setting": true, "/setting": true,
"/notification": true, "/notification": true,
"/ddns": true,
"/nat": true, "/nat": true,
"/cron": true, "/cron": true,
"/api": true, "/api": true,
+4
View File
@@ -21,6 +21,10 @@ func GjsonGet(json []byte, path string) (gjson.Result, error) {
} }
func GjsonParseStringMap(jsonObject string) (map[string]string, error) { func GjsonParseStringMap(jsonObject string) (map[string]string, error) {
if jsonObject == "" {
return nil, nil
}
result := gjson.Parse(jsonObject) result := gjson.Parse(jsonObject)
if !result.IsObject() { if !result.IsObject() {
return nil, ErrGjsonWrongType return nil, ErrGjsonWrongType
+2
View File
@@ -22,6 +22,8 @@ func init() {
SkipVerifySSL: false, SkipVerifySSL: false,
}), }),
}) })
http.DefaultClient.Timeout = time.Minute * 10
} }
type _httpTransport struct { type _httpTransport struct {
+5 -14
View File
@@ -3,7 +3,6 @@ package utils
import ( import (
"crypto/rand" "crypto/rand"
"math/big" "math/big"
"net/http"
"os" "os"
"regexp" "regexp"
"strings" "strings"
@@ -11,7 +10,11 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
var Json = jsoniter.ConfigCompatibleWithStandardLibrary var (
Json = jsoniter.ConfigCompatibleWithStandardLibrary
DNSServers = []string{"1.1.1.1:53", "223.5.5.5:53"}
)
func IsWindows() bool { func IsWindows() bool {
return os.PathSeparator == '\\' && os.PathListSeparator == ';' return os.PathSeparator == '\\' && os.PathListSeparator == ';'
@@ -87,15 +90,3 @@ func Uint64SubInt64(a uint64, b int64) uint64 {
} }
return a - uint64(b) return a - uint64(b)
} }
func SetStringHeadersToRequest(req *http.Request, headers []string) {
if req == nil {
return
}
for _, element := range headers {
kv := strings.SplitN(element, ":", 2)
if len(kv) == 2 {
req.Header.Add(kv[0], kv[1])
}
}
}
+51 -6
View File
@@ -622,20 +622,62 @@ other = "Network"
[EnableShowInService] [EnableShowInService]
other = "Enable Show in Service" other = "Enable Show in Service"
[DDNS]
other = "Dynamic DNS"
[DDNSProfiles]
other = "DDNS Profiles"
[AddDDNSProfile]
other = "New Profile"
[EnableDDNS] [EnableDDNS]
other = "Enable DDNS" other = "Enable DDNS"
[EnableIPv4] [EnableIPv4]
other = "Enable DDNS IPv4" other = "IPv4 Enabled"
[EnableIpv6] [EnableIPv6]
other = "Enable DDNS IPv6" other = "IPv6 Enabled"
[DDNSDomain] [DDNSDomain]
other = "DDNS Domain" other = "Domains"
[DDNSProfile] [DDNSDomains]
other = "DDNS Profile Name" other = "Domains (separate with comma)"
[DDNSProvider]
other = "DDNS Provider"
[MaxRetries]
other = "Maximum retry attempts"
[DDNSAccessID]
other = "Credential 1"
[DDNSAccessSecret]
other = "Credential 2"
[DDNSTokenID]
other = "Token ID"
[DDNSTokenSecret]
other = "Token Secret"
[WebhookURL]
other = "Webhook URL"
[WebhookMethod]
other = "Webhook Request Method"
[WebhookRequestType]
other = "Webhook Request Type"
[WebhookHeaders]
other = "Webhook Request Headers"
[WebhookRequestBody]
other = "Webhook Request Body"
[Feature] [Feature]
other = "Feature" other = "Feature"
@@ -705,3 +747,6 @@ other = "Expired"
[Days] [Days]
other = "d" other = "d"
[CustomNameservers]
other = "Custom Public DNS Nameservers for DDNS (separate with comma)"
+51 -6
View File
@@ -622,20 +622,62 @@ other = "Red"
[EnableShowInService] [EnableShowInService]
other = "Mostrar en servicio" other = "Mostrar en servicio"
[DDNS]
other = "DNS Dinámico"
[DDNSProfiles]
other = "Perfiles DDNS"
[AddDDNSProfile]
other = "Nuevo Perfil"
[EnableDDNS] [EnableDDNS]
other = "Habilitar DDNS" other = "Habilitar DDNS"
[EnableIPv4] [EnableIPv4]
other = "Habilitar DDNS IPv4" other = "IPv4 Activado"
[EnableIpv6] [EnableIPv6]
other = "Habilitar DDNS IPv6" other = "IPv6 Activado"
[DDNSDomain] [DDNSDomain]
other = "Dominio DDNS" other = "Dominios"
[DDNSProfile] [DDNSDomains]
other = "Nombre del perfil de DDNS" other = "Dominios (separados por comas)"
[DDNSProvider]
other = "Proveedor DDNS"
[MaxRetries]
other = "Número máximo de intentos de reintento"
[DDNSAccessID]
other = "Credencial 1"
[DDNSAccessSecret]
other = "Credencial 2"
[DDNSTokenID]
other = "ID del Token"
[DDNSTokenSecret]
other = "Secreto del Token"
[WebhookURL]
other = "URL del Webhook"
[WebhookMethod]
other = "Método de Solicitud del Webhook"
[WebhookRequestType]
other = "Tipo de solicitud del Webhook"
[WebhookHeaders]
other = "Encabezados de Solicitud del Webhook"
[WebhookRequestBody]
other = "Cuerpo de Solicitud del Webhook"
[Feature] [Feature]
other = "Característica" other = "Característica"
@@ -705,3 +747,6 @@ other = "Expired"
[Days] [Days]
other = "d" other = "d"
[CustomNameservers]
other = "Servidores DNS públicos personalizados para DDNS (separar con coma)"
+52 -7
View File
@@ -622,20 +622,62 @@ other = "网络"
[EnableShowInService] [EnableShowInService]
other = "在服务中显示" other = "在服务中显示"
[DDNS]
other = "动态 DNS"
[DDNSProfiles]
other = "DDNS配置"
[AddDDNSProfile]
other = "新配置"
[EnableDDNS] [EnableDDNS]
other = "启用DDNS" other = "启用 DDNS"
[EnableIPv4] [EnableIPv4]
other = "启用DDNS IPv4" other = "启用 DDNS IPv4"
[EnableIpv6] [EnableIPv6]
other = "启用DDNS IPv6" other = "启用 DDNS IPv6"
[DDNSDomain] [DDNSDomain]
other = "DDNS域名" other = "DDNS 域名"
[DDNSProfile] [DDNSDomains]
other = "DDNS配置名" other = "域名(逗号分隔)"
[DDNSProvider]
other = "DDNS 供应商"
[MaxRetries]
other = "最大重试次数"
[DDNSAccessID]
other = "DDNS 凭据 1"
[DDNSAccessSecret]
other = "DDNS 凭据 2"
[DDNSTokenID]
other = "令牌 ID"
[DDNSTokenSecret]
other = "令牌 Secret"
[WebhookURL]
other = "Webhook 地址"
[WebhookMethod]
other = "Webhook 请求方式"
[WebhookRequestType]
other = "Webhook 请求类型"
[WebhookHeaders]
other = "Webhook 请求头"
[WebhookRequestBody]
other = "Webhook 请求体"
[Feature] [Feature]
other = "功能" other = "功能"
@@ -705,3 +747,6 @@ other = "已到期"
[Days] [Days]
other = "天" other = "天"
[CustomNameservers]
other = "自定义DDNS使用的公共DNS服务器(逗号分隔)"
+52 -7
View File
@@ -622,20 +622,62 @@ other = "網路"
[EnableShowInService] [EnableShowInService]
other = "在服務中顯示" other = "在服務中顯示"
[DDNS]
other = "動態 DNS"
[DDNSProfiles]
other = "DDNS配置"
[AddDDNSProfile]
other = "新增配置"
[EnableDDNS] [EnableDDNS]
other = "啟用DDNS" other = "啟用 DDNS"
[EnableIPv4] [EnableIPv4]
other = "啟用DDNS IPv4" other = "啟用 DDNS IPv4"
[EnableIpv6] [EnableIPv6]
other = "啟用DDNS IPv6" other = "啟用 DDNS IPv6"
[DDNSDomain] [DDNSDomain]
other = "DDNS網域" other = "DDNS 域名"
[DDNSProfile] [DDNSDomains]
other = "DDNS設定名" other = "域名(逗號分隔)"
[DDNSProvider]
other = "DDN S供應商"
[MaxRetries]
other = "最大重試次數"
[DDNSAccessID]
other = "DDNS 憑據 1"
[DDNSAccessSecret]
other = "DDNS 憑據 2"
[DDNSTokenID]
other = "令牌 ID"
[DDNSTokenSecret]
other = "令牌 Secret"
[WebhookURL]
other = "Webhook 地址"
[WebhookMethod]
other = "Webhook 請求方式"
[WebhookRequestType]
other = "Webhook 請求類型"
[WebhookHeaders]
other = "Webhook 請求頭"
[WebhookRequestBody]
other = "Webhook 請求體"
[Feature] [Feature]
other = "功能" other = "功能"
@@ -705,3 +747,6 @@ other = "已到期"
[Days] [Days]
other = "天" other = "天"
[CustomNameservers]
other = "自訂DDNS使用的公共DNS伺服器(逗號分隔)"
+110 -16
View File
@@ -99,7 +99,11 @@ function showFormModal(modelSelector, formID, URL, getData) {
item.name === "DisplayIndex" || item.name === "DisplayIndex" ||
item.name === "Type" || item.name === "Type" ||
item.name === "Cover" || item.name === "Cover" ||
item.name === "Duration" item.name === "Duration" ||
item.name === "MaxRetries" ||
item.name === "Provider" ||
item.name === "WebhookMethod" ||
item.name === "WebhookRequestType"
) { ) {
obj[item.name] = parseInt(item.value); obj[item.name] = parseInt(item.value);
} else if (item.name.endsWith("Latency")) { } else if (item.name.endsWith("Latency")) {
@@ -128,6 +132,16 @@ function showFormModal(modelSelector, formID, URL, getData) {
} }
} }
if (item.name.endsWith("DDNSProfilesRaw")) {
if (item.value.length > 2) {
obj[item.name] = JSON.stringify(
[...item.value.matchAll(/\d+/gm)].map((k) =>
parseInt(k[0])
)
);
}
}
return obj; return obj;
}, {}); }, {});
$.post(URL, JSON.stringify(data)) $.post(URL, JSON.stringify(data))
@@ -207,6 +221,7 @@ function addOrEditAlertRule(rule) {
); );
} }
} }
// 需要在 showFormModal 进一步拼接数组
modal modal
.find("input[name=FailTriggerTasksRaw]") .find("input[name=FailTriggerTasksRaw]")
.val(rule ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]"); .val(rule ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
@@ -256,6 +271,55 @@ function addOrEditNotification(notification) {
); );
} }
function addOrEditDDNS(ddns) {
const modal = $(".ddns.modal");
modal.children(".header").text((ddns ? LANG.Edit : LANG.Add));
modal
.find(".nezha-primary-btn.button")
.html(
ddns
? LANG.Edit + '<i class="edit icon"></i>'
: LANG.Add + '<i class="add icon"></i>'
);
modal.find("input[name=ID]").val(ddns ? ddns.ID : null);
modal.find("input[name=Name]").val(ddns ? ddns.Name : null);
modal.find("input[name=DomainsRaw]").val(ddns ? ddns.DomainsRaw : null);
modal.find("input[name=AccessID]").val(ddns ? ddns.AccessID : null);
modal.find("input[name=AccessSecret]").val(ddns ? ddns.AccessSecret : null);
modal.find("input[name=MaxRetries]").val(ddns ? ddns.MaxRetries : 3);
modal.find("input[name=WebhookURL]").val(ddns ? ddns.WebhookURL : null);
modal
.find("textarea[name=WebhookHeaders]")
.val(ddns ? ddns.WebhookHeaders : null);
modal
.find("textarea[name=WebhookRequestBody]")
.val(ddns ? ddns.WebhookRequestBody : null);
modal
.find("select[name=Provider]")
.val(ddns ? ddns.Provider : 0);
modal
.find("select[name=WebhookMethod]")
.val(ddns ? ddns.WebhookMethod : 1);
modal
.find("select[name=WebhookRequestType]")
.val(ddns ? ddns.WebhookRequestType : 1);
if (ddns && ddns.EnableIPv4) {
modal.find(".ui.enableipv4.checkbox").checkbox("set checked");
} else {
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked");
}
if (ddns && ddns.EnableIPv6) {
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
} else {
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
}
showFormModal(
".ddns.modal",
"#ddnsForm",
"/api/ddns"
);
}
function addOrEditNAT(nat) { function addOrEditNAT(nat) {
const modal = $(".nat.modal"); const modal = $(".nat.modal");
modal.children(".header").text((nat ? LANG.Edit : LANG.Add)); modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
@@ -325,8 +389,33 @@ function addOrEditServer(server, conf) {
modal.find("input[name=id]").val(server ? server.ID : null); modal.find("input[name=id]").val(server ? server.ID : null);
modal.find("input[name=name]").val(server ? server.Name : null); modal.find("input[name=name]").val(server ? server.Name : null);
modal.find("input[name=Tag]").val(server ? server.Tag : null); modal.find("input[name=Tag]").val(server ? server.Tag : null);
modal.find("input[name=DDNSDomain]").val(server ? server.DDNSDomain : null); modal.find("a.ui.label.visible").each((i, el) => {
modal.find("input[name=DDNSProfile]").val(server ? server.DDNSProfile : null); el.remove();
});
var ddns;
if (server) {
ddns = server.DDNSProfilesRaw;
let serverList;
try {
serverList = JSON.parse(ddns);
} catch (error) {
serverList = "[]";
}
const node = modal.find("i.dropdown.icon.ddnsProfiles");
for (let i = 0; i < serverList.length; i++) {
node.after(
'<a class="ui label transition visible" data-value="' +
serverList[i] +
'" style="display: inline-block !important;">ID:' +
serverList[i] +
'<i class="delete icon"></i></a>'
);
}
}
// 需要在 showFormModal 进一步拼接数组
modal
.find("input[name=DDNSProfilesRaw]")
.val(server ? "[]," + ddns.substr(1, ddns.length - 2) : "[]");
modal modal
.find("input[name=DisplayIndex]") .find("input[name=DisplayIndex]")
.val(server ? server.DisplayIndex : null); .val(server ? server.DisplayIndex : null);
@@ -342,26 +431,17 @@ function addOrEditServer(server, conf) {
modal.find(".command.field").attr("style", "display:none"); modal.find(".command.field").attr("style", "display:none");
modal.find("input[name=secret]").val(""); modal.find("input[name=secret]").val("");
} }
if (server && server.HideForGuest) {
modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
} else {
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
}
if (server && server.EnableDDNS) { if (server && server.EnableDDNS) {
modal.find(".ui.enableddns.checkbox").checkbox("set checked"); modal.find(".ui.enableddns.checkbox").checkbox("set checked");
} else { } else {
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked"); modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
} }
if (server && server.EnableIPv4) { if (server && server.HideForGuest) {
modal.find(".ui.enableipv4.checkbox").checkbox("set checked"); modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
} else { } else {
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked"); modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
}
if (server && server.EnableIpv6) {
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
} else {
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
} }
showFormModal(".server.modal", "#serverForm", "/api/server"); showFormModal(".server.modal", "#serverForm", "/api/server");
} }
@@ -447,6 +527,7 @@ function addOrEditMonitor(monitor) {
); );
} }
} }
// 需要在 showFormModal 进一步拼接数组
modal modal
.find("input[name=FailTriggerTasksRaw]") .find("input[name=FailTriggerTasksRaw]")
.val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]"); .val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
@@ -492,6 +573,7 @@ function addOrEditCron(cron) {
); );
} }
} }
// 需要在 showFormModal 进一步拼接数组
modal modal
.find("input[name=ServersRaw]") .find("input[name=ServersRaw]")
.val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]"); .val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
@@ -621,3 +703,15 @@ $(document).ready(() => {
}); });
} catch (error) { } } catch (error) { }
}); });
$(document).ready(() => {
try {
$(".ui.ddns.search.dropdown").dropdown({
clearable: true,
apiSettings: {
url: "/api/search-ddns?word={query}",
cache: false,
},
});
} catch (error) { }
});
+1 -1
View File
@@ -10,7 +10,7 @@
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script> <script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="/static/main.js?v2024927"></script> <script src="/static/main.js?v20241011"></script>
<script> <script>
(function () { (function () {
updateLang({{.LANG }}); updateLang({{.LANG }});
+1
View File
@@ -9,6 +9,7 @@
<a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a> <a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a>
<a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a> <a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a>
<a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a> <a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a>
<a class='item{{if eq .MatchedPath "/ddns"}} active{{end}}' href="/ddns"><i class="globe icon"></i>{{tr "DDNS"}}</a>
<a class='item{{if eq .MatchedPath "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a> <a class='item{{if eq .MatchedPath "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting"> <a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
<i class="settings icon"></i>{{tr "Settings"}} <i class="settings icon"></i>{{tr "Settings"}}
+86
View File
@@ -0,0 +1,86 @@
{{define "component/ddns"}}
<div class="ui tiny ddns modal transition hidden">
<div class="header">Add</div>
<div class="content">
<form id="ddnsForm" class="ui form">
<input type="hidden" name="ID">
<div class="field">
<label>{{tr "Name"}}</label>
<input type="text" name="Name">
</div>
<div class="field">
<label>{{tr "DDNSProvider"}}</label>
<select name="Provider" class="ui fluid dropdown" id="providerSelect" onchange="toggleFields()">
{{ range $provider := .ProviderList }}
<option value="{{ $provider.ID }}">
{{ $provider.Name }}
</option>
{{ end }}
</select>
</div>
<div class="field">
<label>{{tr "DDNSDomains"}}</label>
<input type="text" name="DomainsRaw" placeholder="www.example.com">
</div>
<div class="field">
<label>{{tr "DDNSAccessID"}}</label>
<input type="text" name="AccessID" placeholder="{{tr "DDNSTokenID"}}">
</div>
<div class="field">
<label>{{tr "DDNSAccessSecret"}}</label>
<input type="text" name="AccessSecret" placeholder="{{tr "DDNSTokenSecret"}}">
</div>
<div class="field">
<label>{{tr "MaxRetries"}}</label>
<input type="number" name="MaxRetries" placeholder="3">
</div>
<div class="field">
<label>{{tr "WebhookURL"}}</label>
<input type="text" name="WebhookURL" placeholder="https://ddns.example.com/?record=#record#">
</div>
<div class="field">
<label>{{tr "WebhookMethod"}}</label>
<select name="WebhookMethod" class="ui fluid dropdown">
<option value="1">GET</option>
<option value="2">POST</option>
<option value="3">PATCH</option>
<option value="4">DELETE</option>
<option value="5">PUT</option>
</select>
</div>
<div class="field">
<label>{{tr "WebhookRequestType"}}</label>
<select name="WebhookRequestType" class="ui fluid dropdown">
<option value="1">JSON</option>
<option value="2">Form</option>
</select>
</div>
<div class="field">
<label>{{tr "WebhookHeaders"}}</label>
<textarea name="WebhookHeaders" placeholder='{"User-Agent":"Nezha-Agent"}'></textarea>
</div>
<div class="field">
<label>{{tr "WebhookRequestBody"}}</label>
<textarea name="WebhookRequestBody" placeholder='{&#13;&#10; "ip": #ip#,&#13;&#10; "domain": "#domain#"&#13;&#10;}'></textarea>
</div>
<div class="field">
<div class="ui enableipv4 checkbox">
<input name="EnableIPv4" type="checkbox" tabindex="0" class="hidden">
<label>{{tr "EnableIPv4"}}</label>
</div>
</div>
<div class="field">
<div class="ui enableipv6 checkbox">
<input name="EnableIPv6" type="checkbox" tabindex="0" class="hidden">
<label>{{tr "EnableIPv6"}}</label>
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui negative button">{{tr "Cancel"}}</div>
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
</button>
</div>
</div>
{{end}}
+10 -21
View File
@@ -21,37 +21,26 @@
<input type="text" name="secret"> <input type="text" name="secret">
</div> </div>
<div class="field"> <div class="field">
<div class="ui hideforguest checkbox"> <label>{{tr "DDNSProfiles"}}</label>
<input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" /> <div class="ui fluid multiple ddns search selection dropdown">
<label>{{tr "HideForGuest"}}</label> <input type="hidden" name="DDNSProfilesRaw">
<i class="dropdown icon ddnsProfiles"></i>
<div class="default text">{{tr "EnterIdAndNameToSearch"}}</div>
<div class="menu"></div>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui enableddns checkbox"> <div class="ui enableddns checkbox">
<input name="EnableDDNS" type="checkbox" tabindex="0" /> <input name="EnableDDNS" type="checkbox" tabindex="0" class="hidden" />
<label>{{tr "EnableDDNS"}}</label> <label>{{tr "EnableDDNS"}}</label>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui enableipv4 checkbox"> <div class="ui hideforguest checkbox">
<input name="EnableIPv4" type="checkbox" tabindex="0" /> <input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" />
<label>{{tr "EnableIPv4"}}</label> <label>{{tr "HideForGuest"}}</label>
</div> </div>
</div> </div>
<div class="field">
<div class="ui enableipv6 checkbox">
<input name="EnableIpv6" type="checkbox" tabindex="0" />
<label>{{tr "EnableIpv6"}}</label>
</div>
</div>
<div class="field">
<label>{{tr "DDNSDomain"}}</label>
<input type="text" name="DDNSDomain" placeholder="{{tr "DDNSDomain"}}">
</div>
<div class="field">
<label>{{tr "DDNSProfile"}}</label>
<input type="text" name="DDNSProfile" placeholder="{{tr "DDNSProfile"}}">
</div>
<div class="field"> <div class="field">
<label>{{tr "Note"}}</label> <label>{{tr "Note"}}</label>
<textarea name="Note"></textarea> <textarea name="Note"></textarea>
+58
View File
@@ -0,0 +1,58 @@
{{define "dashboard-default/ddns"}}
{{template "common/header" .}}
{{template "common/menu" .}}
<div class="nb-container">
<div class="ui container">
<div class="ui grid">
<div class="right floated right aligned twelve wide column">
<button class="ui right labeled nezha-primary-btn icon button" onclick="addOrEditDDNS()"><i
class="add icon"></i> {{tr "AddDDNSProfile"}}
</button>
</div>
</div>
<table class="ui basic table">
<thead>
<tr>
<th>ID</th>
<th>{{tr "Name"}}</th>
<th>{{tr "EnableIPv4"}}</th>
<th>{{tr "EnableIPv6"}}</th>
<th>{{tr "DDNSProvider"}}</th>
<th>{{tr "DDNSDomain"}}</th>
<th>{{tr "MaxRetries"}}</th>
<th>{{tr "Administration"}}</th>
</tr>
</thead>
<tbody>
{{range $item := .DDNS}}
<tr>
<td>{{$item.ID}}</td>
<td>{{$item.Name}}</td>
<td>{{$item.EnableIPv4}}</td>
<td>{{$item.EnableIPv6}}</td>
<td>{{index $.ProviderMap $item.Provider}}</td>
<td>{{$item.DomainsRaw}}</td>
<td>{{$item.MaxRetries}}</td>
<td>
<div class="ui mini icon buttons">
<button class="ui button" onclick="addOrEditDDNS({{$item}})">
<i class="edit icon"></i>
</button>
<button class="ui button"
onclick="showConfirm('确定删除DDNS配置?','确认删除',deleteRequest,'/api/ddns/'+{{$item.ID}})">
<i class="trash alternate outline icon"></i>
</button>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{template "component/ddns" .}}
{{template "common/footer" .}}
<script>
$('.checkbox').checkbox()
</script>
{{end}}
+2 -2
View File
@@ -28,8 +28,8 @@
<th>{{tr "ServerGroup"}}</th> <th>{{tr "ServerGroup"}}</th>
<th>IP</th> <th>IP</th>
<th>{{tr "VersionNumber"}}</th> <th>{{tr "VersionNumber"}}</th>
<th>{{tr "HideForGuest"}}</th>
<th>{{tr "EnableDDNS"}}</th> <th>{{tr "EnableDDNS"}}</th>
<th>{{tr "HideForGuest"}}</th>
<th>{{tr "Secret"}}</th> <th>{{tr "Secret"}}</th>
<th>{{tr "OneKeyInstall"}}</th> <th>{{tr "OneKeyInstall"}}</th>
<th>{{tr "Note"}}</th> <th>{{tr "Note"}}</th>
@@ -46,8 +46,8 @@
<td>{{$server.Tag}}</td> <td>{{$server.Tag}}</td>
<td>{{$server.Host.IP}}</td> <td>{{$server.Host.IP}}</td>
<td>{{$server.Host.Version}}</td> <td>{{$server.Host.Version}}</td>
<td>{{$server.HideForGuest}}</td>
<td>{{$server.EnableDDNS}}</td> <td>{{$server.EnableDDNS}}</td>
<td>{{$server.HideForGuest}}</td>
<td> <td>
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}"> <button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
<i class="copy icon"></i> <i class="copy icon"></i>
+4
View File
@@ -53,6 +53,10 @@
<label>{{tr "PanelServerDomainAndIP"}}</label> <label>{{tr "PanelServerDomainAndIP"}}</label>
<input type="text" name="GRPCHost" placeholder="" value="{{.Conf.GRPCHost}}"> <input type="text" name="GRPCHost" placeholder="" value="{{.Conf.GRPCHost}}">
</div> </div>
<div class="field">
<label>{{tr "CustomNameservers"}}</label>
<input type="text" name="CustomNameservers" placeholder="" value="{{.Conf.DNSServers}}">
</div>
<div class="field"> <div class="field">
<label>{{tr "IPChangeAlert"}}</label> <label>{{tr "IPChangeAlert"}}</label>
</div> </div>
+1 -1
View File
@@ -13,7 +13,7 @@
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script> <script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script> <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/main.js?v20240330"></script> <script src="/static/main.js?v20241011"></script>
<script src="/static/theme-default/js/mixin.js?v20240302"></script> <script src="/static/theme-default/js/mixin.js?v20240302"></script>
<script> <script>
(function () { (function () {
-19
View File
@@ -12,22 +12,3 @@ site:
brand: "nz_site_title" brand: "nz_site_title"
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改 cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
theme: "default" theme: "default"
ddns:
enable: false
provider: "webhook" # 如需使用多配置功能,请把此项留空
accessid: ""
accesssecret: ""
webhookmethod: ""
webhookurl: ""
webhookrequestbody: ""
webhookheaders: ""
maxretries: 3
profiles:
example:
provider: ""
accessid: ""
accesssecret: ""
webhookmethod: ""
webhookurl: ""
webhookrequestbody: ""
webhookheaders: ""
+1 -1
View File
@@ -51,7 +51,7 @@ if ([string]::IsNullOrWhiteSpace($agenttag)) {
#Region判断 #Region判断
$ipapi = "" $ipapi = ""
$region = "Unknown" $region = "Unknown"
foreach ($url in ("https://dash.cloudflare.com/cdn-cgi/trace","https://cf-ns.com/cdn-cgi/trace","https://1.0.0.1/cdn-cgi/trace")) { foreach ($url in ("https://dash.cloudflare.com/cdn-cgi/trace","https://developers.cloudflare.com/cdn-cgi/trace","https://1.0.0.1/cdn-cgi/trace")) {
try { try {
$ipapi = Invoke-RestMethod -Uri $url -TimeoutSec 5 -UseBasicParsing $ipapi = Invoke-RestMethod -Uri $url -TimeoutSec 5 -UseBasicParsing
if ($ipapi -match "loc=(\w+)" ) { if ($ipapi -match "loc=(\w+)" ) {
+103 -91
View File
@@ -12,13 +12,13 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service" NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard" NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.19.2" NZ_VERSION="v0.20.2"
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
yellow='\033[0;33m' yellow='\033[0;33m'
plain='\033[0m' plain='\033[0m'
export PATH=$PATH:/usr/local/bin export PATH="$PATH:/usr/local/bin"
os_arch="" os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1' [ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
@@ -45,20 +45,28 @@ check_systemd() {
} }
err() { err() {
printf "${red}$*${plain}\n" >&2 printf "${red}%s${plain}\n" "$*" >&2
}
success() {
printf "${green}%s${plain}\n" "$*"
}
info() {
printf "${yellow}%s${plain}\n" "$*"
} }
geo_check() { geo_check() {
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://cf-ns.com/cdn-cgi/trace" api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://developers.cloudflare.com/cdn-cgi/trace"
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0" ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
set -- $api_list set -- "$api_list"
for url in $api_list; do for url in $api_list; do
text="$(curl -A "$ua" -m 10 -s $url)" text="$(curl -A "$ua" -m 10 -s "$url")"
endpoint="$(echo $text | sed -n 's/.*h=\([^ ]*\).*/\1/p')" endpoint="$(echo "$text" | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
if echo $text | grep -qw 'CN'; then if echo "$text" | grep -qw 'CN'; then
isCN=true isCN=true
break break
elif echo $url | grep -q $endpoint; then elif echo "$url" | grep -q "$endpoint"; then
break break
fi fi
done done
@@ -85,9 +93,9 @@ pre_check() {
## China_IP ## China_IP
if [ -z "$CN" ]; then if [ -z "$CN" ]; then
geo_check geo_check
if [ ! -z "$isCN" ]; then if [ -n "$isCN" ]; then
echo "根据geoip api提供的信息,当前IP可能在中国" echo "根据geoip api提供的信息,当前IP可能在中国"
printf "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3):" printf "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3)"
read -r input read -r input
case $input in case $input in
[yY][eE][sS] | [yY]) [yY][eE][sS] | [yY])
@@ -180,7 +188,9 @@ installation_check() {
select_version() { select_version() {
if [ -z "$IS_DOCKER_NEZHA" ]; then if [ -z "$IS_DOCKER_NEZHA" ]; then
printf "${yellow}请自行选择您的安装方式(如果你是安装Agent,输入哪个都是一样的):\n1. Docker\n2. 独立安装${plain}\n" info "请自行选择您的安装方式(如果你是安装Agent,输入哪个都是一样的):"
info "1. Docker"
info "2. 独立安装"
while true; do while true; do
printf "请输入选择 [1-2]" printf "请输入选择 [1-2]"
read -r option read -r option
@@ -206,7 +216,7 @@ update_script() {
curl -sL https://${GITHUB_RAW_URL}/script/install.sh -o /tmp/nezha.sh curl -sL https://${GITHUB_RAW_URL}/script/install.sh -o /tmp/nezha.sh
new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g') new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$new_version" ]; then if [ -z "$new_version" ]; then
echo "脚本获取失败,请检查本机能否链接 https://${GITHUB_RAW_URL}/script/install.sh" echo "脚本获取失败,请检查本机能否链接 https://${GITHUB_RAW_URL}/script/install.sh"
return 1 return 1
fi fi
@@ -221,7 +231,7 @@ update_script() {
} }
before_show_menu() { before_show_menu() {
echo && printf "${yellow}* 按回车返回主菜单 *${plain}" && read temp echo && info "* 按回车返回主菜单 *" && read temp
show_menu show_menu
} }
@@ -231,7 +241,7 @@ install_base() {
} }
install_arch() { install_arch() {
printf "${green}提示: ${plain} Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次\n" info "提示:Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次"
read -r -p "是否安装libselinux? [Y/n] " input read -r -p "是否安装libselinux? [Y/n] " input
case $input in case $input in
[yY][eE][sS] | [yY]) [yY][eE][sS] | [yY])
@@ -255,11 +265,11 @@ install_arch() {
} }
install_soft() { install_soft() {
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install $* selinux-policy -y) || (command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install "$@" selinux-policy -y) ||
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install $* selinux-utils -y) || (command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install "$@" selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu $* base-devel --noconfirm && install_arch) || (command -v pacman >/dev/null 2>&1 && sudo pacman -Syu "$@" base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install $* selinux-utils -y) || (command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install "$@" selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add $* -f) (command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add "$@" -f)
} }
install_dashboard() { install_dashboard() {
@@ -305,12 +315,10 @@ install_dashboard() {
install_dashboard_docker() { install_dashboard_docker() {
if [ ! "$FRESH_INSTALL" = 0 ]; then if [ ! "$FRESH_INSTALL" = 0 ]; then
command -v docker >/dev/null 2>&1 if ! command -v docker >/dev/null 2>&1; then
if [ $? != 0 ]; then
echo "正在安装 Docker" echo "正在安装 Docker"
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
curl -sL https://${Get_Docker_URL} | sudo bash -s ${Get_Docker_Argu} if ! curl -sL https://${Get_Docker_URL} | sudo bash -s "${Get_Docker_Argu}"; then
if [ $? != 0 ]; then
err "下载脚本失败,请检查本机能否连接 ${Get_Docker_URL}" err "下载脚本失败,请检查本机能否连接 ${Get_Docker_URL}"
return 0 return 0
fi fi
@@ -321,7 +329,7 @@ install_dashboard_docker() {
sudo rc-update add docker sudo rc-update add docker
sudo rc-service docker start sudo rc-service docker start
fi fi
printf "${green}Docker${plain} 安装成功\n" success "Docker 安装成功"
installation_check installation_check
fi fi
fi fi
@@ -334,13 +342,11 @@ install_dashboard_standalone() {
} }
selinux() { selinux() {
#判断当前的状态 #Check SELinux
command -v getenforce >/dev/null 2>&1 if command -v getenforce >/dev/null 2>&1; then
if [ $? -eq 0 ]; then if getenforce | grep '[Ee]nfor'; then
getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ]; then
echo "SELinux是开启状态,正在关闭!" echo "SELinux是开启状态,正在关闭!"
sudo setenforce 0 &>/dev/null sudo setenforce 0 >/dev/null 2>&1
find_key="SELINUX=" find_key="SELINUX="
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
fi fi
@@ -355,22 +361,23 @@ install_agent() {
echo "正在获取监控Agent版本号" echo "正在获取监控Agent版本号"
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then _version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}') if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g') _version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g') _version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/nezhahq/agent/releases/latest" err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1 return 1
else else
echo "当前最新版本为: ${version}" echo "当前最新版本为: ${_version}"
fi fi
# 哪吒监控文件夹 # 哪吒监控文件夹
@@ -378,12 +385,13 @@ install_agent() {
echo "正在下载监控端" echo "正在下载监控端"
if [ -z "$CN" ]; then if [ -z "$CN" ]; then
NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip" NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
else else
NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip" NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
fi fi
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1
if [ $? != 0 ]; then _cmd="wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1"
if ! eval "$_cmd"; then
err "Release 下载失败,请检查本机能否连接 ${GITHUB_URL}" err "Release 下载失败,请检查本机能否连接 ${GITHUB_URL}"
return 1 return 1
fi fi
@@ -435,14 +443,14 @@ modify_agent_config() {
fi fi
fi fi
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1 _cmd="sudo ${NZ_AGENT_PATH}/nezha-agent service install -s $nz_grpc_host:$nz_grpc_port -p $nz_client_secret $args >/dev/null 2>&1"
if [ $? -ne 0 ]; then if ! eval "$_cmd"; then
sudo ${NZ_AGENT_PATH}/nezha-agent service uninstall >/dev/null 2>&1 sudo "${NZ_AGENT_PATH}"/nezha-agent service uninstall >/dev/null 2>&1
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1 sudo "${NZ_AGENT_PATH}"/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p "$nz_client_secret" "$args" >/dev/null 2>&1
fi fi
printf "Agent配置 ${green}修改成功,请稍等重启生效${plain}\n" success "Agent配置 修改成功,请稍等重启生效"
#if [[ $# == 0 ]]; then #if [[ $# == 0 ]]; then
# before_show_menu # before_show_menu
@@ -453,10 +461,10 @@ modify_dashboard_config() {
echo "> 修改面板配置" echo "> 修改面板配置"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
if [ ! -z "$DOCKER_COMPOSE_COMMAND" ]; then if [ -n "$DOCKER_COMPOSE_COMMAND" ]; then
echo "正在下载 Docker 脚本" echo "正在下载 Docker 脚本"
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1 _cmd="wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1"
if [ $? != 0 ]; then if ! eval "$_cmd"; then
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}" err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
@@ -466,8 +474,8 @@ modify_dashboard_config() {
fi fi
fi fi
wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1 _cmd="wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1"
if [ $? != 0 ]; then if ! eval "$_cmd"; then
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}" err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
@@ -529,18 +537,22 @@ modify_dashboard_config() {
if [ "$IS_DOCKER_NEZHA" = 0 ]; then if [ "$IS_DOCKER_NEZHA" = 0 ]; then
echo "正在下载服务文件" echo "正在下载服务文件"
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1 _download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1"
else if ! eval "$_download"; then
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
sudo chmod +x $NZ_DASHBOARD_SERVICERC
if [ $? != 0 ]; then
err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}" err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
else
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1"
if ! eval "$_download"; then
err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0
fi
sudo chmod +x $NZ_DASHBOARD_SERVICERC
fi fi
fi fi
printf "面板配置 ${green}修改成功,请稍等重启生效${plain}\n" success "面板配置 修改成功,请稍等重启生效"
restart_and_update restart_and_update
@@ -553,14 +565,14 @@ restart_and_update() {
echo "> 重启并更新面板" echo "> 重启并更新面板"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
restart_and_update_docker _cmd="restart_and_update_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
restart_and_update_standalone _cmd="restart_and_update_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}哪吒监控 重启成功${plain}\n" success "哪吒监控 重启成功"
printf "默认管理面板地址:${yellow}域名:站点访问端口${plain}\n" info "默认管理面板地址:域名:站点访问端口"
else else
err "重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息" err "重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息"
fi fi
@@ -577,15 +589,22 @@ restart_and_update_docker() {
} }
restart_and_update_standalone() { restart_and_update_standalone() {
local version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g') _version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}') _version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g') _version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g') _version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi
if [ -z "$_version" ]; then
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/naiba/nezha/releases/latest"
return 1
else
echo "当前最新版本为: ${_version}"
fi fi
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
@@ -595,20 +614,13 @@ restart_and_update_standalone() {
sudo rc-service nezha-dashboard stop sudo rc-service nezha-dashboard stop
fi fi
if [ ! -n "$version" ]; then
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/naiba/nezha/releases/latest"
return 1
else
echo "当前最新版本为: ${version}"
fi
if [ -z "$CN" ]; then if [ -z "$CN" ]; then
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/$version/dashboard-linux-$os_arch.zip" NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
else else
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip" NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
fi fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip sudo wget -qO $NZ_DASHBOARD_PATH/app.zip "$NZ_DASHBOARD_URL" >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard sudo systemctl enable nezha-dashboard
@@ -623,13 +635,13 @@ start_dashboard() {
echo "> 启动面板" echo "> 启动面板"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
start_dashboard_docker _cmd="start_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
start_dashboard_standalone _cmd="start_dashboard_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}哪吒监控 启动成功${plain}\n" success "哪吒监控 启动成功"
else else
err "启动失败,请稍后查看日志信息" err "启动失败,请稍后查看日志信息"
fi fi
@@ -655,13 +667,13 @@ stop_dashboard() {
echo "> 停止面板" echo "> 停止面板"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
stop_dashboard_docker _cmd="stop_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
stop_dashboard_standalone _cmd="stop_dashboard_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}哪吒监控 停止成功${plain}\n" success "哪吒监控 停止成功"
else else
err "停止失败,请稍后查看日志信息" err "停止失败,请稍后查看日志信息"
fi fi
@@ -940,4 +952,4 @@ if [ $# -gt 0 ]; then
else else
select_version select_version
show_menu show_menu
fi fi
+124 -107
View File
@@ -12,13 +12,13 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service" NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard" NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.19.2" NZ_VERSION="v0.20.2"
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
yellow='\033[0;33m' yellow='\033[0;33m'
plain='\033[0m' plain='\033[0m'
export PATH=$PATH:/usr/local/bin export PATH="$PATH:/usr/local/bin"
os_arch="" os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1' [ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
@@ -45,20 +45,28 @@ check_systemd() {
} }
err() { err() {
printf "${red}$*${plain}\n" >&2 printf "${red}%s${plain}\n" "$*" >&2
}
success() {
printf "${green}%s${plain}\n" "$*"
}
info() {
printf "${yellow}%s${plain}\n" "$*"
} }
geo_check() { geo_check() {
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://cf-ns.com/cdn-cgi/trace" api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://developers.cloudflare.com/cdn-cgi/trace"
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0" ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
set -- $api_list set -- "$api_list"
for url in $api_list; do for url in $api_list; do
text="$(curl -A "$ua" -m 10 -s $url)" text="$(curl -A "$ua" -m 10 -s "$url")"
endpoint="$(echo $text | sed -n 's/.*h=\([^ ]*\).*/\1/p')" endpoint="$(echo "$text" | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
if echo $text | grep -qw 'CN'; then if echo "$text" | grep -qw 'CN'; then
isCN=true isCN=true
break break
elif echo $url | grep -q $endpoint; then elif echo "$url" | grep -q "$endpoint"; then
break break
fi fi
done done
@@ -85,7 +93,7 @@ pre_check() {
## China_IP ## China_IP
if [ -z "$CN" ]; then if [ -z "$CN" ]; then
geo_check geo_check
if [ ! -z "$isCN" ]; then if [ -n "$isCN" ]; then
echo "According to the information provided by various geoip api, the current IP may be in China" echo "According to the information provided by various geoip api, the current IP may be in China"
printf "Will the installation be done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3): " printf "Will the installation be done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3): "
read -r input read -r input
@@ -179,7 +187,9 @@ installation_check() {
select_version() { select_version() {
if [ -z "$IS_DOCKER_NEZHA" ]; then if [ -z "$IS_DOCKER_NEZHA" ]; then
printf "${yellow}Select your installation method(Input anything is ok if you are installing agent):\n1. Docker\n2. Standalone${plain}\n" info "Select your installation method(Input anything is ok if you are installing agent):"
info "1. Docker"
info "2. Standalone"
while true; do while true; do
printf "Please enter [1-2]: " printf "Please enter [1-2]: "
read -r option read -r option
@@ -205,7 +215,7 @@ update_script() {
curl -sL https://${GITHUB_RAW_URL}/script/install_en.sh -o /tmp/nezha.sh curl -sL https://${GITHUB_RAW_URL}/script/install_en.sh -o /tmp/nezha.sh
new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g') new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$new_version" ]; then if [ -z "$new_version" ]; then
echo "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install.sh" echo "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install.sh"
return 1 return 1
fi fi
@@ -220,7 +230,7 @@ update_script() {
} }
before_show_menu() { before_show_menu() {
echo && printf "${yellow}* Press Enter to return to the main menu *${plain}" && read temp echo && info "* Press Enter to return to the main menu *" && read temp
show_menu show_menu
} }
@@ -230,7 +240,7 @@ install_base() {
} }
install_arch() { install_arch() {
printf "${green}Info: ${plain} Archlinux needs to add nezha-agent user to install libselinux. It will be deleted automatically after installation. It is recommended to check manually\n" info "Archlinux needs to add nezha-agent user to install libselinux. It will be deleted automatically after installation. It is recommended to check manually"
read -r -p "Do you need to install libselinux? [Y/n] " input read -r -p "Do you need to install libselinux? [Y/n] " input
case $input in case $input in
[yY][eE][sS] | [yY]) [yY][eE][sS] | [yY])
@@ -254,11 +264,11 @@ install_arch() {
} }
install_soft() { install_soft() {
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install $* selinux-policy -y) || (command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install "$@" selinux-policy -y) ||
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install $* selinux-utils -y) || (command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install "$@" selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu $* base-devel --noconfirm && install_arch) || (command -v pacman >/dev/null 2>&1 && sudo pacman -Syu "$@" base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install $* selinux-utils -y) || (command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install "$@" selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add $* -f) (command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add "$@" -f)
} }
install_dashboard() { install_dashboard() {
@@ -304,12 +314,10 @@ install_dashboard() {
install_dashboard_docker() { install_dashboard_docker() {
if [ ! "$FRESH_INSTALL" = 0 ]; then if [ ! "$FRESH_INSTALL" = 0 ]; then
command -v docker >/dev/null 2>&1 if ! command -v docker >/dev/null 2>&1; then
if [ $? != 0 ]; then
echo "Installing Docker" echo "Installing Docker"
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
curl -sL https://${Get_Docker_URL} | sudo bash -s ${Get_Docker_Argu} if ! curl -sL https://${Get_Docker_URL} | sudo bash -s "${Get_Docker_Argu}"; then
if [ $? != 0 ]; then
err "Script failed to get, please check if the network can link ${Get_Docker_URL}" err "Script failed to get, please check if the network can link ${Get_Docker_URL}"
return 0 return 0
fi fi
@@ -320,7 +328,7 @@ install_dashboard_docker() {
sudo rc-update add docker sudo rc-update add docker
sudo rc-service docker start sudo rc-service docker start
fi fi
printf "${green}Docker${plain} installed successfully\n" success "Docker installed successfully"
installation_check installation_check
fi fi
fi fi
@@ -334,12 +342,10 @@ install_dashboard_standalone() {
selinux() { selinux() {
#Check SELinux #Check SELinux
command -v getenforce >/dev/null 2>&1 if command -v getenforce >/dev/null 2>&1; then
if [ $? -eq 0 ]; then if getenforce | grep '[Ee]nfor'; then
getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ]; then
echo "SELinux running, closing now!" echo "SELinux running, closing now!"
sudo setenforce 0 &>/dev/null sudo setenforce 0 >/dev/null 2>&1
find_key="SELINUX=" find_key="SELINUX="
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
fi fi
@@ -354,30 +360,37 @@ install_agent() {
echo "Obtaining Agent version number" echo "Obtaining Agent version number"
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then _version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}') if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g') _version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g') _version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
err "Fail to obtaine agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest" err "Fail to obtain agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1 return 1
else else
echo "The current latest version is: ${version}" echo "The current latest version is: ${_version}"
fi fi
# Nezha Monitoring Folder # Nezha Monitoring Folder
sudo mkdir -p $NZ_AGENT_PATH sudo mkdir -p $NZ_AGENT_PATH
echo "Downloading Agent" echo "Downloading Agent"
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip >/dev/null 2>&1 if [ -z "$CN" ]; then
if [ $? != 0 ]; then NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
else
NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
fi
_cmd="wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1"
if ! eval "$_cmd"; then
err "Fail to download agent, please check if the network can link ${GITHUB_URL}" err "Fail to download agent, please check if the network can link ${GITHUB_URL}"
return 1 return 1
fi fi
@@ -401,10 +414,10 @@ modify_agent_config() {
echo "> Modify Agent Configuration" echo "> Modify Agent Configuration"
if [ $# -lt 3 ]; then if [ $# -lt 3 ]; then
echo "Please add Agent in the admin panel first, record the secret" echo "Please add Agent in the Dashboard first, record the secret"
printf "Please enter a domain that resolves to the IP where the panel is located (no CDN): " printf "Please enter a domain that resolves to the IP where Dashboard is located (no CDN): "
read -r nz_grpc_host read -r nz_grpc_host
printf "Please enter the panel RPC port (default 5555): " printf "Please enter Dashboard RPC port (default 5555): "
read -r nz_grpc_port read -r nz_grpc_port
printf "Please enter the Agent secret: " printf "Please enter the Agent secret: "
read -r nz_client_secret read -r nz_client_secret
@@ -429,14 +442,14 @@ modify_agent_config() {
fi fi
fi fi
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1 _cmd="sudo ${NZ_AGENT_PATH}/nezha-agent service install -s $nz_grpc_host:$nz_grpc_port -p $nz_client_secret $args >/dev/null 2>&1"
if [ $? -ne 0 ]; then if ! eval "$_cmd"; then
sudo ${NZ_AGENT_PATH}/nezha-agent service uninstall >/dev/null 2>&1 sudo "${NZ_AGENT_PATH}"/nezha-agent service uninstall >/dev/null 2>&1
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1 sudo "${NZ_AGENT_PATH}"/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p "$nz_client_secret" "$args" >/dev/null 2>&1
fi fi
printf "Agent configuration ${green} modified successfully, please wait for agent self-restart to take effect${plain}\n" success "Agent configuration modified successfully, please wait for agent self-restart to take effect"
#if [[ $# == 0 ]]; then #if [[ $# == 0 ]]; then
# before_show_menu # before_show_menu
@@ -447,21 +460,21 @@ modify_dashboard_config() {
echo "> Modify Dashboard Configuration" echo "> Modify Dashboard Configuration"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
if [ ! -z "$DOCKER_COMPOSE_COMMAND" ]; then if [ -n "$DOCKER_COMPOSE_COMMAND" ]; then
echo "Download Docker Script" echo "Download Docker Script"
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1 _cmd="wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1"
if [ $? != 0 ]; then if ! eval "$_cmd"; then
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}" err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
else else
err "请手动安装 docker-composehttps://docs.docker.com/compose/install/linux/" err "Please install docker-compose manually. https://docs.docker.com/compose/install/linux/"
before_show_menu before_show_menu
fi fi
fi fi
wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1 _cmd="wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1"
if [ $? != 0 ]; then if ! eval "$_cmd"; then
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}" err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
@@ -523,18 +536,22 @@ modify_dashboard_config() {
if [ "$IS_DOCKER_NEZHA" = 0 ]; then if [ "$IS_DOCKER_NEZHA" = 0 ]; then
echo "Downloading service file" echo "Downloading service file"
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1 _download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1"
else if ! eval "$_download"; then
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
sudo chmod +x $NZ_DASHBOARD_SERVICERC
if [ $? != 0 ]; then
err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}" err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
else
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1"
if ! eval "$_download"; then
err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0
fi
sudo chmod +x $NZ_DASHBOARD_SERVICERC
fi fi
fi fi
printf "Dashboard configuration ${green} modified successfully, please wait for Dashboard self-restart to take effect${plain}\n" success "Dashboard configuration modified successfully, please wait for Dashboard self-restart to take effect"
restart_and_update restart_and_update
@@ -544,17 +561,17 @@ modify_dashboard_config() {
} }
restart_and_update() { restart_and_update() {
echo "> Restart and Update the Panel" echo "> Restart and Update Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
restart_and_update_docker _cmd="restart_and_update_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
restart_and_update_standalone _cmd="restart_and_update_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}Nezha Monitoring Restart Successful${plain}\n" success "Nezha Monitoring Restart Successful"
printf "Default panel address: ${yellow}domain:Site_access_port${plain}\n" info "Default Dashboard address: domain:site_access_port"
else else
err "The restart failed, probably because the boot time exceeded two seconds, please check the log information later" err "The restart failed, probably because the boot time exceeded two seconds, please check the log information later"
fi fi
@@ -571,22 +588,22 @@ restart_and_update_docker() {
} }
restart_and_update_standalone() { restart_and_update_standalone() {
local version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g') _version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}') _version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g') _version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g') _version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
err "Fail to obtaine agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest" err "Fail to obtain agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1 return 1
else else
echo "The current latest version is: ${version}" echo "The current latest version is: ${_version}"
fi fi
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
@@ -597,12 +614,12 @@ restart_and_update_standalone() {
fi fi
if [ -z "$CN" ]; then if [ -z "$CN" ]; then
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/$version/dashboard-linux-$os_arch.zip" NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
else else
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip" NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
fi fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip sudo wget -qO $NZ_DASHBOARD_PATH/app.zip "$NZ_DASHBOARD_URL" >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard sudo systemctl enable nezha-dashboard
@@ -614,16 +631,16 @@ restart_and_update_standalone() {
} }
start_dashboard() { start_dashboard() {
echo "> Start Panel" echo "> Start Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
start_dashboard_docker _cmd="start_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
start_dashboard_standalone _cmd="start_dashboard_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}Nezha Monitoring Start Successful${plain}\n" success "Nezha Monitoring Start Successful"
else else
err "Failed to start, please check the log message later" err "Failed to start, please check the log message later"
fi fi
@@ -646,16 +663,16 @@ start_dashboard_standalone() {
} }
stop_dashboard() { stop_dashboard() {
echo "> Stop Panel" echo "> Stop Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
stop_dashboard_docker _cmd="stop_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
stop_dashboard_standalone _cmd="stop_dashboard_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}Nezha Monitoring Stop Successful${plain}\n" success "Nezha Monitoring Stop Successful"
else else
err "Failed to stop, please check the log message later" err "Failed to stop, please check the log message later"
fi fi
@@ -678,7 +695,7 @@ stop_dashboard_standalone() {
} }
show_dashboard_log() { show_dashboard_log() {
echo "> View Panel Log" echo "> View Dashboard Log"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
show_dashboard_log_docker show_dashboard_log_docker
@@ -704,7 +721,7 @@ show_dashboard_log_standalone() {
} }
uninstall_dashboard() { uninstall_dashboard() {
echo "> Uninstall Panel" echo "> Uninstall Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
uninstall_dashboard_docker uninstall_dashboard_docker
@@ -791,13 +808,13 @@ show_usage() {
echo "Nezha Monitor Management Script Usage: " echo "Nezha Monitor Management Script Usage: "
echo "--------------------------------------------------------" echo "--------------------------------------------------------"
echo "./nezha.sh - Show Menu" echo "./nezha.sh - Show Menu"
echo "./nezha.sh install_dashboard - Install Panel" echo "./nezha.sh install_dashboard - Install Dashboard"
echo "./nezha.sh modify_dashboard_config - Modify Panel Configuration" echo "./nezha.sh modify_dashboard_config - Modify Dashboard Configuration"
echo "./nezha.sh start_dashboard - Start Panel" echo "./nezha.sh start_dashboard - Start Dashboard"
echo "./nezha.sh stop_dashboard - Stop Panel" echo "./nezha.sh stop_dashboard - Stop Dashboard"
echo "./nezha.sh restart_and_update - Restart and Update the Panel" echo "./nezha.sh restart_and_update - Restart and Update the Dashboard"
echo "./nezha.sh show_dashboard_log - View Panel Log" echo "./nezha.sh show_dashboard_log - View Dashboard Log"
echo "./nezha.sh uninstall_dashboard - Uninstall Panel" echo "./nezha.sh uninstall_dashboard - Uninstall Dashboard"
echo "--------------------------------------------------------" echo "--------------------------------------------------------"
echo "./nezha.sh install_agent - Install Agent" echo "./nezha.sh install_agent - Install Agent"
echo "./nezha.sh modify_agent_config - Modify Agent Configuration" echo "./nezha.sh modify_agent_config - Modify Agent Configuration"
@@ -812,13 +829,13 @@ show_menu() {
printf " printf "
${green}Nezha Monitor Management Script${plain} ${red}${NZ_VERSION}${plain} ${green}Nezha Monitor Management Script${plain} ${red}${NZ_VERSION}${plain}
--- https://github.com/naiba/nezha --- --- https://github.com/naiba/nezha ---
${green}1.${plain} Install Panel ${green}1.${plain} Install Dashboard
${green}2.${plain} Modify Panel Configuration ${green}2.${plain} Modify Dashbaord Configuration
${green}3.${plain} Start Panel ${green}3.${plain} Start Dashboard
${green}4.${plain} Stop Panel ${green}4.${plain} Stop Dashboard
${green}5.${plain} Restart and Update the Panel ${green}5.${plain} Restart and Update Dashboard
${green}6.${plain} View Panel Log ${green}6.${plain} View Dashboard Log
${green}7.${plain} Uninstall Panel ${green}7.${plain} Uninstall Dashboard
————————————————- ————————————————-
${green}8.${plain} Install Agent ${green}8.${plain} Install Agent
${green}9.${plain} Modify Agent Configuration ${green}9.${plain} Modify Agent Configuration
@@ -934,4 +951,4 @@ if [ $# -gt 0 ]; then
else else
select_version select_version
show_menu show_menu
fi fi
+9 -24
View File
@@ -125,7 +125,6 @@ func (s *NezhaHandler) ReportSystemState(c context.Context, r *pb.State) (*pb.Re
func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Receipt, error) { func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Receipt, error) {
var clientID uint64 var clientID uint64
var provider ddns.Provider
var err error var err error
if clientID, err = s.Auth.Check(c); err != nil { if clientID, err = s.Auth.Check(c); err != nil {
return nil, err return nil, err
@@ -135,33 +134,19 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
defer singleton.ServerLock.RUnlock() defer singleton.ServerLock.RUnlock()
// 检查并更新DDNS // 检查并更新DDNS
if singleton.Conf.DDNS.Enable && if singleton.ServerList[clientID].EnableDDNS && host.IP != "" &&
singleton.ServerList[clientID].EnableDDNS &&
host.IP != "" &&
(singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) { (singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) {
serverDomain := singleton.ServerList[clientID].DDNSDomain ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
if singleton.Conf.DDNS.Provider == "" { providers, err := singleton.GetDDNSProvidersFromProfiles(singleton.ServerList[clientID].DDNSProfiles, &ddns.IP{Ipv4Addr: ipv4, Ipv6Addr: ipv6})
provider, err = singleton.GetDDNSProviderFromProfile(singleton.ServerList[clientID].DDNSProfile) if err == nil {
} else { for _, provider := range providers {
provider, err = singleton.GetDDNSProviderFromString(singleton.Conf.DDNS.Provider) go func(provider *ddns.Provider) {
} provider.UpdateDomain(context.Background())
if err == nil && serverDomain != "" { }(provider)
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
config := &ddns.DomainConfig{
EnableIPv4: singleton.ServerList[clientID].EnableIPv4,
EnableIpv6: singleton.ServerList[clientID].EnableIpv6,
FullDomain: serverDomain,
Ipv4Addr: ipv4,
Ipv6Addr: ipv6,
} }
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
} else { } else {
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG log.Printf("NEZHA>> 获取DDNS配置时发生错误: %v", err)
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置", singleton.ServerList[clientID].DDNSProfile)
} }
} }
// 发送IP变动通知 // 发送IP变动通知
+56 -55
View File
@@ -2,73 +2,74 @@ package singleton
import ( import (
"fmt" "fmt"
"log" "sync"
"slices"
"github.com/libdns/cloudflare"
tencentcloud "github.com/nezhahq/libdns-tencentcloud"
"github.com/naiba/nezha/model"
ddns2 "github.com/naiba/nezha/pkg/ddns" ddns2 "github.com/naiba/nezha/pkg/ddns"
"github.com/naiba/nezha/pkg/ddns/dummy"
"github.com/naiba/nezha/pkg/ddns/webhook"
) )
const ( var (
ProviderWebHook = "webhook" ddnsCache map[uint64]*model.DDNSProfile
ProviderCloudflare = "cloudflare" ddnsCacheLock sync.RWMutex
ProviderTencentCloud = "tencentcloud"
) )
type ProviderFunc func(*ddns2.DomainConfig) ddns2.Provider func initDDNS() {
OnDDNSUpdate()
OnNameserverUpdate()
}
func RetryableUpdateDomain(provider ddns2.Provider, domainConfig *ddns2.DomainConfig, maxRetries int) { func OnDDNSUpdate() {
if domainConfig == nil { var ddns []*model.DDNSProfile
return DB.Find(&ddns)
ddnsCacheLock.Lock()
defer ddnsCacheLock.Unlock()
ddnsCache = make(map[uint64]*model.DDNSProfile)
for i := 0; i < len(ddns); i++ {
ddnsCache[ddns[i].ID] = ddns[i]
} }
for retries := 0; retries < maxRetries; retries++ { }
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", domainConfig.FullDomain, retries+1, maxRetries)
if err := provider.UpdateDomain(domainConfig); err != nil { func OnNameserverUpdate() {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", domainConfig.FullDomain, err) ddns2.InitDNSServers(Conf.DNSServers)
}
func GetDDNSProvidersFromProfiles(profileId []uint64, ip *ddns2.IP) ([]*ddns2.Provider, error) {
profiles := make([]*model.DDNSProfile, 0, len(profileId))
ddnsCacheLock.RLock()
for _, id := range profileId {
if profile, ok := ddnsCache[id]; ok {
profiles = append(profiles, profile)
} else { } else {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", domainConfig.FullDomain) ddnsCacheLock.RUnlock()
break return nil, fmt.Errorf("无法找到DDNS配置 ID %d", id)
} }
} }
} ddnsCacheLock.RUnlock()
// Deprecated providers := make([]*ddns2.Provider, 0, len(profiles))
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) { for _, profile := range profiles {
switch provider { provider := &ddns2.Provider{DDNSProfile: profile, IPAddrs: ip}
case ProviderWebHook: switch profile.Provider {
return ddns2.NewProviderWebHook(Conf.DDNS.WebhookURL, Conf.DDNS.WebhookMethod, Conf.DDNS.WebhookRequestBody, Conf.DDNS.WebhookHeaders), nil case model.ProviderDummy:
case ProviderCloudflare: provider.Setter = &dummy.Provider{}
return ddns2.NewProviderCloudflare(Conf.DDNS.AccessSecret), nil providers = append(providers, provider)
case ProviderTencentCloud: case model.ProviderWebHook:
return ddns2.NewProviderTencentCloud(Conf.DDNS.AccessID, Conf.DDNS.AccessSecret), nil provider.Setter = &webhook.Provider{DDNSProfile: profile}
default: providers = append(providers, provider)
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", provider) case model.ProviderCloudflare:
} provider.Setter = &cloudflare.Provider{APIToken: profile.AccessSecret}
} providers = append(providers, provider)
case model.ProviderTencentCloud:
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) { provider.Setter = &tencentcloud.Provider{SecretId: profile.AccessID, SecretKey: profile.AccessSecret}
profile, ok := Conf.DDNS.Profiles[profileName] providers = append(providers, provider)
if !ok { default:
return new(ddns2.ProviderDummy), fmt.Errorf("找到配置 %s", profileName) return nil, fmt.Errorf("无法找到配置的DDNS提供者ID %d", profile.Provider)
}
switch profile.Provider {
case ProviderWebHook:
return ddns2.NewProviderWebHook(profile.WebhookURL, profile.WebhookMethod, profile.WebhookRequestBody, profile.WebhookHeaders), nil
case ProviderCloudflare:
return ddns2.NewProviderCloudflare(profile.AccessSecret), nil
case ProviderTencentCloud:
return ddns2.NewProviderTencentCloud(profile.AccessID, profile.AccessSecret), nil
default:
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", profile.Provider)
}
}
func ValidateDDNSProvidersFromProfiles() error {
validProviders := []string{ProviderWebHook, ProviderCloudflare, ProviderTencentCloud}
for _, profile := range Conf.DDNS.Profiles {
if ok := slices.Contains(validProviders, profile.Provider); !ok {
return fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
} }
} }
return nil return providers, nil
} }
+2 -21
View File
@@ -1,7 +1,6 @@
package singleton package singleton
import ( import (
"fmt"
"log" "log"
"time" "time"
@@ -39,6 +38,7 @@ func LoadSingleton() {
loadCronTasks() // 加载定时任务 loadCronTasks() // 加载定时任务
loadAPI() loadAPI()
initNAT() initNAT()
initDDNS()
} }
// InitConfigFromPath 从给出的文件路径中加载配置 // InitConfigFromPath 从给出的文件路径中加载配置
@@ -48,25 +48,6 @@ func InitConfigFromPath(path string) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
validateConfig()
}
// validateConfig 验证配置文件有效性
func validateConfig() {
var err error
if Conf.DDNS.Provider == "" {
err = ValidateDDNSProvidersFromProfiles()
} else {
_, err = GetDDNSProviderFromString(Conf.DDNS.Provider)
}
if err != nil {
panic(err)
}
if Conf.DDNS.Enable {
if Conf.DDNS.MaxRetries < 1 || Conf.DDNS.MaxRetries > 10 {
panic(fmt.Errorf("DDNS.MaxRetries值域为[1, 10]的整数, 当前为 %d", Conf.DDNS.MaxRetries))
}
}
} }
// InitDBFromPath 从给出的文件路径中加载数据库 // InitDBFromPath 从给出的文件路径中加载数据库
@@ -84,7 +65,7 @@ func InitDBFromPath(path string) {
err = DB.AutoMigrate(model.Server{}, model.User{}, err = DB.AutoMigrate(model.Server{}, model.User{},
model.Notification{}, model.AlertRule{}, model.Monitor{}, model.Notification{}, model.AlertRule{}, model.Monitor{},
model.MonitorHistory{}, model.Cron{}, model.Transfer{}, model.MonitorHistory{}, model.Cron{}, model.Transfer{},
model.ApiToken{}, model.NAT{}) model.ApiToken{}, model.NAT{}, model.DDNSProfile{})
if err != nil { if err != nil {
panic(err) panic(err)
} }