270 lines
7 KiB
Go
270 lines
7 KiB
Go
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
|
|
)
|
|
|
|
// Object types.
|
|
const (
|
|
ConfigType = "Configuration"
|
|
ViewType = "View"
|
|
ZoneType = "Zone"
|
|
TXTType = "TXTRecord"
|
|
)
|
|
|
|
const authorizationHeader = "Authorization"
|
|
|
|
type Client struct {
|
|
username string
|
|
password string
|
|
|
|
tokenExp *regexp.Regexp
|
|
|
|
baseURL *url.URL
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
func NewClient(baseURL string, username, password string) *Client {
|
|
bu, _ := url.Parse(baseURL)
|
|
|
|
return &Client{
|
|
username: username,
|
|
password: password,
|
|
tokenExp: regexp.MustCompile("BAMAuthToken: [^ ]+"),
|
|
baseURL: bu,
|
|
HTTPClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
|
|
q := endpoint.Query()
|
|
q.Set("entityId", strconv.FormatUint(uint64(entityID), 10))
|
|
endpoint.RawQuery = q.Encode()
|
|
|
|
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := c.doAuthenticated(ctx, req)
|
|
if err != nil {
|
|
return errutils.NewHTTPDoError(req, err)
|
|
}
|
|
|
|
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 {
|
|
return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddEntity A generic method for adding configurations, DNS zones, and DNS resource records.
|
|
// 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()
|
|
|
|
req, err := newJSONRequest(ctx, http.MethodPost, endpoint, entity)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
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 {
|
|
return 0, errutils.NewUnexpectedResponseStatusCodeError(req, resp)
|
|
}
|
|
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
|
|
// addEntity responds only with body text containing the ID of the created record
|
|
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.
|
|
// 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()
|
|
|
|
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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 {
|
|
return nil, errutils.NewUnexpectedResponseStatusCodeError(req, resp)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
return &entity, nil
|
|
}
|
|
|
|
// Delete Deletes an object using the generic delete method.
|
|
// 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()
|
|
|
|
req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := c.doAuthenticated(ctx, req)
|
|
if err != nil {
|
|
return errutils.NewHTTPDoError(req, err)
|
|
}
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// The API doc says that 204 is expected but in the reality 200 is returned.
|
|
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
|
|
return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LookupViewID Find the DNS view with the given name within.
|
|
func (c *Client) LookupViewID(ctx context.Context, configName, viewName string) (uint, error) {
|
|
// Lookup the entity ID of the configuration named in our properties.
|
|
conf, err := c.GetEntityByName(ctx, 0, configName, ConfigType)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
view, err := c.GetEntityByName(ctx, conf.ID, viewName, ViewType)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return view.ID, nil
|
|
}
|
|
|
|
// LookupParentZoneID Return the entityId of the parent zone by recursing from the root view.
|
|
// Also return the simple name of the host.
|
|
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-- {
|
|
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
|
|
}
|
|
|
|
func (c *Client) createEndpoint(resource string) *url.URL {
|
|
return c.baseURL.JoinPath("Services", "REST", "v1", resource)
|
|
}
|
|
|
|
func (c *Client) doAuthenticated(ctx context.Context, req *http.Request) (*http.Response, error) {
|
|
tok := getToken(ctx)
|
|
if tok != "" {
|
|
req.Header.Set(authorizationHeader, tok)
|
|
}
|
|
|
|
return c.HTTPClient.Do(req)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
if payload != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
return req, nil
|
|
}
|