client.go raw
1 // Package freemyip contains a client of the DNS API of freemyip.
2 package freemyip
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "strings"
12 "time"
13
14 querystring "github.com/google/go-querystring/query"
15 )
16
17 // RootDomain the root domain of all domains.
18 const RootDomain = "freemyip.com"
19
20 const defaultBaseURL = "https://freemyip.com"
21
22 const (
23 codeError = "ERROR"
24 codeOK = "OK"
25 )
26
27 type query struct {
28 Token string `url:"token"`
29 Domain string `url:"domain"`
30 TXT string `url:"txt,omitempty"`
31 MyIP string `url:"myip,omitempty"`
32 Delete string `url:"delete,omitempty"`
33 Verbose string `url:"verbose,omitempty"`
34 }
35
36 // Client an API client for freemyip.
37 type Client struct {
38 HTTPClient *http.Client
39 baseURL *url.URL
40
41 token string
42 verbose bool
43 }
44
45 // New creates a new Client.
46 func New(token string, verbose bool) *Client {
47 baseURL, _ := url.Parse(defaultBaseURL)
48
49 return &Client{
50 HTTPClient: &http.Client{Timeout: 10 * time.Second},
51 baseURL: baseURL,
52 token: token,
53 verbose: verbose,
54 }
55 }
56
57 // CheckIP checks your current external IP address.
58 func (c *Client) CheckIP(ctx context.Context) (string, error) {
59 endpoint := c.baseURL.JoinPath("checkip")
60
61 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
62 if err != nil {
63 return "", fmt.Errorf("creates request: %w", err)
64 }
65
66 resp, err := c.HTTPClient.Do(req)
67 if err != nil {
68 return "", fmt.Errorf("do API call: %w", err)
69 }
70
71 defer func() { _ = resp.Body.Close() }()
72
73 if resp.StatusCode != http.StatusOK {
74 all, _ := io.ReadAll(resp.Body)
75 return "", fmt.Errorf("error: %d: %s", resp.StatusCode, string(all))
76 }
77
78 all, err := io.ReadAll(resp.Body)
79 if err != nil {
80 return "", fmt.Errorf("reads response body: %w", err)
81 }
82
83 return strings.TrimSpace(string(all)), nil
84 }
85
86 // UpdateDomain updates a domain.
87 // - `domain` is the custom part of the real domain. (ex: `YOUR_DOMAIN` in `YOUR_DOMAIN.freemyip.com`)
88 // - `myIP` is optional.
89 func (c *Client) UpdateDomain(ctx context.Context, domain, myIP string) (string, error) {
90 q := query{
91 Token: c.token,
92 Domain: fmt.Sprintf("%s.%s", domain, RootDomain),
93 MyIP: myIP,
94 Verbose: boolToString(c.verbose),
95 }
96
97 return c.doUpdate(ctx, q)
98 }
99
100 // DeleteDomain deletes a domain.
101 // - `domain` is the custom part of the real domain. (ex: `YOUR_DOMAIN` in `YOUR_DOMAIN.freemyip.com`)
102 func (c *Client) DeleteDomain(ctx context.Context, domain string) (string, error) {
103 q := query{
104 Token: c.token,
105 Domain: fmt.Sprintf("%s.%s", domain, RootDomain),
106 Delete: "yes",
107 Verbose: boolToString(c.verbose),
108 }
109
110 return c.doUpdate(ctx, q)
111 }
112
113 // EditTXTRecord creates or updates a TXT record value for a domain.
114 // - `domain` is the custom part of the real domain. (ex: `YOUR_DOMAIN` in `YOUR_DOMAIN.freemyip.com`)
115 // - `value` is the TXT record content.
116 func (c *Client) EditTXTRecord(ctx context.Context, domain, value string) (string, error) {
117 q := query{
118 Token: c.token,
119 Domain: fmt.Sprintf("%s.%s", domain, RootDomain),
120 TXT: value,
121 Verbose: boolToString(c.verbose),
122 }
123
124 return c.doUpdate(ctx, q)
125 }
126
127 // DeleteTXTRecord delete a TXT record for a domain.
128 // - `domain` is the custom part of the real domain. (ex: `YOUR_DOMAIN` in `YOUR_DOMAIN.freemyip.com`)
129 // - `value` is the TXT record content.
130 func (c *Client) DeleteTXTRecord(ctx context.Context, domain string) (string, error) {
131 q := query{
132 Token: c.token,
133 Domain: fmt.Sprintf("%s.%s", domain, RootDomain),
134 TXT: "null",
135 Verbose: boolToString(c.verbose),
136 }
137
138 return c.doUpdate(ctx, q)
139 }
140
141 func (c *Client) doUpdate(ctx context.Context, q query) (string, error) {
142 endpoint := c.baseURL.JoinPath("update")
143
144 values, err := querystring.Values(q)
145 if err != nil {
146 return "", fmt.Errorf("query parameters: %w", err)
147 }
148
149 endpoint.RawQuery = values.Encode()
150
151 req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody)
152 if err != nil {
153 return "", fmt.Errorf("creates request: %w", err)
154 }
155
156 resp, err := c.HTTPClient.Do(req)
157 if err != nil {
158 return "", fmt.Errorf("do API call: %w", err)
159 }
160
161 defer func() { _ = resp.Body.Close() }()
162
163 if resp.StatusCode != http.StatusOK {
164 all, _ := io.ReadAll(resp.Body)
165 return "", fmt.Errorf("error: %d: %s", resp.StatusCode, string(all))
166 }
167
168 all, err := io.ReadAll(resp.Body)
169 if err != nil {
170 return "", fmt.Errorf("reads response body: %w", err)
171 }
172
173 body := strings.TrimSpace(string(all))
174
175 parts := strings.SplitN(body, "\n", 2)
176
177 switch parts[0] {
178 case codeError:
179 return "", errors.New(strings.Join(parts, " "))
180 case codeOK:
181 return body, nil
182 default:
183 return "", errors.New(strings.Join(parts, " "))
184 }
185 }
186
187 func boolToString(v bool) string {
188 if v {
189 return "yes"
190 }
191
192 return ""
193 }
194