2021-06-30 20:49:02 +00:00
|
|
|
package internal
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/sha1"
|
|
|
|
"encoding/json"
|
|
|
|
"encoding/xml"
|
|
|
|
"fmt"
|
2021-08-25 09:44:11 +00:00
|
|
|
"io"
|
2021-06-30 20:49:02 +00:00
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
authEndpoint = "https://kasapi.kasserver.com/soap/KasAuth.php"
|
|
|
|
apiEndpoint = "https://kasapi.kasserver.com/soap/KasApi.php"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Client a KAS server client.
|
|
|
|
type Client struct {
|
|
|
|
login string
|
|
|
|
password string
|
|
|
|
|
|
|
|
authEndpoint string
|
|
|
|
apiEndpoint string
|
|
|
|
HTTPClient *http.Client
|
|
|
|
floodTime time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewClient creates a new Client.
|
|
|
|
func NewClient(login string, password string) *Client {
|
|
|
|
return &Client{
|
|
|
|
login: login,
|
|
|
|
password: password,
|
|
|
|
authEndpoint: authEndpoint,
|
|
|
|
apiEndpoint: apiEndpoint,
|
|
|
|
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Authentication Creates a credential token.
|
|
|
|
// - sessionLifetime: Validity of the token in seconds.
|
|
|
|
// - sessionUpdateLifetime: with `true` the session is extended with every request.
|
|
|
|
func (c Client) Authentication(sessionLifetime int, sessionUpdateLifetime bool) (string, error) {
|
|
|
|
hash := sha1.New()
|
|
|
|
hash.Write([]byte(c.password))
|
|
|
|
|
|
|
|
sul := "N"
|
|
|
|
if sessionUpdateLifetime {
|
|
|
|
sul = "Y"
|
|
|
|
}
|
|
|
|
|
|
|
|
ar := AuthRequest{
|
|
|
|
Login: c.login,
|
|
|
|
AuthData: fmt.Sprintf("%x", hash.Sum(nil)),
|
|
|
|
AuthType: "sha1",
|
|
|
|
SessionLifetime: sessionLifetime,
|
|
|
|
SessionUpdateLifetime: sul,
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := json.Marshal(ar)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("request marshal: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAuthEnvelope, body)))
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPost, c.authEndpoint, bytes.NewReader(payload))
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("request creation: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("request execution: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
2021-08-25 09:44:11 +00:00
|
|
|
data, _ := io.ReadAll(resp.Body)
|
2021-06-30 20:49:02 +00:00
|
|
|
return "", fmt.Errorf("invalid status code: %d %s", resp.StatusCode, string(data))
|
|
|
|
}
|
|
|
|
|
2021-08-25 09:44:11 +00:00
|
|
|
data, err := io.ReadAll(resp.Body)
|
2021-06-30 20:49:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("response read: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var e KasAuthEnvelope
|
|
|
|
decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(data))})
|
|
|
|
err = decoder.Decode(&e)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("response xml decode: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if e.Body.Fault != nil {
|
|
|
|
return "", e.Body.Fault
|
|
|
|
}
|
|
|
|
|
|
|
|
return e.Body.KasAuthResponse.Return.Text, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetDNSSettings Reading out the DNS settings of a zone.
|
|
|
|
// - zone: host zone.
|
|
|
|
// - recordID: the ID of the resource record (optional).
|
|
|
|
func (c *Client) GetDNSSettings(credentialToken, zone, recordID string) ([]ReturnInfo, error) {
|
|
|
|
requestParams := map[string]string{"zone_host": zone}
|
|
|
|
|
|
|
|
if recordID != "" {
|
|
|
|
requestParams["record_id"] = recordID
|
|
|
|
}
|
|
|
|
|
|
|
|
item, err := c.do(credentialToken, "get_dns_settings", requestParams)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
raw := getValue(item)
|
|
|
|
|
|
|
|
var g GetDNSSettingsAPIResponse
|
|
|
|
err = mapstructure.Decode(raw, &g)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("response struct decode: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
c.updateFloodTime(g.Response.KasFloodDelay)
|
|
|
|
|
|
|
|
return g.Response.ReturnInfo, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddDNSSettings Creation of a DNS resource record.
|
|
|
|
func (c *Client) AddDNSSettings(credentialToken string, record DNSRequest) (string, error) {
|
|
|
|
item, err := c.do(credentialToken, "add_dns_settings", record)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
raw := getValue(item)
|
|
|
|
|
|
|
|
var g AddDNSSettingsAPIResponse
|
|
|
|
err = mapstructure.Decode(raw, &g)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("response struct decode: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
c.updateFloodTime(g.Response.KasFloodDelay)
|
|
|
|
|
|
|
|
return g.Response.ReturnInfo, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteDNSSettings Deleting a DNS Resource Record.
|
|
|
|
func (c *Client) DeleteDNSSettings(credentialToken, recordID string) (bool, error) {
|
|
|
|
requestParams := map[string]string{"record_id": recordID}
|
|
|
|
|
|
|
|
item, err := c.do(credentialToken, "delete_dns_settings", requestParams)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
raw := getValue(item)
|
|
|
|
|
|
|
|
var g DeleteDNSSettingsAPIResponse
|
|
|
|
err = mapstructure.Decode(raw, &g)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("response struct decode: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
c.updateFloodTime(g.Response.KasFloodDelay)
|
|
|
|
|
|
|
|
return g.Response.ReturnInfo, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Client) do(credentialToken, action string, requestParams interface{}) (*Item, error) {
|
|
|
|
time.Sleep(time.Until(c.floodTime))
|
|
|
|
|
|
|
|
ar := KasRequest{
|
|
|
|
Login: c.login,
|
|
|
|
AuthType: "session",
|
|
|
|
AuthData: credentialToken,
|
|
|
|
Action: action,
|
|
|
|
RequestParams: requestParams,
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := json.Marshal(ar)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("request marshal: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body)))
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(payload))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("request creation: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("request execution: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
2021-08-25 09:44:11 +00:00
|
|
|
data, _ := io.ReadAll(resp.Body)
|
2021-06-30 20:49:02 +00:00
|
|
|
return nil, fmt.Errorf("invalid status code: %d %s", resp.StatusCode, string(data))
|
|
|
|
}
|
|
|
|
|
2021-08-25 09:44:11 +00:00
|
|
|
data, err := io.ReadAll(resp.Body)
|
2021-06-30 20:49:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("response read: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var e KasAPIResponseEnvelope
|
|
|
|
decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(data))})
|
|
|
|
err = decoder.Decode(&e)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("response xml decode: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if e.Body.Fault != nil {
|
|
|
|
return nil, e.Body.Fault
|
|
|
|
}
|
|
|
|
|
|
|
|
return e.Body.KasAPIResponse.Return, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) updateFloodTime(delay float64) {
|
|
|
|
c.floodTime = time.Now().Add(time.Duration(delay * float64(time.Second)))
|
|
|
|
}
|
|
|
|
|
|
|
|
func getValue(item *Item) interface{} {
|
|
|
|
switch {
|
|
|
|
case item.Raw != "":
|
|
|
|
v, _ := strconv.ParseBool(item.Raw)
|
|
|
|
return v
|
|
|
|
|
|
|
|
case item.Text != "":
|
|
|
|
switch item.Type {
|
|
|
|
case "xsd:string":
|
|
|
|
return item.Text
|
|
|
|
case "xsd:float":
|
|
|
|
v, _ := strconv.ParseFloat(item.Text, 64)
|
|
|
|
return v
|
|
|
|
case "xsd:int":
|
|
|
|
v, _ := strconv.ParseInt(item.Text, 10, 64)
|
|
|
|
return v
|
|
|
|
default:
|
|
|
|
return item.Text
|
|
|
|
}
|
|
|
|
|
|
|
|
case item.Value != nil:
|
|
|
|
return getValue(item.Value)
|
|
|
|
|
|
|
|
case len(item.Items) > 0 && item.Type == "SOAP-ENC:Array":
|
|
|
|
var v []interface{}
|
|
|
|
for _, i := range item.Items {
|
|
|
|
v = append(v, getValue(i))
|
|
|
|
}
|
|
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
case len(item.Items) > 0:
|
|
|
|
v := map[string]interface{}{}
|
|
|
|
for _, i := range item.Items {
|
|
|
|
v[getKey(i)] = getValue(i)
|
|
|
|
}
|
|
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
default:
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getKey(item *Item) string {
|
|
|
|
if item.Key == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return item.Key.Text
|
|
|
|
}
|