1package internal 2 3import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "time" 11) 12 13// defaultBaseURL for reaching the jSON-based API-Endpoint of netcup. 14const defaultBaseURL = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" 15 16// success response status. 17const success = "success" 18 19// Request wrapper as specified in netcup wiki 20// needed for every request to netcup API around *Msg. 21// https://www.netcup-wiki.de/wiki/CCP_API#Anmerkungen_zu_JSON-Requests 22type Request struct { 23 Action string `json:"action"` 24 Param interface{} `json:"param"` 25} 26 27// LoginRequest as specified in netcup WSDL. 28// https://ccp.netcup.net/run/webservice/servers/endpoint.php#login 29type LoginRequest struct { 30 CustomerNumber string `json:"customernumber"` 31 APIKey string `json:"apikey"` 32 APIPassword string `json:"apipassword"` 33 ClientRequestID string `json:"clientrequestid,omitempty"` 34} 35 36// LogoutRequest as specified in netcup WSDL. 37// https://ccp.netcup.net/run/webservice/servers/endpoint.php#logout 38type LogoutRequest struct { 39 CustomerNumber string `json:"customernumber"` 40 APIKey string `json:"apikey"` 41 APISessionID string `json:"apisessionid"` 42 ClientRequestID string `json:"clientrequestid,omitempty"` 43} 44 45// UpdateDNSRecordsRequest as specified in netcup WSDL. 46// https://ccp.netcup.net/run/webservice/servers/endpoint.php#updateDnsRecords 47type UpdateDNSRecordsRequest struct { 48 DomainName string `json:"domainname"` 49 CustomerNumber string `json:"customernumber"` 50 APIKey string `json:"apikey"` 51 APISessionID string `json:"apisessionid"` 52 ClientRequestID string `json:"clientrequestid,omitempty"` 53 DNSRecordSet DNSRecordSet `json:"dnsrecordset"` 54} 55 56// DNSRecordSet as specified in netcup WSDL. 57// needed in UpdateDNSRecordsRequest. 58// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecordset 59type DNSRecordSet struct { 60 DNSRecords []DNSRecord `json:"dnsrecords"` 61} 62 63// InfoDNSRecordsRequest as specified in netcup WSDL. 64// https://ccp.netcup.net/run/webservice/servers/endpoint.php#infoDnsRecords 65type InfoDNSRecordsRequest struct { 66 DomainName string `json:"domainname"` 67 CustomerNumber string `json:"customernumber"` 68 APIKey string `json:"apikey"` 69 APISessionID string `json:"apisessionid"` 70 ClientRequestID string `json:"clientrequestid,omitempty"` 71} 72 73// DNSRecord as specified in netcup WSDL. 74// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecord 75type DNSRecord struct { 76 ID int `json:"id,string,omitempty"` 77 Hostname string `json:"hostname"` 78 RecordType string `json:"type"` 79 Priority string `json:"priority,omitempty"` 80 Destination string `json:"destination"` 81 DeleteRecord bool `json:"deleterecord,omitempty"` 82 State string `json:"state,omitempty"` 83 TTL int `json:"ttl,omitempty"` 84} 85 86// ResponseMsg as specified in netcup WSDL. 87// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Responsemessage 88type ResponseMsg struct { 89 ServerRequestID string `json:"serverrequestid"` 90 ClientRequestID string `json:"clientrequestid,omitempty"` 91 Action string `json:"action"` 92 Status string `json:"status"` 93 StatusCode int `json:"statuscode"` 94 ShortMessage string `json:"shortmessage"` 95 LongMessage string `json:"longmessage"` 96 ResponseData json.RawMessage `json:"responsedata,omitempty"` 97} 98 99func (r *ResponseMsg) Error() string { 100 return fmt.Sprintf("an error occurred during the action %s: [Status=%s, StatusCode=%d, ShortMessage=%s, LongMessage=%s]", 101 r.Action, r.Status, r.StatusCode, r.ShortMessage, r.LongMessage) 102} 103 104// LoginResponse response to login action. 105type LoginResponse struct { 106 APISessionID string `json:"apisessionid"` 107} 108 109// InfoDNSRecordsResponse response to infoDnsRecords action. 110type InfoDNSRecordsResponse struct { 111 APISessionID string `json:"apisessionid"` 112 DNSRecords []DNSRecord `json:"dnsrecords,omitempty"` 113} 114 115// Client netcup DNS client. 116type Client struct { 117 customerNumber string 118 apiKey string 119 apiPassword string 120 HTTPClient *http.Client 121 BaseURL string 122} 123 124// NewClient creates a netcup DNS client. 125func NewClient(customerNumber, apiKey, apiPassword string) (*Client, error) { 126 if customerNumber == "" || apiKey == "" || apiPassword == "" { 127 return nil, errors.New("credentials missing") 128 } 129 130 return &Client{ 131 customerNumber: customerNumber, 132 apiKey: apiKey, 133 apiPassword: apiPassword, 134 BaseURL: defaultBaseURL, 135 HTTPClient: &http.Client{ 136 Timeout: 10 * time.Second, 137 }, 138 }, nil 139} 140 141// Login performs the login as specified by the netcup WSDL 142// returns sessionID needed to perform remaining actions. 143// https://ccp.netcup.net/run/webservice/servers/endpoint.php 144func (c *Client) Login() (string, error) { 145 payload := &Request{ 146 Action: "login", 147 Param: &LoginRequest{ 148 CustomerNumber: c.customerNumber, 149 APIKey: c.apiKey, 150 APIPassword: c.apiPassword, 151 ClientRequestID: "", 152 }, 153 } 154 155 var responseData LoginResponse 156 err := c.doRequest(payload, &responseData) 157 if err != nil { 158 return "", fmt.Errorf("loging error: %w", err) 159 } 160 161 return responseData.APISessionID, nil 162} 163 164// Logout performs the logout with the supplied sessionID as specified by the netcup WSDL. 165// https://ccp.netcup.net/run/webservice/servers/endpoint.php 166func (c *Client) Logout(sessionID string) error { 167 payload := &Request{ 168 Action: "logout", 169 Param: &LogoutRequest{ 170 CustomerNumber: c.customerNumber, 171 APIKey: c.apiKey, 172 APISessionID: sessionID, 173 ClientRequestID: "", 174 }, 175 } 176 177 err := c.doRequest(payload, nil) 178 if err != nil { 179 return fmt.Errorf("logout error: %w", err) 180 } 181 182 return nil 183} 184 185// UpdateDNSRecord performs an update of the DNSRecords as specified by the netcup WSDL. 186// https://ccp.netcup.net/run/webservice/servers/endpoint.php 187func (c *Client) UpdateDNSRecord(sessionID, domainName string, records []DNSRecord) error { 188 payload := &Request{ 189 Action: "updateDnsRecords", 190 Param: UpdateDNSRecordsRequest{ 191 DomainName: domainName, 192 CustomerNumber: c.customerNumber, 193 APIKey: c.apiKey, 194 APISessionID: sessionID, 195 ClientRequestID: "", 196 DNSRecordSet: DNSRecordSet{DNSRecords: records}, 197 }, 198 } 199 200 err := c.doRequest(payload, nil) 201 if err != nil { 202 return fmt.Errorf("error when sending the request: %w", err) 203 } 204 205 return nil 206} 207 208// GetDNSRecords retrieves all dns records of an DNS-Zone as specified by the netcup WSDL 209// returns an array of DNSRecords. 210// https://ccp.netcup.net/run/webservice/servers/endpoint.php 211func (c *Client) GetDNSRecords(hostname, apiSessionID string) ([]DNSRecord, error) { 212 payload := &Request{ 213 Action: "infoDnsRecords", 214 Param: InfoDNSRecordsRequest{ 215 DomainName: hostname, 216 CustomerNumber: c.customerNumber, 217 APIKey: c.apiKey, 218 APISessionID: apiSessionID, 219 ClientRequestID: "", 220 }, 221 } 222 223 var responseData InfoDNSRecordsResponse 224 err := c.doRequest(payload, &responseData) 225 if err != nil { 226 return nil, fmt.Errorf("error when sending the request: %w", err) 227 } 228 229 return responseData.DNSRecords, nil 230} 231 232// doRequest marshals given body to JSON, send the request to netcup API 233// and returns body of response. 234func (c *Client) doRequest(payload, responseData interface{}) error { 235 body, err := json.Marshal(payload) 236 if err != nil { 237 return err 238 } 239 240 req, err := http.NewRequest(http.MethodPost, c.BaseURL, bytes.NewReader(body)) 241 if err != nil { 242 return err 243 } 244 245 req.Close = true 246 req.Header.Set("content-type", "application/json") 247 248 resp, err := c.HTTPClient.Do(req) 249 if err != nil { 250 return err 251 } 252 253 if err = checkResponse(resp); err != nil { 254 return err 255 } 256 257 respMsg, err := decodeResponseMsg(resp) 258 if err != nil { 259 return err 260 } 261 262 if respMsg.Status != success { 263 return respMsg 264 } 265 266 if responseData != nil { 267 err = json.Unmarshal(respMsg.ResponseData, responseData) 268 if err != nil { 269 return fmt.Errorf("%v: unmarshaling %T error: %w: %s", 270 respMsg, responseData, err, string(respMsg.ResponseData)) 271 } 272 } 273 274 return nil 275} 276 277func checkResponse(resp *http.Response) error { 278 if resp.StatusCode > 299 { 279 if resp.Body == nil { 280 return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode) 281 } 282 283 defer resp.Body.Close() 284 285 raw, err := io.ReadAll(resp.Body) 286 if err != nil { 287 return fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err) 288 } 289 290 return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw)) 291 } 292 293 return nil 294} 295 296func decodeResponseMsg(resp *http.Response) (*ResponseMsg, error) { 297 if resp.Body == nil { 298 return nil, fmt.Errorf("response body is nil, status code=%d", resp.StatusCode) 299 } 300 301 defer resp.Body.Close() 302 303 raw, err := io.ReadAll(resp.Body) 304 if err != nil { 305 return nil, fmt.Errorf("unable to read body: status code=%d, error=%w", resp.StatusCode, err) 306 } 307 308 var respMsg ResponseMsg 309 err = json.Unmarshal(raw, &respMsg) 310 if err != nil { 311 return nil, fmt.Errorf("unmarshaling %T error [status code=%d]: %w: %s", respMsg, resp.StatusCode, err, string(raw)) 312 } 313 314 return &respMsg, nil 315} 316 317// GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord 318// equivalence is determined by Destination and RecortType attributes 319// returns index of given DNSRecord in given array of DNSRecords. 320func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) { 321 for index, element := range records { 322 if record.Destination == element.Destination && record.RecordType == element.RecordType { 323 return index, nil 324 } 325 } 326 return -1, errors.New("no DNS Record found") 327} 328