mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-22 12:48:14 -05:00
ddns: store configuation in database (#435)
* ddns: store configuation in database Co-authored-by: nap0o <144927971+nap0o@users.noreply.github.com> * 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 <144927971+nap0o@users.noreply.github.com>
This commit is contained in:
parent
0b7f43b149
commit
a503f0cf40
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -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: |
|
||||||
|
@ -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,79 @@ 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
|
||||||
|
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.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
|
||||||
|
@ -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
go.mod
7
go.mod
@ -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/libdns/tencentcloud v1.0.0
|
||||||
|
github.com/miekg/dns v1.1.62
|
||||||
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
|
||||||
@ -71,6 +75,7 @@ require (
|
|||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
@ -79,8 +84,10 @@ require (
|
|||||||
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
|
||||||
|
16
go.sum
16
go.sum
@ -107,6 +107,12 @@ 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/libdns/tencentcloud v1.0.0 h1:u4LXnYu/lu/9P5W+MCVPeSDnwI+6w+DxYhQ1wSnQOuU=
|
||||||
|
github.com/libdns/tencentcloud v1.0.0/go.mod h1:NlCgPumzUsZWSOo1+Q/Hfh8G6TNRAaTUeWQdg6LbtUI=
|
||||||
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 +122,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=
|
||||||
@ -180,6 +188,8 @@ 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/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 h1:C0GHdLTfikLVoEzfhgPfrZ7LwlG0xiCmk6iwNKE+xs0=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||||
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=
|
||||||
@ -209,6 +219,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 +250,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=
|
||||||
|
@ -125,30 +125,6 @@ type Config struct {
|
|||||||
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
||||||
MaxTCPPingValue int32
|
MaxTCPPingValue int32
|
||||||
AvgPingCount int
|
AvgPingCount int
|
||||||
|
|
||||||
// 动态域名解析更新
|
|
||||||
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 +165,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"
|
||||||
}
|
}
|
||||||
|
98
model/ddns.go
Normal file
98
model/ddns.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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,
|
||||||
|
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
|
||||||
|
WebhookRequestBody bool
|
||||||
|
WebhookHeaders bool
|
||||||
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
125
pkg/ddns/ddns.go
125
pkg/ddns/ddns.go
@ -1,24 +1,121 @@
|
|||||||
package ddns
|
package ddns
|
||||||
|
|
||||||
import "golang.org/x/net/publicsuffix"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"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
|
||||||
|
|
||||||
|
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 (provider *Provider) UpdateDomain(ctx context.Context) {
|
||||||
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
|
provider.ctx = ctx
|
||||||
prefix = domain[:len(domain)-len(realDomain)-1]
|
for _, domain := range provider.DDNSProfile.Domains {
|
||||||
return prefix, realDomain
|
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)
|
||||||
|
|
||||||
|
var r *dns.Msg
|
||||||
|
for _, idx := range indexes {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(domain[idx:], dns.TypeSOA)
|
||||||
|
|
||||||
|
for _, server := range utils.DNSServers {
|
||||||
|
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 = domain[:len(domain)-len(zone)-1]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", fmt.Errorf("SOA record not found for domain: %s", domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecordString(isIpv4 bool) string {
|
func getRecordString(isIpv4 bool) string {
|
||||||
|
44
pkg/ddns/ddns_test.go
Normal file
44
pkg/ddns/ddns_test.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
package ddns
|
|
||||||
|
|
||||||
type ProviderDummy struct{}
|
|
||||||
|
|
||||||
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
16
pkg/ddns/dummy/dummy.go
Normal file
16
pkg/ddns/dummy/dummy.go
Normal 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
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
178
pkg/ddns/webhook/webhook.go
Normal file
178
pkg/ddns/webhook/webhook.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
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,
|
||||||
|
"\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
pkg/ddns/webhook/webhook_test.go
Normal file
116
pkg/ddns/webhook/webhook_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -22,6 +22,8 @@ func init() {
|
|||||||
SkipVerifySSL: false,
|
SkipVerifySSL: false,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
http.DefaultClient.Timeout = time.Minute * 10
|
||||||
}
|
}
|
||||||
|
|
||||||
type _httpTransport struct {
|
type _httpTransport struct {
|
||||||
|
@ -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", "[2606:4700:4700::1111]:53", "[2400:3200::1]: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
resource/l10n/en-US.toml
vendored
51
resource/l10n/en-US.toml
vendored
@ -622,20 +622,59 @@ 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"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Webhook Request Headers"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Webhook Request Body"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "Feature"
|
other = "Feature"
|
||||||
|
51
resource/l10n/es-ES.toml
vendored
51
resource/l10n/es-ES.toml
vendored
@ -622,20 +622,59 @@ 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"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Encabezados de Solicitud del Webhook"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Cuerpo de Solicitud del Webhook"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "Característica"
|
other = "Característica"
|
||||||
|
45
resource/l10n/zh-CN.toml
vendored
45
resource/l10n/zh-CN.toml
vendored
@ -622,20 +622,59 @@ 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 请求方式"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Webhook 请求头"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Webhook 请求体"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "功能"
|
other = "功能"
|
||||||
|
47
resource/l10n/zh-TW.toml
vendored
47
resource/l10n/zh-TW.toml
vendored
@ -622,20 +622,59 @@ 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請求方式"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Webhook請求頭"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Webhook請求體"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "功能"
|
other = "功能"
|
||||||
|
@ -99,7 +99,10 @@ 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"
|
||||||
) {
|
) {
|
||||||
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 +131,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 +220,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 +270,52 @@ 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);
|
||||||
|
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 +385,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 +427,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 +523,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 +569,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 +699,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) { }
|
||||||
|
});
|
||||||
|
2
resource/template/common/footer.html
vendored
2
resource/template/common/footer.html
vendored
@ -10,7 +10,7 @@
|
|||||||
<script src="https://unpkg.com/semantic-ui@2.4.0/dist/semantic.min.js"></script>
|
<script src="https://unpkg.com/semantic-ui@2.4.0/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/vue@2.6.14/dist/vue.min.js"></script>
|
<script src="https://unpkg.com/vue@2.6.14/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
resource/template/common/menu.html
vendored
1
resource/template/common/menu.html
vendored
@ -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"}}
|
||||||
|
79
resource/template/component/ddns.html
vendored
Normal file
79
resource/template/component/ddns.html
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{{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 "WebhookHeaders"}}</label>
|
||||||
|
<textarea name="WebhookHeaders" placeholder='{"User-Agent":"Nezha-Agent"}'></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookRequestBody"}}</label>
|
||||||
|
<textarea name="WebhookRequestBody" placeholder='{ "ip": #ip#, "domain": "#domain#" }'></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}}
|
31
resource/template/component/server.html
vendored
31
resource/template/component/server.html
vendored
@ -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
resource/template/dashboard-default/ddns.html
vendored
Normal file
58
resource/template/dashboard-default/ddns.html
vendored
Normal 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}}
|
@ -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>
|
||||||
|
@ -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 () {
|
||||||
|
@ -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: ""
|
|
@ -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变动通知
|
||||||
|
@ -2,73 +2,68 @@ package singleton
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"sync"
|
||||||
"slices"
|
|
||||||
|
|
||||||
|
"github.com/libdns/cloudflare"
|
||||||
|
"github.com/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()
|
||||||
|
}
|
||||||
|
|
||||||
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 GetDDNSProvidersFromProfiles(profileId []uint64, ip *ddns2.IP) ([]*ddns2.Provider, error) {
|
||||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", domainConfig.FullDomain, err)
|
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)
|
return nil, fmt.Errorf("无法找到DDNS配置 ID %d", id)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user