mirror of
https://github.com/go-acme/lego
synced 2026-03-14 14:35:48 +01:00
hostinger: fix record update (#2690)
This commit is contained in:
parent
fe0a1f8668
commit
4bb17b0234
10 changed files with 124 additions and 132 deletions
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
|
|
@ -103,38 +104,16 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
|||
|
||||
ctx := context.Background()
|
||||
|
||||
recordSets, err := d.client.GetDNSRecords(ctx, dns01.UnFqdn(authZone))
|
||||
if err != nil {
|
||||
return fmt.Errorf("hostinger: get DNS records: %w", err)
|
||||
}
|
||||
|
||||
var newRecordSet []internal.RecordSet
|
||||
|
||||
var added bool
|
||||
|
||||
for _, recordSet := range recordSets {
|
||||
if recordSet.Name == subDomain && recordSet.Type == "TXT" {
|
||||
recordSet.Records = append(recordSet.Records, internal.Record{Content: info.Value})
|
||||
added = true
|
||||
}
|
||||
|
||||
newRecordSet = append(newRecordSet, recordSet)
|
||||
}
|
||||
|
||||
if !added {
|
||||
newRecordSet = append(newRecordSet, internal.RecordSet{
|
||||
request := internal.ZoneRequest{
|
||||
Overwrite: false,
|
||||
Zone: []internal.RecordSet{{
|
||||
Name: subDomain,
|
||||
Type: "TXT",
|
||||
TTL: d.config.TTL,
|
||||
Records: []internal.Record{
|
||||
{Content: info.Value},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
request := internal.ZoneRequest{
|
||||
Overwrite: false,
|
||||
Zone: newRecordSet,
|
||||
}},
|
||||
}
|
||||
|
||||
err = d.client.UpdateDNSRecords(ctx, dns01.UnFqdn(authZone), request)
|
||||
|
|
@ -161,45 +140,45 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
|||
|
||||
ctx := context.Background()
|
||||
|
||||
recordSets, err := d.client.GetDNSRecords(ctx, dns01.UnFqdn(authZone))
|
||||
recordSet, err := d.findRecordSet(ctx, authZone, subDomain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hostinger: get DNS records: %w", err)
|
||||
return fmt.Errorf("hostinger: %w", err)
|
||||
}
|
||||
|
||||
var changed bool
|
||||
var newRecords []internal.Record
|
||||
|
||||
var newRecordSet []internal.RecordSet
|
||||
|
||||
for _, recordSet := range recordSets {
|
||||
if recordSet.Name == subDomain && recordSet.Type == "TXT" {
|
||||
var rs []internal.Record
|
||||
|
||||
for _, record := range recordSet.Records {
|
||||
if record.Content == info.Value {
|
||||
changed = true
|
||||
} else {
|
||||
rs = append(rs, record)
|
||||
}
|
||||
}
|
||||
|
||||
recordSet.Records = rs
|
||||
for _, record := range recordSet.Records {
|
||||
if record.Content == info.Value || record.Content == strconv.Quote(info.Value) {
|
||||
continue
|
||||
}
|
||||
|
||||
newRecordSet = append(newRecordSet, recordSet)
|
||||
newRecords = append(newRecords, record)
|
||||
}
|
||||
|
||||
if !changed {
|
||||
recordSet.Records = newRecords
|
||||
|
||||
if len(recordSet.Records) > 0 {
|
||||
request := internal.ZoneRequest{
|
||||
Overwrite: true,
|
||||
Zone: []internal.RecordSet{recordSet},
|
||||
}
|
||||
|
||||
err = d.client.UpdateDNSRecords(ctx, dns01.UnFqdn(authZone), request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hostinger: update DNS records (delete): %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
request := internal.ZoneRequest{
|
||||
Overwrite: false,
|
||||
Zone: newRecordSet,
|
||||
}
|
||||
filters := []internal.Filter{{
|
||||
Name: subDomain,
|
||||
Type: "TXT",
|
||||
}}
|
||||
|
||||
err = d.client.UpdateDNSRecords(ctx, dns01.UnFqdn(authZone), request)
|
||||
err = d.client.DeleteDNSRecords(ctx, dns01.UnFqdn(authZone), filters)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hostinger: update DNS records (delete): %w", err)
|
||||
return fmt.Errorf("hostinger: delete DNS records: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -210,3 +189,20 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
|||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
func (d *DNSProvider) findRecordSet(ctx context.Context, authZone, subDomain string) (internal.RecordSet, error) {
|
||||
recordSets, err := d.client.GetDNSRecords(ctx, dns01.UnFqdn(authZone))
|
||||
if err != nil {
|
||||
return internal.RecordSet{}, fmt.Errorf("get DNS records: %w", err)
|
||||
}
|
||||
|
||||
for _, recordSet := range recordSets {
|
||||
if recordSet.Name != subDomain || recordSet.Type != "TXT" {
|
||||
continue
|
||||
}
|
||||
|
||||
return recordSet, nil
|
||||
}
|
||||
|
||||
return internal.RecordSet{}, fmt.Errorf("no record found for domain %q and subdomain %q", authZone, subDomain)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package hostinger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
|
@ -116,8 +115,6 @@ func mockBuilder() *servermock.Builder[*DNSProvider] {
|
|||
|
||||
func TestDNSProvider_Present(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /api/dns/v1/zones/example.com",
|
||||
servermock.ResponseFromInternal("get_dns_records.json")).
|
||||
Route("PUT /api/dns/v1/zones/example.com",
|
||||
servermock.ResponseFromInternal("update_dns_records.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("update_dns_records-request.json")).
|
||||
|
|
@ -127,20 +124,7 @@ func TestDNSProvider_Present(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_Present_empty(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /api/dns/v1/zones/example.com",
|
||||
servermock.ResponseFromInternal("get_dns_records_empty.json")).
|
||||
Route("PUT /api/dns/v1/zones/example.com",
|
||||
servermock.ResponseFromInternal("update_dns_records.json"),
|
||||
servermock.CheckRequestJSONBodyFromInternal("update_dns_records_empty-request.json")).
|
||||
Build(t)
|
||||
|
||||
err := provider.Present("example.com", "", "123d==")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp(t *testing.T) {
|
||||
func TestDNSProvider_CleanUp_update(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /api/dns/v1/zones/example.com",
|
||||
servermock.ResponseFromInternal("get_dns_records_acme.json")).
|
||||
|
|
@ -153,12 +137,13 @@ func TestDNSProvider_CleanUp(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDNSProvider_CleanUp_empty(t *testing.T) {
|
||||
func TestDNSProvider_CleanUp_delete(t *testing.T) {
|
||||
provider := mockBuilder().
|
||||
Route("GET /api/dns/v1/zones/example.com",
|
||||
servermock.ResponseFromInternal("get_dns_records_empty.json")).
|
||||
Route("PUT /api/dns/v1/zones/example.com",
|
||||
servermock.Noop().WithStatusCode(http.StatusServiceUnavailable)).
|
||||
Route("DELETE /api/dns/v1/zones/example.com",
|
||||
servermock.ResponseFromInternal("delete_dns_records.json"),
|
||||
servermock.CheckRequestJSONBody(`{"filters":[{"name":"_acme-challenge","type":"TXT"}]}`)).
|
||||
Build(t)
|
||||
|
||||
err := provider.CleanUp("example.com", "", "123d==")
|
||||
|
|
|
|||
|
|
@ -73,6 +73,19 @@ func (c *Client) UpdateDNSRecords(ctx context.Context, domain string, zone ZoneR
|
|||
return c.do(req, nil)
|
||||
}
|
||||
|
||||
// DeleteDNSRecords deletes DNS records for the selected domain.
|
||||
// https://developers.hostinger.com/#tag/dns-zone/delete/api/dns/v1/zones/{domain}
|
||||
func (c *Client) DeleteDNSRecords(ctx context.Context, domain string, filters []Filter) error {
|
||||
endpoint := c.BaseURL.JoinPath("/api/dns/v1/zones/", domain)
|
||||
|
||||
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, Filters{Filters: filters})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.do(req, nil)
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, result any) error {
|
||||
req.Header.Set(authorizationHeader, "Bearer "+c.token)
|
||||
|
||||
|
|
|
|||
|
|
@ -85,20 +85,11 @@ func TestClient_UpdateDNSRecords(t *testing.T) {
|
|||
{
|
||||
Name: "_acme-challenge",
|
||||
Records: []Record{
|
||||
{Content: "aaa"},
|
||||
{Content: "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"},
|
||||
},
|
||||
TTL: 14400,
|
||||
TTL: 120,
|
||||
Type: "TXT",
|
||||
},
|
||||
{
|
||||
Name: "_acme-challenge",
|
||||
Records: []Record{{
|
||||
Content: "example.com.",
|
||||
}},
|
||||
TTL: 14400,
|
||||
Type: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -128,3 +119,36 @@ func TestClient_UpdateDNSRecords_error(t *testing.T) {
|
|||
|
||||
require.EqualError(t, err, "26a91bd9-f8c8-4a83-9df9-83e23d696fe3: The name field is required. (and 1 more error): field_1: The field_1 field is required., The field_1 must be a number.")
|
||||
}
|
||||
|
||||
func TestClient_DeleteDNSRecords(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("DELETE /api/dns/v1/zones/example.com",
|
||||
servermock.ResponseFromFixture("delete_dns_records.json"),
|
||||
servermock.CheckRequestJSONBody(`{"filters":[{"name":"_acme-challenge","type":"TXT"}]}`)).
|
||||
Build(t)
|
||||
|
||||
filters := []Filter{{
|
||||
Name: "_acme-challenge",
|
||||
Type: "TXT",
|
||||
}}
|
||||
|
||||
err := client.DeleteDNSRecords(t.Context(), "example.com", filters)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_DeleteDNSRecords_error(t *testing.T) {
|
||||
client := mockBuilder().
|
||||
Route("DELETE /api/dns/v1/zones/example.com",
|
||||
servermock.ResponseFromFixture("error_401.json").
|
||||
WithStatusCode(http.StatusUnauthorized)).
|
||||
Build(t)
|
||||
|
||||
filters := []Filter{{
|
||||
Name: "_acme-challenge",
|
||||
Type: "TXT",
|
||||
}}
|
||||
|
||||
err := client.DeleteDNSRecords(t.Context(), "example.com", filters)
|
||||
|
||||
require.EqualError(t, err, "26a91bd9-f8c8-4a83-9df9-83e23d696fe3: Unauthenticated")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"message": "Request accepted"
|
||||
}
|
||||
|
|
@ -1,4 +1,14 @@
|
|||
[
|
||||
{
|
||||
"name": "_acme-challenge",
|
||||
"records": [
|
||||
{
|
||||
"content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
|
||||
}
|
||||
],
|
||||
"ttl": 14400,
|
||||
"type": "TXT"
|
||||
},
|
||||
{
|
||||
"name": "_acme-challenge",
|
||||
"records": [
|
||||
|
|
|
|||
|
|
@ -4,25 +4,12 @@
|
|||
{
|
||||
"name": "_acme-challenge",
|
||||
"records": [
|
||||
{
|
||||
"content": "aaa"
|
||||
},
|
||||
{
|
||||
"content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
|
||||
}
|
||||
],
|
||||
"ttl": 14400,
|
||||
"ttl": 120,
|
||||
"type": "TXT"
|
||||
},
|
||||
{
|
||||
"name": "_acme-challenge",
|
||||
"records": [
|
||||
{
|
||||
"content": "example.com."
|
||||
}
|
||||
],
|
||||
"ttl": 14400,
|
||||
"type": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"overwrite": false,
|
||||
"overwrite": true,
|
||||
"zone": [
|
||||
{
|
||||
"name": "_acme-challenge",
|
||||
|
|
@ -10,16 +10,6 @@
|
|||
],
|
||||
"ttl": 14400,
|
||||
"type": "TXT"
|
||||
},
|
||||
{
|
||||
"name": "_acme-challenge",
|
||||
"records": [
|
||||
{
|
||||
"content": "example.com."
|
||||
}
|
||||
],
|
||||
"ttl": 14400,
|
||||
"type": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"overwrite": false,
|
||||
"zone": [
|
||||
{
|
||||
"name": "_acme-challenge",
|
||||
"records": [
|
||||
{
|
||||
"content": "example.com."
|
||||
}
|
||||
],
|
||||
"ttl": 14400,
|
||||
"type": "A"
|
||||
},
|
||||
{
|
||||
"name": "_acme-challenge",
|
||||
"records": [
|
||||
{
|
||||
"content": "ADw2sEd82DUgXcQ9hNBZThJs7zVJkR5v9JeSbAb9mZY"
|
||||
}
|
||||
],
|
||||
"ttl": 120,
|
||||
"type": "TXT"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -37,3 +37,12 @@ type Record struct {
|
|||
Content string `json:"content,omitempty"`
|
||||
IsDisabled bool `json:"is_disabled,omitempty"`
|
||||
}
|
||||
|
||||
type Filters struct {
|
||||
Filters []Filter `json:"filters"`
|
||||
}
|
||||
|
||||
type Filter struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue