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 "net/url"
12 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 )
16
17 const defaultBaseURL = "https://developers.hostinger.com"
18
19 const authorizationHeader = "Authorization"
20
21 // Client the Hostinger API client.
22 type Client struct {
23 token string
24
25 BaseURL *url.URL
26 HTTPClient *http.Client
27 }
28
29 // NewClient creates a new Client.
30 func NewClient(token string) (*Client, error) {
31 if token == "" {
32 return nil, errors.New("credentials missing")
33 }
34
35 baseURL, _ := url.Parse(defaultBaseURL)
36
37 return &Client{
38 token: token,
39 BaseURL: baseURL,
40 HTTPClient: &http.Client{Timeout: 10 * time.Second},
41 }, nil
42 }
43
44 // GetDNSRecords retrieves DNS zone records for a specific domain.
45 // https://developers.hostinger.com/#tag/dns-zone/get/api/dns/v1/zones/{domain}
46 func (c *Client) GetDNSRecords(ctx context.Context, domain string) ([]RecordSet, error) {
47 endpoint := c.BaseURL.JoinPath("/api/dns/v1/zones/", domain)
48
49 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
50 if err != nil {
51 return nil, err
52 }
53
54 var result []RecordSet
55
56 err = c.do(req, &result)
57 if err != nil {
58 return nil, err
59 }
60
61 return result, nil
62 }
63
64 // UpdateDNSRecords updates DNS records for the selected domain.
65 // https://developers.hostinger.com/#tag/dns-zone/put/api/dns/v1/zones/{domain}
66 func (c *Client) UpdateDNSRecords(ctx context.Context, domain string, zone ZoneRequest) error {
67 endpoint := c.BaseURL.JoinPath("/api/dns/v1/zones/", domain)
68
69 req, err := newJSONRequest(ctx, http.MethodPut, endpoint, zone)
70 if err != nil {
71 return err
72 }
73
74 return c.do(req, nil)
75 }
76
77 // DeleteDNSRecords deletes DNS records for the selected domain.
78 // https://developers.hostinger.com/#tag/dns-zone/delete/api/dns/v1/zones/{domain}
79 func (c *Client) DeleteDNSRecords(ctx context.Context, domain string, filters []Filter) error {
80 endpoint := c.BaseURL.JoinPath("/api/dns/v1/zones/", domain)
81
82 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, Filters{Filters: filters})
83 if err != nil {
84 return err
85 }
86
87 return c.do(req, nil)
88 }
89
90 func (c *Client) do(req *http.Request, result any) error {
91 req.Header.Set(authorizationHeader, "Bearer "+c.token)
92
93 resp, err := c.HTTPClient.Do(req)
94 if err != nil {
95 return errutils.NewHTTPDoError(req, err)
96 }
97
98 defer func() { _ = resp.Body.Close() }()
99
100 if resp.StatusCode/100 != 2 {
101 return parseError(req, resp)
102 }
103
104 if result == nil {
105 return nil
106 }
107
108 raw, err := io.ReadAll(resp.Body)
109 if err != nil {
110 return errutils.NewReadResponseError(req, resp.StatusCode, err)
111 }
112
113 err = json.Unmarshal(raw, result)
114 if err != nil {
115 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
116 }
117
118 return nil
119 }
120
121 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
122 buf := new(bytes.Buffer)
123
124 if payload != nil {
125 err := json.NewEncoder(buf).Encode(payload)
126 if err != nil {
127 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
128 }
129 }
130
131 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
132 if err != nil {
133 return nil, fmt.Errorf("unable to create request: %w", err)
134 }
135
136 req.Header.Set("Accept", "application/json")
137
138 if payload != nil {
139 req.Header.Set("Content-Type", "application/json")
140 }
141
142 return req, nil
143 }
144
145 func parseError(req *http.Request, resp *http.Response) error {
146 raw, _ := io.ReadAll(resp.Body)
147
148 var errAPI APIError
149
150 err := json.Unmarshal(raw, &errAPI)
151 if err != nil {
152 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
153 }
154
155 return &errAPI
156 }
157