1package internal 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/url" 10 "strings" 11 "time" 12 13 "github.com/go-acme/lego/v4/challenge/dns01" 14) 15 16const baseURL = "https://api.wedos.com/wapi/json" 17 18const codeOk = 1000 19 20const ( 21 commandPing = "ping" 22 commandDNSDomainCommit = "dns-domain-commit" 23 commandDNSRowsList = "dns-rows-list" 24 commandDNSRowDelete = "dns-row-delete" 25 commandDNSRowAdd = "dns-row-add" 26 commandDNSRowUpdate = "dns-row-update" 27) 28 29type ResponsePayload struct { 30 Code int `json:"code,omitempty"` 31 Result string `json:"result,omitempty"` 32 Timestamp int `json:"timestamp,omitempty"` 33 SvTRID string `json:"svTRID,omitempty"` 34 Command string `json:"command,omitempty"` 35 Data json.RawMessage `json:"data"` 36} 37 38type DNSRow struct { 39 ID string `json:"ID,omitempty"` 40 Name string `json:"name,omitempty"` 41 TTL json.Number `json:"ttl,omitempty" type:"integer"` 42 Type string `json:"rdtype,omitempty"` 43 Data string `json:"rdata"` 44} 45 46type DNSRowRequest struct { 47 ID string `json:"row_id,omitempty"` 48 Domain string `json:"domain,omitempty"` 49 Name string `json:"name,omitempty"` 50 TTL json.Number `json:"ttl,omitempty" type:"integer"` 51 Type string `json:"type,omitempty"` 52 Data string `json:"rdata"` 53} 54 55type APIRequest struct { 56 User string `json:"user,omitempty"` 57 Auth string `json:"auth,omitempty"` 58 Command string `json:"command,omitempty"` 59 Data interface{} `json:"data,omitempty"` 60} 61 62type Client struct { 63 username string 64 password string 65 baseURL string 66 HTTPClient *http.Client 67} 68 69func NewClient(username string, password string) *Client { 70 return &Client{ 71 username: username, 72 password: password, 73 baseURL: baseURL, 74 HTTPClient: &http.Client{Timeout: 10 * time.Second}, 75 } 76} 77 78// GetRecords lists all the records in the zone. 79// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-rows-list/ 80func (c *Client) GetRecords(ctx context.Context, zone string) ([]DNSRow, error) { 81 payload := map[string]interface{}{ 82 "domain": dns01.UnFqdn(zone), 83 } 84 85 resp, err := c.do(ctx, commandDNSRowsList, payload) 86 if err != nil { 87 return nil, err 88 } 89 90 arrayWrapper := struct { 91 Rows []DNSRow `json:"row"` 92 }{} 93 94 err = json.Unmarshal(resp.Data, &arrayWrapper) 95 if err != nil { 96 return nil, err 97 } 98 99 return arrayWrapper.Rows, err 100} 101 102// AddRecord adds a record in the zone, either by updating existing records or creating new ones. 103// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-add-row/ 104// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-update/ 105func (c *Client) AddRecord(ctx context.Context, zone string, record DNSRow) error { 106 payload := DNSRowRequest{ 107 Domain: dns01.UnFqdn(zone), 108 TTL: record.TTL, 109 Type: record.Type, 110 Data: record.Data, 111 } 112 113 cmd := commandDNSRowAdd 114 if record.ID == "" { 115 payload.Name = record.Name 116 } else { 117 cmd = commandDNSRowUpdate 118 payload.ID = record.ID 119 } 120 121 _, err := c.do(ctx, cmd, payload) 122 if err != nil { 123 return err 124 } 125 126 return nil 127} 128 129// DeleteRecord deletes a record from the zone. 130// If a record does not have an ID, it will be looked up. 131// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-row-delete/ 132func (c *Client) DeleteRecord(ctx context.Context, zone string, recordID string) error { 133 payload := DNSRowRequest{ 134 Domain: dns01.UnFqdn(zone), 135 ID: recordID, 136 } 137 138 _, err := c.do(ctx, commandDNSRowDelete, payload) 139 if err != nil { 140 return err 141 } 142 143 return nil 144} 145 146// Commit not really required, all changes will be auto-committed after 5 minutes. 147// https://kb.wedos.com/en/wapi-api-interface/wapi-command-dns-domain-commit/ 148func (c *Client) Commit(ctx context.Context, zone string) error { 149 payload := map[string]interface{}{ 150 "name": dns01.UnFqdn(zone), 151 } 152 153 _, err := c.do(ctx, commandDNSDomainCommit, payload) 154 if err != nil { 155 return err 156 } 157 158 return nil 159} 160 161func (c *Client) Ping(ctx context.Context) error { 162 _, err := c.do(ctx, commandPing, nil) 163 if err != nil { 164 return err 165 } 166 167 return nil 168} 169 170func (c *Client) do(ctx context.Context, command string, payload interface{}) (*ResponsePayload, error) { 171 requestObject := map[string]interface{}{ 172 "request": APIRequest{ 173 User: c.username, 174 Auth: authToken(c.username, c.password), 175 Command: command, 176 Data: payload, 177 }, 178 } 179 180 jsonBytes, err := json.Marshal(requestObject) 181 if err != nil { 182 return nil, err 183 } 184 185 form := url.Values{} 186 form.Add("request", string(jsonBytes)) 187 188 req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL, strings.NewReader(form.Encode())) 189 if err != nil { 190 return nil, err 191 } 192 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 193 194 resp, err := c.HTTPClient.Do(req) 195 if err != nil { 196 return nil, err 197 } 198 199 body, err := io.ReadAll(resp.Body) 200 if err != nil { 201 return nil, err 202 } 203 204 if resp.StatusCode/100 != 2 { 205 return nil, fmt.Errorf("API error, status code: %d", resp.StatusCode) 206 } 207 208 responseWrapper := struct { 209 Response ResponsePayload `json:"response"` 210 }{} 211 212 err = json.Unmarshal(body, &responseWrapper) 213 if err != nil { 214 return nil, err 215 } 216 217 if responseWrapper.Response.Code != codeOk { 218 return nil, fmt.Errorf("wedos responded with error code %d = %s", responseWrapper.Response.Code, responseWrapper.Response.Result) 219 } 220 221 return &responseWrapper.Response, err 222} 223