2020-03-14 12:32:50 +00:00
|
|
|
package rimuhosting
|
|
|
|
|
|
|
|
import (
|
2023-05-05 07:49:38 +00:00
|
|
|
"context"
|
2020-03-14 12:32:50 +00:00
|
|
|
"encoding/xml"
|
|
|
|
"errors"
|
2023-05-05 07:49:38 +00:00
|
|
|
"fmt"
|
2021-08-25 09:44:11 +00:00
|
|
|
"io"
|
2020-03-14 12:32:50 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"regexp"
|
2023-05-05 07:49:38 +00:00
|
|
|
"time"
|
2020-03-14 12:32:50 +00:00
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
2020-03-14 12:32:50 +00:00
|
|
|
querystring "github.com/google/go-querystring/query"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Base URL for the RimuHosting DNS services.
|
|
|
|
const (
|
|
|
|
DefaultZonomiBaseURL = "https://zonomi.com/app/dns/dyndns.jsp"
|
2023-04-14 07:00:38 +00:00
|
|
|
DefaultRimuHostingBaseURL = "https://rimuhosting.com/dns/dyndns.jsp"
|
2020-03-14 12:32:50 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Action names.
|
|
|
|
const (
|
|
|
|
SetAction = "SET"
|
|
|
|
QueryAction = "QUERY"
|
|
|
|
DeleteAction = "DELETE"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Client the RimuHosting/Zonomi client.
|
|
|
|
type Client struct {
|
|
|
|
apiKey string
|
|
|
|
|
|
|
|
HTTPClient *http.Client
|
|
|
|
BaseURL string
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewClient Creates a RimuHosting/Zonomi client.
|
|
|
|
func NewClient(apiKey string) *Client {
|
|
|
|
return &Client{
|
|
|
|
apiKey: apiKey,
|
2023-05-05 07:49:38 +00:00
|
|
|
BaseURL: DefaultZonomiBaseURL,
|
|
|
|
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
2020-03-14 12:32:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindTXTRecords Finds TXT records.
|
|
|
|
// ex:
|
|
|
|
// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=example.com&api_key=apikeyvaluehere
|
|
|
|
// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=**.example.com&api_key=apikeyvaluehere
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c Client) FindTXTRecords(ctx context.Context, domain string) ([]Record, error) {
|
2020-03-14 12:32:50 +00:00
|
|
|
action := ActionParameter{
|
|
|
|
Action: QueryAction,
|
|
|
|
Name: domain,
|
|
|
|
Type: "TXT",
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
resp, err := c.DoActions(ctx, action)
|
2020-03-14 12:32:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp.Actions.Action.Records, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DoActions performs actions.
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c Client) DoActions(ctx context.Context, actions ...ActionParameter) (*DNSAPIResult, error) {
|
2020-03-14 12:32:50 +00:00
|
|
|
if len(actions) == 0 {
|
|
|
|
return nil, errors.New("no action")
|
|
|
|
}
|
|
|
|
|
|
|
|
resp := &DNSAPIResult{}
|
|
|
|
|
|
|
|
if len(actions) == 1 {
|
|
|
|
action := actionParameter{
|
|
|
|
ActionParameter: actions[0],
|
|
|
|
APIKey: c.apiKey,
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
err := c.do(ctx, action, resp)
|
2020-03-14 12:32:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
multi := c.toMultiParameters(actions)
|
2023-05-05 07:49:38 +00:00
|
|
|
err := c.do(ctx, multi, resp)
|
2020-03-14 12:32:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Client) toMultiParameters(params []ActionParameter) multiActionParameter {
|
|
|
|
multi := multiActionParameter{
|
|
|
|
APIKey: c.apiKey,
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, parameters := range params {
|
|
|
|
multi.Action = append(multi.Action, parameters.Action)
|
|
|
|
multi.Name = append(multi.Name, parameters.Name)
|
|
|
|
multi.Type = append(multi.Type, parameters.Type)
|
|
|
|
multi.Value = append(multi.Value, parameters.Value)
|
|
|
|
multi.TTL = append(multi.TTL, parameters.TTL)
|
|
|
|
}
|
|
|
|
|
|
|
|
return multi
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
func (c Client) do(ctx context.Context, params, result any) error {
|
2020-03-14 12:32:50 +00:00
|
|
|
baseURL, err := url.Parse(c.BaseURL)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
v, err := querystring.Values(params)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
exp := regexp.MustCompile(`(%5B)(%5D)(\d+)=`)
|
|
|
|
baseURL.RawQuery = exp.ReplaceAllString(v.Encode(), "${1}${3}${2}=")
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL.String(), http.NoBody)
|
2020-03-14 12:32:50 +00:00
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return fmt.Errorf("unable to create request: %w", err)
|
2020-03-14 12:32:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
|
|
if err != nil {
|
2023-05-05 07:49:38 +00:00
|
|
|
return errutils.NewHTTPDoError(req, err)
|
2020-03-14 12:32:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
if resp.StatusCode/100 != 2 {
|
|
|
|
return parseError(req, resp)
|
2020-03-14 12:32:50 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
if result == nil {
|
|
|
|
return nil
|
2020-03-14 12:32:50 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return errutils.NewReadResponseError(req, resp.StatusCode, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = xml.Unmarshal(raw, result)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unmarshaling %T error: %w: %s", result, err, string(raw))
|
2020-03-14 12:32:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
func parseError(req *http.Request, resp *http.Response) error {
|
|
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
|
|
|
|
|
|
errAPI := APIError{}
|
|
|
|
err := xml.Unmarshal(raw, &errAPI)
|
|
|
|
if err != nil {
|
|
|
|
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
|
|
|
|
}
|
|
|
|
|
|
|
|
return errAPI
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewAddRecordAction helper to create an action to add a TXT record.
|
|
|
|
func NewAddRecordAction(domain, content string, ttl int) ActionParameter {
|
2020-03-14 12:32:50 +00:00
|
|
|
return ActionParameter{
|
|
|
|
Action: SetAction,
|
|
|
|
Name: domain,
|
|
|
|
Type: "TXT",
|
|
|
|
Value: content,
|
|
|
|
TTL: ttl,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-05 07:49:38 +00:00
|
|
|
// NewDeleteRecordAction helper to create an action to delete a TXT record.
|
|
|
|
func NewDeleteRecordAction(domain, content string) ActionParameter {
|
2020-03-14 12:32:50 +00:00
|
|
|
return ActionParameter{
|
|
|
|
Action: DeleteAction,
|
|
|
|
Name: domain,
|
|
|
|
Type: "TXT",
|
|
|
|
Value: content,
|
|
|
|
}
|
|
|
|
}
|