client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/http"
11 "time"
12
13 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
14 )
15
16 // defaultBaseURL for reaching the jSON-based API-Endpoint of netcup.
17 const defaultBaseURL = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
18
19 // Client netcup DNS client.
20 type Client struct {
21 customerNumber string
22 apiKey string
23 apiPassword string
24
25 baseURL string
26 HTTPClient *http.Client
27 }
28
29 // NewClient creates a netcup DNS client.
30 func NewClient(customerNumber, apiKey, apiPassword string) (*Client, error) {
31 if customerNumber == "" || apiKey == "" || apiPassword == "" {
32 return nil, errors.New("credentials missing")
33 }
34
35 return &Client{
36 customerNumber: customerNumber,
37 apiKey: apiKey,
38 apiPassword: apiPassword,
39 baseURL: defaultBaseURL,
40 HTTPClient: &http.Client{Timeout: 10 * time.Second},
41 }, nil
42 }
43
44 // UpdateDNSRecord performs an update of the DNSRecords as specified by the netcup WSDL.
45 // https://ccp.netcup.net/run/webservice/servers/endpoint.php
46 func (c *Client) UpdateDNSRecord(ctx context.Context, domainName string, records []DNSRecord) error {
47 payload := &Request{
48 Action: "updateDnsRecords",
49 Param: UpdateDNSRecordsRequest{
50 DomainName: domainName,
51 CustomerNumber: c.customerNumber,
52 APIKey: c.apiKey,
53 APISessionID: getSessionID(ctx),
54 ClientRequestID: "",
55 DNSRecordSet: DNSRecordSet{DNSRecords: records},
56 },
57 }
58
59 err := c.doRequest(ctx, payload, nil)
60 if err != nil {
61 return fmt.Errorf("error when sending the request: %w", err)
62 }
63
64 return nil
65 }
66
67 // GetDNSRecords retrieves all dns records of an DNS-Zone as specified by the netcup WSDL
68 // returns an array of DNSRecords.
69 // https://ccp.netcup.net/run/webservice/servers/endpoint.php
70 func (c *Client) GetDNSRecords(ctx context.Context, hostname string) ([]DNSRecord, error) {
71 payload := &Request{
72 Action: "infoDnsRecords",
73 Param: InfoDNSRecordsRequest{
74 DomainName: hostname,
75 CustomerNumber: c.customerNumber,
76 APIKey: c.apiKey,
77 APISessionID: getSessionID(ctx),
78 ClientRequestID: "",
79 },
80 }
81
82 var responseData InfoDNSRecordsResponse
83
84 err := c.doRequest(ctx, payload, &responseData)
85 if err != nil {
86 return nil, fmt.Errorf("error when sending the request: %w", err)
87 }
88
89 return responseData.DNSRecords, nil
90 }
91
92 // doRequest marshals given body to JSON, send the request to netcup API
93 // and returns body of response.
94 func (c *Client) doRequest(ctx context.Context, payload, result any) error {
95 req, err := newJSONRequest(ctx, http.MethodPost, c.baseURL, payload)
96 if err != nil {
97 return err
98 }
99
100 req.Close = true
101
102 resp, err := c.HTTPClient.Do(req)
103 if err != nil {
104 return errutils.NewHTTPDoError(req, err)
105 }
106
107 defer func() { _ = resp.Body.Close() }()
108
109 if resp.StatusCode >= http.StatusMultipleChoices {
110 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
111 }
112
113 respMsg, err := unmarshalResponseMsg(req, resp)
114 if err != nil {
115 return err
116 }
117
118 if respMsg.Status != success {
119 return respMsg
120 }
121
122 if result == nil {
123 return nil
124 }
125
126 err = json.Unmarshal(respMsg.ResponseData, result)
127 if err != nil {
128 return errutils.NewUnmarshalError(req, resp.StatusCode, respMsg.ResponseData, err)
129 }
130
131 return nil
132 }
133
134 // GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord
135 // equivalence is determined by Destination and RecortType attributes
136 // returns index of given DNSRecord in given array of DNSRecords.
137 func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) {
138 for index, element := range records {
139 if record.Destination == element.Destination && record.RecordType == element.RecordType {
140 return index, nil
141 }
142 }
143
144 return -1, errors.New("no DNS Record found")
145 }
146
147 func newJSONRequest(ctx context.Context, method, endpoint string, payload any) (*http.Request, error) {
148 buf := new(bytes.Buffer)
149
150 if payload != nil {
151 err := json.NewEncoder(buf).Encode(payload)
152 if err != nil {
153 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
154 }
155 }
156
157 req, err := http.NewRequestWithContext(ctx, method, endpoint, buf)
158 if err != nil {
159 return nil, fmt.Errorf("unable to create request: %w", err)
160 }
161
162 req.Header.Set("Accept", "application/json")
163
164 if payload != nil {
165 req.Header.Set("Content-Type", "application/json")
166 }
167
168 return req, nil
169 }
170
171 func unmarshalResponseMsg(req *http.Request, resp *http.Response) (*ResponseMsg, error) {
172 raw, err := io.ReadAll(resp.Body)
173 if err != nil {
174 return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
175 }
176
177 var respMsg ResponseMsg
178
179 err = json.Unmarshal(raw, &respMsg)
180 if err != nil {
181 return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
182 }
183
184 return &respMsg, nil
185 }
186