client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "strconv"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 "github.com/go-viper/mapstructure/v2"
16 )
17
18 const apiEndpoint = "https://kasapi.kasserver.com/soap/KasApi.php"
19
20 type Authentication interface {
21 Authentication(ctx context.Context, sessionLifetime int, sessionUpdateLifetime bool) (string, error)
22 }
23
24 // Client a KAS server client.
25 type Client struct {
26 login string
27
28 floodTime time.Time
29 muFloodTime sync.Mutex
30
31 baseURL string
32 HTTPClient *http.Client
33 }
34
35 // NewClient creates a new Client.
36 func NewClient(login string) *Client {
37 return &Client{
38 login: login,
39 baseURL: apiEndpoint,
40 HTTPClient: &http.Client{Timeout: 10 * time.Second},
41 }
42 }
43
44 // GetDNSSettings Reading out the DNS settings of a zone.
45 // - zone: host zone.
46 // - recordID: the ID of the resource record (optional).
47 func (c *Client) GetDNSSettings(ctx context.Context, zone, recordID string) ([]ReturnInfo, error) {
48 requestParams := map[string]string{"zone_host": zone}
49
50 if recordID != "" {
51 requestParams["record_id"] = recordID
52 }
53
54 req, err := c.newRequest(ctx, "get_dns_settings", requestParams)
55 if err != nil {
56 return nil, err
57 }
58
59 var g GetDNSSettingsAPIResponse
60
61 err = c.do(req, &g)
62 if err != nil {
63 return nil, err
64 }
65
66 c.updateFloodTime(g.Response.KasFloodDelay)
67
68 return g.Response.ReturnInfo, nil
69 }
70
71 // AddDNSSettings Creation of a DNS resource record.
72 func (c *Client) AddDNSSettings(ctx context.Context, record DNSRequest) (string, error) {
73 req, err := c.newRequest(ctx, "add_dns_settings", record)
74 if err != nil {
75 return "", err
76 }
77
78 var g AddDNSSettingsAPIResponse
79
80 err = c.do(req, &g)
81 if err != nil {
82 return "", err
83 }
84
85 c.updateFloodTime(g.Response.KasFloodDelay)
86
87 return g.Response.ReturnInfo, nil
88 }
89
90 // DeleteDNSSettings Deleting a DNS Resource Record.
91 func (c *Client) DeleteDNSSettings(ctx context.Context, recordID string) (string, error) {
92 requestParams := map[string]string{"record_id": recordID}
93
94 req, err := c.newRequest(ctx, "delete_dns_settings", requestParams)
95 if err != nil {
96 return "", err
97 }
98
99 var g DeleteDNSSettingsAPIResponse
100
101 err = c.do(req, &g)
102 if err != nil {
103 return "", err
104 }
105
106 c.updateFloodTime(g.Response.KasFloodDelay)
107
108 return g.Response.ReturnString, nil
109 }
110
111 func (c *Client) newRequest(ctx context.Context, action string, requestParams any) (*http.Request, error) {
112 ar := KasRequest{
113 Login: c.login,
114 AuthType: "session",
115 AuthData: getToken(ctx),
116 Action: action,
117 RequestParams: requestParams,
118 }
119
120 body, err := json.Marshal(ar)
121 if err != nil {
122 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
123 }
124
125 payload := []byte(strings.TrimSpace(fmt.Sprintf(kasAPIEnvelope, body)))
126
127 req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, bytes.NewReader(payload))
128 if err != nil {
129 return nil, fmt.Errorf("unable to create request: %w", err)
130 }
131
132 return req, nil
133 }
134
135 func (c *Client) do(req *http.Request, result any) error {
136 c.muFloodTime.Lock()
137 time.Sleep(time.Until(c.floodTime))
138 c.muFloodTime.Unlock()
139
140 resp, err := c.HTTPClient.Do(req)
141 if err != nil {
142 return errutils.NewHTTPDoError(req, err)
143 }
144
145 defer func() { _ = resp.Body.Close() }()
146
147 if resp.StatusCode != http.StatusOK {
148 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
149 }
150
151 envlp, err := decodeXML[KasAPIResponseEnvelope](resp.Body)
152 if err != nil {
153 return err
154 }
155
156 if envlp.Body.Fault != nil {
157 return envlp.Body.Fault
158 }
159
160 raw := getValue(envlp.Body.KasAPIResponse.Return)
161
162 err = mapstructure.Decode(raw, result)
163 if err != nil {
164 return fmt.Errorf("response struct decode: %w", err)
165 }
166
167 return nil
168 }
169
170 func (c *Client) updateFloodTime(delay float64) {
171 c.muFloodTime.Lock()
172 c.floodTime = time.Now().Add(time.Duration(delay * float64(time.Second)))
173 c.muFloodTime.Unlock()
174 }
175
176 func getValue(item *Item) any {
177 switch {
178 case item.Raw != "":
179 v, _ := strconv.ParseBool(item.Raw)
180 return v
181
182 case item.Text != "":
183 switch item.Type {
184 case "xsd:string":
185 return item.Text
186 case "xsd:float":
187 v, _ := strconv.ParseFloat(item.Text, 64)
188 return v
189 case "xsd:int":
190 v, _ := strconv.ParseInt(item.Text, 10, 64)
191 return v
192 default:
193 return item.Text
194 }
195
196 case item.Value != nil:
197 return getValue(item.Value)
198
199 case len(item.Items) > 0 && item.Type == "SOAP-ENC:Array":
200 var v []any
201 for _, i := range item.Items {
202 v = append(v, getValue(i))
203 }
204
205 return v
206
207 case len(item.Items) > 0:
208 v := map[string]any{}
209 for _, i := range item.Items {
210 v[getKey(i)] = getValue(i)
211 }
212
213 return v
214
215 default:
216 return ""
217 }
218 }
219
220 func getKey(item *Item) string {
221 if item.Key == nil {
222 return ""
223 }
224
225 return item.Key.Text
226 }
227