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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user