dnscontrol/pkg/cloudflare-go/zone.go
Tom Limoncelli 7fd6a74e0c
CLOUDFLAREAPI: CF_REDIRECT/CF_TEMP_REDIRECT should dtrt using Single Redirects (#3002)
Co-authored-by: Josh Zhang <jzhang1@stackoverflow.com>
2024-06-18 17:38:50 -04:00

1077 lines
33 KiB
Go

package cloudflare
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/goccy/go-json"
"golang.org/x/net/idna"
)
var (
// ErrMissingSettingName is for when setting name is required but missing.
ErrMissingSettingName = errors.New("zone setting name required but missing")
)
// Owner describes the resource owner.
type Owner struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
OwnerType string `json:"type"`
}
// Zone describes a Cloudflare zone.
type Zone struct {
ID string `json:"id"`
Name string `json:"name"`
// DevMode contains the time in seconds until development expires (if
// positive) or since it expired (if negative). It will be 0 if never used.
DevMode int `json:"development_mode"`
OriginalNS []string `json:"original_name_servers"`
OriginalRegistrar string `json:"original_registrar"`
OriginalDNSHost string `json:"original_dnshost"`
CreatedOn time.Time `json:"created_on"`
ModifiedOn time.Time `json:"modified_on"`
NameServers []string `json:"name_servers"`
Owner Owner `json:"owner"`
Permissions []string `json:"permissions"`
Plan ZonePlan `json:"plan"`
PlanPending ZonePlan `json:"plan_pending,omitempty"`
Status string `json:"status"`
Paused bool `json:"paused"`
Type string `json:"type"`
Host struct {
Name string
Website string
} `json:"host"`
VanityNS []string `json:"vanity_name_servers"`
Betas []string `json:"betas"`
DeactReason string `json:"deactivation_reason"`
Meta ZoneMeta `json:"meta"`
Account Account `json:"account"`
VerificationKey string `json:"verification_key"`
}
// ZoneMeta describes metadata about a zone.
type ZoneMeta struct {
// custom_certificate_quota is broken - sometimes it's a string, sometimes a number!
// CustCertQuota int `json:"custom_certificate_quota"`
PageRuleQuota int `json:"page_rule_quota"`
WildcardProxiable bool `json:"wildcard_proxiable"`
PhishingDetected bool `json:"phishing_detected"`
}
// ZonePlan contains the plan information for a zone.
type ZonePlan struct {
ZonePlanCommon
LegacyID string `json:"legacy_id"`
IsSubscribed bool `json:"is_subscribed"`
CanSubscribe bool `json:"can_subscribe"`
LegacyDiscount bool `json:"legacy_discount"`
ExternallyManaged bool `json:"externally_managed"`
}
// ZoneRatePlan contains the plan information for a zone.
type ZoneRatePlan struct {
ZonePlanCommon
Components []zoneRatePlanComponents `json:"components,omitempty"`
}
// ZonePlanCommon contains fields used by various Plan endpoints.
type ZonePlanCommon struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Price int `json:"price,omitempty"`
Currency string `json:"currency,omitempty"`
Frequency string `json:"frequency,omitempty"`
}
type zoneRatePlanComponents struct {
Name string `json:"name"`
Default int `json:"Default"`
UnitPrice int `json:"unit_price"`
}
// ZoneID contains only the zone ID.
type ZoneID struct {
ID string `json:"id"`
}
// ZoneResponse represents the response from the Zone endpoint containing a single zone.
type ZoneResponse struct {
Response
Result Zone `json:"result"`
}
// ZonesResponse represents the response from the Zone endpoint containing an array of zones.
type ZonesResponse struct {
Response
Result []Zone `json:"result"`
ResultInfo `json:"result_info"`
}
// ZoneIDResponse represents the response from the Zone endpoint, containing only a zone ID.
type ZoneIDResponse struct {
Response
Result ZoneID `json:"result"`
}
// AvailableZoneRatePlansResponse represents the response from the Available Rate Plans endpoint.
type AvailableZoneRatePlansResponse struct {
Response
Result []ZoneRatePlan `json:"result"`
ResultInfo `json:"result_info"`
}
// AvailableZonePlansResponse represents the response from the Available Plans endpoint.
type AvailableZonePlansResponse struct {
Response
Result []ZonePlan `json:"result"`
ResultInfo
}
// ZoneRatePlanResponse represents the response from the Plan Details endpoint.
type ZoneRatePlanResponse struct {
Response
Result ZoneRatePlan `json:"result"`
}
// ZoneSetting contains settings for a zone.
type ZoneSetting struct {
ID string `json:"id"`
Editable bool `json:"editable"`
ModifiedOn string `json:"modified_on,omitempty"`
Value interface{} `json:"value"`
TimeRemaining int `json:"time_remaining"`
}
// ZoneSettingResponse represents the response from the Zone Setting endpoint.
type ZoneSettingResponse struct {
Response
Result []ZoneSetting `json:"result"`
}
// ZoneSettingSingleResponse represents the response from the Zone Setting endpoint for the specified setting.
type ZoneSettingSingleResponse struct {
Response
Result ZoneSetting `json:"result"`
}
// ZoneSSLSetting contains ssl setting for a zone.
type ZoneSSLSetting struct {
ID string `json:"id"`
Editable bool `json:"editable"`
ModifiedOn string `json:"modified_on"`
Value string `json:"value"`
CertificateStatus string `json:"certificate_status"`
}
// ZoneSSLSettingResponse represents the response from the Zone SSL Setting
// endpoint.
type ZoneSSLSettingResponse struct {
Response
Result ZoneSSLSetting `json:"result"`
}
// ZoneAnalyticsData contains totals and timeseries analytics data for a zone.
type ZoneAnalyticsData struct {
Totals ZoneAnalytics `json:"totals"`
Timeseries []ZoneAnalytics `json:"timeseries"`
}
// zoneAnalyticsDataResponse represents the response from the Zone Analytics Dashboard endpoint.
type zoneAnalyticsDataResponse struct {
Response
Result ZoneAnalyticsData `json:"result"`
}
// ZoneAnalyticsColocation contains analytics data by datacenter.
type ZoneAnalyticsColocation struct {
ColocationID string `json:"colo_id"`
Timeseries []ZoneAnalytics `json:"timeseries"`
}
// zoneAnalyticsColocationResponse represents the response from the Zone Analytics By Co-location endpoint.
type zoneAnalyticsColocationResponse struct {
Response
Result []ZoneAnalyticsColocation `json:"result"`
}
// ZoneAnalytics contains analytics data for a zone.
type ZoneAnalytics struct {
Since time.Time `json:"since"`
Until time.Time `json:"until"`
Requests struct {
All int `json:"all"`
Cached int `json:"cached"`
Uncached int `json:"uncached"`
ContentType map[string]int `json:"content_type"`
Country map[string]int `json:"country"`
SSL struct {
Encrypted int `json:"encrypted"`
Unencrypted int `json:"unencrypted"`
} `json:"ssl"`
HTTPStatus map[string]int `json:"http_status"`
} `json:"requests"`
Bandwidth struct {
All int `json:"all"`
Cached int `json:"cached"`
Uncached int `json:"uncached"`
ContentType map[string]int `json:"content_type"`
Country map[string]int `json:"country"`
SSL struct {
Encrypted int `json:"encrypted"`
Unencrypted int `json:"unencrypted"`
} `json:"ssl"`
} `json:"bandwidth"`
Threats struct {
All int `json:"all"`
Country map[string]int `json:"country"`
Type map[string]int `json:"type"`
} `json:"threats"`
Pageviews struct {
All int `json:"all"`
SearchEngines map[string]int `json:"search_engines"`
} `json:"pageviews"`
Uniques struct {
All int `json:"all"`
}
}
// ZoneAnalyticsOptions represents the optional parameters in Zone Analytics
// endpoint requests.
type ZoneAnalyticsOptions struct {
Since *time.Time
Until *time.Time
Continuous *bool
}
// PurgeCacheRequest represents the request format made to the purge endpoint.
type PurgeCacheRequest struct {
Everything bool `json:"purge_everything,omitempty"`
// Purge by filepath (exact match). Limit of 30
Files []string `json:"files,omitempty"`
// Purge by Tag (Enterprise only):
// https://support.cloudflare.com/hc/en-us/articles/206596608-How-to-Purge-Cache-Using-Cache-Tags-Enterprise-only-
Tags []string `json:"tags,omitempty"`
// Purge by hostname - e.g. "assets.example.com"
Hosts []string `json:"hosts,omitempty"`
// Purge by prefix - e.g. "example.com/css"
Prefixes []string `json:"prefixes,omitempty"`
}
// PurgeCacheResponse represents the response from the purge endpoint.
type PurgeCacheResponse struct {
Response
Result struct {
ID string `json:"id"`
} `json:"result"`
}
// newZone describes a new zone.
type newZone struct {
Name string `json:"name"`
JumpStart bool `json:"jump_start"`
Type string `json:"type"`
// We use a pointer to get a nil type when the field is empty.
// This allows us to completely omit this with json.Marshal().
Account *Account `json:"organization,omitempty"`
}
// FallbackOrigin describes a fallback origin.
type FallbackOrigin struct {
Value string `json:"value"`
ID string `json:"id,omitempty"`
}
// FallbackOriginResponse represents the response from the fallback_origin endpoint.
type FallbackOriginResponse struct {
Response
Result FallbackOrigin `json:"result"`
}
// zoneSubscriptionRatePlanPayload is used to build the JSON payload for
// setting a particular rate plan on an existing zone.
type zoneSubscriptionRatePlanPayload struct {
RatePlan struct {
ID string `json:"id"`
} `json:"rate_plan"`
}
type GetZoneSettingParams struct {
Name string `json:"-"`
PathPrefix string `json:"-"`
}
type UpdateZoneSettingParams struct {
Name string `json:"-"`
PathPrefix string `json:"-"`
Value interface{} `json:"value"`
}
// CreateZone creates a zone on an account.
//
// Setting jumpstart to true will attempt to automatically scan for existing
// DNS records. Setting this to false will create the zone with no DNS records.
//
// If account is non-empty, it must have at least the ID field populated.
// This will add the new zone to the specified multi-user account.
//
// API reference: https://api.cloudflare.com/#zone-create-a-zone
func (api *API) CreateZone(ctx context.Context, name string, jumpstart bool, account Account, zoneType string) (Zone, error) {
var newzone newZone
newzone.Name = name
newzone.JumpStart = jumpstart
if account.ID != "" {
newzone.Account = &account
}
if zoneType == "partial" {
newzone.Type = "partial"
} else {
newzone.Type = "full"
}
res, err := api.makeRequestContext(ctx, http.MethodPost, "/zones", newzone)
if err != nil {
return Zone{}, err
}
var r ZoneResponse
err = json.Unmarshal(res, &r)
if err != nil {
return Zone{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// ZoneActivationCheck initiates another zone activation check for newly-created zones.
//
// API reference: https://api.cloudflare.com/#zone-initiate-another-zone-activation-check
func (api *API) ZoneActivationCheck(ctx context.Context, zoneID string) (Response, error) {
res, err := api.makeRequestContext(ctx, http.MethodPut, "/zones/"+zoneID+"/activation_check", nil)
if err != nil {
return Response{}, err
}
var r Response
err = json.Unmarshal(res, &r)
if err != nil {
return Response{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r, nil
}
// ListZones lists zones on an account. Optionally takes a list of zone names
// to filter against.
//
// API reference: https://api.cloudflare.com/#zone-list-zones
func (api *API) ListZones(ctx context.Context, z ...string) ([]Zone, error) {
var zones []Zone
if len(z) > 0 {
var (
v = url.Values{}
r ZonesResponse
)
for _, zone := range z {
v.Set("name", normalizeZoneName(zone))
res, err := api.makeRequestContext(ctx, http.MethodGet, "/zones?"+v.Encode(), nil)
if err != nil {
return []Zone{}, err
}
err = json.Unmarshal(res, &r)
if err != nil {
return []Zone{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
if !r.Success {
// TODO: Provide an actual error message instead of always returning nil
return []Zone{}, err
}
zones = append(zones, r.Result...)
}
} else {
res, err := api.ListZonesContext(ctx)
if err != nil {
return nil, err
}
zones = res.Result
}
return zones, nil
}
const listZonesPerPage = 50
// listZonesFetch fetches one page of zones.
// This is placed as a separate function to prevent any possibility of unintended capturing.
func (api *API) listZonesFetch(ctx context.Context, wg *sync.WaitGroup, errc chan error,
path string, pageSize int, buf []Zone) {
defer wg.Done()
// recordError sends the error to errc in a non-blocking manner
recordError := func(err error) {
select {
case errc <- err:
default:
}
}
res, err := api.makeRequestContext(ctx, http.MethodGet, path, nil)
if err != nil {
recordError(err)
return
}
var r ZonesResponse
err = json.Unmarshal(res, &r)
if err != nil {
recordError(err)
return
}
if len(r.Result) != pageSize {
recordError(errors.New(errResultInfo))
return
}
copy(buf, r.Result)
}
// ListZonesContext lists all zones on an account automatically handling the
// pagination. Optionally takes a list of ReqOptions.
func (api *API) ListZonesContext(ctx context.Context, opts ...ReqOption) (r ZonesResponse, err error) {
opt := reqOption{
params: url.Values{},
}
for _, of := range opts {
of(&opt)
}
if opt.params.Get("page") != "" || opt.params.Get("per_page") != "" {
return ZonesResponse{}, errors.New(errManualPagination)
}
opt.params.Add("per_page", strconv.Itoa(listZonesPerPage))
res, err := api.makeRequestContext(ctx, http.MethodGet, "/zones?"+opt.params.Encode(), nil)
if err != nil {
return ZonesResponse{}, err
}
err = json.Unmarshal(res, &r)
if err != nil {
return ZonesResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
// avoid overhead in most common cases where the total #zones <= 50
if r.TotalPages < 2 {
return r, nil
}
// parameters of pagination
var (
totalPageCount = r.TotalPages
totalCount = r.Total
// zones is a large slice to prevent resizing during concurrent writes.
zones = make([]Zone, totalCount)
)
// Copy the first page into zones.
copy(zones, r.Result)
var wg sync.WaitGroup
wg.Add(totalPageCount - 1) // all pages except the first one.
errc := make(chan error, 1) // getting the first error
// Creating all the workers.
for pageNum := 2; pageNum <= totalPageCount; pageNum++ {
// Note: URL.Values is just a map[string], so this would override the existing 'page'
opt.params.Set("page", strconv.Itoa(pageNum))
// start is the first index in the zone buffer
start := listZonesPerPage * (pageNum - 1)
pageSize := listZonesPerPage
if pageNum == totalPageCount {
// The size of the last page (which would be <= 50).
pageSize = totalCount - start
}
go api.listZonesFetch(ctx, &wg, errc, "/zones?"+opt.params.Encode(), pageSize, zones[start:])
}
wg.Wait()
select {
case err := <-errc: // if there were any errors
return ZonesResponse{}, err
default: // if there were no errors, the receive statement should block
r.Result = zones
return r, nil
}
}
// ZoneDetails fetches information about a zone.
//
// API reference: https://api.cloudflare.com/#zone-zone-details
func (api *API) ZoneDetails(ctx context.Context, zoneID string) (Zone, error) {
res, err := api.makeRequestContext(ctx, http.MethodGet, "/zones/"+zoneID, nil)
if err != nil {
return Zone{}, err
}
var r ZoneResponse
err = json.Unmarshal(res, &r)
if err != nil {
return Zone{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// ZoneOptions is a subset of Zone, for editable options.
type ZoneOptions struct {
Paused *bool `json:"paused,omitempty"`
VanityNS []string `json:"vanity_name_servers,omitempty"`
Plan *ZonePlan `json:"plan,omitempty"`
Type string `json:"type,omitempty"`
}
// ZoneSetPaused pauses Cloudflare service for the entire zone, sending all
// traffic direct to the origin.
func (api *API) ZoneSetPaused(ctx context.Context, zoneID string, paused bool) (Zone, error) {
zoneopts := ZoneOptions{Paused: &paused}
zone, err := api.EditZone(ctx, zoneID, zoneopts)
if err != nil {
return Zone{}, err
}
return zone, nil
}
// ZoneSetType toggles the type for an existing zone.
//
// Valid values for `type` are "full" and "partial"
//
// API reference: https://api.cloudflare.com/#zone-edit-zone
func (api *API) ZoneSetType(ctx context.Context, zoneID string, zoneType string) (Zone, error) {
zoneopts := ZoneOptions{Type: zoneType}
zone, err := api.EditZone(ctx, zoneID, zoneopts)
if err != nil {
return Zone{}, err
}
return zone, nil
}
// ZoneSetVanityNS sets custom nameservers for the zone.
// These names must be within the same zone.
func (api *API) ZoneSetVanityNS(ctx context.Context, zoneID string, ns []string) (Zone, error) {
zoneopts := ZoneOptions{VanityNS: ns}
zone, err := api.EditZone(ctx, zoneID, zoneopts)
if err != nil {
return Zone{}, err
}
return zone, nil
}
// ZoneSetPlan sets the rate plan of an existing zone.
//
// Valid values for `planType` are "CF_FREE", "CF_PRO", "CF_BIZ" and
// "CF_ENT".
//
// API reference: https://api.cloudflare.com/#zone-subscription-create-zone-subscription
func (api *API) ZoneSetPlan(ctx context.Context, zoneID string, planType string) error {
zonePayload := zoneSubscriptionRatePlanPayload{}
zonePayload.RatePlan.ID = planType
uri := fmt.Sprintf("/zones/%s/subscription", zoneID)
_, err := api.makeRequestContext(ctx, http.MethodPost, uri, zonePayload)
if err != nil {
return err
}
return nil
}
// ZoneUpdatePlan updates the rate plan of an existing zone.
//
// Valid values for `planType` are "CF_FREE", "CF_PRO", "CF_BIZ" and
// "CF_ENT".
//
// API reference: https://api.cloudflare.com/#zone-subscription-update-zone-subscription
func (api *API) ZoneUpdatePlan(ctx context.Context, zoneID string, planType string) error {
zonePayload := zoneSubscriptionRatePlanPayload{}
zonePayload.RatePlan.ID = planType
uri := fmt.Sprintf("/zones/%s/subscription", zoneID)
_, err := api.makeRequestContext(ctx, http.MethodPut, uri, zonePayload)
if err != nil {
return err
}
return nil
}
// EditZone edits the given zone.
//
// This is usually called by ZoneSetPaused, ZoneSetType, or ZoneSetVanityNS.
//
// API reference: https://api.cloudflare.com/#zone-edit-zone-properties
func (api *API) EditZone(ctx context.Context, zoneID string, zoneOpts ZoneOptions) (Zone, error) {
res, err := api.makeRequestContext(ctx, http.MethodPatch, "/zones/"+zoneID, zoneOpts)
if err != nil {
return Zone{}, err
}
var r ZoneResponse
err = json.Unmarshal(res, &r)
if err != nil {
return Zone{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// PurgeEverything purges the cache for the given zone.
//
// Note: this will substantially increase load on the origin server for that
// zone if there is a high cached vs. uncached request ratio.
//
// API reference: https://api.cloudflare.com/#zone-purge-all-files
func (api *API) PurgeEverything(ctx context.Context, zoneID string) (PurgeCacheResponse, error) {
uri := fmt.Sprintf("/zones/%s/purge_cache", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodPost, uri, PurgeCacheRequest{true, nil, nil, nil, nil})
if err != nil {
return PurgeCacheResponse{}, err
}
var r PurgeCacheResponse
err = json.Unmarshal(res, &r)
if err != nil {
return PurgeCacheResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r, nil
}
// PurgeCache purges the cache using the given PurgeCacheRequest (zone/url/tag).
//
// API reference: https://api.cloudflare.com/#zone-purge-individual-files-by-url-and-cache-tags
func (api *API) PurgeCache(ctx context.Context, zoneID string, pcr PurgeCacheRequest) (PurgeCacheResponse, error) {
return api.PurgeCacheContext(ctx, zoneID, pcr)
}
// PurgeCacheContext purges the cache using the given PurgeCacheRequest (zone/url/tag).
//
// API reference: https://api.cloudflare.com/#zone-purge-individual-files-by-url-and-cache-tags
func (api *API) PurgeCacheContext(ctx context.Context, zoneID string, pcr PurgeCacheRequest) (PurgeCacheResponse, error) {
// manually build the payload to ensure we don't escape HTML entities to
// match their keys for purging.
payload, err := json.MarshalWithOption(pcr, json.DisableHTMLEscape())
if err != nil {
return PurgeCacheResponse{}, err
}
uri := fmt.Sprintf("/zones/%s/purge_cache", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodPost, uri, payload)
if err != nil {
return PurgeCacheResponse{}, err
}
var r PurgeCacheResponse
err = json.Unmarshal(res, &r)
if err != nil {
return PurgeCacheResponse{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r, nil
}
// DeleteZone deletes the given zone.
//
// API reference: https://api.cloudflare.com/#zone-delete-a-zone
func (api *API) DeleteZone(ctx context.Context, zoneID string) (ZoneID, error) {
res, err := api.makeRequestContext(ctx, http.MethodDelete, "/zones/"+zoneID, nil)
if err != nil {
return ZoneID{}, err
}
var r ZoneIDResponse
err = json.Unmarshal(res, &r)
if err != nil {
return ZoneID{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// AvailableZoneRatePlans returns information about all plans available to the specified zone.
//
// API reference: https://api.cloudflare.com/#zone-plan-available-plans
func (api *API) AvailableZoneRatePlans(ctx context.Context, zoneID string) ([]ZoneRatePlan, error) {
uri := fmt.Sprintf("/zones/%s/available_rate_plans", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return []ZoneRatePlan{}, err
}
var r AvailableZoneRatePlansResponse
err = json.Unmarshal(res, &r)
if err != nil {
return []ZoneRatePlan{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// AvailableZonePlans returns information about all plans available to the specified zone.
//
// API reference: https://api.cloudflare.com/#zone-rate-plan-list-available-plans
func (api *API) AvailableZonePlans(ctx context.Context, zoneID string) ([]ZonePlan, error) {
uri := fmt.Sprintf("/zones/%s/available_plans", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return []ZonePlan{}, err
}
var r AvailableZonePlansResponse
err = json.Unmarshal(res, &r)
if err != nil {
return []ZonePlan{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// encode encodes non-nil fields into URL encoded form.
func (o ZoneAnalyticsOptions) encode() string {
v := url.Values{}
if o.Since != nil {
v.Set("since", o.Since.Format(time.RFC3339))
}
if o.Until != nil {
v.Set("until", o.Until.Format(time.RFC3339))
}
if o.Continuous != nil {
v.Set("continuous", fmt.Sprintf("%t", *o.Continuous))
}
return v.Encode()
}
// ZoneAnalyticsDashboard returns zone analytics information.
//
// API reference: https://api.cloudflare.com/#zone-analytics-dashboard
func (api *API) ZoneAnalyticsDashboard(ctx context.Context, zoneID string, options ZoneAnalyticsOptions) (ZoneAnalyticsData, error) {
uri := fmt.Sprintf("/zones/%s/analytics/dashboard?%s", zoneID, options.encode())
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return ZoneAnalyticsData{}, err
}
var r zoneAnalyticsDataResponse
err = json.Unmarshal(res, &r)
if err != nil {
return ZoneAnalyticsData{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// ZoneAnalyticsByColocation returns zone analytics information by datacenter.
//
// API reference: https://api.cloudflare.com/#zone-analytics-analytics-by-co-locations
func (api *API) ZoneAnalyticsByColocation(ctx context.Context, zoneID string, options ZoneAnalyticsOptions) ([]ZoneAnalyticsColocation, error) {
uri := fmt.Sprintf("/zones/%s/analytics/colos?%s", zoneID, options.encode())
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return nil, err
}
var r zoneAnalyticsColocationResponse
err = json.Unmarshal(res, &r)
if err != nil {
return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// ZoneSettings returns all of the settings for a given zone.
//
// API reference: https://api.cloudflare.com/#zone-settings-get-all-zone-settings
func (api *API) ZoneSettings(ctx context.Context, zoneID string) (*ZoneSettingResponse, error) {
uri := fmt.Sprintf("/zones/%s/settings", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return nil, err
}
response := &ZoneSettingResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return response, nil
}
// UpdateZoneSettings updates the settings for a given zone.
//
// API reference: https://api.cloudflare.com/#zone-settings-edit-zone-settings-info
func (api *API) UpdateZoneSettings(ctx context.Context, zoneID string, settings []ZoneSetting) (*ZoneSettingResponse, error) {
uri := fmt.Sprintf("/zones/%s/settings", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, struct {
Items []ZoneSetting `json:"items"`
}{settings})
if err != nil {
return nil, err
}
response := &ZoneSettingResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return response, nil
}
// ZoneSSLSettings returns information about SSL setting to the specified zone.
//
// API reference: https://api.cloudflare.com/#zone-settings-get-ssl-setting
func (api *API) ZoneSSLSettings(ctx context.Context, zoneID string) (ZoneSSLSetting, error) {
uri := fmt.Sprintf("/zones/%s/settings/ssl", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return ZoneSSLSetting{}, err
}
var r ZoneSSLSettingResponse
err = json.Unmarshal(res, &r)
if err != nil {
return ZoneSSLSetting{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// UpdateZoneSSLSettings update information about SSL setting to the specified zone.
//
// API reference: https://api.cloudflare.com/#zone-settings-change-ssl-setting
func (api *API) UpdateZoneSSLSettings(ctx context.Context, zoneID string, sslValue string) (ZoneSSLSetting, error) {
uri := fmt.Sprintf("/zones/%s/settings/ssl", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, ZoneSSLSetting{Value: sslValue})
if err != nil {
return ZoneSSLSetting{}, err
}
var r ZoneSSLSettingResponse
err = json.Unmarshal(res, &r)
if err != nil {
return ZoneSSLSetting{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// FallbackOrigin returns information about the fallback origin for the specified zone.
//
// API reference: https://developers.cloudflare.com/ssl/ssl-for-saas/api-calls/#fallback-origin-configuration
func (api *API) FallbackOrigin(ctx context.Context, zoneID string) (FallbackOrigin, error) {
uri := fmt.Sprintf("/zones/%s/fallback_origin", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return FallbackOrigin{}, err
}
var r FallbackOriginResponse
err = json.Unmarshal(res, &r)
if err != nil {
return FallbackOrigin{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// UpdateFallbackOrigin updates the fallback origin for a given zone.
//
// API reference: https://developers.cloudflare.com/ssl/ssl-for-saas/api-calls/#4-example-patch-to-change-fallback-origin
func (api *API) UpdateFallbackOrigin(ctx context.Context, zoneID string, fbo FallbackOrigin) (*FallbackOriginResponse, error) {
uri := fmt.Sprintf("/zones/%s/fallback_origin", zoneID)
res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, fbo)
if err != nil {
return nil, err
}
response := &FallbackOriginResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return response, nil
}
// normalizeZoneName tries to convert IDNs (international domain names)
// from Punycode to Unicode form. If the given zone name is not represented
// as Punycode, or converting fails (for invalid representations), it
// is returned unchanged.
//
// Because all the zone name comparison is currently done using the API service
// (except for comparison with the empty string), theoretically, we could
// remove this function from the Go library. However, there should be no harm
// calling this function other than gelable performance penalty.
//
// Note: conversion errors are silently discarded.
func normalizeZoneName(name string) string {
if n, err := idna.ToUnicode(name); err == nil {
return n
}
return name
}
// GetZoneSetting returns information about specified setting to the specified
// zone.
//
// API reference: https://api.cloudflare.com/#zone-settings-get-all-zone-settings
func (api *API) GetZoneSetting(ctx context.Context, rc *ResourceContainer, params GetZoneSettingParams) (ZoneSetting, error) {
if rc.Level != ZoneRouteLevel {
return ZoneSetting{}, ErrRequiredZoneLevelResourceContainer
}
if rc.Identifier == "" {
return ZoneSetting{}, ErrMissingName
}
pathPrefix := "settings"
if params.PathPrefix != "" {
pathPrefix = params.PathPrefix
}
uri := fmt.Sprintf("/zones/%s/%s/%s", rc.Identifier, pathPrefix, params.Name)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return ZoneSetting{}, err
}
var r ZoneSettingSingleResponse
err = json.Unmarshal(res, &r)
if err != nil {
return ZoneSetting{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return r.Result, nil
}
// UpdateZoneSetting updates the specified setting for a given zone.
//
// API reference: https://api.cloudflare.com/#zone-settings-edit-zone-settings-info
func (api *API) UpdateZoneSetting(ctx context.Context, rc *ResourceContainer, params UpdateZoneSettingParams) (ZoneSetting, error) {
if rc.Level != ZoneRouteLevel {
return ZoneSetting{}, ErrRequiredZoneLevelResourceContainer
}
if rc.Identifier == "" {
return ZoneSetting{}, ErrMissingName
}
pathPrefix := "settings"
if params.PathPrefix != "" {
pathPrefix = params.PathPrefix
}
uri := fmt.Sprintf("/zones/%s/%s/%s", rc.Identifier, pathPrefix, params.Name)
res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
if err != nil {
return ZoneSetting{}, err
}
response := &ZoneSettingSingleResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
return ZoneSetting{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return response.Result, nil
}
// ZoneExport returns the text BIND config for the given zone
//
// API reference: https://api.cloudflare.com/#dns-records-for-a-zone-export-dns-records
func (api *API) ZoneExport(ctx context.Context, zoneID string) (string, error) {
res, err := api.makeRequestContext(ctx, http.MethodGet, "/zones/"+zoneID+"/dns_records/export", nil)
if err != nil {
return "", err
}
return string(res), nil
}
// ZoneDNSSECResponse represents the response from the Zone DNSSEC Setting.
type ZoneDNSSECResponse struct {
Response
Result ZoneDNSSEC `json:"result"`
}
// ZoneDNSSEC represents the response from the Zone DNSSEC Setting result.
type ZoneDNSSEC struct {
Status string `json:"status"`
Flags int `json:"flags"`
Algorithm string `json:"algorithm"`
KeyType string `json:"key_type"`
DigestType string `json:"digest_type"`
DigestAlgorithm string `json:"digest_algorithm"`
Digest string `json:"digest"`
DS string `json:"ds"`
KeyTag int `json:"key_tag"`
PublicKey string `json:"public_key"`
ModifiedOn time.Time `json:"modified_on"`
}
// ZoneDNSSECSetting returns the DNSSEC details of a zone
//
// API reference: https://api.cloudflare.com/#dnssec-dnssec-details
func (api *API) ZoneDNSSECSetting(ctx context.Context, zoneID string) (ZoneDNSSEC, error) {
res, err := api.makeRequestContext(ctx, http.MethodGet, "/zones/"+zoneID+"/dnssec", nil)
if err != nil {
return ZoneDNSSEC{}, err
}
response := ZoneDNSSECResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
return ZoneDNSSEC{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return response.Result, nil
}
// ZoneDNSSECDeleteResponse represents the response from the Zone DNSSEC Delete request.
type ZoneDNSSECDeleteResponse struct {
Response
Result string `json:"result"`
}
// DeleteZoneDNSSEC deletes DNSSEC for zone
//
// API reference: https://api.cloudflare.com/#dnssec-delete-dnssec-records
func (api *API) DeleteZoneDNSSEC(ctx context.Context, zoneID string) (string, error) {
res, err := api.makeRequestContext(ctx, http.MethodDelete, "/zones/"+zoneID+"/dnssec", nil)
if err != nil {
return "", err
}
response := ZoneDNSSECDeleteResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
return "", fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return response.Result, nil
}
// ZoneDNSSECUpdateOptions represents the options for DNSSEC update.
type ZoneDNSSECUpdateOptions struct {
Status string `json:"status"`
}
// UpdateZoneDNSSEC updates DNSSEC for a zone
//
// API reference: https://api.cloudflare.com/#dnssec-edit-dnssec-status
func (api *API) UpdateZoneDNSSEC(ctx context.Context, zoneID string, options ZoneDNSSECUpdateOptions) (ZoneDNSSEC, error) {
res, err := api.makeRequestContext(ctx, http.MethodPatch, "/zones/"+zoneID+"/dnssec", options)
if err != nil {
return ZoneDNSSEC{}, err
}
response := ZoneDNSSECResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
return ZoneDNSSEC{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return response.Result, nil
}