lego/providers/dns/loopia/internal/client.go

202 lines
4.6 KiB
Go
Raw Normal View History

2020-12-26 16:22:01 +00:00
package internal
import (
"bytes"
2023-05-05 07:49:38 +00:00
"context"
2020-12-26 16:22:01 +00:00
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
2023-05-05 07:49:38 +00:00
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
2020-12-26 16:22:01 +00:00
)
// DefaultBaseURL is url to the XML-RPC api.
const DefaultBaseURL = "https://api.loopia.se/RPCSERV"
// Client the Loopia client.
type Client struct {
2023-05-05 07:49:38 +00:00
apiUser string
apiPassword string
BaseURL string
HTTPClient *http.Client
2020-12-26 16:22:01 +00:00
}
// NewClient creates a new Loopia Client.
func NewClient(apiUser, apiPassword string) *Client {
return &Client{
2023-05-05 07:49:38 +00:00
apiUser: apiUser,
apiPassword: apiPassword,
2020-12-26 16:22:01 +00:00
BaseURL: DefaultBaseURL,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
}
}
// AddTXTRecord adds a TXT record.
2023-05-05 07:49:38 +00:00
func (c *Client) AddTXTRecord(ctx context.Context, domain string, subdomain string, ttl int, value string) error {
2020-12-26 16:22:01 +00:00
call := &methodCall{
MethodName: "addZoneRecord",
Params: []param{
2023-05-05 07:49:38 +00:00
paramString{Value: c.apiUser},
paramString{Value: c.apiPassword},
2020-12-26 16:22:01 +00:00
paramString{Value: domain},
paramString{Value: subdomain},
paramStruct{
StructMembers: []structMember{
structMemberString{Name: "type", Value: "TXT"},
structMemberInt{Name: "ttl", Value: ttl},
structMemberInt{Name: "priority", Value: 0},
structMemberString{Name: "rdata", Value: value},
structMemberInt{Name: "record_id", Value: 0},
},
},
},
}
resp := &responseString{}
2023-05-05 07:49:38 +00:00
err := c.rpcCall(ctx, call, resp)
2020-12-26 16:22:01 +00:00
if err != nil {
return err
}
return checkResponse(resp.Value)
}
// RemoveTXTRecord removes a TXT record.
2023-05-05 07:49:38 +00:00
func (c *Client) RemoveTXTRecord(ctx context.Context, domain string, subdomain string, recordID int) error {
2020-12-26 16:22:01 +00:00
call := &methodCall{
MethodName: "removeZoneRecord",
Params: []param{
2023-05-05 07:49:38 +00:00
paramString{Value: c.apiUser},
paramString{Value: c.apiPassword},
2020-12-26 16:22:01 +00:00
paramString{Value: domain},
paramString{Value: subdomain},
paramInt{Value: recordID},
},
}
resp := &responseString{}
2023-05-05 07:49:38 +00:00
err := c.rpcCall(ctx, call, resp)
2020-12-26 16:22:01 +00:00
if err != nil {
return err
}
return checkResponse(resp.Value)
}
// GetTXTRecords gets TXT records.
2023-05-05 07:49:38 +00:00
func (c *Client) GetTXTRecords(ctx context.Context, domain string, subdomain string) ([]RecordObj, error) {
2020-12-26 16:22:01 +00:00
call := &methodCall{
MethodName: "getZoneRecords",
Params: []param{
2023-05-05 07:49:38 +00:00
paramString{Value: c.apiUser},
paramString{Value: c.apiPassword},
2020-12-26 16:22:01 +00:00
paramString{Value: domain},
paramString{Value: subdomain},
},
}
resp := &recordObjectsResponse{}
2023-05-05 07:49:38 +00:00
err := c.rpcCall(ctx, call, resp)
2020-12-26 16:22:01 +00:00
return resp.Params, err
}
// RemoveSubdomain remove a sub-domain.
2023-05-05 07:49:38 +00:00
func (c *Client) RemoveSubdomain(ctx context.Context, domain, subdomain string) error {
2020-12-26 16:22:01 +00:00
call := &methodCall{
MethodName: "removeSubdomain",
Params: []param{
2023-05-05 07:49:38 +00:00
paramString{Value: c.apiUser},
paramString{Value: c.apiPassword},
2020-12-26 16:22:01 +00:00
paramString{Value: domain},
paramString{Value: subdomain},
},
}
resp := &responseString{}
2023-05-05 07:49:38 +00:00
err := c.rpcCall(ctx, call, resp)
2020-12-26 16:22:01 +00:00
if err != nil {
return err
}
return checkResponse(resp.Value)
}
2023-05-05 07:49:38 +00:00
// rpcCall makes an XML-RPC call to Loopia's RPC endpoint by marshaling the data given in the call argument to XML
// and sending that via HTTP Post to Loopia.
2020-12-26 16:22:01 +00:00
// The response is then unmarshalled into the resp argument.
2023-05-05 07:49:38 +00:00
func (c *Client) rpcCall(ctx context.Context, call *methodCall, result response) error {
req, err := newXMLRequest(ctx, c.BaseURL, call)
2020-12-26 16:22:01 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return err
2020-12-26 16:22:01 +00:00
}
2023-05-05 07:49:38 +00:00
resp, err := c.HTTPClient.Do(req)
if err != nil {
return errutils.NewHTTPDoError(req, err)
}
2020-12-26 16:22:01 +00:00
2023-05-05 07:49:38 +00:00
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
}
raw, err := io.ReadAll(resp.Body)
2020-12-26 16:22:01 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return errutils.NewReadResponseError(req, resp.StatusCode, err)
2020-12-26 16:22:01 +00:00
}
2023-05-05 07:49:38 +00:00
err = xml.Unmarshal(raw, result)
2020-12-26 16:22:01 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return fmt.Errorf("unmarshal error: %w", err)
2020-12-26 16:22:01 +00:00
}
2023-05-05 07:49:38 +00:00
if result.faultCode() != 0 {
return RPCError{
FaultCode: result.faultCode(),
FaultString: strings.TrimSpace(result.faultString()),
2020-12-26 16:22:01 +00:00
}
}
return nil
}
2023-05-05 07:49:38 +00:00
func newXMLRequest(ctx context.Context, endpoint string, payload any) (*http.Request, error) {
body := new(bytes.Buffer)
body.WriteString(xml.Header)
2020-12-26 16:22:01 +00:00
2023-05-05 07:49:38 +00:00
encoder := xml.NewEncoder(body)
encoder.Indent("", " ")
2020-12-26 16:22:01 +00:00
2023-05-05 07:49:38 +00:00
err := encoder.Encode(payload)
if err != nil {
return nil, err
2020-12-26 16:22:01 +00:00
}
2023-05-05 07:49:38 +00:00
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
2020-12-26 16:22:01 +00:00
if err != nil {
2023-05-05 07:49:38 +00:00
return nil, fmt.Errorf("unable to create request: %w", err)
2020-12-26 16:22:01 +00:00
}
2023-05-05 07:49:38 +00:00
req.Header.Set("Content-Type", "text/xml")
return req, nil
2020-12-26 16:22:01 +00:00
}
func checkResponse(value string) error {
switch v := strings.TrimSpace(value); v {
case "OK":
return nil
case "AUTH_ERROR":
return errors.New("authentication error")
default:
return fmt.Errorf("unknown error: %q", v)
}
}