mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-02-04 12:40:07 +00:00
ddns: store configuation in database (#435)
* ddns: store configuation in database Co-authored-by: nap0o <144927971+nap0o@users.noreply.github.com> * feat: split domain with soa lookup * switch to libdns interface * ddns: add unit test * ddns: skip TestSplitDomainSOA on ci network is not steady * fix error handling * fix error handling --------- Co-authored-by: nap0o <144927971+nap0o@users.noreply.github.com>
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user