From 7eee3fcccf94e66ebc3606b60b04c1a13ee7f06b Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Sun, 28 Jan 2024 09:40:28 +0000 Subject: [PATCH] fix(dondominio): use endpoint api.dondominio.com --- docs/dondominio.md | 4 +- internal/provider/providers/dondominio/api.go | 66 ++++++++++++ .../providers/dondominio/errorcodes.go | 100 ++++++++++++++++++ .../provider/providers/dondominio/list.go | 54 ++++++++++ .../provider/providers/dondominio/provider.go | 80 +++----------- .../provider/providers/dondominio/update.go | 33 ++++++ 6 files changed, 270 insertions(+), 67 deletions(-) create mode 100644 internal/provider/providers/dondominio/api.go create mode 100644 internal/provider/providers/dondominio/errorcodes.go create mode 100644 internal/provider/providers/dondominio/list.go create mode 100644 internal/provider/providers/dondominio/update.go diff --git a/docs/dondominio.md b/docs/dondominio.md index e5a42b44f..025f73c31 100644 --- a/docs/dondominio.md +++ b/docs/dondominio.md @@ -22,7 +22,7 @@ ### Compulsory parameters - `"domain"` -- `"name"` is the name server associated with the domain +- `"name"` is the name of the service/hosting - `"username"` - `"password"` @@ -31,3 +31,5 @@ - `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6` ## Domain setup + +See [dondominio.dev/en/dondns/docs/api/#before-start](https://dondominio.dev/en/dondns/docs/api/#before-start) diff --git a/internal/provider/providers/dondominio/api.go b/internal/provider/providers/dondominio/api.go new file mode 100644 index 000000000..b5a193daf --- /dev/null +++ b/internal/provider/providers/dondominio/api.go @@ -0,0 +1,66 @@ +package dondominio + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/qdm12/ddns-updater/internal/provider/headers" +) + +func apiCall(ctx context.Context, client *http.Client, + path string, requestData any) (responseData json.RawMessage, err error) { + u := url.URL{ + Scheme: "https", + Host: "api.dondominio.com", + Path: path, + } + + body := bytes.NewBuffer(nil) + encoder := json.NewEncoder(body) + err = encoder.Encode(requestData) + if err != nil { + return nil, fmt.Errorf("encoding request data: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), body) + if err != nil { + return nil, fmt.Errorf("creating http request: %w", err) + } + headers.SetUserAgent(request) + headers.SetContentType(request, "application/json") + headers.SetAccept(request, "application/json") + + response, err := client.Do(request) + if err != nil { + return nil, err + } + + var data struct { + Success bool `json:"success"` + ErrorCode int `json:"errorCode"` + ErrorCodeMsg string `json:"errorCodeMsg"` + ResponseData json.RawMessage `json:"responseData"` + } + decoder := json.NewDecoder(response.Body) + err = decoder.Decode(&data) + if err != nil { + _ = response.Body.Close() + return nil, fmt.Errorf("decoding response body: %w", err) + } + + err = response.Body.Close() + if err != nil { + return nil, fmt.Errorf("closing response body: %w", err) + } + + if !data.Success { + _ = response.Body.Close() + return nil, makeError(data.ErrorCode, data.ErrorCodeMsg) + } + + return data.ResponseData, nil +} diff --git a/internal/provider/providers/dondominio/errorcodes.go b/internal/provider/providers/dondominio/errorcodes.go new file mode 100644 index 000000000..35ac14ed5 --- /dev/null +++ b/internal/provider/providers/dondominio/errorcodes.go @@ -0,0 +1,100 @@ +package dondominio + +import ( + "fmt" + + "github.com/qdm12/ddns-updater/internal/provider/errors" +) + +func makeError(errorCode int, errorMessage string) error { + switch errorCode { + case syntaxError, syntaxErrorParameterFault, invalidObjectOrAction, + notImplementedObjectOrAction, syntaxErrorInvalidParameter, accountDeleted, + accountNotExists, invalidDomainName, tldNotSupported: + return fmt.Errorf("%w: %s (%d)", errors.ErrBadRequest, errorMessage, errorCode) + case notAllowedObjectOrAction, loginRequired, loginInvalid, sessionInvalid, + actionNotAllowed, accountInvalidPass1, accountInvalidPass2, accountInvalidPass3, + domainUpdateNotAllowed: + return fmt.Errorf("%w: %s (%d)", errors.ErrAuth, errorMessage, errorCode) + case accountBlocked1, accountBlocked2, accountBlocked3, accountBlocked4, + accountBlocked5, accountBlocked6, accountFiltered1, accountFiltered2, + accountBanned, domainUpdateBlocked: + return fmt.Errorf("%w: %s (%d)", errors.ErrBannedAbuse, errorMessage, errorCode) + case accountInactive: + return fmt.Errorf("%w: %s (%d)", errors.ErrAccountInactive, errorMessage, errorCode) + case domainCheckError, domainNotFound: + return fmt.Errorf("%w: %s (%d)", errors.ErrDomainNotFound, errorMessage, errorCode) + default: + return fmt.Errorf("%w: %s (%d)", errors.ErrUnknownResponse, errorMessage, errorCode) + } +} + +// See section "10.1.1 Error codes" at https://dondominio.dev/en/api/docs/api/#tables +const ( + success = 0 + undefinedError = 1 + syntaxError = 100 + syntaxErrorParameterFault = 101 + invalidObjectOrAction = 102 + notAllowedObjectOrAction = 103 + notImplementedObjectOrAction = 104 + syntaxErrorInvalidParameter = 105 + loginRequired = 200 + loginInvalid = 201 + sessionInvalid = 210 + actionNotAllowed = 300 + accountBlocked1 = 1000 + accountDeleted = 1001 + accountInactive = 1002 + accountNotExists = 1003 + accountInvalidPass1 = 1004 + accountInvalidPass2 = 1005 + accountBlocked2 = 1006 + accountFiltered1 = 1007 + accountInvalidPass3 = 1009 + accountBlocked3 = 1010 + accountBlocked4 = 1011 + accountBlocked5 = 1012 + accountBlocked6 = 1013 + accountFiltered2 = 1014 + accountBanned = 1030 + insufficientBalance = 1100 + invalidDomainName = 2001 + tldNotSupported = 2002 + tldInMaintenance = 2003 + domainCheckError = 2004 + domainTransferNotAllowed = 2005 + domainWhoisNotAllowed = 2006 + domainWhoisError = 2007 + domainNotFound = 2008 + domainCreateError = 2009 + domainCreateErrorTaken = 2010 + domainCreateErrorDomainPremium = 2011 + domainTransferError = 2012 + domainRenewError = 2100 + domainRenewNotAllowed = 2101 + domainRenewBlocked = 2102 + domainUpdateError = 2200 + domainUpdateNotAllowed = 2201 + domainUpdateBlocked = 2202 + invalidOperationDueToTheOwnerContactDataVerificationStatus = 2210 + contactNotExists = 3001 + contactDataError = 3002 + invalidOperationDueToTheContactDataVerification = 3003 + userNotExists = 3500 + userCreateError = 3501 + userUpdateError = 3502 + serviceNotFound = 4001 + serviceEntityNotFound = 4002 + maximumAmountOfEntitiesError = 4003 + failureToCreateTheEntity = 4004 + failureToUpdateTheEntity = 4005 + failureToDeleteTheEntity = 4006 + failureToCreateTheService = 4007 + failureToUpgradeTheService = 4008 + failureToRenewTheService = 4009 + failureToMotifyTheParkingSystem = 4010 + sslError = 5000 + sslNotFound = 5001 + webConstructorError = 10001 +) diff --git a/internal/provider/providers/dondominio/list.go b/internal/provider/providers/dondominio/list.go new file mode 100644 index 000000000..80bae8591 --- /dev/null +++ b/internal/provider/providers/dondominio/list.go @@ -0,0 +1,54 @@ +package dondominio + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/qdm12/ddns-updater/internal/provider/constants" +) + +// See https://dondominio.dev/en/api/docs/api/#dns-zone-list-service-dnslist +func (p *Provider) list(ctx context.Context, client *http.Client) (aIDs, aaaaIDs []string, err error) { + requestData := struct { + APIUser string `json:"apiuser"` + APIPasswd string `json:"apipasswd"` + ServiceName string `json:"serviceName"` + }{ + APIUser: p.username, + APIPasswd: p.password, + ServiceName: p.name, + } + + data, err := apiCall(ctx, client, "/service/dnslist", requestData) + if err != nil { + return nil, nil, err + } + + var responseData struct { + DNS []struct { + EntityID string `json:"entityID"` + Name string `json:"name"` + Type string `json:"type"` + } `json:"dns"` + } + err = json.Unmarshal(data, &responseData) + if err != nil { + return nil, nil, fmt.Errorf("json decoding response data: %w", err) + } + + for _, record := range responseData.DNS { + if record.Name != p.BuildDomainName() { + continue + } + switch record.Type { + case constants.A: + aIDs = append(aIDs, record.EntityID) + case constants.AAAA: + aaaaIDs = append(aaaaIDs, record.EntityID) + } + } + + return aIDs, aaaaIDs, nil +} diff --git a/internal/provider/providers/dondominio/provider.go b/internal/provider/providers/dondominio/provider.go index fd36dcfba..9b6ccfd51 100644 --- a/internal/provider/providers/dondominio/provider.go +++ b/internal/provider/providers/dondominio/provider.go @@ -6,13 +6,10 @@ import ( "fmt" "net/http" "net/netip" - "net/url" - "strings" "github.com/qdm12/ddns-updater/internal/models" "github.com/qdm12/ddns-updater/internal/provider/constants" "github.com/qdm12/ddns-updater/internal/provider/errors" - "github.com/qdm12/ddns-updater/internal/provider/headers" "github.com/qdm12/ddns-updater/internal/provider/utils" "github.com/qdm12/ddns-updater/pkg/publicip/ipversion" ) @@ -103,74 +100,25 @@ func (p *Provider) HTML() models.HTMLRow { } func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) { - u := url.URL{ - Scheme: "https", - Host: "simple-api.dondominio.net", - } - values := url.Values{} - values.Set("apiuser", p.username) - values.Set("apipasswd", p.password) - values.Set("domain", p.domain) - values.Set("name", p.name) - isIPv4 := ip.Is4() - if isIPv4 { - values.Set("ipv4", ip.String()) - } else { - values.Set("ipv6", ip.String()) - } - encodedValues := values.Encode() - buffer := strings.NewReader(encodedValues) - - request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer) - if err != nil { - return netip.Addr{}, fmt.Errorf("creating http request: %w", err) - } - headers.SetUserAgent(request) - headers.SetContentType(request, "application/x-www-form-urlencoded") - headers.SetAccept(request, "application/json") - - response, err := client.Do(request) + aIDs, aaaaIDs, err := p.list(ctx, client) if err != nil { - return netip.Addr{}, err + return netip.Addr{}, fmt.Errorf("listing records: %w", err) } - defer response.Body.Close() - if response.StatusCode != http.StatusOK { - return netip.Addr{}, fmt.Errorf("%w: %d: %s", - errors.ErrHTTPStatusNotValid, response.StatusCode, utils.BodyToSingleLine(response.Body)) + recordType := constants.A + ids := aIDs + if ip.Is6() { + recordType = constants.AAAA + ids = aaaaIDs } - decoder := json.NewDecoder(response.Body) - var responseData struct { - Success bool `json:"success"` - ErrorCode int `json:"errorCode"` - ErrorCodeMessage string `json:"errorCodeMsg"` - ResponseData struct { - GlueRecords []struct { - IPv4 string `json:"ipv4"` - IPv6 string `json:"ipv6"` - } `json:"gluerecords"` - } `json:"responseData"` - } - err = decoder.Decode(&responseData) - if err != nil { - return netip.Addr{}, fmt.Errorf("json decoding response body: %w", err) + for _, id := range ids { + err = p.update(ctx, client, id, ip) + if err != nil { + return netip.Addr{}, fmt.Errorf("updating %s record for %s: %w", + recordType, p.BuildDomainName(), err) + } } - if !responseData.Success { - return netip.Addr{}, fmt.Errorf("%w: %s (error code %d)", - errors.ErrUnsuccessful, responseData.ErrorCodeMessage, responseData.ErrorCode) - } - ipString := responseData.ResponseData.GlueRecords[0].IPv4 - if !isIPv4 { - ipString = responseData.ResponseData.GlueRecords[0].IPv6 - } - newIP, err = netip.ParseAddr(ipString) - if err != nil { - return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err) - } else if ip.Compare(newIP) != 0 { - return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s", - errors.ErrIPReceivedMismatch, ip, newIP) - } - return newIP, nil + return ip, nil } diff --git a/internal/provider/providers/dondominio/update.go b/internal/provider/providers/dondominio/update.go new file mode 100644 index 000000000..0ee225e95 --- /dev/null +++ b/internal/provider/providers/dondominio/update.go @@ -0,0 +1,33 @@ +package dondominio + +import ( + "context" + "fmt" + "net/http" + "net/netip" +) + +// See https://dondominio.dev/en/api/docs/api/#dns-zone-update-service-dnsupdate +func (p *Provider) update(ctx context.Context, client *http.Client, entityID string, + ip netip.Addr) (err error) { + requestData := struct { + APIUser string `json:"apiuser"` + APIPasswd string `json:"apipasswd"` + ServiceName string `json:"serviceName"` + EntityID string `json:"entityID"` + Value string `json:"value"` + }{ + APIUser: p.username, + APIPasswd: p.password, + ServiceName: p.name, + EntityID: entityID, + Value: ip.String(), + } + + _, err = apiCall(ctx, client, "/service/dnsupdate", requestData) + if err != nil { + return fmt.Errorf("for entity id %s: %w", entityID, err) + } + + return nil +}