2020-12-26 16:22:01 +00:00
|
|
|
package internal
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/xml"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
// DefaultBaseURL is url to the XML-RPC api.
|
|
|
|
const DefaultBaseURL = "https://api.loopia.se/RPCSERV"
|
|
|
|
|
|
|
|
// Client the Loopia client.
|
|
|
|
type Client struct {
|
|
|
|
APIUser string
|
|
|
|
APIPassword string
|
|
|
|
BaseURL string
|
|
|
|
HTTPClient *http.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewClient creates a new Loopia Client.
|
|
|
|
func NewClient(apiUser, apiPassword string) *Client {
|
|
|
|
return &Client{
|
|
|
|
APIUser: apiUser,
|
|
|
|
APIPassword: apiPassword,
|
|
|
|
BaseURL: DefaultBaseURL,
|
|
|
|
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddTXTRecord adds a TXT record.
|
|
|
|
func (c *Client) AddTXTRecord(domain string, subdomain string, ttl int, value string) error {
|
|
|
|
call := &methodCall{
|
|
|
|
MethodName: "addZoneRecord",
|
|
|
|
Params: []param{
|
|
|
|
paramString{Value: c.APIUser},
|
|
|
|
paramString{Value: c.APIPassword},
|
|
|
|
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{}
|
|
|
|
|
|
|
|
err := c.rpcCall(call, resp)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return checkResponse(resp.Value)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveTXTRecord removes a TXT record.
|
|
|
|
func (c *Client) RemoveTXTRecord(domain string, subdomain string, recordID int) error {
|
|
|
|
call := &methodCall{
|
|
|
|
MethodName: "removeZoneRecord",
|
|
|
|
Params: []param{
|
|
|
|
paramString{Value: c.APIUser},
|
|
|
|
paramString{Value: c.APIPassword},
|
|
|
|
paramString{Value: domain},
|
|
|
|
paramString{Value: subdomain},
|
|
|
|
paramInt{Value: recordID},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
resp := &responseString{}
|
|
|
|
|
|
|
|
err := c.rpcCall(call, resp)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return checkResponse(resp.Value)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetTXTRecords gets TXT records.
|
|
|
|
func (c *Client) GetTXTRecords(domain string, subdomain string) ([]RecordObj, error) {
|
|
|
|
call := &methodCall{
|
|
|
|
MethodName: "getZoneRecords",
|
|
|
|
Params: []param{
|
|
|
|
paramString{Value: c.APIUser},
|
|
|
|
paramString{Value: c.APIPassword},
|
|
|
|
paramString{Value: domain},
|
|
|
|
paramString{Value: subdomain},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
resp := &recordObjectsResponse{}
|
|
|
|
|
|
|
|
err := c.rpcCall(call, resp)
|
|
|
|
|
|
|
|
return resp.Params, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveSubdomain remove a sub-domain.
|
|
|
|
func (c *Client) RemoveSubdomain(domain, subdomain string) error {
|
|
|
|
call := &methodCall{
|
|
|
|
MethodName: "removeSubdomain",
|
|
|
|
Params: []param{
|
|
|
|
paramString{Value: c.APIUser},
|
|
|
|
paramString{Value: c.APIPassword},
|
|
|
|
paramString{Value: domain},
|
|
|
|
paramString{Value: subdomain},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
resp := &responseString{}
|
|
|
|
|
|
|
|
err := c.rpcCall(call, resp)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return checkResponse(resp.Value)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
// The response is then unmarshalled into the resp argument.
|
|
|
|
func (c *Client) rpcCall(call *methodCall, resp response) error {
|
|
|
|
body, err := xml.MarshalIndent(call, "", " ")
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error during unmarshalling the request body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
body = append([]byte(`<?xml version="1.0"?>`+"\n"), body...)
|
|
|
|
|
|
|
|
respBody, err := c.httpPost(c.BaseURL, "text/xml", bytes.NewReader(body))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = xml.Unmarshal(respBody, resp)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error during unmarshalling the response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.faultCode() != 0 {
|
|
|
|
return rpcError{
|
|
|
|
faultCode: resp.faultCode(),
|
|
|
|
faultString: strings.TrimSpace(resp.faultString()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) httpPost(url string, bodyType string, body io.Reader) ([]byte, error) {
|
|
|
|
resp, err := c.HTTPClient.Post(url, bodyType, body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("HTTP Post Error: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("HTTP Post Error: %d", resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
2021-08-25 09:44:11 +00:00
|
|
|
b, err := io.ReadAll(resp.Body)
|
2020-12-26 16:22:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("HTTP Post Error: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|