lego/providers/dns/bluecat/internal/client.go

271 lines
7 KiB
Go
Raw Permalink Normal View History

package internal
import (
"bytes"
2023-05-05 07:49:38 +00:00
"context"
"encoding/json"
"fmt"
"io"
"net/http"
2023-05-05 07:49:38 +00:00
"net/url"
"regexp"
"strconv"
"strings"
"time"
2023-05-05 07:49:38 +00:00
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
)
// Object types.
const (
ConfigType = "Configuration"
ViewType = "View"
ZoneType = "Zone"
TXTType = "TXTRecord"
)
2023-05-05 07:49:38 +00:00
const authorizationHeader = "Authorization"
2023-05-05 07:49:38 +00:00
type Client struct {
username string
password string
tokenExp *regexp.Regexp
2023-05-05 07:49:38 +00:00
baseURL *url.URL
HTTPClient *http.Client
}
2023-05-05 07:49:38 +00:00
func NewClient(baseURL string, username, password string) *Client {
bu, _ := url.Parse(baseURL)
return &Client{
2023-05-05 07:49:38 +00:00
username: username,
password: password,
tokenExp: regexp.MustCompile("BAMAuthToken: [^ ]+"),
2023-05-05 07:49:38 +00:00
baseURL: bu,
HTTPClient: &http.Client{Timeout: 30 * time.Second},
}
}
2023-05-05 07:49:38 +00:00
// Deploy the DNS config for the specified entity to the authoritative servers.
// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/POST/v1/quickDeploy/9.5.0
func (c *Client) Deploy(ctx context.Context, entityID uint) error {
endpoint := c.createEndpoint("quickDeploy")
2023-05-05 07:49:38 +00:00
q := endpoint.Query()
q.Set("entityId", strconv.FormatUint(uint64(entityID), 10))
endpoint.RawQuery = q.Encode()
2023-05-05 07:49:38 +00:00
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, nil)
if err != nil {
return err
}
2023-05-05 07:49:38 +00:00
resp, err := c.doAuthenticated(ctx, req)
if err != nil {
2023-05-05 07:49:38 +00:00
return errutils.NewHTTPDoError(req, err)
}
2023-05-05 07:49:38 +00:00
defer func() { _ = resp.Body.Close() }()
// The API doc says that 201 is expected but in the reality 200 is return.
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
2023-05-05 07:49:38 +00:00
return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
}
return nil
}
// AddEntity A generic method for adding configurations, DNS zones, and DNS resource records.
2023-05-05 07:49:38 +00:00
// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/POST/v1/addEntity/9.5.0
func (c *Client) AddEntity(ctx context.Context, parentID uint, entity Entity) (uint64, error) {
endpoint := c.createEndpoint("addEntity")
q := endpoint.Query()
q.Set("parentId", strconv.FormatUint(uint64(parentID), 10))
endpoint.RawQuery = q.Encode()
2023-05-05 07:49:38 +00:00
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, entity)
if err != nil {
return 0, err
}
2023-05-05 07:49:38 +00:00
resp, err := c.doAuthenticated(ctx, req)
if err != nil {
return 0, errutils.NewHTTPDoError(req, err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
2023-05-05 07:49:38 +00:00
return 0, errutils.NewUnexpectedResponseStatusCodeError(req, resp)
}
2023-05-05 07:49:38 +00:00
raw, _ := io.ReadAll(resp.Body)
// addEntity responds only with body text containing the ID of the created record
2023-05-05 07:49:38 +00:00
addTxtResp := string(raw)
id, err := strconv.ParseUint(addTxtResp, 10, 64)
if err != nil {
return 0, fmt.Errorf("addEntity request failed: %s", addTxtResp)
}
return id, nil
}
// GetEntityByName Returns objects from the database referenced by their database ID and with its properties fields populated.
2023-05-05 07:49:38 +00:00
// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/GET/v1/getEntityById/9.5.0
func (c *Client) GetEntityByName(ctx context.Context, parentID uint, name, objType string) (*EntityResponse, error) {
endpoint := c.createEndpoint("getEntityByName")
q := endpoint.Query()
q.Set("parentId", strconv.FormatUint(uint64(parentID), 10))
q.Set("name", name)
q.Set("type", objType)
endpoint.RawQuery = q.Encode()
2023-05-05 07:49:38 +00:00
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
2023-05-05 07:49:38 +00:00
resp, err := c.doAuthenticated(ctx, req)
if err != nil {
return nil, errutils.NewHTTPDoError(req, err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
2023-05-05 07:49:38 +00:00
return nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)
}
2023-05-05 07:49:38 +00:00
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
}
var entity EntityResponse
err = json.Unmarshal(raw, &entity)
if err != nil {
return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
}
2023-05-05 07:49:38 +00:00
return &entity, nil
}
// Delete Deletes an object using the generic delete method.
2023-05-05 07:49:38 +00:00
// https://docs.bluecatnetworks.com/r/Address-Manager-Legacy-v1-API-Guide/DELETE/v1/delete/9.5.0
func (c *Client) Delete(ctx context.Context, objectID uint) error {
endpoint := c.createEndpoint("delete")
q := endpoint.Query()
q.Set("objectId", strconv.FormatUint(uint64(objectID), 10))
endpoint.RawQuery = q.Encode()
2023-05-05 07:49:38 +00:00
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}
2023-05-05 07:49:38 +00:00
resp, err := c.doAuthenticated(ctx, req)
if err != nil {
return errutils.NewHTTPDoError(req, err)
}
defer func() { _ = resp.Body.Close() }()
2023-05-05 07:49:38 +00:00
// The API doc says that 204 is expected but in the reality 200 is returned.
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
2023-05-05 07:49:38 +00:00
return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
}
return nil
}
// LookupViewID Find the DNS view with the given name within.
2023-05-05 07:49:38 +00:00
func (c *Client) LookupViewID(ctx context.Context, configName, viewName string) (uint, error) {
// Lookup the entity ID of the configuration named in our properties.
2023-05-05 07:49:38 +00:00
conf, err := c.GetEntityByName(ctx, 0, configName, ConfigType)
if err != nil {
return 0, err
}
2023-05-05 07:49:38 +00:00
view, err := c.GetEntityByName(ctx, conf.ID, viewName, ViewType)
if err != nil {
return 0, err
}
return view.ID, nil
}
// LookupParentZoneID returns the entityId of the parent zone by iterating through the root labels.
// Also return the simple name of the host.
2023-05-05 07:49:38 +00:00
func (c *Client) LookupParentZoneID(ctx context.Context, viewID uint, fqdn string) (uint, string, error) {
if fqdn == "" {
return viewID, "", nil
}
zones := strings.Split(strings.Trim(fqdn, "."), ".")
name := zones[0]
parentViewID := viewID
for i := len(zones) - 1; i > -1; i-- {
2023-05-05 07:49:38 +00:00
zone, err := c.GetEntityByName(ctx, parentViewID, zones[i], ZoneType)
if err != nil {
return 0, "", fmt.Errorf("could not find zone named %s: %w", name, err)
}
if zone == nil || zone.ID == 0 {
break
}
if i > 0 {
name = strings.Join(zones[0:i], ".")
}
parentViewID = zone.ID
}
return parentViewID, name, nil
}
2023-05-05 07:49:38 +00:00
func (c *Client) createEndpoint(resource string) *url.URL {
return c.baseURL.JoinPath("Services", "REST", "v1", resource)
}
2023-05-05 07:49:38 +00:00
func (c *Client) doAuthenticated(ctx context.Context, req *http.Request) (*http.Response, error) {
tok := getToken(ctx)
if tok != "" {
req.Header.Set(authorizationHeader, tok)
}
2023-05-05 07:49:38 +00:00
return c.HTTPClient.Do(req)
}
2023-05-05 07:49:38 +00:00
func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
buf := new(bytes.Buffer)
if payload != nil {
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
}
}
2023-05-05 07:49:38 +00:00
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
if err != nil {
return nil, fmt.Errorf("unable to create request: %w", err)
}
2023-05-05 07:49:38 +00:00
req.Header.Set("Accept", "application/json")
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
2023-05-05 07:49:38 +00:00
return req, nil
}