client.go raw
1 package internal
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "strconv"
12 "strings"
13 "time"
14
15 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
16 querystring "github.com/google/go-querystring/query"
17 )
18
19 const defaultBaseURL = "https://api.panel.octenium.com/"
20
21 const statusSuccess = "success"
22
23 // Client the Octenium API client.
24 type Client struct {
25 apiKey string
26
27 BaseURL *url.URL
28 HTTPClient *http.Client
29 }
30
31 // NewClient creates a new Client.
32 func NewClient(apiKey string) (*Client, error) {
33 if apiKey == "" {
34 return nil, errors.New("credentials missing")
35 }
36
37 baseURL, _ := url.Parse(defaultBaseURL)
38
39 return &Client{
40 apiKey: apiKey,
41 BaseURL: baseURL,
42 HTTPClient: &http.Client{Timeout: 10 * time.Second},
43 }, nil
44 }
45
46 // ListDomains retrieves a list of domains.
47 // https://octenium.com/api#tag/Domains/operation/listdomains
48 func (c *Client) ListDomains(ctx context.Context, domain string) (map[string]Domain, error) {
49 endpoint := c.BaseURL.JoinPath("domains")
50
51 data := endpoint.Query()
52 data.Set("domain-name", domain)
53 endpoint.RawQuery = data.Encode()
54
55 req, err := newRequest(ctx, http.MethodGet, endpoint, nil)
56 if err != nil {
57 return nil, err
58 }
59
60 result := &DomainsResponse{}
61
62 err = c.do(req, result)
63 if err != nil {
64 return nil, err
65 }
66
67 return result.Domains, nil
68 }
69
70 // ListDNSRecords retrieves a list of DNS records.
71 // https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-list
72 func (c *Client) ListDNSRecords(ctx context.Context, orderID, recordType string) ([]Record, error) {
73 endpoint := c.BaseURL.JoinPath("domains", "dns-records", "list")
74
75 data := make(url.Values)
76 data.Set("order-id", orderID)
77 data.Set("types[]", recordType)
78
79 req, err := newRequest(ctx, http.MethodPost, endpoint, data)
80 if err != nil {
81 return nil, err
82 }
83
84 result := &ListRecordsResponse{}
85
86 err = c.do(req, result)
87 if err != nil {
88 return nil, err
89 }
90
91 return result.Records, nil
92 }
93
94 // AddDNSRecord adds a DNS record.
95 // https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-add
96 func (c *Client) AddDNSRecord(ctx context.Context, orderID string, record Record) (*Record, error) {
97 endpoint := c.BaseURL.JoinPath("domains", "dns-records", "add")
98
99 data, err := querystring.Values(record)
100 if err != nil {
101 return nil, err
102 }
103
104 data.Set("order-id", orderID)
105
106 req, err := newRequest(ctx, http.MethodPost, endpoint, data)
107 if err != nil {
108 return nil, err
109 }
110
111 result := &AddRecordResponse{}
112
113 err = c.do(req, result)
114 if err != nil {
115 return nil, err
116 }
117
118 return result.Record, nil
119 }
120
121 // DeleteDNSRecord deletes a DNS record.
122 // https://octenium.com/api#tag/Domains-DNS/operation/domains-dns-records-delete
123 func (c *Client) DeleteDNSRecord(ctx context.Context, orderID string, recordID int) (*DeletedRecordInfo, error) {
124 endpoint := c.BaseURL.JoinPath("domains", "dns-records", "delete")
125
126 data := make(url.Values)
127 data.Set("order-id", orderID)
128 data.Set("line", strconv.Itoa(recordID))
129
130 req, err := newRequest(ctx, http.MethodPost, endpoint, data)
131 if err != nil {
132 return nil, err
133 }
134
135 result := &DeleteRecordResponse{}
136
137 err = c.do(req, result)
138 if err != nil {
139 return nil, err
140 }
141
142 return result.Deleted, nil
143 }
144
145 func (c *Client) do(req *http.Request, result any) error {
146 req.Header.Set("X-Api-Key", c.apiKey)
147
148 resp, err := c.HTTPClient.Do(req)
149 if err != nil {
150 return errutils.NewHTTPDoError(req, err)
151 }
152
153 defer func() { _ = resp.Body.Close() }()
154
155 if resp.StatusCode/100 != 2 {
156 raw, _ := io.ReadAll(resp.Body)
157
158 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
159 }
160
161 raw, err := io.ReadAll(resp.Body)
162 if err != nil {
163 return errutils.NewReadResponseError(req, resp.StatusCode, err)
164 }
165
166 var response APIResponse
167
168 err = json.Unmarshal(raw, &response)
169 if err != nil {
170 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
171 }
172
173 if response.Status != statusSuccess {
174 return fmt.Errorf("unexpected status: %s: %s", response.Status, response.Error)
175 }
176
177 err = json.Unmarshal(response.Response, result)
178 if err != nil {
179 return errutils.NewUnmarshalError(req, resp.StatusCode, response.Response, err)
180 }
181
182 return nil
183 }
184
185 func newRequest(ctx context.Context, method string, endpoint *url.URL, payload url.Values) (*http.Request, error) {
186 var body io.Reader = http.NoBody
187
188 if method == http.MethodPost && payload != nil {
189 body = strings.NewReader(payload.Encode())
190 }
191
192 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
193 if err != nil {
194 return nil, fmt.Errorf("unable to create request: %w", err)
195 }
196
197 req.Header.Set("Accept", "application/json")
198
199 if method == http.MethodPost && payload != nil {
200 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
201 }
202
203 return req, nil
204 }
205