forked from TrueCloudLab/lego
314 lines
7.7 KiB
Go
314 lines
7.7 KiB
Go
|
package internal
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"regexp"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// Object types.
|
||
|
const (
|
||
|
ConfigType = "Configuration"
|
||
|
ViewType = "View"
|
||
|
ZoneType = "Zone"
|
||
|
TXTType = "TXTRecord"
|
||
|
)
|
||
|
|
||
|
type Client struct {
|
||
|
HTTPClient *http.Client
|
||
|
|
||
|
baseURL string
|
||
|
|
||
|
token string
|
||
|
tokenExp *regexp.Regexp
|
||
|
}
|
||
|
|
||
|
func NewClient(baseURL string) *Client {
|
||
|
return &Client{
|
||
|
HTTPClient: &http.Client{Timeout: 30 * time.Second},
|
||
|
baseURL: baseURL,
|
||
|
tokenExp: regexp.MustCompile("BAMAuthToken: [^ ]+"),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Login Logs in as API user.
|
||
|
// Authenticates and receives a token to be used in for subsequent requests.
|
||
|
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/GET/v1/login/9.1.0
|
||
|
func (c *Client) Login(username, password string) error {
|
||
|
queryArgs := map[string]string{
|
||
|
"username": username,
|
||
|
"password": password,
|
||
|
}
|
||
|
|
||
|
resp, err := c.sendRequest(http.MethodGet, "login", nil, queryArgs)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
data, _ := io.ReadAll(resp.Body)
|
||
|
return &APIError{
|
||
|
StatusCode: resp.StatusCode,
|
||
|
Resource: "login",
|
||
|
Message: string(data),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
authBytes, err := io.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
authResp := string(authBytes)
|
||
|
if strings.Contains(authResp, "Authentication Error") {
|
||
|
return fmt.Errorf("request failed: %s", strings.Trim(authResp, `"`))
|
||
|
}
|
||
|
|
||
|
// Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username"
|
||
|
c.token = c.tokenExp.FindString(authResp)
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Logout Logs out of the current API session.
|
||
|
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/GET/v1/logout/9.1.0
|
||
|
func (c *Client) Logout() error {
|
||
|
if c.token == "" {
|
||
|
// nothing to do
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
resp, err := c.sendRequest(http.MethodGet, "logout", nil, nil)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
data, _ := io.ReadAll(resp.Body)
|
||
|
return &APIError{
|
||
|
StatusCode: resp.StatusCode,
|
||
|
Resource: "logout",
|
||
|
Message: string(data),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
authBytes, err := io.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
authResp := string(authBytes)
|
||
|
if !strings.Contains(authResp, "successfully") {
|
||
|
return fmt.Errorf("request failed to delete session: %s", strings.Trim(authResp, `"`))
|
||
|
}
|
||
|
|
||
|
c.token = ""
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Deploy the DNS config for the specified entity to the authoritative servers.
|
||
|
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/POST/v1/quickDeploy/9.1.0
|
||
|
func (c *Client) Deploy(entityID uint) error {
|
||
|
queryArgs := map[string]string{
|
||
|
"entityId": strconv.FormatUint(uint64(entityID), 10),
|
||
|
}
|
||
|
|
||
|
resp, err := c.sendRequest(http.MethodPost, "quickDeploy", nil, queryArgs)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer 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 {
|
||
|
data, _ := io.ReadAll(resp.Body)
|
||
|
return &APIError{
|
||
|
StatusCode: resp.StatusCode,
|
||
|
Resource: "quickDeploy",
|
||
|
Message: string(data),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// AddEntity A generic method for adding configurations, DNS zones, and DNS resource records.
|
||
|
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/POST/v1/addEntity/9.1.0
|
||
|
func (c *Client) AddEntity(parentID uint, entity Entity) (uint64, error) {
|
||
|
queryArgs := map[string]string{
|
||
|
"parentId": strconv.FormatUint(uint64(parentID), 10),
|
||
|
}
|
||
|
|
||
|
resp, err := c.sendRequest(http.MethodPost, "addEntity", entity, queryArgs)
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
data, _ := io.ReadAll(resp.Body)
|
||
|
return 0, &APIError{
|
||
|
StatusCode: resp.StatusCode,
|
||
|
Resource: "addEntity",
|
||
|
Message: string(data),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
addTxtBytes, _ := io.ReadAll(resp.Body)
|
||
|
|
||
|
// addEntity responds only with body text containing the ID of the created record
|
||
|
addTxtResp := string(addTxtBytes)
|
||
|
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-API-Guide/GET/v1/getEntityById/9.1.0
|
||
|
func (c *Client) GetEntityByName(parentID uint, name, objType string) (*EntityResponse, error) {
|
||
|
queryArgs := map[string]string{
|
||
|
"parentId": strconv.FormatUint(uint64(parentID), 10),
|
||
|
"name": name,
|
||
|
"type": objType,
|
||
|
}
|
||
|
|
||
|
resp, err := c.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
data, _ := io.ReadAll(resp.Body)
|
||
|
return nil, &APIError{
|
||
|
StatusCode: resp.StatusCode,
|
||
|
Resource: "getEntityByName",
|
||
|
Message: string(data),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var txtRec EntityResponse
|
||
|
if err = json.NewDecoder(resp.Body).Decode(&txtRec); err != nil {
|
||
|
return nil, fmt.Errorf("JSON decode: %w", err)
|
||
|
}
|
||
|
|
||
|
return &txtRec, nil
|
||
|
}
|
||
|
|
||
|
// Delete Deletes an object using the generic delete method.
|
||
|
// https://docs.bluecatnetworks.com/r/Address-Manager-API-Guide/DELETE/v1/delete/9.1.0
|
||
|
func (c *Client) Delete(objectID uint) error {
|
||
|
queryArgs := map[string]string{
|
||
|
"objectId": strconv.FormatUint(uint64(objectID), 10),
|
||
|
}
|
||
|
|
||
|
resp, err := c.sendRequest(http.MethodDelete, "delete", nil, queryArgs)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
// The API doc says that 204 is expected but in the reality 200 is return.
|
||
|
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
|
||
|
data, _ := io.ReadAll(resp.Body)
|
||
|
return &APIError{
|
||
|
StatusCode: resp.StatusCode,
|
||
|
Resource: "delete",
|
||
|
Message: string(data),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// LookupViewID Find the DNS view with the given name within.
|
||
|
func (c *Client) LookupViewID(configName, viewName string) (uint, error) {
|
||
|
// Lookup the entity ID of the configuration named in our properties.
|
||
|
conf, err := c.GetEntityByName(0, configName, ConfigType)
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
|
||
|
view, err := c.GetEntityByName(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(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(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
|
||
|
}
|
||
|
|
||
|
// Send a REST request, using query parameters specified.
|
||
|
// The Authorization header will be set if we have an active auth token.
|
||
|
func (c *Client) sendRequest(method, resource string, payload interface{}, queryParams map[string]string) (*http.Response, error) {
|
||
|
url := fmt.Sprintf("%s/Services/REST/v1/%s", c.baseURL, resource)
|
||
|
|
||
|
body, err := json.Marshal(payload)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
||
|
if c.token != "" {
|
||
|
req.Header.Set("Authorization", c.token)
|
||
|
}
|
||
|
|
||
|
q := req.URL.Query()
|
||
|
for k, v := range queryParams {
|
||
|
q.Set(k, v)
|
||
|
}
|
||
|
req.URL.RawQuery = q.Encode()
|
||
|
|
||
|
return c.HTTPClient.Do(req)
|
||
|
}
|