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