Skip to content

Commit

Permalink
ddns: store configuation in database (#435)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
uubulb and nap0o authored Oct 17, 2024
1 parent 0b7f43b commit a503f0c
Show file tree
Hide file tree
Showing 38 changed files with 1,252 additions and 827 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
name: Build artifacts
runs-on: ubuntu-latest
container:
image: goreleaser/goreleaser-cross:v1.21
image: goreleaser/goreleaser-cross:v1.23
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
Expand All @@ -43,7 +43,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.21.x"
go-version: "1.23.x"

- name: Build
uses: goreleaser/goreleaser-action@v6
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: "1.21.x"
go-version: "1.23.x"

- name: Unit test
run: |
Expand Down
147 changes: 123 additions & 24 deletions cmd/dashboard/controller/member_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"golang.org/x/net/idna"
"gorm.io/gorm"

"github.com/naiba/nezha/model"
Expand All @@ -38,6 +39,7 @@ func (ma *memberAPI) serve() {

mr.GET("/search-server", ma.searchServer)
mr.GET("/search-tasks", ma.searchTask)
mr.GET("/search-ddns", ma.searchDDNS)
mr.POST("/server", ma.addOrEditServer)
mr.POST("/monitor", ma.addOrEditMonitor)
mr.POST("/cron", ma.addOrEditCron)
Expand All @@ -46,6 +48,7 @@ func (ma *memberAPI) serve() {
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
mr.POST("/batch-delete-server", ma.batchDeleteServer)
mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/ddns", ma.addOrEditDDNS)
mr.POST("/nat", ma.addOrEditNAT)
mr.POST("/alert-rule", ma.addOrEditAlertRule)
mr.POST("/setting", ma.updateSetting)
Expand Down Expand Up @@ -211,6 +214,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
if err == nil {
singleton.OnDeleteNotification(id)
}
case "ddns":
err = singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id = ?", id).Error
if err == nil {
singleton.OnDDNSUpdate()
}
case "nat":
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
if err == nil {
Expand Down Expand Up @@ -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 {
ID uint64
Name string `binding:"required"`
DisplayIndex int
Secret string
Tag string
Note string
PublicNote string
HideForGuest string
EnableDDNS string
EnableIPv4 string
EnableIpv6 string
DDNSDomain string
DDNSProfile string
ID uint64
Name string `binding:"required"`
DisplayIndex int
Secret string
Tag string
Note string
PublicNote string
HideForGuest string
EnableDDNS string
DDNSProfilesRaw string
}

func (ma *memberAPI) addOrEditServer(c *gin.Context) {
Expand All @@ -330,18 +356,18 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
s.PublicNote = sf.PublicNote
s.HideForGuest = sf.HideForGuest == "on"
s.EnableDDNS = sf.EnableDDNS == "on"
s.EnableIPv4 = sf.EnableIPv4 == "on"
s.EnableIpv6 = sf.EnableIpv6 == "on"
s.DDNSDomain = sf.DDNSDomain
s.DDNSProfile = sf.DDNSProfile
if s.ID == 0 {
s.Secret, err = utils.GenerateRandomString(18)
if err == nil {
err = singleton.DB.Create(&s).Error
s.DDNSProfilesRaw = sf.DDNSProfilesRaw
err = utils.Json.Unmarshal([]byte(sf.DDNSProfilesRaw), &s.DDNSProfiles)
if err == nil {
if s.ID == 0 {
s.Secret, err = utils.GenerateRandomString(18)
if err == nil {
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 {
Expand Down Expand Up @@ -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 {
ID uint64
Name string
Expand Down
12 changes: 12 additions & 0 deletions cmd/dashboard/controller/member_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
mr.GET("/monitor", mp.monitor)
mr.GET("/cron", mp.cron)
mr.GET("/notification", mp.notification)
mr.GET("/ddns", mp.ddns)
mr.GET("/nat", mp.nat)
mr.GET("/setting", mp.setting)
mr.GET("/api", mp.api)
Expand Down Expand Up @@ -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) {
var data []model.NAT
singleton.DB.Find(&data)
Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ require (
github.com/hashicorp/go-uuid v1.0.3
github.com/jinzhu/copier v0.4.0
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/ory/graceful v0.1.3
github.com/oschwald/maxminddb-golang v1.13.1
Expand Down Expand Up @@ -71,6 +75,7 @@ require (
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast 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/pretty v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
Expand All @@ -79,8 +84,10 @@ require (
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // 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/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
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
16 changes: 14 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
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/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
Expand All @@ -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-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
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/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down Expand Up @@ -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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
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/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
Expand Down Expand Up @@ -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/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/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
Expand Down Expand Up @@ -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/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.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
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=
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=
Expand Down
27 changes: 0 additions & 27 deletions model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,30 +125,6 @@ type Config struct {
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
MaxTCPPingValue int32
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 读取配置文件并应用
Expand Down Expand Up @@ -189,9 +165,6 @@ func (c *Config) Read(path string) error {
if c.AvgPingCount == 0 {
c.AvgPingCount = 2
}
if c.DDNS.MaxRetries == 0 {
c.DDNS.MaxRetries = 3
}
if c.Oauth2.OidcScopes == "" {
c.Oauth2.OidcScopes = "openid,profile,email"
}
Expand Down
Loading

0 comments on commit a503f0c

Please sign in to comment.