diff --git a/client.go b/client.go index 72e315f..2d69701 100644 --- a/client.go +++ b/client.go @@ -46,7 +46,7 @@ func (c *client) updateRRs(ctx context.Context, zoneID string, recs []zones.Reso return nil } -func mergeRRecs(fullZone *zones.Zone, records []libdns.Record) ([]zones.ResourceRecordSet, error) { +func mergeRRecs(fullZone *zones.Zone, records []libdns.RR) ([]zones.ResourceRecordSet, error) { // pdns doesn't really have an append functionality, so we have to fake it by // fetching existing rrsets for the zone and see if any already exist. If so, // merge those with the existing data. Otherwise just add the record. @@ -72,11 +72,11 @@ func mergeRRecs(fullZone *zones.Zone, records []libdns.Record) ([]zones.Resource } // now for our additions for _, rec := range recs { - if !dupes[rec.Value] { + if !dupes[rec.Data] { rr.Records = append(rr.Records, zones.Record{ - Content: rec.Value, + Content: rec.Data, }) - dupes[rec.Value] = true + dupes[rec.Data] = true } } rrsets = append(rrsets, rr) @@ -89,7 +89,7 @@ func mergeRRecs(fullZone *zones.Zone, records []libdns.Record) ([]zones.Resource } // generate RessourceRecordSets that will delete records from zone -func cullRRecs(fullZone *zones.Zone, records []libdns.Record) []zones.ResourceRecordSet { +func cullRRecs(fullZone *zones.Zone, records []libdns.RR) []zones.ResourceRecordSet { inHash := makeLDRecHash(records) var rRSets []zones.ResourceRecordSet for _, t := range fullZone.ResourceRecordSets { @@ -109,24 +109,23 @@ func cullRRecs(fullZone *zones.Zone, records []libdns.Record) []zones.ResourceRe } // remove culls from rRSet record values -func removeRecords(rRSet zones.ResourceRecordSet, culls []libdns.Record) zones.ResourceRecordSet { +func removeRecords(rRSet zones.ResourceRecordSet, culls []libdns.RR) zones.ResourceRecordSet { deleteItem := func(item string) []zones.Record { recs := rRSet.Records for i := len(recs) - 1; i >= 0; i-- { if recs[i].Content == item { - copy(recs[i:], recs[:i+1]) - recs = recs[:len(recs)-1] + recs = append(recs[:i], recs[i+1:]...) } } return recs } for _, c := range culls { - rRSet.Records = deleteItem(c.Value) + rRSet.Records = deleteItem(c.Data) } return rRSet } -func convertLDHash(inHash map[string][]libdns.Record) []zones.ResourceRecordSet { +func convertLDHash(inHash map[string][]libdns.RR) []zones.ResourceRecordSet { var rrsets []zones.ResourceRecordSet for _, recs := range inHash { if len(recs) == 0 { @@ -141,7 +140,7 @@ func convertLDHash(inHash map[string][]libdns.Record) []zones.ResourceRecordSet } for _, rec := range recs { rr.Records = append(rr.Records, zones.Record{ - Content: rec.Value, + Content: rec.Data, }) } rrsets = append(rrsets, rr) @@ -153,9 +152,9 @@ func key(Name, Type string) string { return Name + ":" + Type } -func makeLDRecHash(records []libdns.Record) map[string][]libdns.Record { +func makeLDRecHash(records []libdns.RR) map[string][]libdns.RR { // Keep track of records grouped by name + type - inHash := make(map[string][]libdns.Record) + inHash := make(map[string][]libdns.RR) for _, r := range records { k := key(r.Name, r.Type) @@ -197,9 +196,16 @@ func (c *client) zoneID(ctx context.Context, zoneName string) (string, error) { return shortZone.ID, nil } -func convertNamesToAbsolute(zone string, records []libdns.Record) []libdns.Record { - out := make([]libdns.Record, len(records)) - copy(out, records) +func convertNamesToAbsolute(zone string, records []libdns.Record) []libdns.RR { + out := make([]libdns.RR, len(records)) + for i, r := range records { + svcb, ok := r.(libdns.ServiceBinding) + if ok { + out[i] = svcbToRr(svcb) + } else { + out[i] = r.RR() + } + } for i := range out { name := libdns.AbsoluteName(out[i].Name, zone) if !strings.HasSuffix(name, ".") { @@ -207,8 +213,89 @@ func convertNamesToAbsolute(zone string, records []libdns.Record) []libdns.Recor } out[i].Name = name if out[i].Type == "TXT" { - out[i].Value = txtsanitize.TXTSanitize(out[i].Value) + out[i].Data = txtsanitize.TXTSanitize(out[i].Data) } } return out } + +// This function is taken from libdns itself. +func svcbToRr(s libdns.ServiceBinding) libdns.RR { + var name string + var recType string + if s.Scheme == "https" || s.Scheme == "http" || s.Scheme == "wss" || s.Scheme == "ws" { + recType = "HTTPS" + name = s.Name + if s.URLSchemePort == 443 || s.URLSchemePort == 80 { + // Ok, we'll correct your mistake for you. + s.URLSchemePort = 0 + } + } else { + recType = "SVCB" + name = fmt.Sprintf("_%s.%s", s.Scheme, s.Name) + } + + if s.URLSchemePort != 0 { + name = fmt.Sprintf("_%d.%s", s.URLSchemePort, name) + } + + var params string + if s.Priority == 0 && len(s.Params) != 0 { + // The SvcParams should be empty in AliasMode, so we'll fix that for + // you. + params = "" + } else { + params = paramsToString(s.Params) + } + + return libdns.RR{ + Name: name, + TTL: s.TTL, + Type: recType, + Data: fmt.Sprintf("%d %s %s", s.Priority, s.Target, params), + } +} + +// This function is taken from libdns itself and modified to quote ECH params. +func paramsToString(params libdns.SvcParams) string { + var sb strings.Builder + for key, vals := range params { + if sb.Len() > 0 { + sb.WriteRune(' ') + } + sb.WriteString(key) + var hasVal, needsQuotes bool + if key == "ech" { + needsQuotes = true + } + for _, val := range vals { + if len(val) > 0 { + hasVal = true + } + if strings.ContainsAny(val, `" `) { + needsQuotes = true + } + if hasVal && needsQuotes { + break + } + } + if hasVal { + sb.WriteRune('=') + } + if needsQuotes { + sb.WriteRune('"') + } + for i, val := range vals { + if i > 0 { + sb.WriteRune(',') + } + val = strings.ReplaceAll(val, `"`, `\"`) + val = strings.ReplaceAll(val, `,`, `\,`) + sb.WriteString(val) + } + if needsQuotes { + sb.WriteRune('"') + } + } + return sb.String() +} diff --git a/client_test.go b/client_test.go index 44fb9bd..470f414 100644 --- a/client_test.go +++ b/client_test.go @@ -3,6 +3,7 @@ package powerdns import ( "context" "fmt" + "net/netip" "os" "os/exec" "reflect" @@ -135,10 +136,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "A", records: []libdns.Record{ - { - Name: "2", - Type: "A", - Value: "127.0.0.7", + libdns.Address{ + Name: "2", + IP: netip.MustParseAddr("127.0.0.7"), }, }, want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", @@ -150,10 +150,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "TXT", records: []libdns.Record{ - { - Name: "1", - Type: "TXT", - Value: "\"This is also some text\"", + libdns.TXT{ + Name: "1", + Text: "\"This is also some text\"", }, }, want: []string{ @@ -167,10 +166,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "TXT", records: []libdns.Record{ - { - Name: "1", - Type: "TXT", - Value: "This is some weird text that isn't quoted", + libdns.TXT{ + Name: "1", + Text: "This is some weird text that isn't quoted", }, }, want: []string{ @@ -185,10 +183,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "TXT", records: []libdns.Record{ - { - Name: "1", - Type: "TXT", - Value: `This is some weird text that "has embedded quoting"`, + libdns.TXT{ + Name: "1", + Text: `This is some weird text that "has embedded quoting"`, }, }, want: []string{`1:"This is text"`, `1:"This is also some text"`, @@ -201,10 +198,9 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "TXT", records: []libdns.Record{ - { - Name: "1", - Type: "TXT", - Value: `ç is equal to \195\167`, + libdns.TXT{ + Name: "1", + Text: `ç is equal to \195\167`, }, }, want: []string{`1:"This is text"`, `1:"This is also some text"`, @@ -219,13 +215,12 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "A", records: []libdns.Record{ - { - Name: "2", - Type: "A", - Value: "127.0.0.7", + libdns.Address{ + Name: "2", + IP: netip.MustParseAddr("127.0.0.5"), }, }, - want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", "2:127.0.0.4", "2:127.0.0.5", "2:127.0.0.6"}, + want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", "2:127.0.0.4", "2:127.0.0.6", "2:127.0.0.7"}, }, { name: "Test Append and Add Zone", @@ -233,19 +228,17 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "A", records: []libdns.Record{ - { - Name: "2", - Type: "A", - Value: "127.0.0.8", + libdns.Address{ + Name: "2", + IP: netip.MustParseAddr("127.0.0.8"), }, - { - Name: "3", - Type: "A", - Value: "127.0.0.9", + libdns.Address{ + Name: "3", + IP: netip.MustParseAddr("127.0.0.9"), }, }, want: []string{"1:127.0.0.1", "1:127.0.0.2", "1:127.0.0.3", - "2:127.0.0.4", "2:127.0.0.5", "2:127.0.0.6", "2:127.0.0.8", + "2:127.0.0.4", "2:127.0.0.6", "2:127.0.0.7", "2:127.0.0.8", "3:127.0.0.9"}, }, { @@ -254,15 +247,13 @@ func TestPDNSClient(t *testing.T) { zone: "example.org.", Type: "A", records: []libdns.Record{ - { - Name: "2", - Type: "A", - Value: "127.0.0.1", + libdns.Address{ + Name: "2", + IP: netip.MustParseAddr("127.0.0.1"), }, - { - Name: "1", - Type: "A", - Value: "127.0.0.1", + libdns.Address{ + Name: "1", + IP: netip.MustParseAddr("127.0.0.1"), }, }, want: []string{"1:127.0.0.1", "2:127.0.0.1", "3:127.0.0.9"}, @@ -294,10 +285,10 @@ func TestPDNSClient(t *testing.T) { } var have []string for _, rr := range recs { - if rr.Type != table.Type { + if rr.RR().Type != table.Type { continue } - have = append(have, fmt.Sprintf("%s:%s", rr.Name, rr.Value)) + have = append(have, fmt.Sprintf("%s:%s", rr.RR().Name, rr.RR().Data)) } sort.Strings(have) diff --git a/docker-compose.yml b/docker-compose.yml index 5d1d8f5..75e9ccd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,11 @@ -version: "3" services: - mysql: - image: mariadb:10.6 - environment: - MYSQL_ROOT_PASSWORD: immelting - MYSQL_USER: powerdns - MYSQL_PASSWORD: secret - MYSQL_DATABASE: powerdns - volumes: - - database:/var/lib/mysql - networks: - - backend powerdns: - image: psitrax/powerdns - environment: - MYSQL_HOST: mysql - MYSQL_USER: powerdns - MYSQL_PASS: secret - MYSQL_DB: powerdns + image: powerdns/pdns-auth-49 networks: - backend volumes: - - ./.docker/pdns:/etc/pdns/conf.d + - ./.docker/pdns:/etc/powerdns/pdns.d ports: - 8081:8081 -volumes: - database: {} networks: backend: {} diff --git a/example/main.go b/example/main.go index 92cdd87..af121ca 100644 --- a/example/main.go +++ b/example/main.go @@ -10,16 +10,21 @@ import ( func main() { p := &powerdns.Provider{ - ServerURL: "http://localhost", // required - ServerID: "localhost", // if left empty, defaults to localhost. - APIToken: "asdfasdfasdf", // required + ServerURL: "http://127.0.0.1:8081/", // required + ServerID: "localhost", // if left empty, defaults to localhost. + APIToken: "key", // required } - _, err := p.AppendRecords(context.Background(), "example.org.", []libdns.Record{ - { - Name: "_acme_whatever", - Type: "TXT", - Value: "123456", + _, err := p.AppendRecords(context.Background(), "cavoj.net.", []libdns.Record{ + libdns.ServiceBinding{ + Name: "pdns-test", + Scheme: "https", + TTL: 0, + Priority: 1, + Target: "cavoj.net.", + Params: libdns.SvcParams{ + "ech": []string{"asdf"}, + }, }, }) if err != nil { diff --git a/go.mod b/go.mod index 35444e0..7c4d62c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/libdns/powerdns go 1.16 require ( - github.com/libdns/libdns v0.2.2 + github.com/libdns/libdns v1.0.0-beta.1 github.com/mittwald/go-powerdns v0.6.6 ) diff --git a/go.sum b/go.sum index b13dc47..f949b10 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= -github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= -github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= -github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/mittwald/go-powerdns v0.5.2 h1:kfqr9ZNIuxOjjBaoJcOFiy/19VmKEUgfJPmObDglPJU= -github.com/mittwald/go-powerdns v0.5.2/go.mod h1:bI/sZBAWyTViDknOTp19VfDxVEnh1U7rWPx2aRKtlzg= -github.com/mittwald/go-powerdns v0.6.3 h1:t7HhWNosL+To+gJ+K4TRh1sZiFtB64ZrRlVvKPkpRGw= -github.com/mittwald/go-powerdns v0.6.3/go.mod h1:adWJ860laOgm14afg+7V0nCa5NQT37oEYe2HRhoS/CA= +github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ= +github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/mittwald/go-powerdns v0.6.6 h1:yQcuszhl98+jJgELjD5ecfxCQWoshhnArexpwrwQxLY= github.com/mittwald/go-powerdns v0.6.6/go.mod h1:adWJ860laOgm14afg+7V0nCa5NQT37oEYe2HRhoS/CA= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= diff --git a/provider.go b/provider.go index 885749d..ab96c01 100644 --- a/provider.go +++ b/provider.go @@ -19,19 +19,19 @@ type Provider struct { // ServerID is the id of the server. localhost will be used // if this is omitted. - ServerID string `json:"server_id,omitempty"` + ServerID string `json:"server_id,omitempty"` // APIToken is the auth token. - APIToken string `json:"api_token,omitempty"` + APIToken string `json:"api_token,omitempty"` // Debug - can set this to stdout or stderr to dump // debugging information about the API interaction with // powerdns. This will dump your auth token in plain text // so be careful. - Debug string `json:"debug,omitempty"` + Debug string `json:"debug,omitempty"` - mu sync.Mutex - c *client + mu sync.Mutex + c *client } // GetRecords lists all the records in the zone. @@ -47,14 +47,16 @@ func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record recs := make([]libdns.Record, 0, len(prec.ResourceRecordSets)) for _, rec := range prec.ResourceRecordSets { for _, v := range rec.Records { - recs = append(recs, libdns.Record{ - ID: prec.ID, - Type: rec.Type, - Name: libdns.RelativeName(rec.Name, zone), - Value: v.Content, - TTL: time.Second * time.Duration(rec.TTL), - Priority: 0, - }) + lrec, err := (libdns.RR{ + Type: rec.Type, + Name: libdns.RelativeName(rec.Name, zone), + Data: v.Content, + TTL: time.Second * time.Duration(rec.TTL), + }).Parse() + if err != nil { + return nil, err + } + recs = append(recs, lrec) } } return recs, nil