lego/vendor/github.com/cpu/goacmedns/client.go

192 lines
5 KiB
Go
Raw Normal View History

package goacmedns
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"runtime"
"time"
)
const (
// ua is a custom user-agent identifier
ua = "goacmedns"
)
// userAgent returns a string that can be used as a HTTP request `User-Agent`
// header. It includes the `ua` string alongside the OS and architecture of the
// system.
func userAgent() string {
return fmt.Sprintf("%s (%s; %s)", ua, runtime.GOOS, runtime.GOARCH)
}
var (
// defaultTimeout is used for the httpClient Timeout settings
defaultTimeout = 30 * time.Second
// httpClient is a `http.Client` that is customized with the `defaultTimeout`
httpClient = http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: defaultTimeout,
KeepAlive: defaultTimeout,
}).Dial,
TLSHandshakeTimeout: defaultTimeout,
ResponseHeaderTimeout: defaultTimeout,
ExpectContinueTimeout: 1 * time.Second,
},
}
)
// postAPI makes an HTTP POST request to the given URL, sending the given body
// and attaching the requested custom headers to the request. If there is no
// error the HTTP response body and HTTP response object are returned, otherwise
// an error is returned.. All POST requests include a `User-Agent` header
// populated with the `userAgent` function and a `Content-Type` header of
// `application/json`.
func postAPI(url string, body []byte, headers map[string]string) ([]byte, *http.Response, error) {
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
fmt.Printf("Failed to make req: %s\n", err.Error())
return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent())
for h, v := range headers {
req.Header.Set(h, v)
}
resp, err := httpClient.Do(req)
if err != nil {
fmt.Printf("Failed to do req: %s\n", err.Error())
return nil, resp, err
}
defer func() { _ = resp.Body.Close() }()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Failed to read body: %s\n", err.Error())
return nil, resp, err
}
return respBody, resp, nil
}
// ClientError represents an error from the ACME-DNS server. It holds
// a `Message` describing the operation the client was doing, a `HTTPStatus`
// code returned by the server, and the `Body` of the HTTP Response from the
// server.
type ClientError struct {
// Message is a string describing the client operation that failed
Message string
// HTTPStatus is the HTTP status code the ACME DNS server returned
HTTPStatus int
// Body is the response body the ACME DNS server returned
Body []byte
}
// Error collects all of the ClientError fields into a single string
func (e ClientError) Error() string {
return fmt.Sprintf("%s : status code %d response: %s",
e.Message, e.HTTPStatus, string(e.Body))
}
// newClientError creates a ClientError instance populated with the given
// arguments
func newClientError(msg string, respCode int, respBody []byte) ClientError {
return ClientError{
Message: msg,
HTTPStatus: respCode,
Body: respBody,
}
}
// Client is a struct that can be used to interact with an ACME DNS server to
// register accounts and update TXT records.
type Client struct {
// baseURL is the address of the ACME DNS server
baseURL string
}
// NewClient returns a Client configured to interact with the ACME DNS server at
// the given URL.
func NewClient(url string) Client {
return Client{
baseURL: url,
}
}
// RegisterAccount creates an Account with the ACME DNS server. The optional
// `allowFrom` argument is used to constrain which CIDR ranges can use the
// created Account.
func (c Client) RegisterAccount(allowFrom []string) (Account, error) {
var body []byte
if len(allowFrom) > 0 {
req := struct {
AllowFrom []string
}{
AllowFrom: allowFrom,
}
reqBody, err := json.Marshal(req)
if err != nil {
return Account{}, err
}
body = reqBody
}
url := fmt.Sprintf("%s/register", c.baseURL)
respBody, resp, err := postAPI(url, body, nil)
if err != nil {
return Account{}, err
}
if resp.StatusCode != http.StatusCreated {
return Account{}, newClientError(
"failed to register account", resp.StatusCode, respBody)
}
var acct Account
err = json.Unmarshal(respBody, &acct)
if err != nil {
return Account{}, err
}
return acct, nil
}
// UpdateTXTRecord updates a TXT record with the ACME DNS server to the `value`
// provided using the `account` specified.
func (c Client) UpdateTXTRecord(account Account, value string) error {
update := struct {
SubDomain string
Txt string
}{
SubDomain: account.SubDomain,
Txt: value,
}
updateBody, err := json.Marshal(update)
if err != nil {
fmt.Printf("Failed to marshal update: %s\n", update)
return err
}
headers := map[string]string{
"X-Api-User": account.Username,
"X-Api-Key": account.Password,
}
url := fmt.Sprintf("%s/update", c.baseURL)
respBody, resp, err := postAPI(url, updateBody, headers)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return newClientError(
"failed to update txt record", resp.StatusCode, respBody)
}
return nil
}