cloudflare-2.0.4/0000755000076500000240000000000013241223516014251 5ustar martinstaff00000000000000cloudflare-2.0.4/cli4/0000755000076500000240000000000013241223516015104 5ustar martinstaff00000000000000cloudflare-2.0.4/cli4/__init__.py0000644000076500000240000000000013147001703017200 0ustar martinstaff00000000000000cloudflare-2.0.4/cli4/__main__.py0000755000076500000240000000045713147001703017204 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API via command line""" from __future__ import absolute_import import sys from .cli4 import cli4 def main(args=None): """Cloudflare API via command line""" if args is None: args = sys.argv[1:] cli4(args) if __name__ == '__main__': main() cloudflare-2.0.4/cli4/cli4.py0000644000076500000240000003105413241131067016313 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API via command line""" import sys import re import getopt import json try: import yaml except ImportError: yaml = None from . import converters import CloudFlare def dump_commands(cf): """dump a tree of all the known API commands""" w = cf.api_list() sys.stdout.write('\n'.join(w) + '\n') def cli4(args): """Cloudflare API via command line""" verbose = False output = 'json' raw = False dump = False method = 'GET' usage = ('usage: cli4 ' + '[-V|--version] [-h|--help] [-v|--verbose] [-q|--quiet] [-j|--json] [-y|--yaml] ' + '[-r|--raw] ' + '[-d|--dump] ' + '[--get|--patch|--post|--put|--delete] ' + '[item=value ...] ' + '/command...') try: opts, args = getopt.getopt(args, 'VhvqjyrdGPOUD', [ 'version', 'help', 'verbose', 'quiet', 'json', 'yaml', 'raw', 'dump', 'get', 'patch', 'post', 'put', 'delete' ]) except getopt.GetoptError: exit(usage) for opt, arg in opts: if opt in ('-V', '--version'): exit('Cloudflare library version: %s' % (CloudFlare.__version__)) if opt in ('-h', '--help'): exit(usage) elif opt in ('-v', '--verbose'): verbose = True elif opt in ('-q', '--quiet'): output = None elif opt in ('-j', '--json'): output = 'json' elif opt in ('-y', '--yaml'): if yaml is None: exit('cli4: install yaml support') output = 'yaml' elif opt in ('-r', '--raw'): raw = True elif opt in ('-d', '--dump'): dump = True elif opt in ('-G', '--get'): method = 'GET' elif opt in ('-P', '--patch'): method = 'PATCH' elif opt in ('-O', '--post'): method = 'POST' elif opt in ('-U', '--put'): method = 'PUT' elif opt in ('-D', '--delete'): method = 'DELETE' if dump: cf = CloudFlare.CloudFlare() dump_commands(cf) exit(0) digits_only = re.compile('^-?[0-9]+$') floats_only = re.compile('^-?[0-9.]+$') # next grab the params. These are in the form of tag=value params = None content = None files = None while len(args) > 0 and '=' in args[0]: tag_string, value_string = args.pop(0).split('=', 1) if value_string.lower() == 'true': value = True elif value_string.lower() == 'false': value = False elif value_string == '' or value_string.lower() == 'none': value = None elif value_string[0] is '=' and value_string[1:] == '': exit('cli4: %s== - no number value passed' % (tag_string)) elif value_string[0] is '=' and digits_only.match(value_string[1:]): value = int(value_string[1:]) elif value_string[0] is '=' and floats_only.match(value_string[1:]): value = float(value_string[1:]) elif value_string[0] is '=': exit('cli4: %s== - invalid number value passed' % (tag_string)) elif value_string[0] in '[{' and value_string[-1] in '}]': # a json structure - used in pagerules try: #value = json.loads(value) - changed to yaml code to remove unicode string issues if yaml is None: exit('cli4: install yaml support') value = yaml.safe_load(value_string) except ValueError: exit('cli4: %s="%s" - can\'t parse json value' % (tag_string, value_string)) elif value_string[0] is '@': filename = value_string[1:] # a file to be uploaded - used in dns_records/import - only via POST if tag_string == '': if method != 'PUT': exit('cli4: %s - raw file upload only with PUT' % (filename)) try: if filename == '-': content = sys.stdin.read() else: with open(filename, 'r') as f: content = f.read() except: exit('cli4: %s - file open failure' % (filename)) else: if method != 'POST': exit('cli4: %s=%s - file upload only with POST' % (tag_string, filename)) files = {} try: files[tag_string] = open(filename, 'rb') except: exit('cli4: %s=%s - file open failure' % (tag_string, filename)) # no need for param code below continue else: value = value_string if tag_string == '': # There's no tag; it's just an unnamed list if params is None: params = [] try: params.append(value) except AttributeError: exit('cli4: %s=%s - param error. Can\'t mix unnamed and named list' % (tag_string, value_string)) else: if params is None: params = {} tag = tag_string try: params[tag] = value except TypeError: exit('cli4: %s=%s - param error. Can\'t mix unnamed and named list' % (tag_string, value_string)) # what's left is the command itself if len(args) != 1: exit(usage) command = args[0] # remove leading and trailing /'s if command[0] == '/': command = command[1:] if command[-1] == '/': command = command[:-1] # break down command into it's seperate pieces # these are then checked against the Cloudflare class # to confirm there is a method that matches parts = command.split('/') cmd = [] identifier1 = None identifier2 = None identifier3 = None hex_only = re.compile('^[0-9a-fA-F]+$') waf_rules = re.compile('^[0-9]+[A-Z]*$') cf = CloudFlare.CloudFlare(debug=verbose, raw=raw) m = cf for element in parts: if element[0] == ':': element = element[1:] if identifier1 is None: if len(element) in [32, 40, 48] and hex_only.match(element): # raw identifier - lets just use it as-is identifier1 = element elif element[0] == ':': # raw string - used for workers script_name - use ::script_name identifier1 = element[1:] elif cmd[0] == 'certificates': # identifier1 = convert_certificates_to_identifier(cf, element) identifier1 = converters.convert_zones_to_identifier(cf, element) elif cmd[0] == 'zones': identifier1 = converters.convert_zones_to_identifier(cf, element) elif cmd[0] == 'organizations': identifier1 = converters.convert_organizations_to_identifier(cf, element) elif (cmd[0] == 'user') and (cmd[1] == 'organizations'): identifier1 = converters.convert_organizations_to_identifier(cf, element) elif (cmd[0] == 'user') and (cmd[1] == 'invites'): identifier1 = converters.convert_invites_to_identifier(cf, element) elif (cmd[0] == 'user') and (cmd[1] == 'virtual_dns'): identifier1 = converters.convert_virtual_dns_to_identifier(cf, element) elif (cmd[0] == 'user') and (cmd[1] == 'load_balancers') and (cmd[2] == 'pools'): identifier1 = converters.convert_load_balancers_pool_to_identifier(cf, element) else: exit("/%s/%s :NOT CODED YET 1" % ('/'.join(cmd), element)) cmd.append(':' + identifier1) elif identifier2 is None: if len(element) in [32, 40, 48] and hex_only.match(element): # raw identifier - lets just use it as-is identifier2 = element elif element[0] == ':': # raw string - used for workers script_names identifier2 = element[1:] elif (cmd[0] and cmd[0] == 'zones') and (cmd[2] and cmd[2] == 'dns_records'): identifier2 = converters.convert_dns_record_to_identifier(cf, identifier1, element) else: exit("/%s/%s :NOT CODED YET 2" % ('/'.join(cmd), element)) # identifier2 may be an array - this needs to be dealt with later if isinstance(identifier2, list): cmd.append(':' + '[' + ','.join(identifier2) + ']') else: cmd.append(':' + identifier2) identifier2 = [identifier2] else: if len(element) in [32, 40, 48] and hex_only.match(element): # raw identifier - lets just use it as-is identifier3 = element elif waf_rules.match(element): identifier3 = element else: exit("/%s/%s :NOT CODED YET 3" % ('/'.join(cmd), element)) else: try: m = getattr(m, element) cmd.append(element) except AttributeError: # the verb/element was not found if len(cmd) == 0: exit('cli4: /%s - not found' % (element)) else: exit('cli4: /%s/%s - not found' % ('/'.join(cmd), element)) if content and params: exit('cli4: /%s - content and params not allowed together' % (command)) if content: params = content results = [] if identifier2 is None: identifier2 = [None] for i2 in identifier2: try: if method is 'GET': r = m.get(identifier1=identifier1, identifier2=i2, identifier3=identifier3, params=params) elif method is 'PATCH': r = m.patch(identifier1=identifier1, identifier2=i2, identifier3=identifier3, data=params) elif method is 'POST': r = m.post(identifier1=identifier1, identifier2=i2, identifier3=identifier3, data=params, files=files) elif method is 'PUT': r = m.put(identifier1=identifier1, identifier2=i2, identifier3=identifier3, data=params) elif method is 'DELETE': r = m.delete(identifier1=identifier1, identifier2=i2, identifier3=identifier3, data=params) else: pass except CloudFlare.exceptions.CloudFlareAPIError as e: if len(e) > 0: # more than one error returned by the API for x in e: sys.stderr.write('cli4: /%s - %d %s\n' % (command, x, x)) exit('cli4: /%s - %d %s' % (command, e, e)) except Exception as e: exit('cli4: /%s - %s - api error' % (command, e)) results.append(r) if len(results) == 1: results = results[0] if isinstance(results, str): # if the results are a simple string, then it should be dumped directly # this is only used for /zones/:id/dns_records/export presently pass else: # anything more complex (dict, list, etc) should be dumped as JSON/YAML if output == 'json': try: results = json.dumps(results, indent=4, sort_keys=True, ensure_ascii=False, encoding='utf8') except TypeError as e: results = json.dumps(results, indent=4, sort_keys=True, ensure_ascii=False) if output == 'yaml': results = yaml.safe_dump(results) sys.stdout.write(results) if not results.endswith('\n'): sys.stdout.write('\n') cloudflare-2.0.4/cli4/converters.py0000644000076500000240000000756413147001703017661 0ustar martinstaff00000000000000"""Cloudflare API via command line""" from __future__ import absolute_import import CloudFlare def convert_zones_to_identifier(cf, zone_name): """zone names to numbers""" params = {'name':zone_name, 'per_page':1} try: zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('cli4: %s - %d %s' % (zone_name, e, e)) except Exception as e: exit('cli4: %s - %s' % (zone_name, e)) if len(zones) == 1: return zones[0]['id'] exit('cli4: %s - zone not found' % (zone_name)) def convert_dns_record_to_identifier(cf, zone_id, dns_name): """dns record names to numbers""" # this can return an array of results as there can be more than one DNS entry for a name. params = {'name':dns_name} try: dns_records = cf.zones.dns_records.get(zone_id, params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('cli4: %s - %d %s' % (dns_name, e, e)) except Exception as e: exit('cli4: %s - %s' % (dns_name, e)) r = [] for dns_record in dns_records: if dns_name == dns_record['name']: r.append(dns_record['id']) if len(r) > 0: return r exit('cli4: %s - dns name not found' % (dns_name)) def convert_certificates_to_identifier(cf, certificate_name): """certificate names to numbers""" try: certificates = cf.certificates.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('cli4: %s - %d %s' % (certificate_name, e, e)) except Exception as e: exit('cli4: %s - %s' % (certificate_name, e)) for certificate in certificates: if certificate_name in certificate['hostnames']: return certificate['id'] exit('cli4: %s - no zone certificates found' % (certificate_name)) def convert_organizations_to_identifier(cf, organization_name): """organizations names to numbers""" try: organizations = cf.user.organizations.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('cli4: %s - %d %s' % (organization_name, e, e)) except Exception as e: exit('cli4: %s - %s' % (organization_name, e)) for organization in organizations: if organization_name == organization['name']: return organization['id'] exit('cli4: %s - no organizations found' % (organization_name)) def convert_invites_to_identifier(cf, invite_name): """invite names to numbers""" try: invites = cf.user.invites.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('cli4: %s - %d %s' % (invite_name, e, e)) except Exception as e: exit('cli4: %s - %s' % (invite_name, e)) for invite in invites: if invite_name == invite['organization_name']: return invite['id'] exit('cli4: %s - no invites found' % (invite_name)) def convert_virtual_dns_to_identifier(cf, virtual_dns_name): """virtual dns names to numbers""" try: virtual_dnss = cf.user.virtual_dns.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('cli4: %s - %d %s\n' % (virtual_dns_name, e, e)) except Exception as e: exit('cli4: %s - %s\n' % (virtual_dns_name, e)) for virtual_dns in virtual_dnss: if virtual_dns_name == virtual_dns['name']: return virtual_dns['id'] exit('cli4: %s - no virtual_dns found' % (virtual_dns_name)) def convert_load_balancers_pool_to_identifier(cf, pool_name): """load balancer pool names to numbers""" try: pools = cf.user.load_balancers.pools.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('cli4: %s - %d %s' % (pool_name, e, e)) except Exception as e: exit('cli4: %s - %s' % (pool_name, e)) for p in pools: if pool_name == p['description']: return p['id'] exit('cli4: %s - no pools found' % (pool_name)) cloudflare-2.0.4/CloudFlare/0000755000076500000240000000000013241223516016271 5ustar martinstaff00000000000000cloudflare-2.0.4/CloudFlare/__init__.py0000644000076500000240000000022513241223214020374 0ustar martinstaff00000000000000""" Cloudflare v4 API""" from __future__ import absolute_import __version__ = '2.0.4' from .cloudflare import CloudFlare __all__ = ['CloudFlare'] cloudflare-2.0.4/CloudFlare/api_extras.py0000644000076500000240000000370213147001703021001 0ustar martinstaff00000000000000""" API extras for Cloudflare API""" import re def api_extras(self, extras=None): """ API extras for Cloudflare API""" for extra in extras: if extra == '': continue extra = re.sub(r"^.*/client/v4/", '/', extra) extra = re.sub(r"^.*/v4/", '/', extra) extra = re.sub(r"^/", '', extra) # build parts of the extra command parts = [] nn = 0 for element in extra.split('/'): if element[0] == ':': nn += 1 continue try: parts[nn] except IndexError: parts.append([]) parts[nn].append(element) # insert extra command into class element_path = [] current = self for element in parts[0]: element_path.append(element) try: m = getattr(current, element) # exists - but still add it there's a second part if element == parts[0][-1] and len(parts) > 1: api_call_part1 = '/'.join(element_path) api_call_part2 = '/'.join(parts[1]) setattr(m, parts[1][0], self._add_with_auth(self._base, api_call_part1, api_call_part2)) current = m continue except: pass # does not exist if element == parts[0][-1] and len(parts) > 1: # last element api_call_part1 = '/'.join(element_path) api_call_part2 = '/'.join(parts[1]) setattr(current, element, self._add_with_auth(self._base, api_call_part1, api_call_part2)) else: api_call_part1 = '/'.join(element_path) setattr(current, element, self._add_with_auth(self._base, api_call_part1)) current = getattr(current, element) cloudflare-2.0.4/CloudFlare/api_v4.py0000644000076500000240000005217613241213172020035 0ustar martinstaff00000000000000""" API core commands for Cloudflare API""" def api_v4(self): """ API core commands for Cloudflare API""" # The API commands for /user/ user(self) user_audit_logs(self) user_load_balancers(self) user_load_balancing_analytics(self) user_virtual_dns(self) user_workers(self) # The API commands for /zones/ zones(self) zones_amp(self) zones_analytics(self) zones_argo(self) zones_dns_analytics(self) zones_dnssec(self) zones_firewall(self) zones_load_balancers(self) zones_logs(self) zones_rate_limits(self) zones_settings(self) zones_ssl(self) zones_workers(self) # The API commands for /railguns/ railguns(self) # The API commands for /organizations/ organizations(self) organizations_audit_logs(self) organizations_virtual_dns(self) organizations_workers(self) # The API commands for /certificates/ certificates(self) # The API commands for /ips/ ips(self) # The API commands for /account/ account(self) account_load_balancing_analytics(self) def user(self): """ API core commands for Cloudflare API""" base = self._base setattr(self, "user", self._add_with_auth(base, "user")) branch = self.user setattr(branch, "billing", self._add_unused(base, "user/billing")) branch = self.user.billing setattr(branch, "history", self._add_with_auth(base, "user/billing/history")) setattr(branch, "profile", self._add_with_auth(base, "user/billing/profile")) setattr(branch, "subscriptions", self._add_unused(base, "user/billing/subscriptions")) branch = self.user.billing.subscriptions setattr(branch, "apps", self._add_with_auth(base, "user/billing/subscriptions/apps")) setattr(branch, "zones", self._add_with_auth(base, "user/billing/subscriptions/zones")) branch = self.user setattr(branch, "firewall", self._add_unused(base, "user/firewall")) branch = self.user.firewall setattr(branch, "access_rules", self._add_unused(base, "user/firewall/access_rules")) branch = self.user.firewall.access_rules setattr(branch, "rules", self._add_with_auth(base, "user/firewall/access_rules/rules")) branch = self.user setattr(branch, "organizations", self._add_with_auth(base, "user/organizations")) setattr(branch, "invites", self._add_with_auth(base, "user/invites")) setattr(branch, "subscriptions", self._add_with_auth(base, "user/subscriptions")) def zones(self): """ API core commands for Cloudflare API""" base = self._base setattr(self, "zones", self._add_with_auth(base, "zones")) branch = self.zones setattr(branch, "activation_check", self._add_with_auth(base, "zones", "activation_check")) setattr(branch, "available_plans", self._add_with_auth(base, "zones", "available_plans")) setattr(branch, "available_rate_plans", self._add_with_auth(base, "zones", "available_rate_plans")) setattr(branch, "custom_certificates", self._add_with_auth(base, "zones", "custom_certificates")) branch = self.zones.custom_certificates setattr(branch, "prioritize", self._add_with_auth(base, "zones", "custom_certificates/prioritize")) branch = self.zones setattr(branch, "custom_pages", self._add_with_auth(base, "zones", "custom_pages")) setattr(branch, "dns_records", self._add_with_auth(base, "zones", "dns_records")) setattr(branch, "keyless_certificates", self._add_with_auth(base, "zones", "keyless_certificates")) setattr(branch, "pagerules", self._add_with_auth(base, "zones", "pagerules")) setattr(branch, "purge_cache", self._add_with_auth(base, "zones", "purge_cache")) setattr(branch, "railguns", self._add_with_auth(base, "zones", "railguns")) branch = self.zones.railguns setattr(branch, "diagnose", self._add_with_auth(base, "zones", "railguns", "diagnose")) branch = self.zones setattr(branch, "subscription", self._add_with_auth(base, "zones", "subscription")) setattr(branch, "subscriptions", self._add_with_auth(base, "zones", "subscriptions")) branch = self.zones.dns_records setattr(branch, "export", self._add_with_auth(base, "zones", "dns_records/export")) setattr(branch, "import", self._add_with_auth(base, "zones", "dns_records/import")) branch = self.zones setattr(branch, "custom_hostnames", self._add_with_auth(base, "zones", "custom_hostnames")) def zones_settings(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "settings", self._add_with_auth(base, "zones", "settings")) branch = self.zones.settings setattr(branch, "advanced_ddos", self._add_with_auth(base, "zones", "settings/advanced_ddos")) setattr(branch, "always_online", self._add_with_auth(base, "zones", "settings/always_online")) setattr(branch, "always_use_https", self._add_with_auth(base, "zones", "settings/always_use_https")) setattr(branch, "browser_cache_ttl", self._add_with_auth(base, "zones", "settings/browser_cache_ttl")) setattr(branch, "browser_check", self._add_with_auth(base, "zones", "settings/browser_check")) setattr(branch, "cache_level", self._add_with_auth(base, "zones", "settings/cache_level")) setattr(branch, "challenge_ttl", self._add_with_auth(base, "zones", "settings/challenge_ttl")) setattr(branch, "development_mode", self._add_with_auth(base, "zones", "settings/development_mode")) setattr(branch, "email_obfuscation", self._add_with_auth(base, "zones", "settings/email_obfuscation")) setattr(branch, "hotlink_protection", self._add_with_auth(base, "zones", "settings/hotlink_protection")) setattr(branch, "ip_geolocation", self._add_with_auth(base, "zones", "settings/ip_geolocation")) setattr(branch, "ipv6", self._add_with_auth(base, "zones", "settings/ipv6")) setattr(branch, "minify", self._add_with_auth(base, "zones", "settings/minify")) setattr(branch, "mirage", self._add_with_auth(base, "zones", "settings/mirage")) setattr(branch, "mobile_redirect", self._add_with_auth(base, "zones", "settings/mobile_redirect")) setattr(branch, "origin_error_page_pass_thru", self._add_with_auth(base, "zones", "settings/origin_error_page_pass_thru")) setattr(branch, "polish", self._add_with_auth(base, "zones", "settings/polish")) setattr(branch, "prefetch_preload", self._add_with_auth(base, "zones", "settings/prefetch_preload")) setattr(branch, "response_buffering", self._add_with_auth(base, "zones", "settings/response_buffering")) setattr(branch, "rocket_loader", self._add_with_auth(base, "zones", "settings/rocket_loader")) setattr(branch, "security_header", self._add_with_auth(base, "zones", "settings/security_header")) setattr(branch, "security_level", self._add_with_auth(base, "zones", "settings/security_level")) setattr(branch, "server_side_exclude", self._add_with_auth(base, "zones", "settings/server_side_exclude")) setattr(branch, "sort_query_string_for_cache", self._add_with_auth(base, "zones", "settings/sort_query_string_for_cache")) setattr(branch, "ssl", self._add_with_auth(base, "zones", "settings/ssl")) setattr(branch, "tls_client_auth", self._add_with_auth(base, "zones", "settings/tls_client_auth")) setattr(branch, "true_client_ip_header", self._add_with_auth(base, "zones", "settings/true_client_ip_header")) setattr(branch, "tls_1_2_only", self._add_with_auth(base, "zones", "settings/tls_1_2_only")) setattr(branch, "tls_1_3", self._add_with_auth(base, "zones", "settings/tls_1_3")) # setattr(branch, "tlsadd_auth", # self._add_with_auth(base, "zones", "settings/tlsadd_auth")) # setattr(branch, "trueadd_ip_header", # self._add_with_auth(base, "zones", "settings/trueadd_ip_header")) setattr(branch, "websockets", self._add_with_auth(base, "zones", "settings/websockets")) setattr(branch, "waf", self._add_with_auth(base, "zones", "settings/waf")) setattr(branch, "webp", self._add_with_auth(base, "zones", "settings/webp")) setattr(branch, "http2", self._add_with_auth(base, "zones", "settings/http2")) setattr(branch, "pseudo_ipv4", self._add_with_auth(base, "zones", "settings/pseudo_ipv4")) setattr(branch, "opportunistic_encryption", self._add_with_auth(base, "zones", "settings/opportunistic_encryption")) setattr(branch, "automatic_https_rewrites", self._add_with_auth(base, "zones", "settings/automatic_https_rewrites")) setattr(branch, "brotli", self._add_with_auth(base, "zones", "settings/brotli")) setattr(branch, "privacy_pass", self._add_with_auth(base, "zones", "settings/privacy_pass")) def zones_analytics(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "analytics", self._add_unused(base, "zones", "analytics")) branch = self.zones.analytics setattr(branch, "colos", self._add_with_auth(base, "zones", "analytics/colos")) setattr(branch, "dashboard", self._add_with_auth(base, "zones", "analytics/dashboard")) def zones_firewall(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "firewall", self._add_unused(branch, "zones", "firewall")) branch = self.zones.firewall setattr(branch, "access_rules", self._add_unused(base, "zones", "firewall/access_rules")) setattr(branch, "waf", self._add_unused(base, "zones", "firewall/waf")) branch = self.zones.firewall.waf setattr(branch, "packages", self._add_with_auth(base, "zones", "firewall/waf/packages")) branch = self.zones.firewall.waf.packages setattr(branch, "groups", self._add_with_auth(base, "zones", "firewall/waf/packages", "groups")) setattr(branch, "rules", self._add_with_auth(base, "zones", "firewall/waf/packages", "rules")) branch = self.zones.firewall.access_rules setattr(branch, "rules", self._add_with_auth(base, "zones", "firewall/access_rules/rules")) branch = self.zones.firewall setattr(branch, "lockdowns", self._add_with_auth(base, "zones", "firewall/lockdowns")) setattr(branch, "ua_rules", self._add_with_auth(base, "zones", "firewall/ua_rules")) def zones_rate_limits(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "rate_limits", self._add_with_auth(base, "zones", "rate_limits")) def zones_dns_analytics(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "dns_analytics", self._add_unused(base, "zones", "dns_analytics")) branch = self.zones.dns_analytics setattr(branch, "report", self._add_with_auth(base, "zones", "dns_analytics/report")) branch = self.zones.dns_analytics.report setattr(branch, "bytime", self._add_with_auth(base, "zones", "dns_analytics/report/bytime")) def zones_amp(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "amp", self._add_unused(base, "zones", "amp")) branch = self.zones.amp setattr(branch, "viewer", self._add_with_auth(base, "zones", "amp/viewer")) def zones_logs(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "logs", self._add_unused(base, "zones", "logs")) branch = self.zones.logs setattr(branch, "received", self._add_with_auth_unwrapped(base, "zones", "logs/received")) branch = self.zones.logs.received setattr(branch, "fields", self._add_with_auth_unwrapped(base, "zones", "logs/received/fields")) def railguns(self): """ API core commands for Cloudflare API""" base = self._base setattr(self, "railguns", self._add_with_auth(base, "railguns")) branch = self.railguns setattr(branch, "zones", self._add_with_auth(base, "railguns", "zones")) def organizations(self): """ API core commands for Cloudflare API""" base = self._base setattr(self, "organizations", self._add_with_auth(base, "organizations")) branch = self.organizations setattr(branch, "members", self._add_with_auth(base, "organizations", "members")) setattr(branch, "invite", self._add_with_auth(base, "organizations", "invite")) setattr(branch, "invites", self._add_with_auth(base, "organizations", "invites")) setattr(branch, "railguns", self._add_with_auth(base, "organizations", "railguns")) branch = self.organizations.railguns setattr(branch, "zones", self._add_with_auth(base, "organizations", "railguns", "zones")) branch = self.organizations setattr(branch, "roles", self._add_with_auth(base, "organizations", "roles")) setattr(branch, "firewall", self._add_unused(base, "organizations", "firewall")) branch = self.organizations.firewall setattr(branch, "access_rules", self._add_unused(base, "organizations", "firewall/access_rules")) branch = self.organizations.firewall.access_rules setattr(branch, "rules", self._add_with_auth(base, "organizations", "firewall/access_rules/rules")) branch = self.organizations setattr(branch, "load_balancers", self._add_unused(base, "organizations", "load_balancers")) branch = self.organizations.load_balancers setattr(branch, "monitors", self._add_with_auth(base, "organizations", "load_balancers/monitors")) setattr(branch, "pools", self._add_with_auth(base, "organizations", "load_balancers/pools")) def certificates(self): """ API core commands for Cloudflare API""" base = self._base setattr(self, "certificates", self._add_with_cert_auth(base, "certificates")) def ips(self): """ API core commands for Cloudflare API""" base = self._base setattr(self, "ips", self._add_noauth(base, "ips")) def zones_argo(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "argo", self._add_unused(base, "zones", "argo")) branch = self.zones.argo setattr(branch, "tiered_caching", self._add_with_auth(base, "zones", "argo/tiered_caching")) setattr(branch, "smart_routing", self._add_with_auth(base, "zones", "argo/smart_routing")) def zones_dnssec(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "dnssec", self._add_with_auth(base, "zones", "dnssec")) def zones_ssl(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "ssl", self._add_unused(base, "zones", "ssl")) branch = self.zones.ssl setattr(branch, "analyze", self._add_with_auth(base, "zones", "ssl/analyze")) setattr(branch, "certificate_packs", self._add_with_auth(base, "zones", "ssl/certificate_packs")) setattr(branch, "verification", self._add_with_auth(base, "zones", "ssl/verification")) setattr(branch, "universal", self._add_unused(base, "zones", "ssl/universal")) branch = self.zones.ssl.universal setattr(branch, "settings", self._add_with_auth(base, "zones", "ssl/universal/settings")) def zones_workers(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "workers", self._add_unused(base, "zones", "workers")) branch = self.zones.workers setattr(branch, "filters", self._add_with_auth(base, "zones", "workers/filters")) setattr(branch, "routes", self._add_with_auth(base, "zones", "workers/routes")) setattr(branch, "script", self._add_with_auth(base, "zones", "workers/script")) def zones_load_balancers(self): """ API core commands for Cloudflare API""" base = self._base branch = self.zones setattr(branch, "load_balancers", self._add_with_auth(base, "zones", "load_balancers")) def user_load_balancers(self): """ API core commands for Cloudflare API""" base = self._base branch = self.user setattr(branch, "load_balancers", self._add_unused(base, "user/load_balancers")) branch = self.user.load_balancers setattr(branch, "monitors", self._add_with_auth(base, "user/load_balancers/monitors")) setattr(branch, "pools", self._add_with_auth(base, "user/load_balancers/pools")) def user_virtual_dns(self): """ API core commands for Cloudflare API""" base = self._base branch = self.user setattr(branch, "virtual_dns", self._add_with_auth(base, "user/virtual_dns")) branch = self.user.virtual_dns setattr(branch, "dns_analytics", self._add_unused(base, "user/virtual_dns", "dns_analytics")) branch = self.user.virtual_dns.dns_analytics setattr(branch, "report", self._add_with_auth(base, "user/virtual_dns", "dns_analytics/report")) branch = self.user.virtual_dns.dns_analytics.report setattr(branch, "bytime", self._add_with_auth(base, "user/virtual_dns", "dns_analytics/report/bytime")) def user_workers(self): """ API core commands for Cloudflare API""" base = self._base branch = self.user setattr(branch, "workers", self._add_unused(base, "user/workers")) branch = self.user.workers setattr(branch, "scripts", self._add_with_auth(base, "user/workers/scripts")) def organizations_virtual_dns(self): """ API core commands for Cloudflare API""" base = self._base branch = self.organizations setattr(branch, "virtual_dns", self._add_with_auth(base, "organizations", "virtual_dns")) branch = self.organizations.virtual_dns setattr(branch, "dns_analytics", self._add_unused(base, "organizations", "virtual_dns", "dns_analytics")) branch = self.organizations.virtual_dns.dns_analytics setattr(branch, "report", self._add_with_auth(base, "organizations", "virtual_dns", "dns_analytics/report")) branch = self.organizations.virtual_dns.dns_analytics.report setattr(branch, "bytime", self._add_with_auth(base, "organizations", "virtual_dns", "dns_analytics/report/bytime")) def user_audit_logs(self): """ API core commands for Cloudflare API""" base = self._base branch = self.user setattr(branch, "audit_logs", self._add_with_auth(base, "user/audit_logs")) def user_load_balancing_analytics(self): """ API core commands for Cloudflare API""" base = self._base branch = self.user setattr(branch, "load_balancing_analytics", self._add_unused(base, "user/load_balancing_analytics")) branch = self.user.load_balancing_analytics setattr(branch, "events", self._add_with_auth(base, "user/load_balancing_analytics/events")) setattr(branch, "entities", self._add_with_auth(base, "user/load_balancing_analytics/entities")) def organizations_audit_logs(self): """ API core commands for Cloudflare API""" base = self._base branch = self.organizations setattr(branch, "audit_logs", self._add_with_auth(base, "organizations", "audit_logs")) def organizations_workers(self): """ API core commands for Cloudflare API""" base = self._base branch = self.organizations setattr(branch, "workers", self._add_unused(base, "organizations", "workers")) branch = self.organizations.workers setattr(branch, "scripts", self._add_with_auth(base, "organizations", "workers/scripts")) def account(self): """ API core commands for Cloudflare API""" base = self._base setattr(self, "account", self._add_unused(base, "account")) def account_load_balancing_analytics(self): """ API core commands for Cloudflare API""" base = self._base branch = self.account setattr(branch, "load_balancing_analytics", self._add_unused(base, "account", "load_balancing_analytics")) branch = self.account.load_balancing_analytics setattr(branch, "events", self._add_with_auth(base, "account", "load_balancing_analytics/events")) setattr(branch, "entities", self._add_with_auth(base, "account", "load_balancing_analytics/entities")) cloudflare-2.0.4/CloudFlare/cloudflare.py0000644000076500000240000010557713240710172021000 0ustar martinstaff00000000000000""" Cloudflare v4 API""" from __future__ import absolute_import import json import requests from .logging_helper import CFlogger from .utils import user_agent, sanitize_secrets from .read_configs import read_configs from .api_v4 import api_v4 from .api_extras import api_extras from .exceptions import CloudFlareError, CloudFlareAPIError, CloudFlareInternalError BASE_URL = 'https://api.cloudflare.com/client/v4' class CloudFlare(object): """ Cloudflare v4 API""" class _v4base(object): """ Cloudflare v4 API""" def __init__(self, email, token, certtoken, base_url, debug, raw, use_sessions): """ Cloudflare v4 API""" self.email = email self.token = token self.certtoken = certtoken self.base_url = base_url self.raw = raw self.use_sessions = use_sessions self.session = None self.user_agent = user_agent() if debug: self.logger = CFlogger(debug).getLogger() else: self.logger = None def call_with_no_auth(self, method, parts, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" headers = { 'User-Agent': self.user_agent, 'Content-Type': 'application/json' } return self._call(method, headers, parts, identifier1, identifier2, identifier3, params, data, files) def call_with_auth(self, method, parts, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" if self.email is '' or self.token is '': raise CloudFlareAPIError(0, 'no email and/or token defined') headers = { 'User-Agent': self.user_agent, 'X-Auth-Email': self.email, 'X-Auth-Key': self.token, 'Content-Type': 'application/json' } if type(data) == str: # passing javascript vs JSON headers['Content-Type'] = 'application/javascript' if files: # overwrite Content-Type as we are uploading data headers['Content-Type'] = 'multipart/form-data' # however something isn't right and this works ... look at again later! del headers['Content-Type'] return self._call(method, headers, parts, identifier1, identifier2, identifier3, params, data, files) def call_with_auth_unwrapped(self, method, parts, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" if self.email is '' or self.token is '': raise CloudFlareAPIError(0, 'no email and/or token defined') headers = { 'User-Agent': self.user_agent, 'X-Auth-Email': self.email, 'X-Auth-Key': self.token, 'Content-Type': 'application/json' } if type(data) == str: # passing javascript vs JSON headers['Content-Type'] = 'application/javascript' if files: # overwrite Content-Type as we are uploading data headers['Content-Type'] = 'multipart/form-data' # however something isn't right and this works ... look at again later! del headers['Content-Type'] return self._call_unwrapped(method, headers, parts, identifier1, identifier2, identifier3, params, data, files) def call_with_certauth(self, method, parts, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" if self.certtoken is '' or self.certtoken is None: raise CloudFlareAPIError(0, 'no cert token defined') headers = { 'User-Agent': self.user_agent, 'X-Auth-User-Service-Key': self.certtoken, 'Content-Type': 'application/json' } return self._call(method, headers, parts, identifier1, identifier2, identifier3, params, data, files) def _network(self, method, headers, parts, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" if self.logger: self.logger.debug('Call: %s,%s,%s,%s,%s,%s' % (str(parts[0]), str(identifier1), str(parts[1]), str(identifier2), str(parts[2]), str(identifier3))) self.logger.debug('Call: optional params and data %s %s' % (str(params), str(data))) if files: self.logger.debug('Call: upload file %r' % (files)) if (method is None) or (parts[0] is None): # should never happen raise CloudFlareInternalError(0, 'You must specify a method and endpoint') if parts[1] is not None or (data is not None and method == 'GET'): if identifier1 is None: raise CloudFlareAPIError(0, 'You must specify identifier1') if identifier2 is None: url = (self.base_url + '/' + parts[0] + '/' + identifier1 + '/' + parts[1]) else: url = (self.base_url + '/' + parts[0] + '/' + identifier1 + '/' + parts[1] + '/' + identifier2) else: if identifier1 is None: url = (self.base_url + '/' + parts[0]) else: url = (self.base_url + '/' + parts[0] + '/' + identifier1) if parts[2]: url += '/' + parts[2] if identifier3: url += '/' + identifier3 if self.logger: self.logger.debug('Call: method and url %s %s' % (str(method), str(url))) self.logger.debug('Call: headers %s' % str(sanitize_secrets(headers))) method = method.upper() if self.logger: self.logger.debug('Call: doit!') if self.use_sessions: if self.session is None: self.session = requests.Session() else: self.session = requests try: if method == 'GET': response = self.session.get(url, headers=headers, params=params, data=data) elif method == 'POST': if type(data) == str: response = self.session.post(url, headers=headers, params=params, data=data, files=files) else: response = self.session.post(url, headers=headers, params=params, json=data, files=files) elif method == 'PUT': if type(data) == str: response = self.session.put(url, headers=headers, params=params, data=data) else: response = self.session.put(url, headers=headers, params=params, json=data) elif method == 'DELETE': if type(data) == str: response = self.session.delete(url, headers=headers, params=params, data=data) else: response = self.session.delete(url, headers=headers, params=params, json=data) elif method == 'PATCH': if type(data) == str: response = self.session.request('PATCH', url, headers=headers, params=params, data=data) else: response = self.session.request('PATCH', url, headers=headers, params=params, json=data) else: # should never happen raise CloudFlareAPIError(0, 'method not supported') if self.logger: self.logger.debug('Call: done!') except Exception as e: if self.logger: self.logger.debug('Call: exception!') raise CloudFlareAPIError(0, 'connection failed.') if self.logger: self.logger.debug('Response: url %s', response.url) # Create response_{type|code|data} try: response_type = response.headers['Content-Type'] if ';' in response_type: # remove the ;paramaters part (like charset=, etc.) response_type = response_type[0:response_type.rfind(';')] response_type = response_type.strip().lower() except: # API should always response; but if it doesn't; here's the default response_type = 'application/octet-stream' response_code = response.status_code response_data = response.content if type(response_data) != str: response_data = response_data.decode("utf-8") if self.logger: self.logger.debug('Response: %d, %s, %s' % (response_code, response_type, response_data)) if response_code >= 500 and response_code <= 599: # 500 Internal Server Error # 501 Not Implemented # 502 Bad Gateway # 503 Service Unavailable # 504 Gateway Timeout # 505 HTTP Version Not Supported # 506 Variant Also Negotiates # 507 Insufficient Storage # 508 Loop Detected # 509 Unassigned # 510 Not Extended # 511 Network Authentication Required # the libary doesn't deal with these errors, just pass upwards! # there's no value to add and the returned data is questionable or not useful response.raise_for_status() # should not be reached raise CloudFlareInternalError(0, 'internal error in status code processing') #if response_code >= 400 and response_code <= 499: # # 400 Bad Request # # 401 Unauthorized # # 403 Forbidden # # 405 Method Not Allowed # # 415 Unsupported Media Type # # 429 Too many requests # # # don't deal with these errors, just pass upwards! # response.raise_for_status() # #if response_code >= 300 and response_code <= 399: # # 304 Not Modified # # # don't deal with these errors, just pass upwards! # response.raise_for_status() # # should be a 200 response at this point return [response_type, response_code, response_data] def _raw(self, method, headers, parts, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" [response_type, response_code, response_data] = self._network(method, headers, parts, identifier1, identifier2, identifier3, params, data, files) if response_type == 'application/json': # API says it's JSON; so it better be parsable as JSON try: if hasattr(response_data, 'decode'): response_data = json.loads(response_data.decode('utf-8')) else: response_data = json.loads(response_data) except ValueError: if response_data == '': # This should really be 'null' but it isn't. Even then, it's wrong! if response_code == requests.codes.ok: # 200 ok response_data = {'success': True, 'result': None} else: # 3xx & 4xx errors response_data = {'success': False, 'code': response_code, 'result': None} else: # While this should not happen; it's always possible self.logger.debug('Response data not JSON: %r' % (response_data)) raise CloudFlareAPIError(0, 'JSON parse failed - report to Cloudflare.') if response_code == requests.codes.ok: # 200 ok - so nothing needs to be done pass else: # 3xx & 4xx errors - we should report that somehow - but not quite yet # response_data['code'] = response_code pass elif response_type == 'text/plain' or response_type == 'application/octet-stream': # API says it's text; but maybe it's actually JSON? - should be fixed in API try: if hasattr(response_data, 'decode'): response_data = json.loads(response_data.decode('utf-8')) else: response_data = json.loads(response_data) except ValueError: # So it wasn't JSON - moving on as if it's text! # A single value is returned (vs an array or object) if response_code == requests.codes.ok: # 200 ok response_data = {'success': True, 'result': str(response_data)} else: # 3xx & 4xx errors response_data = {'success': False, 'code': response_code, 'result': str(response_data)} elif response_type == 'text/javascript' or response_type == 'application/javascript': # used by Cloudflare workers if response_code == requests.codes.ok: # 200 ok response_data = {'success': True, 'result': str(response_data)} else: # 3xx & 4xx errors response_data = {'success': False, 'code': response_code, 'result': str(response_data)} else: # Assuming nothing - but continuing anyway # A single value is returned (vs an array or object) if response_code == requests.codes.ok: # 200 ok response_data = {'success': True, 'result': str(response_data)} else: # 3xx & 4xx errors response_data = {'success': False, 'code': response_code, 'result': str(response_data)} # it would be nice to return the error code and content type values; but not quite yet return response_data def _call(self, method, headers, parts, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" response_data = self._raw(method, headers, parts, identifier1, identifier2, identifier3, params, data, files) # Sanatize the returned results - just in case API is messed up if 'success' not in response_data: if 'errors' in response_data: if self.logger: self.logger.debug('Response: assuming success = "False"') response_data['success'] = False else: if 'result' not in response_data: # Only happens on /certificates call # should be fixed in /certificates API if self.logger: self.logger.debug('Response: assuming success = "False"') r = response_data response_data['errors'] = [] response_data['errors'].append(r) response_data['success'] = False else: if self.logger: self.logger.debug('Response: assuming success = "True"') response_data['success'] = True if response_data['success'] is False: errors = response_data['errors'][0] code = errors['code'] if 'message' in errors: message = errors['message'] elif 'error' in errors: message = errors['error'] else: message = '' if 'error_chain' in errors: error_chain = errors['error_chain'] for error in error_chain: if self.logger: self.logger.debug('Response: error %d %s - chain' % (error['code'], error['message'])) if self.logger: self.logger.debug('Response: error %d %s' % (code, message)) raise CloudFlareAPIError(code, message, error_chain) else: if self.logger: self.logger.debug('Response: error %d %s' % (code, message)) raise CloudFlareAPIError(code, message) if self.logger: self.logger.debug('Response: %s' % (response_data['result'])) if self.raw: result = {} # theres always a result value result['result'] = response_data['result'] # theres may not be a result_info on every call if 'result_info' in response_data: result['result_info'] = response_data['result_info'] # no need to return success, errors, or messages as they return via an exception else: # theres always a result value result = response_data['result'] return result def _call_unwrapped(self, method, headers, parts, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" response_data = self._raw(method, headers, parts, identifier1, identifier2, identifier3, params, data, files) if self.logger: self.logger.debug('Response: %s' % (response_data)) result = response_data return result class _add_unused(object): """ Cloudflare v4 API""" def __init__(self, base, api_call_part1, api_call_part2=None, api_call_part3=None): """ Cloudflare v4 API""" self._base = base self._parts_unused = [api_call_part1, api_call_part2, api_call_part3] def __call__(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" # This is the same as a get() return self.get(identifier1, identifier2, identifier3, params, data) def __str__(self): """ Cloudflare v4 API""" return '[%s]' % ('/' + '/:id/'.join(filter(None, self._parts_unused))) def get(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" raise CloudFlareAPIError(0, 'get() call not available for this endpoint') def patch(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" raise CloudFlareAPIError(0, 'patch() call not available for this endpoint') def post(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" raise CloudFlareAPIError(0, 'post() call not available for this endpoint') def put(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" raise CloudFlareAPIError(0, 'put() call not available for this endpoint') def delete(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" raise CloudFlareAPIError(0, 'delete() call not available for this endpoint') class _add_noauth(object): """ Cloudflare v4 API""" def __init__(self, base, api_call_part1, api_call_part2=None, api_call_part3=None): """ Cloudflare v4 API""" self._base = base self._parts = [api_call_part1, api_call_part2, api_call_part3] def __call__(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" # This is the same as a get() return self.get(identifier1, identifier2, identifier3, params, data) def __str__(self): """ Cloudflare v4 API""" return '[%s]' % ('/' + '/:id/'.join(filter(None, self._parts))) def get(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_no_auth('GET', self._parts, identifier1, identifier2, identifier3, params, data) def patch(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" raise CloudFlareAPIError(0, 'patch() call not available for this endpoint') def post(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" raise CloudFlareAPIError(0, 'post() call not available for this endpoint') def put(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" raise CloudFlareAPIError(0, 'put() call not available for this endpoint') def delete(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" raise CloudFlareAPIError(0, 'delete() call not available for this endpoint') class _add_with_auth(object): """ Cloudflare v4 API""" def __init__(self, base, api_call_part1, api_call_part2=None, api_call_part3=None): """ Cloudflare v4 API""" self._base = base self._parts = [api_call_part1, api_call_part2, api_call_part3] def __call__(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" # This is the same as a get() return self.get(identifier1, identifier2, identifier3, params, data) def __str__(self): """ Cloudflare v4 API""" return '[%s]' % ('/' + '/:id/'.join(filter(None, self._parts))) def get(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_auth('GET', self._parts, identifier1, identifier2, identifier3, params, data) def patch(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_auth('PATCH', self._parts, identifier1, identifier2, identifier3, params, data) def post(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" return self._base.call_with_auth('POST', self._parts, identifier1, identifier2, identifier3, params, data, files) def put(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_auth('PUT', self._parts, identifier1, identifier2, identifier3, params, data) def delete(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_auth('DELETE', self._parts, identifier1, identifier2, identifier3, params, data) class _add_with_auth_unwrapped(object): """ Cloudflare v4 API""" def __init__(self, base, api_call_part1, api_call_part2=None, api_call_part3=None): """ Cloudflare v4 API""" self._base = base self._parts = [api_call_part1, api_call_part2, api_call_part3] def __call__(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" # This is the same as a get() return self.get(identifier1, identifier2, identifier3, params, data) def __str__(self): """ Cloudflare v4 API""" return '[%s]' % ('/' + '/:id/'.join(filter(None, self._parts))) def get(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_auth_unwrapped('GET', self._parts, identifier1, identifier2, identifier3, params, data) def patch(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_auth_unwrapped('PATCH', self._parts, identifier1, identifier2, identifier3, params, data) def post(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" return self._base.call_with_auth_unwrapped('POST', self._parts, identifier1, identifier2, identifier3, params, data, files) def put(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_auth_unwrapped('PUT', self._parts, identifier1, identifier2, identifier3, params, data) def delete(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_auth_unwrapped('DELETE', self._parts, identifier1, identifier2, identifier3, params, data) class _add_with_cert_auth(object): """ Cloudflare v4 API""" def __init__(self, base, api_call_part1, api_call_part2=None, api_call_part3=None): """ Cloudflare v4 API""" self._base = base self._parts = [api_call_part1, api_call_part2, api_call_part3] def __call__(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" # This is the same as a get() return self.get(identifier1, identifier2, identifier3, params, data) def __str__(self): """ Cloudflare v4 API""" return '[%s]' % ('/' + '/:id/'.join(filter(None, self._parts))) def get(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_certauth('GET', self._parts, identifier1, identifier2, identifier3, params, data) def patch(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_certauth('PATCH', self._parts, identifier1, identifier2, identifier3, params, data) def post(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None, files=None): """ Cloudflare v4 API""" return self._base.call_with_certauth('POST', self._parts, identifier1, identifier2, identifier3, params, data, files) def put(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_certauth('PUT', self._parts, identifier1, identifier2, identifier3, params, data) def delete(self, identifier1=None, identifier2=None, identifier3=None, params=None, data=None): """ Cloudflare v4 API""" return self._base.call_with_certauth('DELETE', self._parts, identifier1, identifier2, identifier3, params, data) def api_list(self, m=None, s=''): """recursive walk of the api tree returning a list of api calls""" if m is None: m = self w = [] for n in sorted(dir(m)): if n[0] == '_': # internal continue if n in ['delete', 'get', 'patch', 'post', 'put']: # gone too far continue a = getattr(m, n) d = dir(a) if '_base' in d: # it's a known api call - lets show the result and continue down the tree if 'delete' in d or 'get' in d or 'patch' in d or 'post' in d or 'put' in d: # only show the result if a call exists for this part if '_parts' in d: w.append(s + '/' + n) w = w + self.api_list(a, s + '/' + n) return w def __init__(self, email=None, token=None, certtoken=None, debug=False, raw=False, use_sessions=True): """ Cloudflare v4 API""" base_url = BASE_URL # class creation values override configuration values [conf_email, conf_token, conf_certtoken, extras] = read_configs() if email is None: email = conf_email if token is None: token = conf_token if certtoken is None: certtoken = conf_certtoken self._base = self._v4base(email, token, certtoken, base_url, debug, raw, use_sessions) # add the API calls api_v4(self) if extras: api_extras(self, extras) def __call__(self): """ Cloudflare v4 API""" raise TypeError('object is not callable') def __enter__(self): """ Cloudflare v4 API""" return self def __exit__(self, t, v, tb): """ Cloudflare v4 API""" if t is None: return True # pretend we didn't deal with raised error - which is true return False def __str__(self): """ Cloudflare v4 API""" return '["%s","%s"]' % (self._base.email, "REDACTED") def __repr__(self): """ Cloudflare v4 API""" return '%s,%s(%s,"%s","%s","%s",%s,"%s")' % ( self.__module__, type(self).__name__, self._base.email, 'REDACTED', 'REDACTED', self._base.base_url, self._base.raw, self._base.user_agent ) cloudflare-2.0.4/CloudFlare/exceptions.py0000644000076500000240000000407113147001703021023 0ustar martinstaff00000000000000""" errors for Cloudflare API""" class CloudFlareError(Exception): """ errors for Cloudflare API""" class CodeMessage(object): """ a small class to save away an interger and string (the code and the message)""" def __init__(self, code, message): self.code = code self.message = message def __int__(self): return self.code def __str__(self): return self.message def __init__(self, code, message, error_chain=None): """ errors for Cloudflare API""" self.evalue = self.CodeMessage(int(code), str(message)) self.error_chain = None if error_chain != None: self.error_chain = [] for evalue in error_chain: self.error_chain.append( self.CodeMessage(int(evalue['code']), str(evalue['message']))) # self.error_chain.append({'code': self.code, 'message': str(self.message)}) def __int__(self): """ integer value for Cloudflare API errors""" return int(self.evalue) def __str__(self): """ string value for Cloudflare API errors""" return str(self.evalue) def __len__(self): """ Cloudflare API errors can contain a chain of errors""" if self.error_chain is None: return 0 else: return len(self.error_chain) def __getitem__(self, ii): """ Cloudflare API errors can contain a chain of errors""" return self.error_chain[ii] def __iter__(self): """ Cloudflare API errors can contain a chain of errors""" if self.error_chain is None: raise StopIteration for evalue in self.error_chain: yield evalue def next(self): """ Cloudflare API errors can contain a chain of errors""" if self.error_chain is None: raise StopIteration() class CloudFlareAPIError(CloudFlareError): """ errors for Cloudflare API""" pass class CloudFlareInternalError(CloudFlareError): """ errors for Cloudflare API""" pass cloudflare-2.0.4/CloudFlare/logging_helper.py0000644000076500000240000000256013241157467021646 0ustar martinstaff00000000000000""" Logging for Cloudflare API""" import logging # try: # import http.client as http_client # except ImportError: # # Python 2 # import httplib as http_client DEBUG = 0 INFO = 1 class CFlogger(object): """ Logging for Cloudflare API""" def __init__(self, level): """ Logging for Cloudflare API""" self.logger_level = self._get_logging_level(level) #logging.basicConfig(level=self.logger_level) request_logger = logging.getLogger("requests.packages.urllib3") request_logger.setLevel(self.logger_level) request_logger.propagate = level def getLogger(self): """ Logging for Cloudflare API""" # create logger logger = logging.getLogger('Python Cloudflare API v4') logger.setLevel(self.logger_level) ch = logging.StreamHandler() ch.setLevel(self.logger_level) # create formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # add formatter to ch ch.setFormatter(formatter) # add ch to logger logger.addHandler(ch) # http_client.HTTPConnection.debuglevel = 1 return logger def _get_logging_level(self, level): """ Logging for Cloudflare API""" if level is True: return logging.DEBUG else: return logging.INFO cloudflare-2.0.4/CloudFlare/read_configs.py0000644000076500000240000000317713147001703021273 0ustar martinstaff00000000000000""" reading the config file for Cloudflare API""" import os import re try: import ConfigParser # py2 except ImportError: import configparser as ConfigParser # py3 def read_configs(): """ reading the config file for Cloudflare API""" # envioronment variables override config files email = os.getenv('CF_API_EMAIL') token = os.getenv('CF_API_KEY') certtoken = os.getenv('CF_API_CERTKEY') extras = os.getenv('CF_API_EXTRAS') # grab values from config files config = ConfigParser.RawConfigParser() config.read([ '.cloudflare.cfg', os.path.expanduser('~/.cloudflare.cfg'), os.path.expanduser('~/.cloudflare/cloudflare.cfg') ]) if email is None: try: email = re.sub(r"\s+", '', config.get('CloudFlare', 'email')) except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): email = None if token is None: try: token = re.sub(r"\s+", '', config.get('CloudFlare', 'token')) except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): token = None if certtoken is None: try: certtoken = re.sub(r"\s+", '', config.get('CloudFlare', 'certtoken')) except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): certtoken = None if extras is None: try: extras = re.sub(r"\s+", ' ', config.get('CloudFlare', 'extras')) except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): extras = None if extras: extras = extras.split(' ') return [email, token, certtoken, extras] cloudflare-2.0.4/CloudFlare/utils.py0000644000076500000240000000203613147001703020001 0ustar martinstaff00000000000000""" misc utilities for Cloudflare API""" from __future__ import absolute_import import sys import requests from . import __version__ def user_agent(): """ misc utilities for Cloudflare API""" # the default User-Agent is something like 'python-requests/2.11.1' # this additional data helps support @ Cloudflare help customers return ('python-cloudflare/' + __version__ + '/' + 'python-requests/' + str(requests.__version__) + '/' + 'python/' + '.'.join(map(str, sys.version_info[:3])) ) def sanitize_secrets(secrets): """ misc utilities for Cloudflare API""" redacted_phrase = 'REDACTED' if secrets is None: return None secrets_copy = secrets.copy() if 'password' in secrets_copy: secrets_copy['password'] = redacted_phrase elif 'X-Auth-Key' in secrets_copy: secrets_copy['X-Auth-Key'] = redacted_phrase elif 'X-Auth-User-Service-Key' in secrets_copy: secrets_copy['X-Auth-User-Service-Key'] = redacted_phrase return secrets_copy cloudflare-2.0.4/cloudflare.egg-info/0000755000076500000240000000000013241223516020063 5ustar martinstaff00000000000000cloudflare-2.0.4/cloudflare.egg-info/dependency_links.txt0000644000076500000240000000000113241223516024131 0ustar martinstaff00000000000000 cloudflare-2.0.4/cloudflare.egg-info/entry_points.txt0000644000076500000240000000005513241223516023361 0ustar martinstaff00000000000000[console_scripts] cli4 = cli4.__main__:main cloudflare-2.0.4/cloudflare.egg-info/PKG-INFO0000644000076500000240000017102113241223516021162 0ustar martinstaff00000000000000Metadata-Version: 1.1 Name: cloudflare Version: 2.0.4 Summary: Python wrapper for the Cloudflare v4 API Home-page: https://github.com/cloudflare/python-cloudflare Author: Martin J. Levy Author-email: martin@cloudflare.com License: MIT Description: cloudflare-python ================= Installation ------------ Two methods are provided to install this software. Use PyPi (see `package `__ details) or GitHub (see `package `__ details). Via PyPI ~~~~~~~~ .. code:: bash $ sudo pip install cloudflare $ Yes - that simple! (the sudo may not be needed in some cases). Via github ~~~~~~~~~~ .. code:: bash $ git clone https://github.com/cloudflare/python-cloudflare $ cd python-cloudflare $ ./setup.py build $ sudo ./setup.py install $ Or whatever variance of that you want to use. There is a Makefile included. Cloudflare name change - dropping the capital F ----------------------------------------------- In Sepember/October 2016 the company modified its company name and dropped the capital F. However, for now (and for backward compatibility reasons) the class name stays the same. Cloudflare API version 4 ------------------------ The Cloudflare API can be found `here `__. Each API call is provided via a similarly named function within the **CloudFlare** class. A full list is provided below. Example code ------------ All example code is available on GitHub (see `package `__ in the `examples `__ folder. Blog ---- This package was initially introduced `here `__ via Cloudflare's `blog `__. Getting Started --------------- A very simple listing of zones within your account; including the IPv6 status of the zone. .. code:: python import CloudFlare def main(): cf = CloudFlare.CloudFlare() zones = cf.zones.get() for zone in zones: zone_id = zone['id'] zone_name = zone['name'] print zone_id, zone_name if __name__ == '__main__': main() This example works when there are less than 50 zones (50 is the default number of values returned from a query like this). Now lets expand on that and add code to show the IPv6 and SSL status of the zones. Lets also query 100 zones. .. code:: python import CloudFlare def main(): cf = CloudFlare.CloudFlare() zones = cf.zones.get(params = {'per_page':100}) for zone in zones: zone_id = zone['id'] zone_name = zone['name'] settings_ssl = cf.zones.settings.ssl.get(zone_id) ssl_status = settings_ssl['value'] settings_ipv6 = cf.zones.settings.ipv6.get(zone_id) ipv6_status = settings_ipv6['value'] print zone_id, zone_name, ssl_status, ipv6_status if __name__ == '__main__': main() In order to query more than a single page of zones, we would have to use the raw mode (decribed more below). We can loop over many get calls and pass the page paramater to facilitate the paging. Raw mode is only needed when a get request has the possibility of returning many items. .. code:: python import CloudFlare def main(): cf = CloudFlare.CloudFlare(raw=True) page_number = 0 while True: page_number += 1 raw_results = cf.zones.get(params={'per_page':5,'page':page_number}) zones = raw_results['result'] for zone in zones: zone_id = zone['id'] zone_name = zone['name'] print zone_id, zone_name total_pages = raw_results['result_info']['total_pages'] if page_number == total_pages: break if __name__ == '__main__': main() A more complex example follows. .. code:: python import CloudFlare def main(): zone_name = 'example.com' cf = CloudFlare.CloudFlare() # query for the zone name and expect only one value back try: zones = cf.zones.get(params = {'name':zone_name,'per_page':1}) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.get %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) if len(zones) == 0: exit('No zones found') # extract the zone_id which is needed to process that zone zone = zones[0] zone_id = zone['id'] # request the DNS records from that zone try: dns_records = cf.zones.dns_records.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records.get %d %s - api call failed' % (e, e)) # print the results - first the zone name print zone_id, zone_name # then all the DNS records for that zone for dns_record in dns_records: r_name = dns_record['name'] r_type = dns_record['type'] r_value = dns_record['content'] r_id = dns_record['id'] print '\t', r_id, r_name, r_type, r_value exit(0) if __name__ == '__main__': main() Providing Cloudflare Username and API Key ----------------------------------------- When you create a **CloudFlare** class you can pass up to four paramaters. - Account email - Account API key - Optional Origin-CA Certificate Token - Optional Debug flag (True/False) .. code:: python import CloudFlare # A minimal call - reading values from environment variables or configuration file cf = CloudFlare.CloudFlare() # A minimal call with debug enabled cf = CloudFlare.CloudFlare(debug=True)) # A full blown call with passed basic account information cf = CloudFlare.CloudFlare(email='user@example.com', token='00000000000000000000000000000000') # A full blown call with passed basic account information and CA-Origin info cf = CloudFlare.CloudFlare(email='user@example.com', token='00000000000000000000000000000000', certtoken='v1.0-...') If the account email and API key are not passed when you create the class, then they are retreived from either the users exported shell environment variables or the .cloudflare.cfg or ~/.cloudflare.cfg or ~/.cloudflare/cloudflare.cfg files, in that order. There is one call that presently doesn't need any email or token certification (the */ips* call); hence you can test without any values saved away. Using shell environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash $ export CF_API_EMAIL='user@example.com' $ export CF_API_KEY='00000000000000000000000000000000' $ export CF_API_CERTKEY='v1.0-...' $ These are optional environment variables; however, they do override the values set within a configuration file. Using configuration file to store email and keys ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash $ cat ~/.cloudflare/cloudflare.cfg [CloudFlare] email = user@example.com token = 00000000000000000000000000000000 certtoken = v1.0-... extras = $ The *CF\_API\_CERTKEY* or *certtoken* values are used for the Origin-CA */certificates* API calls. You can leave *certtoken* in the configuration with a blank value (or omit the option variable fully). The *extras* values are used when adding API calls outside of the core codebase. Technically, this is only useful for internal testing within Cloudflare. You can leave *extras* in the configuration with a blank value (or omit the option variable fully). Exceptions and return values ---------------------------- Response data ~~~~~~~~~~~~~ The response is build from the JSON in the API call. It contains the **results** values; but does not contain the paging values. You can return all the paging values by calling the class with raw=True. Here's an example without paging. .. code:: python #!/usr/bin/env python import json import CloudFlare def main(): cf = CloudFlare.CloudFlare() zones = cf.zones.get(params={'per_page':5}) print len(zones) if __name__ == '__main__': main() The results are as follows. :: 5 When you add the raw option; the APIs full structure is returned. This means the paging values can be seen. .. code:: python #!/usr/bin/env python import json import CloudFlare def main(): cf = CloudFlare.CloudFlare(raw=True) zones = cf.zones.get(params={'per_page':5}) print zones.length() print json.dumps(zones, indent=4, sort_keys=True) if __name__ == '__main__': main() This produces. :: 5 { "result": [ ... ], "result_info": { "count": 5, "page": 1, "per_page": 5, "total_count": 31, "total_pages": 7 } } A full example of paging is provided below. Exceptions ~~~~~~~~~~ The library will raise **CloudFlareAPIError** when the API call fails. The exception returns both an integer and textual message in one value. .. code:: python import CloudFlare ... try r = ... except CloudFlare.exceptions.CloudFlareAPIError as e: exit('api error: %d %s' % (e, e)) ... The other raised response is **CloudFlareInternalError** which can happen when calling an invalid method. In some cases more than one error is returned. In this case the return value **e** is also an array. You can itterate over that array to see the additional error. .. code:: python import sys import CloudFlare ... try r = ... except CloudFlare.exceptions.CloudFlareAPIError as e: if len(e) > 0: sys.stderr.write('api error - more than one error value returned!\n') for x in e: sys.stderr.write('api error: %d %s\n' % (x, x)) exit('api error: %d %s' % (e, e)) ... Exception examples ~~~~~~~~~~~~~~~~~~ Here's examples using the CLI command cli4 of the responses passed back in exceptions. First a simple get with a clean (non-error) response. :: $ cli4 /zones/:example.com/dns_records | jq -c '.[]|{"name":.name,"type":.type,"content":.content}' {"name":"example.com","type":"MX","content":"something.example.com"} {"name":"something.example.com","type":"A","content":"10.10.10.10"} $ Next a simple/single error response. This is simulated by providing incorrect authentication information. :: $ CF_API_EMAIL='someone@example.com' cli4 /zones/ cli4: /zones - 9103 Unknown X-Auth-Key or X-Auth-Email $ Finally, a command that provides more than one error response. This is simulated by passing an invalid IPv4 address to a DNS record creation. :: $ cli4 --post name='foo' type=A content="1" /zones/:example.com/dns_records cli4: /zones/:example.com/dns_records - 9005 Content for A record is invalid. Must be a valid IPv4 address cli4: /zones/:example.com/dns_records - 1004 DNS Validation Error $ Included example code --------------------- The `examples `__ folder contains many examples in both simple and verbose formats. A DNS zone code example ----------------------- .. code:: python #!/usr/bin/env python import sys import CloudFlare def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() zone_info = cf.zones.post(data={'jump_start':False, 'name': zone_name}) zone_id = zone_info['id'] dns_records = [ {'name':'foo', 'type':'AAAA', 'content':'2001:d8b::1'}, {'name':'foo', 'type':'A', 'content':'192.168.0.1'}, {'name':'duh', 'type':'A', 'content':'10.0.0.1', 'ttl':120}, {'name':'bar', 'type':'CNAME', 'content':'foo'}, {'name':'shakespeare', 'type':'TXT', 'content':"What's in a name? That which we call a rose by any other name ..."} ] for dns_record in dns_records: r = cf.zones.dns_records.post(zone_id, data=dns_record) exit(0) if __name__ == '__main__': main() A DNS zone delete code example (be careful) ------------------------------------------- .. code:: python #!/usr/bin/env python import sys import CloudFlare def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() zone_info = cf.zones.get(param={'name': zone_name}) zone_id = zone_info['id'] dns_name = sys.argv[2] dns_records = cf.zones.dns_records.get(zone_id, params={'name':dns_name + '.' + zone_name}) for dns_record in dns_records: dns_record_id = dns_record['id'] r = cf.zones.dns_records.delete(zone_id, dns_record_id) exit(0) if __name__ == '__main__': main() CLI --- All API calls can be called from the command line. The command will convert domain names on-the-fly into zone\_identifier's. .. code:: bash $ cli4 [-V|--version] [-h|--help] [-v|--verbose] [-q|--quiet] [-j|--json] [-y|--yaml] [-r|--raw] [-d|--dump] [--get|--patch|--post|--put|--delete] [item=value ...] /command... CLI paramaters for POST/PUT/PATCH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For API calls that need to pass data or parameters there is various formats to use. The simplest form is ``item=value``. This passes the value as a string within the APIs JSON data. If you need a numeric value passed then **==** can be used to force the value to be treated as a numeric value within the APIs JSON data. For example: ``item==value``. if you need to pass a list of items; then **[]** can be used. For example: :: pool_id1="11111111111111111111111111111111" pool_id2="22222222222222222222222222222222" pool_id3="33333333333333333333333333333333" cli4 --post global_pools="[ ${pool_id1}, ${pool_id2}, ${pool_id3} ]" region_pools="[ ]" /user/load_balancers/maps Data or parameters can be either named or unnamed. It can not be both. Named is the majority format; as described above. Unnamed parameters simply don't have anything before the **=** sign, as in ``=value``. This format is presently only used by the Cloudflare Load Balancer API calls. For example: :: cli4 --put ="00000000000000000000000000000000" /user/load_balancers/maps/:00000000000000000000000000000000/region/:WNAM Data can also be uploaded from file contents. Using the ``item=@filename`` format will open the file and the contents uploaded in the POST. CLI output ~~~~~~~~~~ The output from the CLI command is in JSON or YAML format (and human readable). This is controled by the **--yaml** or **--json** flags (JSON is the default). Simple CLI examples ~~~~~~~~~~~~~~~~~~~ - ``cli4 /user/billing/profile`` - ``cli4 /user/invites`` - ``cli4 /zones/:example.com`` - ``cli4 /zones/:example.com/dnssec`` - ``cli4 /zones/:example.com/settings/ipv6`` - ``cli4 --put /zones/:example.com/activation_check`` - ``cli4 /zones/:example.com/keyless_certificates`` - ``cli4 /zones/:example.com/analytics/dashboard`` More complex CLI examples ~~~~~~~~~~~~~~~~~~~~~~~~~ Here is the creation of a DNS entry, followed by a listing of that entry and then the deletion of that entry. .. code:: bash $ $ cli4 --post name="test" type="A" content="10.0.0.1" /zones/:example.com/dns_records { "id": "00000000000000000000000000000000", "name": "test.example.com", "type": "A", "content": "10.0.0.1", ... } $ $ cli4 /zones/:example.com/dns_records/:test.example.com | jq '{"id":.id,"name":.name,"type":.type,"content":.content}' { "id": "00000000000000000000000000000000", "name": "test.example.com", "type": "A", "content": "10.0.0.1" } $ cli4 --delete /zones/:example.com/dns_records/:test.example.com | jq -c . {"id":"00000000000000000000000000000000"} $ There's the ability to handle dns entries with multiple values. This produces more than one API call within the command. :: $ cli4 /zones/:example.com/dns_records/:test.example.com | jq -c '.[]|{"id":.id,"name":.name,"type":.type,"content":.content}' {"id":"00000000000000000000000000000000","name":"test.example.com","type":"A","content":"192.168.0.1"} {"id":"00000000000000000000000000000000","name":"test.example.com","type":"AAAA","content":"2001:d8b::1"} $ Here are the cache purging commands. .. code:: bash $ cli4 --delete purge_everything=true /zones/:example.com/purge_cache | jq -c . {"id":"00000000000000000000000000000000"} $ $ cli4 --delete files='[http://example.com/css/styles.css]' /zones/:example.com/purge_cache | jq -c . {"id":"00000000000000000000000000000000"} $ $ cli4 --delete files='[http://example.com/css/styles.css,http://example.com/js/script.js]' /zones/:example.com/purge_cache | jq -c . {"id":"00000000000000000000000000000000"} $ $ cli4 --delete tags='[tag1,tag2,tag3]' /zones/:example.com/purge_cache | jq -c . cli4: /zones/:example.com/purge_cache - 1107 Only enterprise zones can purge by tag. $ A somewhat useful listing of available plans for a specific zone. .. code:: bash $ cli4 /zones/:example.com/available_plans | jq -c '.[]|{"id":.id,"name":.name}' {"id":"00000000000000000000000000000000","name":"Pro Website"} {"id":"00000000000000000000000000000000","name":"Business Website"} {"id":"00000000000000000000000000000000","name":"Enterprise Website"} {"id":"0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee","name":"Free Website"} $ Cloudflare CA CLI examples ~~~~~~~~~~~~~~~~~~~~~~~~~~ Here's some Cloudflare CA examples. Note the need of the zone\_id= paramater with the basic **/certificates** call. .. code:: bash $ cli4 /zones/:example.com | jq -c '.|{"id":.id,"name":.name}' {"id":"12345678901234567890123456789012","name":"example.com"} $ $ cli4 zone_id=12345678901234567890123456789012 /certificates | jq -c '.[]|{"id":.id,"expires_on":.expires_on,"hostnames":.hostnames,"certificate":.certificate}' {"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-29 22:36:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "} {"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-28 23:23:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "} {"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-28 23:20:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "} $ A certificate can be viewed via a simple GET request. .. code:: bash $ cli4 /certificates/:123456789012345678901234567890123456789012345678 { "certificate": "-----BEGIN CERTIFICATE-----\n ... ", "expires_on": "2032-01-29 22:36:00 +0000 UTC", "hostnames": [ "*.example.com", "example.com" ], "id": "123456789012345678901234567890123456789012345678", "request_type": "origin-rsa" } $ Creating a certificate. This is done with a **POST** request. Note the use of **==** in order to pass a decimal number (vs. string) in JSON. The CSR is not shown for simplicity sake. .. code:: bash $ CSR=`cat example.com.csr` $ cli4 --post hostnames='["example.com","*.example.com"]' requested_validity==365 request_type="origin-ecc" csr="$CSR" /certificates { "certificate": "-----BEGIN CERTIFICATE-----\n ... ", "csr": "-----BEGIN CERTIFICATE REQUEST-----\n ... ", "expires_on": "2018-09-27 21:47:00 +0000 UTC", "hostnames": [ "*.example.com", "example.com" ], "id": "123456789012345678901234567890123456789012345678", "request_type": "origin-ecc", "requested_validity": 365 } $ Deleting a certificate can be done with a **DELETE** call. .. code:: bash $ cli4 --delete /certificates/:123456789012345678901234567890123456789012345678 { "id": "123456789012345678901234567890123456789012345678", "revoked_at": "0000-00-00T00:00:00Z" } $ Paging CLI examples ~~~~~~~~~~~~~~~~~~~ The **--raw** command provides access to the paging returned values. See the API documentation for all the info. Here's an example of how to page thru a list of zones (it's included in the examples folder as **example\_paging\_thru\_zones.sh**). .. code:: bash : tmp=/tmp/$$_ trap "rm ${tmp}; exit 0" 0 1 2 15 PAGE=0 while true do cli4 --raw per_page=5 page=${PAGE} /zones > ${tmp} domains=`jq -c '.|.result|.[]|.name' < ${tmp} | tr -d '"'` result_info=`jq -c '.|.result_info' < ${tmp}` COUNT=` echo "${result_info}" | jq .count` PAGE=` echo "${result_info}" | jq .page` PER_PAGE=` echo "${result_info}" | jq .per_page` TOTAL_COUNT=`echo "${result_info}" | jq .total_count` TOTAL_PAGES=`echo "${result_info}" | jq .total_pages` echo COUNT=${COUNT} PAGE=${PAGE} PER_PAGE=${PER_PAGE} TOTAL_COUNT=${TOTAL_COUNT} TOTAL_PAGES=${TOTAL_PAGES} -- ${domains} if [ "${PAGE}" == "${TOTAL_PAGES}" ] then ## last section break fi # grab the next page PAGE=`expr ${PAGE} + 1` done It produces the following results. :: COUNT=5 PAGE=1 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- accumsan.example auctor.example consectetur.example dapibus.example elementum.example COUNT=5 PAGE=2 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- felis.example iaculis.example ipsum.example justo.example lacus.example COUNT=5 PAGE=3 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- lectus.example lobortis.example maximus.example morbi.example pharetra.example COUNT=5 PAGE=4 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- porttitor.example potenti.example pretium.example purus.example quisque.example COUNT=5 PAGE=5 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- sagittis.example semper.example sollicitudin.example suspendisse.example tortor.example COUNT=1 PAGE=7 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- varius.example vehicula.example velit.example velit.example vitae.example COUNT=5 PAGE=6 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- vivamus.example DNSSEC CLI examples ~~~~~~~~~~~~~~~~~~~ .. code:: bash $ cli4 /zones/:example.com/dnssec | jq -c '{"status":.status}' {"status":"disabled"} $ $ cli4 --patch status=active /zones/:example.com/dnssec | jq -c '{"status":.status}' {"status":"pending"} $ $ cli4 /zones/:example.com/dnssec { "algorithm": "13", "digest": "41600621c65065b09230ebc9556ced937eb7fd86e31635d0025326ccf09a7194", "digest_algorithm": "SHA256", "digest_type": "2", "ds": "example.com. 3600 IN DS 2371 13 2 41600621c65065b09230ebc9556ced937eb7fd86e31635d0025326ccf09a7194", "flags": 257, "key_tag": 2371, "key_type": "ECDSAP256SHA256", "modified_on": "2016-05-01T22:42:15.591158Z", "public_key": "mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==", "status": "pending" } $ Zone file upload and download CLI examples (uses BIND format files) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Refer to `Import DNS records `__ on API documentation for this feature. .. code:: bash $ cat zone.txt example.com. IN SOA somewhere.example.com. someone.example.com. ( 2017010101 3H 15 1w 3h ) record1.example.com. IN A 10.0.0.1 record2.example.com. IN AAAA 2001:d8b::2 record3.example.com. IN CNAME record1.example.com. record4.example.com. IN TXT "some text" $ $ cli4 --post file=@zone.txt /zones/:example.com/dns_records/import { "recs_added": 4, "total_records_parsed": 4 } $ The following is documented within the **Advanced** option of the DNS page within the Cloudflare portal. :: $ cli4 /zones/:example.com/dns_records/export | egrep -v '^;;|^$' $ORIGIN . @ 3600 IN SOA example.com. root.example.com. ( 2025552311 ; serial 7200 ; refresh 3600 ; retry 86400 ; expire 3600) ; minimum example.com. 300 IN NS REPLACE&ME$WITH^YOUR@NAMESERVER. record4.example.com. 300 IN TXT "some text" record3.example.com. 300 IN CNAME record1.example.com. record1.example.com. 300 IN A 10.0.0.1 record2.example.com. 300 IN AAAA 2001:d8b::2 $ The egrep is used for documentation brevity. This can also be done via Python code with the following example. :: #!/usr/bin/env python import sys import CloudFlare def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() zones = cf.zones.get(params={'name': zone_name}) zone_id = zones[0]['id'] dns_records = cf.zones.dns_records.export.get(zone_id) for l in dns_records.splitlines(): if len(l) == 0 or l[0] == ';': continue print l exit(0) if __name__ == '__main__': main() Cloudflare Workers ~~~~~~~~~~~~~~~~~~ Cloudflare Workers are described on the Cloudflare blog at `here `__ and `here `__, with the beta release announced `here `__. The Python libraries now support the Cloudflare Workers API calls. The following javascript is lifted from https://cloudflareworkers.com/ and slightly modified. :: $ cat modify-body.js addEventListener("fetch", event => { event.respondWith(fetchAndModify(event.request)); }); async function fetchAndModify(request) { console.log("got a request:", request); // Send the request on to the origin server. const response = await fetch(request); // Read response body. const text = await response.text(); // Modify it. const modified = text.replace( "", ""); // Return modified response. return new Response(modified, { status: response.status, statusText: response.statusText, headers: response.headers }); } $ Here's the website with it's simple ```` statement :: $ curl -sS https://example.com/ | fgrep ' $ Now lets add the script. Looking above, you will see that it's simple action is to modify the ```` statement and make the background yellow. :: $ cli4 --put =@- /zones/:example.com/workers/script < modify-body.js { "etag": "1234567890123456789012345678901234567890123456789012345678901234", "id": "example-com", "modified_on": "2018-02-15T00:00:00.000000Z", "script": "addEventListener(\"fetch\", event => {\n event.respondWith(fetchAndModify(event.request));\n});\n\nasync function fetchAndModify(request) {\n console.log(\"got a request:\", request);\n\n // Send the request on to the origin server.\n const response = await fetch(request);\n\n // Read response body.\n const text = await response.text();\n\n // Modify it.\n const modified = text.replace(\n \"\",\n \"\");\n\n // Return modified response.\n return new Response(modified, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers\n });\n}\n", "size": 603 } $ The following call checks that the script is associated with the zone. In this case, it's the only script added by this user. :: $ python3 -m cli4 /user/workers/scripts [ { "created_on": "2018-02-15T00:00:00.000000Z", "etag": "1234567890123456789012345678901234567890123456789012345678901234", "id": "example-com", "modified_on": "2018-02-15T00:00:00.000000Z" } ] $ Next step is to make sure a route is added for that script on that zone. :: $ cli4 --post pattern="example.com/*" script="example-com" /zones/:example.com/workers/routes { "id": "12345678901234567890123456789012" } $ $ cli4 /zones/:example.com/workers/routes [ { "id": "12345678901234567890123456789012", "pattern": "example.com/*", "script": "example-com" } ] $ With that script added to the zone and the route added, we can now see the the website has been modified because of the Cloudflare Worker. :: $ curl -sS https://example.com/ | fgrep ' $ All this can be removed; hence bringing the website back to its initial state. :: $ cli4 --delete /zones/:example.com/workers/script 12345678901234567890123456789012 $ cli4 --delete /zones/:example.com/workers/routes/:12345678901234567890123456789012 true $ $ curl -sS https://example.com/ | fgrep ' $ Refer to the Cloudflare Workers API documentation for more information. Implemented API calls --------------------- The **--dump** argument to cli4 will produce a list of all the call implemented within the library. .. code:: bash $ cli4 --dump /certificates /ips /organizations ... /zones/ssl/analyze /zones/ssl/certificate_packs /zones/ssl/verification $ Table of commands ~~~~~~~~~~~~~~~~~ +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | ``POST`` | ``PATCH`` | ``DELETE`` | API call | +===========+===========+============+=============+==============+===============================================================+ | ``GET`` | | ``POST`` | | ``DELETE`` | /certificates | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /ips | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /organizations | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /organizations/:identifier/firewall/access\_rules/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | | | ``PATCH`` | | /organizations/:identifier/invite | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | | ``DELETE`` | /organizations/:identifier/invites | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | ``DELETE`` | /organizations/:identifier/members | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /organizations/:identifier/railguns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /organizations/:identifier/railguns/:identifier/zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /organizations/:identifier/roles | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /organizations/:identifier/virtual\_dns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /railguns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /railguns/:identifier/zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /user | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/history | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/profile | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/subscriptions/apps | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/subscriptions/zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /user/firewall/access\_rules/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /user/invites | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | ``DELETE`` | /user/organizations | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /user/virtual\_dns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | ``PUT`` | | | | /zones/:identifier/activation\_check | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/analytics/colos | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/analytics/dashboard | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/available\_plans | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | ``PUT`` | | | | /zones/:identifier/custom\_certificates/prioritize | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/custom\_certificates | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | | | | /zones/:identifier/custom\_pages | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | ``POST`` | | ``DELETE`` | /zones/:identifier/dns\_records | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/firewall/waf/packages/:identifier/groups | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/firewall/waf/packages/:identifier/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/firewall/waf/packages | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/firewall/access\_rules/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/keyless\_certificates | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/pagerules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | | | | ``DELETE`` | /zones/:identifier/purge\_cache | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/railguns/:identifier/diagnose | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/railguns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/settings/advanced\_ddos | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/always\_online | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/browser\_cache\_ttl | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/browser\_check | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/cache\_level | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/challenge\_ttl | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/development\_mode | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/email\_obfuscation | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/hotlink\_protection | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/ip\_geolocation | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/ipv6 | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/minify | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/mirage | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/mobile\_redirect | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/origin\_error\_page\_pass\_thru | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/polish | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/prefetch\_preload | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/response\_buffering | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/rocket\_loader | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/security\_header | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/security\_level | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/server\_side\_exclude | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/sort\_query\_string\_for\_cache | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/ssl | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/tls\_1\_2\_only | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/tls\_client\_auth | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/true\_client\_ip\_header | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/waf | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ Adding extra API calls manually ------------------------------- Extra API calls can be added via the configuration file .. code:: bash $ cat ~/.cloudflare/cloudflare.cfg [CloudFlare] extras = /client/v4/command /client/v4/command/:command_identifier /client/v4/command/:command_identifier/settings $ While it's easy to call anything within Cloudflare's API, it's not very useful to add items in here as they will simply return API URL errors. Technically, this is only useful for internal testing within Cloudflare. Issues ------ The following error can be caused by an out of date SSL/TLS library and/or out of date Python. :: /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/util/ssl_.py:318: SNIMissingWarning: An HTTPS request has been made, but the SNI (Subject Name Indication) extension to TLS is not available on this platform. This may cause the server to present an incorrect TLS certificate, which can cause validation failures. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#snimissingwarning. SNIMissingWarning /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/util/ssl_.py:122: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning. InsecurePlatformWarning The solution can be found `here `__ and/or `here `__. Python 2.x vs 3.x support ------------------------- As of May/June 2016 the code is now tested against pylint. This was required in order to move the codebase into Python 3.x. The motivation for this came from `Danielle Madeley (danni) `__. While the codebase has been edited to run on Python 3.x, there's not been enough Python 3.x testing performed. If you can help in this regard; please contact the maintainers. Credit ------ This is based on work by `Felix Wong (gnowxilef) `__ found `here `__. It has been seriously expanded upon. Copyright --------- Portions copyright `Felix Wong (gnowxilef) `__ 2015 and Cloudflare 2016. Keywords: cloudflare Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 cloudflare-2.0.4/cloudflare.egg-info/requires.txt0000644000076500000240000000002713241223516022462 0ustar martinstaff00000000000000requests future pyyaml cloudflare-2.0.4/cloudflare.egg-info/SOURCES.txt0000644000076500000240000000216713241223516021755 0ustar martinstaff00000000000000LICENSE MANIFEST.in README.rst setup.cfg setup.py CloudFlare/__init__.py CloudFlare/api_extras.py CloudFlare/api_v4.py CloudFlare/cloudflare.py CloudFlare/exceptions.py CloudFlare/logging_helper.py CloudFlare/read_configs.py CloudFlare/utils.py cli4/__init__.py cli4/__main__.py cli4/cli4.py cli4/converters.py cloudflare.egg-info/PKG-INFO cloudflare.egg-info/SOURCES.txt cloudflare.egg-info/dependency_links.txt cloudflare.egg-info/entry_points.txt cloudflare.egg-info/requires.txt cloudflare.egg-info/top_level.txt examples/__init__.py examples/example_always_use_https.py examples/example_are_zones_ipv6.py examples/example_are_zones_ipv6_simple.py examples/example_certificates.py examples/example_create_zone_and_populate.py examples/example_delete_zone_entry.py examples/example_dns_export.py examples/example_dnssec_settings.py examples/example_ips.py examples/example_page_rules.sh examples/example_paging_thru_zones.py examples/example_paging_thru_zones.sh examples/example_proxied.py examples/example_settings.py examples/example_update_dynamic_dns.py examples/example_user.py examples/example_with_usage.py examples/example_zones.pycloudflare-2.0.4/cloudflare.egg-info/top_level.txt0000644000076500000240000000003113241223516022607 0ustar martinstaff00000000000000CloudFlare cli4 examples cloudflare-2.0.4/examples/0000755000076500000240000000000013241223516016067 5ustar martinstaff00000000000000cloudflare-2.0.4/examples/__init__.py0000644000076500000240000000000013147001703020163 0ustar martinstaff00000000000000cloudflare-2.0.4/examples/example_always_use_https.py0000755000076500000240000000375413241205405023563 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" update_flag = False try: if sys.argv[1] == '--off': update_flag = True new_value = 'off' sys.argv.pop(1) except IndexError: pass try: if sys.argv[1] == '--on': update_flag = True new_value = 'on' sys.argv.pop(1) except IndexError: pass # Grab the zone name try: zone_name = sys.argv[1] params = {'name':zone_name, 'per_page':1} except IndexError: exit('usage: example_always_use_https.py [--on|--off] zone') cf = CloudFlare.CloudFlare() # grab the zone identifier try: zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.get %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones - %s - api call failed' % (e)) zone_id = zones[0]['id'] # retrieve present value try: r = cf.zones.settings.always_use_https.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.settings.always_use_https.get %d %s - api call failed' % (e, e)) present_value = r['value'] print(zone_id, zone_name, present_value) if update_flag and present_value != new_value: print('\t', '(now updating... %s -> %s)' % (present_value, new_value)) try: r = cf.zones.settings.always_use_https.patch(zone_id, data={'value':new_value}) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.settings.always_use_https.patch %d %s - api call failed' % (e, e)) updated_value = r['value'] if new_value == updated_value: print('\t', '... updated!') if __name__ == '__main__': main() exit(0) cloudflare-2.0.4/examples/example_are_zones_ipv6.py0000755000076500000240000000357313241157727023132 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" # Check for update flag update_ipv6 = False try: if sys.argv[1] == '--update': update_ipv6 = True sys.argv.pop(1) except IndexError: pass # Grab the first argument, if there is one try: zone_name = sys.argv[1] params = {'name':zone_name, 'per_page':1} except IndexError: params = {'per_page':50} cf = CloudFlare.CloudFlare() # grab the zone identifier try: zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.get %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones - %s - api call failed' % (e)) for zone in sorted(zones, key=lambda v: v['name']): zone_name = zone['name'] zone_id = zone['id'] try: ipv6 = cf.zones.settings.ipv6.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.settings.ipv6.get %d %s - api call failed' % (e, e)) ipv6_value = ipv6['value'] if update_ipv6 and ipv6_value == 'off': print(zone_id, ipv6_value, zone_name, '(now updating... off -> on)') try: ipv6 = cf.zones.settings.ipv6.patch(zone_id, data={'value':'on'}) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.settings.ipv6.patch %d %s - api call failed' % (e, e)) ipv6_value = ipv6['value'] if ipv6_value == 'on': print('\t', '... updated!') else: print(zone_id, ipv6_value, zone_name) exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_are_zones_ipv6_simple.py0000755000076500000240000000112513241157737024473 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" cf = CloudFlare.CloudFlare() zones = cf.zones.get(params={'per_page':50}) for zone in zones: zone_name = zone['name'] zone_id = zone['id'] settings_ipv6 = cf.zones.settings.ipv6.get(zone_id) ipv6_on = settings_ipv6['value'] print(zone_id, ipv6_on, zone_name) exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_certificates.py0000755000076500000240000000524413241160113022622 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys import json sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" # Grab the first argument, if there is one try: zone_name = sys.argv[1] params = {'name':zone_name, 'per_page':1} except IndexError: params = {'per_page':50} cf = CloudFlare.CloudFlare() # grab the zone identifier try: zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones - %s - api call failed' % (e)) # there should only be one zone for zone in sorted(zones, key=lambda v: v['name']): zone_name = zone['name'] zone_id = zone['id'] try: certificates = cf.zones.ssl.certificate_packs.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.ssl.certificate_packs %d %s - api call failed' % (e, e)) for certificate in certificates: certificate_type = certificate['type'] primary_certificate = certificate['primary_certificate'] certificate_hosts = certificate['hosts'] certificate_sig = certificate['certificates'][0]['signature'] certificate_sig_count = len(certificate['certificates']) if certificate_sig_count > 1: c = certificate['certificates'][0] print('%-40s %-10s %-32s %-15s [ %s ]' % ( zone_name, certificate_type, primary_certificate, c['signature'], ','.join(certificate_hosts) )) nn = 0 for c in certificate['certificates']: nn += 1 if nn == 1: next print('%-40s %-10s %-32s %2d:%-15s [ %s ]' % ( '', '', '', nn, c['signature'], '' )) else: for c in certificate['certificates']: print('%-40s %-10s %-32s %-15s [ %s ]' % ( zone_name, certificate_type, primary_certificate, c['signature'], ','.join(certificate_hosts) )) exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_create_zone_and_populate.py0000755000076500000240000000760613241160253025217 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" try: zone_name = sys.argv[1] except IndexError: exit('usage: provide a zone name as an argument on the command line') cf = CloudFlare.CloudFlare() # Create zone - which will only work if ... # 1) The zone is not on Cloudflare. # 2) The zone passes a whois test print('Create zone %s ...' % (zone_name)) try: zone_info = cf.zones.post(data={'jump_start':False, 'name': zone_name}) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.post %s - %d %s' % (zone_name, e, e)) except Exception as e: exit('/zones.post %s - %s' % (zone_name, e)) zone_id = zone_info['id'] if 'email' in zone_info['owner']: zone_owner = zone_info['owner']['email'] else: zone_owner = '"' + zone_info['owner']['name'] + '"' zone_plan = zone_info['plan']['name'] zone_status = zone_info['status'] print('\t%s name=%s owner=%s plan=%s status=%s\n' % ( zone_id, zone_name, zone_owner, zone_plan, zone_status )) # DNS records to create dns_records = [ {'name':'ding', 'type':'A', 'content':'216.58.194.206'}, {'name':'foo', 'type':'AAAA', 'content':'2001:d8b::1'}, {'name':'foo', 'type':'A', 'content':'192.168.0.1'}, {'name':'duh', 'type':'A', 'content':'10.0.0.1', 'ttl':120}, {'name':'bar', 'type':'CNAME', 'content':'foo.%s' % (zone_name)}, # CNAME requires FQDN at content {'name':'shakespeare', 'type':'TXT', 'content':"What's in a name? That which we call a rose by any other name would smell as sweet."} ] print('Create DNS records ...') for dns_record in dns_records: # Create DNS record try: r = cf.zones.dns_records.post(zone_id, data=dns_record) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.dns_records.post %s %s - %d %s' % (zone_name, dns_record['name'], e, e)) # Print respose info - they should be the same dns_record = r print('\t%s %30s %6d %-5s %s ; proxied=%s proxiable=%s' % ( dns_record['id'], dns_record['name'], dns_record['ttl'], dns_record['type'], dns_record['content'], dns_record['proxied'], dns_record['proxiable'] )) # set proxied flag to false - for example dns_record_id = dns_record['id'] new_dns_record = { # Must have type/name/content (even if they don't change) 'type':dns_record['type'], 'name':dns_record['name'], 'content':dns_record['content'], # now add new values you want to change 'proxied':False } try: dns_record = cf.zones.dns_records.put(zone_id, dns_record_id, data=new_dns_record) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records.put %d %s - api call failed' % (e, e)) print('') # Now read back all the DNS records print('Read back DNS records ...') try: dns_records = cf.zones.dns_records.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.dns_records.get %s - %d %s' % (zone_name, e, e)) for dns_record in sorted(dns_records, key=lambda v: v['name']): print('\t%s %30s %6d %-5s %s ; proxied=%s proxiable=%s' % ( dns_record['id'], dns_record['name'], dns_record['ttl'], dns_record['type'], dns_record['content'], dns_record['proxied'], dns_record['proxiable'] )) print('') exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_delete_zone_entry.py0000755000076500000240000000407713241160276023710 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys import re import json import requests sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" try: zone_name = sys.argv[1] dns_name = sys.argv[2] except IndexError: exit('usage: example_delete_zone_entry.py zone dns_record') cf = CloudFlare.CloudFlare() # grab the zone identifier try: params = {'name':zone_name} zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) if len(zones) == 0: exit('/zones.get - %s - zone not found' % (zone_name)) if len(zones) != 1: exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones))) zone = zones[0] zone_id = zone['id'] zone_name = zone['name'] print('ZONE:', zone_id, zone_name) try: params = {'name':dns_name + '.' + zone_name} dns_records = cf.zones.dns_records.get(zone_id, params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records %s - %d %s - api call failed' % (dns_name, e, e)) found = False for dns_record in dns_records: dns_record_id = dns_record['id'] dns_record_name = dns_record['name'] dns_record_type = dns_record['type'] dns_record_value = dns_record['content'] print('DNS RECORD:', dns_record_id, dns_record_name, dns_record_type, dns_record_value) try: dns_record = cf.zones.dns_records.delete(zone_id, dns_record_id) print('DELETED') except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.dns_records.delete %s - %d %s - api call failed' % (dns_name, e, e)) found = True if not found: print('RECORD NOT FOUND') exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_dns_export.py0000755000076500000240000000257513241160356022357 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" try: zone_name = sys.argv[1] except IndexError: exit('usage: example_dns_export.py zone') cf = CloudFlare.CloudFlare() # grab the zone identifier try: params = {'name': zone_name} zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) if len(zones) == 0: exit('/zones.get - %s - zone not found' % (zone_name)) if len(zones) != 1: exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones))) zone_id = zones[0]['id'] try: dns_records = cf.zones.dns_records.export.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records/export %s - %d %s - api call failed' % (dns_name, e, e)) for l in dns_records.splitlines(): if len(l) == 0 or l[0] == ';': # blank line or comment line are skipped - to make example easy to see continue print(l) exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_dnssec_settings.py0000755000076500000240000000273213241160375023365 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" # Grab the first argument, if there is one try: zone_name = sys.argv[1] params = {'name':zone_name, 'per_page':1} except IndexError: params = {'per_page':1} cf = CloudFlare.CloudFlare() # grab the zone identifier try: zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.get %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) # there should only be one zone for zone in sorted(zones, key=lambda v: v['name']): zone_name = zone['name'] zone_id = zone['id'] # grab the DNSSEC settings try: settings = cf.zones.dnssec.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.dnssec.get %d %s - api call failed' % (e, e)) print(zone_id, zone_name) # display every setting value for setting in sorted(settings): print('\t%-30s %10s = %s' % ( setting, '(editable)' if setting == 'status' else '', settings[setting] )) print('') exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_ips.py0000755000076500000240000000144213241160447020756 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" cf = CloudFlare.CloudFlare() try: ips = cf.ips.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/ips - %d %s' % (e, e)) except Exception as e: exit('/ips - %s - api call connection failed' % (e)) print('ipv4_cidrs count = ', len(ips['ipv4_cidrs'])) for cidr in sorted(set(ips['ipv4_cidrs'])): print('\t', cidr) print('ipv6_cidrs count = ', len(ips['ipv6_cidrs'])) for cidr in sorted(set(ips['ipv6_cidrs'])): print('\t', cidr) exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_page_rules.sh0000755000076500000240000000067013147001703022267 0ustar martinstaff00000000000000: ZONE=${1-example.com} URL_MATCH="*.${ZONE}/url1*" URL_FORWARDED="http://${ZONE}/url2" cli4 --post \ targets='[ { "target": "url", "constraint": { "operator": "matches", "value": "'${URL_MATCH}'" } } ]' \ actions='[ { "id": "forwarding_url", "value": { "status_code": 302, "url": "'${URL_FORWARDED}'" } } ]' \ status=active \ priority=1 \ /zones/:${ZONE}/pagerules | jq '{"status":.status,"priority":.priority,"id":.id}' exit 0 cloudflare-2.0.4/examples/example_paging_thru_zones.py0000755000076500000240000000233313241160457023711 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): cf = CloudFlare.CloudFlare(raw=True) page_number = 0 while True: page_number += 1 try: raw_results = cf.zones.get(params={'per_page':5,'page':page_number}) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.get %d %s - api call failed' % (e, e)) zones = raw_results['result'] domains = [] for zone in zones: zone_id = zone['id'] zone_name = zone['name'] domains.append(zone_name) count = raw_results['result_info']['count'] page = raw_results['result_info']['page'] per_page = raw_results['result_info']['per_page'] total_count = raw_results['result_info']['total_count'] total_pages = raw_results['result_info']['total_pages'] print("COUNT=%d PAGE=%d PER_PAGE=%d TOTAL_COUNT=%d TOTAL_PAGES=%d -- %s" % (count, page, per_page, total_count, total_pages, domains)) if page_number == total_pages: break if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_paging_thru_zones.sh0000755000076500000240000000141413147006512023666 0ustar martinstaff00000000000000: tmp=/tmp/$$_ trap "rm ${tmp}; exit 0" 0 1 2 15 PAGE_NUMBER=0 while true do # grab the next page PAGE_NUMBER=`expr ${PAGE_NUMBER} + 1` cli4 --raw per_page=5 page=${PAGE_NUMBER} /zones > ${tmp} domains=`jq -c '.|.result|.[]|.name' < ${tmp} | tr -d '"'` result_info=`jq -c '.|.result_info' < ${tmp}` COUNT=` echo "${result_info}" | jq .count` PAGE=` echo "${result_info}" | jq .page` PER_PAGE=` echo "${result_info}" | jq .per_page` TOTAL_COUNT=`echo "${result_info}" | jq .total_count` TOTAL_PAGES=`echo "${result_info}" | jq .total_pages` echo COUNT=${COUNT} PAGE=${PAGE} PER_PAGE=${PER_PAGE} TOTAL_COUNT=${TOTAL_COUNT} TOTAL_PAGES=${TOTAL_PAGES} -- ${domains} if [ "${PAGE_NUMBER}" == "${TOTAL_PAGES}" ] then ## last section break fi done cloudflare-2.0.4/examples/example_proxied.py0000755000076500000240000000654313241160521021635 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Change the proxied value on a FQDN""" try: zone_name = sys.argv[1] dns_name = sys.argv[2] if sys.argv[3] == 'false': new_r_proxied_flag = False elif sys.argv[3] == 'true': new_r_proxied_flag = True else: raise ValueError('bad arg') except IndexError: exit('usage: ./example-make-zone-proxied.py zone dns_record [true|false]') except ValueError: exit('usage: ./example-make-zone-proxied.py zone dns_record [true|false]') cf = CloudFlare.CloudFlare() # grab the zone identifier try: params = {'name':zone_name, 'per_page':1} zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.get %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) if len(zones) != 1: exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones))) # there should only be one zone zone = zones[0] zone_name = zone['name'] zone_id = zone['id'] print("Zone:\t%s %s" % (zone_id, zone_name)) try: params = {'name': dns_name} dns_records = cf.zones.dns_records.get(zone_id, params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records.get %d %s - api call failed' % (e, e)) if len(dns_records) == 0: exit('/zones.dns_records.get - %s - no records found' % (dns_name)) for dns_record in dns_records: r_zone_id = dns_record['zone_id'] r_id = dns_record['id'] r_name = dns_record['name'] r_type = dns_record['type'] r_content = dns_record['content'] r_ttl = dns_record['ttl'] r_proxied = dns_record['proxied'] r_proxiable = dns_record['proxiable'] print('Record:\t%s %s %s %6d %-5s %s ; proxied=%s proxiable=%s' % ( r_zone_id, r_id, r_name, r_ttl, r_type, r_content, r_proxied, r_proxiable )) if r_proxied == new_r_proxied_flag: # Nothing to do continue dns_record_id = dns_record['id'] new_dns_record = { 'zone_id': r_zone_id, 'id': r_id, 'type': r_type, 'name': r_name, 'content': r_content, 'ttl': r_ttl, 'proxied': new_r_proxied_flag } try: dns_record = cf.zones.dns_records.put(zone_id, dns_record_id, data=new_dns_record) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records.put %d %s - api call failed' % (e, e)) r_zone_id = dns_record['zone_id'] r_id = dns_record['id'] r_name = dns_record['name'] r_type = dns_record['type'] r_content = dns_record['content'] r_ttl = dns_record['ttl'] r_proxied = dns_record['proxied'] r_proxiable = dns_record['proxiable'] print('Record:\t%s %s %s %6d %-5s %s ; proxied=%s proxiable=%s <<-- after' % ( r_zone_id, r_id, r_name, r_ttl, r_type, r_content, r_proxied, r_proxiable )) exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_settings.py0000755000076500000240000000351213241160547022024 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" # Grab the first argument, if there is one try: zone_name = sys.argv[1] params = {'name':zone_name, 'per_page':1} except IndexError: params = {'per_page':1} cf = CloudFlare.CloudFlare() # grab the zone identifier try: zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.get %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) # there should only be one zone for zone in sorted(zones, key=lambda v: v['name']): zone_name = zone['name'] zone_id = zone['id'] try: settings = cf.zones.settings.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.settings.get %d %s - api call failed' % (e, e)) print(zone_id, zone_name) for setting in sorted(settings, key=lambda v: v['id']): r_name = setting['id'] r_value = setting['value'] r_editable = setting['editable'] try: k = sorted(r_value.keys()) print('\t%-30s %10s = %s' % (r_name, '(editable)' if r_editable else '', '{')) for k in sorted(r_value.keys()): print('\t%-30s %10s %s = %s' % ('', '', r_name+'/'+k, r_value[k])) print('\t%-30s %10s = %s' % ('', '', '}')) except: print('\t%-30s %10s = %s' % (r_name, '(editable)' if r_editable else '', r_value)) print('') exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_update_dynamic_dns.py0000755000076500000240000001015213241160602024004 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys import re import json import requests sys.path.insert(0, os.path.abspath('..')) import CloudFlare def my_ip_address(): """Cloudflare API code - example""" # This list is adjustable - plus some v6 enabled services are needed # url = 'http://myip.dnsomatic.com' # url = 'http://www.trackip.net/ip' # url = 'http://myexternalip.com/raw' url = 'https://api.ipify.org' try: ip_address = requests.get(url).text except: exit('%s: failed' % (url)) if ip_address == '': exit('%s: failed' % (url)) if ':' in ip_address: ip_address_type = 'AAAA' else: ip_address_type = 'A' return ip_address, ip_address_type def do_dns_update(cf, zone_name, zone_id, dns_name, ip_address, ip_address_type): """Cloudflare API code - example""" try: params = {'name':dns_name, 'match':'all', 'type':ip_address_type} dns_records = cf.zones.dns_records.get(zone_id, params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records %s - %d %s - api call failed' % (dns_name, e, e)) updated = False # update the record - unless it's already correct for dns_record in dns_records: old_ip_address = dns_record['content'] old_ip_address_type = dns_record['type'] if ip_address_type not in ['A', 'AAAA']: # we only deal with A / AAAA records continue if ip_address_type != old_ip_address_type: # only update the correct address type (A or AAAA) # we don't see this becuase of the search params above print('IGNORED: %s %s ; wrong address family' % (dns_name, old_ip_address)) continue if ip_address == old_ip_address: print('UNCHANGED: %s %s' % (dns_name, ip_address)) updated = True continue # Yes, we need to update this record - we know it's the same address type dns_record_id = dns_record['id'] dns_record = { 'name':dns_name, 'type':ip_address_type, 'content':ip_address } try: dns_record = cf.zones.dns_records.put(zone_id, dns_record_id, data=dns_record) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.dns_records.put %s - %d %s - api call failed' % (dns_name, e, e)) print('UPDATED: %s %s -> %s' % (dns_name, old_ip_address, ip_address)) updated = True if updated: return # no exsiting dns record to update - so create dns record dns_record = { 'name':dns_name, 'type':ip_address_type, 'content':ip_address } try: dns_record = cf.zones.dns_records.post(zone_id, data=dns_record) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.dns_records.post %s - %d %s - api call failed' % (dns_name, e, e)) print('CREATED: %s %s' % (dns_name, ip_address)) def main(): """Cloudflare API code - example""" try: dns_name = sys.argv[1] except IndexError: exit('usage: example-update-dynamic-dns.py fqdn-hostname') host_name, zone_name = dns_name.split('.', 1) ip_address, ip_address_type = my_ip_address() print('MY IP: %s %s' % (dns_name, ip_address)) cf = CloudFlare.CloudFlare() # grab the zone identifier try: params = {'name':zone_name} zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) if len(zones) == 0: exit('/zones.get - %s - zone not found' % (zone_name)) if len(zones) != 1: exit('/zones.get - %s - api call returned %d items' % (zone_name, len(zones))) zone = zones[0] zone_name = zone['name'] zone_id = zone['id'] do_dns_update(cf, zone_name, zone_id, dns_name, ip_address, ip_address_type) exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_user.py0000755000076500000240000001267313241160730021144 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys import json sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" cf = CloudFlare.CloudFlare() print('USER:') # grab the user info try: user = cf.user.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/user.get %d %s - api call failed' % (e, e)) except Exception as e: exit('/user.get - %s - api call failed' % (e)) for k in sorted(user.keys()): if isinstance(user[k], list): if isinstance(user[k][0], dict): print('\t%-40s =' % (k)) for l in user[k]: for j in sorted(l.keys()): if isinstance(l[j], list): print('\t%-40s %s = [ %s ]' % ('', j, ', '.join(l[j]))) else: print('\t%-40s %s = %s' % ('', j, l[j])) else: print('\t%-40s = [ %s ]' % (k, ', '.join(user[k]))) elif isinstance(user[k], dict): print('\t%-40s =' % (k)) for j in sorted(user[k].keys()): print('\t%-40s %s = %s' % ('', j, user[k][j])) else: print('\t%-40s = %s' % (k, user[k])) print('') print('ORGANIZATIONS:') # grab the user organizations info try: organizations = cf.user.organizations.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/user.organizations.get %d %s - api call failed' % (e, e)) if len(organizations) == 0: print('\tNo organization') for organization in organizations: organization_name = organization['name'] organization_id = organization['id'] organization_status = organization['status'] print('\t%-40s %-10s %s' % (organization_id, organization_status, organization_name)) print('') print('INVITES:') # grab the user invites info try: invites = cf.user.invites.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/user.invites.get %d %s - api call failed' % (e, e)) if len(invites) == 0: print('\tNo user invites') for invite in invites: invited_member_id = invite['invited_member_id'] invited_member_email = invite['invited_member_email'] organization_id = invite['organization_id'] organization_name = invite['organization_name'] invited_by = invite['invited_by'] invited_on = invite['invited_on'] expires_on = invite['expires_on'] status = invite['status'] print('\t %s %s %s %s %s %s %s %s' % ( organization_id, status, invited_member_id, invited_member_email, organization_name, invited_by, invited_on, expires_on )) print('') print('BILLING:') # grab the user billing profile info try: profile = cf.user.billing.profile.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/user.billing.profile.get %d %s - api call failed' % (e, e)) profile_id = profile['id'] profile_first = profile['first_name'] profile_last = profile['last_name'] profile_company = profile['company'] if profile_company is None: profile_company = '' if profile['payment_email'] != '': payment_email = profile['payment_email'] card_number = None card_expiry_year = None card_expiry_month = None else: payment_email = None card_number = profile['card_number'] card_expiry_year = profile['card_expiry_year'] card_expiry_month = profile['card_expiry_month'] if payment_email is not None: print('\t %s %s %s %s PayPal: %s' % ( profile_id, profile_first, profile_last, profile_company, payment_email )) else: if card_number is None: card_number = '---- ---- ----- ----' if card_expiry_year is not None and card_expiry_month is not None: card_expiry = card_expiry_month + '/' + card_expiry_year else: card_expiry = '--/--' print('\t %s %s %s %s CC: %s %s' % ( profile_id, profile_first, profile_last, profile_company, card_number, card_expiry )) print('') print('BILLING HISTORY:') # grab the user billing history info try: history = cf.user.billing.history.get() except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/user.billing.history.get %d %s - api call failed' % (e, e)) if len(history) == 0: print('\tNo billing history') for h in sorted(history, key=lambda v: v['occurred_at']): history_id = h['id'] history_type = h['type'] history_action = h['action'] history_occurred_at = h['occurred_at'] history_amount = h['amount'] history_currency = h['currency'] history_description = h['description'] print('\t %s %s %s %s %s %s %s' % ( history_id, history_type, history_action, history_occurred_at, history_amount, history_currency, history_description )) print('') exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_with_usage.py0000755000076500000240000000131413241160751022316 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" # Grab the first argument, if there is one try: zone_name = sys.argv[1] params = {'name':zone_name, 'per_page':1} except IndexError: params = {'per_page':50} # # Show how 'with' statement works # with CloudFlare.CloudFlare() as cf: zones = cf.zones(params=params) for zone in sorted(zones, key=lambda v: v['name']): print(zone['id'], zone['name']) exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/examples/example_zones.py0000755000076500000240000000357213235773522021336 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - example""" from __future__ import print_function import os import sys import re sys.path.insert(0, os.path.abspath('..')) import CloudFlare def main(): """Cloudflare API code - example""" # Grab the first argument, if there is one try: zone_name = sys.argv[1] params = {'name':zone_name, 'per_page':1} except IndexError: params = {'per_page':50} cf = CloudFlare.CloudFlare() # grab the zone identifier try: zones = cf.zones.get(params=params) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) # there should only be one zone for zone in sorted(zones, key=lambda v: v['name']): zone_name = zone['name'] zone_id = zone['id'] if 'email' in zone['owner']: zone_owner = zone['owner']['email'] else: zone_owner = '"' + zone['owner']['name'] + '"' zone_plan = zone['plan']['name'] try: dns_records = cf.zones.dns_records.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records %d %s - api call failed' % (e, e)) print(zone_id, zone_name, zone_owner, zone_plan) prog = re.compile('\.*'+zone_name+'$') dns_records = sorted(dns_records, key=lambda v: prog.sub('', v['name']) + '_' + v['type']) for dns_record in dns_records: r_name = dns_record['name'] r_type = dns_record['type'] r_value = dns_record['content'] r_ttl = dns_record['ttl'] r_id = dns_record['id'] print('\t%s %60s %6d %-5s %s' % (r_id, r_name, r_ttl, r_type, r_value)) print('') exit(0) if __name__ == '__main__': main() cloudflare-2.0.4/LICENSE0000644000076500000240000000210613147001703015252 0ustar martinstaff00000000000000The MIT License (MIT) Copyright (c) 2016 Felix Wong and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cloudflare-2.0.4/MANIFEST.in0000644000076500000240000000012313147001703016000 0ustar martinstaff00000000000000include LICENSE recursive-include examples *.sh *.py #recursive-include cli4 *.man cloudflare-2.0.4/PKG-INFO0000644000076500000240000017102113241223516015350 0ustar martinstaff00000000000000Metadata-Version: 1.1 Name: cloudflare Version: 2.0.4 Summary: Python wrapper for the Cloudflare v4 API Home-page: https://github.com/cloudflare/python-cloudflare Author: Martin J. Levy Author-email: martin@cloudflare.com License: MIT Description: cloudflare-python ================= Installation ------------ Two methods are provided to install this software. Use PyPi (see `package `__ details) or GitHub (see `package `__ details). Via PyPI ~~~~~~~~ .. code:: bash $ sudo pip install cloudflare $ Yes - that simple! (the sudo may not be needed in some cases). Via github ~~~~~~~~~~ .. code:: bash $ git clone https://github.com/cloudflare/python-cloudflare $ cd python-cloudflare $ ./setup.py build $ sudo ./setup.py install $ Or whatever variance of that you want to use. There is a Makefile included. Cloudflare name change - dropping the capital F ----------------------------------------------- In Sepember/October 2016 the company modified its company name and dropped the capital F. However, for now (and for backward compatibility reasons) the class name stays the same. Cloudflare API version 4 ------------------------ The Cloudflare API can be found `here `__. Each API call is provided via a similarly named function within the **CloudFlare** class. A full list is provided below. Example code ------------ All example code is available on GitHub (see `package `__ in the `examples `__ folder. Blog ---- This package was initially introduced `here `__ via Cloudflare's `blog `__. Getting Started --------------- A very simple listing of zones within your account; including the IPv6 status of the zone. .. code:: python import CloudFlare def main(): cf = CloudFlare.CloudFlare() zones = cf.zones.get() for zone in zones: zone_id = zone['id'] zone_name = zone['name'] print zone_id, zone_name if __name__ == '__main__': main() This example works when there are less than 50 zones (50 is the default number of values returned from a query like this). Now lets expand on that and add code to show the IPv6 and SSL status of the zones. Lets also query 100 zones. .. code:: python import CloudFlare def main(): cf = CloudFlare.CloudFlare() zones = cf.zones.get(params = {'per_page':100}) for zone in zones: zone_id = zone['id'] zone_name = zone['name'] settings_ssl = cf.zones.settings.ssl.get(zone_id) ssl_status = settings_ssl['value'] settings_ipv6 = cf.zones.settings.ipv6.get(zone_id) ipv6_status = settings_ipv6['value'] print zone_id, zone_name, ssl_status, ipv6_status if __name__ == '__main__': main() In order to query more than a single page of zones, we would have to use the raw mode (decribed more below). We can loop over many get calls and pass the page paramater to facilitate the paging. Raw mode is only needed when a get request has the possibility of returning many items. .. code:: python import CloudFlare def main(): cf = CloudFlare.CloudFlare(raw=True) page_number = 0 while True: page_number += 1 raw_results = cf.zones.get(params={'per_page':5,'page':page_number}) zones = raw_results['result'] for zone in zones: zone_id = zone['id'] zone_name = zone['name'] print zone_id, zone_name total_pages = raw_results['result_info']['total_pages'] if page_number == total_pages: break if __name__ == '__main__': main() A more complex example follows. .. code:: python import CloudFlare def main(): zone_name = 'example.com' cf = CloudFlare.CloudFlare() # query for the zone name and expect only one value back try: zones = cf.zones.get(params = {'name':zone_name,'per_page':1}) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.get %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) if len(zones) == 0: exit('No zones found') # extract the zone_id which is needed to process that zone zone = zones[0] zone_id = zone['id'] # request the DNS records from that zone try: dns_records = cf.zones.dns_records.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records.get %d %s - api call failed' % (e, e)) # print the results - first the zone name print zone_id, zone_name # then all the DNS records for that zone for dns_record in dns_records: r_name = dns_record['name'] r_type = dns_record['type'] r_value = dns_record['content'] r_id = dns_record['id'] print '\t', r_id, r_name, r_type, r_value exit(0) if __name__ == '__main__': main() Providing Cloudflare Username and API Key ----------------------------------------- When you create a **CloudFlare** class you can pass up to four paramaters. - Account email - Account API key - Optional Origin-CA Certificate Token - Optional Debug flag (True/False) .. code:: python import CloudFlare # A minimal call - reading values from environment variables or configuration file cf = CloudFlare.CloudFlare() # A minimal call with debug enabled cf = CloudFlare.CloudFlare(debug=True)) # A full blown call with passed basic account information cf = CloudFlare.CloudFlare(email='user@example.com', token='00000000000000000000000000000000') # A full blown call with passed basic account information and CA-Origin info cf = CloudFlare.CloudFlare(email='user@example.com', token='00000000000000000000000000000000', certtoken='v1.0-...') If the account email and API key are not passed when you create the class, then they are retreived from either the users exported shell environment variables or the .cloudflare.cfg or ~/.cloudflare.cfg or ~/.cloudflare/cloudflare.cfg files, in that order. There is one call that presently doesn't need any email or token certification (the */ips* call); hence you can test without any values saved away. Using shell environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash $ export CF_API_EMAIL='user@example.com' $ export CF_API_KEY='00000000000000000000000000000000' $ export CF_API_CERTKEY='v1.0-...' $ These are optional environment variables; however, they do override the values set within a configuration file. Using configuration file to store email and keys ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash $ cat ~/.cloudflare/cloudflare.cfg [CloudFlare] email = user@example.com token = 00000000000000000000000000000000 certtoken = v1.0-... extras = $ The *CF\_API\_CERTKEY* or *certtoken* values are used for the Origin-CA */certificates* API calls. You can leave *certtoken* in the configuration with a blank value (or omit the option variable fully). The *extras* values are used when adding API calls outside of the core codebase. Technically, this is only useful for internal testing within Cloudflare. You can leave *extras* in the configuration with a blank value (or omit the option variable fully). Exceptions and return values ---------------------------- Response data ~~~~~~~~~~~~~ The response is build from the JSON in the API call. It contains the **results** values; but does not contain the paging values. You can return all the paging values by calling the class with raw=True. Here's an example without paging. .. code:: python #!/usr/bin/env python import json import CloudFlare def main(): cf = CloudFlare.CloudFlare() zones = cf.zones.get(params={'per_page':5}) print len(zones) if __name__ == '__main__': main() The results are as follows. :: 5 When you add the raw option; the APIs full structure is returned. This means the paging values can be seen. .. code:: python #!/usr/bin/env python import json import CloudFlare def main(): cf = CloudFlare.CloudFlare(raw=True) zones = cf.zones.get(params={'per_page':5}) print zones.length() print json.dumps(zones, indent=4, sort_keys=True) if __name__ == '__main__': main() This produces. :: 5 { "result": [ ... ], "result_info": { "count": 5, "page": 1, "per_page": 5, "total_count": 31, "total_pages": 7 } } A full example of paging is provided below. Exceptions ~~~~~~~~~~ The library will raise **CloudFlareAPIError** when the API call fails. The exception returns both an integer and textual message in one value. .. code:: python import CloudFlare ... try r = ... except CloudFlare.exceptions.CloudFlareAPIError as e: exit('api error: %d %s' % (e, e)) ... The other raised response is **CloudFlareInternalError** which can happen when calling an invalid method. In some cases more than one error is returned. In this case the return value **e** is also an array. You can itterate over that array to see the additional error. .. code:: python import sys import CloudFlare ... try r = ... except CloudFlare.exceptions.CloudFlareAPIError as e: if len(e) > 0: sys.stderr.write('api error - more than one error value returned!\n') for x in e: sys.stderr.write('api error: %d %s\n' % (x, x)) exit('api error: %d %s' % (e, e)) ... Exception examples ~~~~~~~~~~~~~~~~~~ Here's examples using the CLI command cli4 of the responses passed back in exceptions. First a simple get with a clean (non-error) response. :: $ cli4 /zones/:example.com/dns_records | jq -c '.[]|{"name":.name,"type":.type,"content":.content}' {"name":"example.com","type":"MX","content":"something.example.com"} {"name":"something.example.com","type":"A","content":"10.10.10.10"} $ Next a simple/single error response. This is simulated by providing incorrect authentication information. :: $ CF_API_EMAIL='someone@example.com' cli4 /zones/ cli4: /zones - 9103 Unknown X-Auth-Key or X-Auth-Email $ Finally, a command that provides more than one error response. This is simulated by passing an invalid IPv4 address to a DNS record creation. :: $ cli4 --post name='foo' type=A content="1" /zones/:example.com/dns_records cli4: /zones/:example.com/dns_records - 9005 Content for A record is invalid. Must be a valid IPv4 address cli4: /zones/:example.com/dns_records - 1004 DNS Validation Error $ Included example code --------------------- The `examples `__ folder contains many examples in both simple and verbose formats. A DNS zone code example ----------------------- .. code:: python #!/usr/bin/env python import sys import CloudFlare def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() zone_info = cf.zones.post(data={'jump_start':False, 'name': zone_name}) zone_id = zone_info['id'] dns_records = [ {'name':'foo', 'type':'AAAA', 'content':'2001:d8b::1'}, {'name':'foo', 'type':'A', 'content':'192.168.0.1'}, {'name':'duh', 'type':'A', 'content':'10.0.0.1', 'ttl':120}, {'name':'bar', 'type':'CNAME', 'content':'foo'}, {'name':'shakespeare', 'type':'TXT', 'content':"What's in a name? That which we call a rose by any other name ..."} ] for dns_record in dns_records: r = cf.zones.dns_records.post(zone_id, data=dns_record) exit(0) if __name__ == '__main__': main() A DNS zone delete code example (be careful) ------------------------------------------- .. code:: python #!/usr/bin/env python import sys import CloudFlare def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() zone_info = cf.zones.get(param={'name': zone_name}) zone_id = zone_info['id'] dns_name = sys.argv[2] dns_records = cf.zones.dns_records.get(zone_id, params={'name':dns_name + '.' + zone_name}) for dns_record in dns_records: dns_record_id = dns_record['id'] r = cf.zones.dns_records.delete(zone_id, dns_record_id) exit(0) if __name__ == '__main__': main() CLI --- All API calls can be called from the command line. The command will convert domain names on-the-fly into zone\_identifier's. .. code:: bash $ cli4 [-V|--version] [-h|--help] [-v|--verbose] [-q|--quiet] [-j|--json] [-y|--yaml] [-r|--raw] [-d|--dump] [--get|--patch|--post|--put|--delete] [item=value ...] /command... CLI paramaters for POST/PUT/PATCH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For API calls that need to pass data or parameters there is various formats to use. The simplest form is ``item=value``. This passes the value as a string within the APIs JSON data. If you need a numeric value passed then **==** can be used to force the value to be treated as a numeric value within the APIs JSON data. For example: ``item==value``. if you need to pass a list of items; then **[]** can be used. For example: :: pool_id1="11111111111111111111111111111111" pool_id2="22222222222222222222222222222222" pool_id3="33333333333333333333333333333333" cli4 --post global_pools="[ ${pool_id1}, ${pool_id2}, ${pool_id3} ]" region_pools="[ ]" /user/load_balancers/maps Data or parameters can be either named or unnamed. It can not be both. Named is the majority format; as described above. Unnamed parameters simply don't have anything before the **=** sign, as in ``=value``. This format is presently only used by the Cloudflare Load Balancer API calls. For example: :: cli4 --put ="00000000000000000000000000000000" /user/load_balancers/maps/:00000000000000000000000000000000/region/:WNAM Data can also be uploaded from file contents. Using the ``item=@filename`` format will open the file and the contents uploaded in the POST. CLI output ~~~~~~~~~~ The output from the CLI command is in JSON or YAML format (and human readable). This is controled by the **--yaml** or **--json** flags (JSON is the default). Simple CLI examples ~~~~~~~~~~~~~~~~~~~ - ``cli4 /user/billing/profile`` - ``cli4 /user/invites`` - ``cli4 /zones/:example.com`` - ``cli4 /zones/:example.com/dnssec`` - ``cli4 /zones/:example.com/settings/ipv6`` - ``cli4 --put /zones/:example.com/activation_check`` - ``cli4 /zones/:example.com/keyless_certificates`` - ``cli4 /zones/:example.com/analytics/dashboard`` More complex CLI examples ~~~~~~~~~~~~~~~~~~~~~~~~~ Here is the creation of a DNS entry, followed by a listing of that entry and then the deletion of that entry. .. code:: bash $ $ cli4 --post name="test" type="A" content="10.0.0.1" /zones/:example.com/dns_records { "id": "00000000000000000000000000000000", "name": "test.example.com", "type": "A", "content": "10.0.0.1", ... } $ $ cli4 /zones/:example.com/dns_records/:test.example.com | jq '{"id":.id,"name":.name,"type":.type,"content":.content}' { "id": "00000000000000000000000000000000", "name": "test.example.com", "type": "A", "content": "10.0.0.1" } $ cli4 --delete /zones/:example.com/dns_records/:test.example.com | jq -c . {"id":"00000000000000000000000000000000"} $ There's the ability to handle dns entries with multiple values. This produces more than one API call within the command. :: $ cli4 /zones/:example.com/dns_records/:test.example.com | jq -c '.[]|{"id":.id,"name":.name,"type":.type,"content":.content}' {"id":"00000000000000000000000000000000","name":"test.example.com","type":"A","content":"192.168.0.1"} {"id":"00000000000000000000000000000000","name":"test.example.com","type":"AAAA","content":"2001:d8b::1"} $ Here are the cache purging commands. .. code:: bash $ cli4 --delete purge_everything=true /zones/:example.com/purge_cache | jq -c . {"id":"00000000000000000000000000000000"} $ $ cli4 --delete files='[http://example.com/css/styles.css]' /zones/:example.com/purge_cache | jq -c . {"id":"00000000000000000000000000000000"} $ $ cli4 --delete files='[http://example.com/css/styles.css,http://example.com/js/script.js]' /zones/:example.com/purge_cache | jq -c . {"id":"00000000000000000000000000000000"} $ $ cli4 --delete tags='[tag1,tag2,tag3]' /zones/:example.com/purge_cache | jq -c . cli4: /zones/:example.com/purge_cache - 1107 Only enterprise zones can purge by tag. $ A somewhat useful listing of available plans for a specific zone. .. code:: bash $ cli4 /zones/:example.com/available_plans | jq -c '.[]|{"id":.id,"name":.name}' {"id":"00000000000000000000000000000000","name":"Pro Website"} {"id":"00000000000000000000000000000000","name":"Business Website"} {"id":"00000000000000000000000000000000","name":"Enterprise Website"} {"id":"0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee","name":"Free Website"} $ Cloudflare CA CLI examples ~~~~~~~~~~~~~~~~~~~~~~~~~~ Here's some Cloudflare CA examples. Note the need of the zone\_id= paramater with the basic **/certificates** call. .. code:: bash $ cli4 /zones/:example.com | jq -c '.|{"id":.id,"name":.name}' {"id":"12345678901234567890123456789012","name":"example.com"} $ $ cli4 zone_id=12345678901234567890123456789012 /certificates | jq -c '.[]|{"id":.id,"expires_on":.expires_on,"hostnames":.hostnames,"certificate":.certificate}' {"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-29 22:36:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "} {"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-28 23:23:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "} {"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-28 23:20:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "} $ A certificate can be viewed via a simple GET request. .. code:: bash $ cli4 /certificates/:123456789012345678901234567890123456789012345678 { "certificate": "-----BEGIN CERTIFICATE-----\n ... ", "expires_on": "2032-01-29 22:36:00 +0000 UTC", "hostnames": [ "*.example.com", "example.com" ], "id": "123456789012345678901234567890123456789012345678", "request_type": "origin-rsa" } $ Creating a certificate. This is done with a **POST** request. Note the use of **==** in order to pass a decimal number (vs. string) in JSON. The CSR is not shown for simplicity sake. .. code:: bash $ CSR=`cat example.com.csr` $ cli4 --post hostnames='["example.com","*.example.com"]' requested_validity==365 request_type="origin-ecc" csr="$CSR" /certificates { "certificate": "-----BEGIN CERTIFICATE-----\n ... ", "csr": "-----BEGIN CERTIFICATE REQUEST-----\n ... ", "expires_on": "2018-09-27 21:47:00 +0000 UTC", "hostnames": [ "*.example.com", "example.com" ], "id": "123456789012345678901234567890123456789012345678", "request_type": "origin-ecc", "requested_validity": 365 } $ Deleting a certificate can be done with a **DELETE** call. .. code:: bash $ cli4 --delete /certificates/:123456789012345678901234567890123456789012345678 { "id": "123456789012345678901234567890123456789012345678", "revoked_at": "0000-00-00T00:00:00Z" } $ Paging CLI examples ~~~~~~~~~~~~~~~~~~~ The **--raw** command provides access to the paging returned values. See the API documentation for all the info. Here's an example of how to page thru a list of zones (it's included in the examples folder as **example\_paging\_thru\_zones.sh**). .. code:: bash : tmp=/tmp/$$_ trap "rm ${tmp}; exit 0" 0 1 2 15 PAGE=0 while true do cli4 --raw per_page=5 page=${PAGE} /zones > ${tmp} domains=`jq -c '.|.result|.[]|.name' < ${tmp} | tr -d '"'` result_info=`jq -c '.|.result_info' < ${tmp}` COUNT=` echo "${result_info}" | jq .count` PAGE=` echo "${result_info}" | jq .page` PER_PAGE=` echo "${result_info}" | jq .per_page` TOTAL_COUNT=`echo "${result_info}" | jq .total_count` TOTAL_PAGES=`echo "${result_info}" | jq .total_pages` echo COUNT=${COUNT} PAGE=${PAGE} PER_PAGE=${PER_PAGE} TOTAL_COUNT=${TOTAL_COUNT} TOTAL_PAGES=${TOTAL_PAGES} -- ${domains} if [ "${PAGE}" == "${TOTAL_PAGES}" ] then ## last section break fi # grab the next page PAGE=`expr ${PAGE} + 1` done It produces the following results. :: COUNT=5 PAGE=1 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- accumsan.example auctor.example consectetur.example dapibus.example elementum.example COUNT=5 PAGE=2 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- felis.example iaculis.example ipsum.example justo.example lacus.example COUNT=5 PAGE=3 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- lectus.example lobortis.example maximus.example morbi.example pharetra.example COUNT=5 PAGE=4 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- porttitor.example potenti.example pretium.example purus.example quisque.example COUNT=5 PAGE=5 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- sagittis.example semper.example sollicitudin.example suspendisse.example tortor.example COUNT=1 PAGE=7 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- varius.example vehicula.example velit.example velit.example vitae.example COUNT=5 PAGE=6 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- vivamus.example DNSSEC CLI examples ~~~~~~~~~~~~~~~~~~~ .. code:: bash $ cli4 /zones/:example.com/dnssec | jq -c '{"status":.status}' {"status":"disabled"} $ $ cli4 --patch status=active /zones/:example.com/dnssec | jq -c '{"status":.status}' {"status":"pending"} $ $ cli4 /zones/:example.com/dnssec { "algorithm": "13", "digest": "41600621c65065b09230ebc9556ced937eb7fd86e31635d0025326ccf09a7194", "digest_algorithm": "SHA256", "digest_type": "2", "ds": "example.com. 3600 IN DS 2371 13 2 41600621c65065b09230ebc9556ced937eb7fd86e31635d0025326ccf09a7194", "flags": 257, "key_tag": 2371, "key_type": "ECDSAP256SHA256", "modified_on": "2016-05-01T22:42:15.591158Z", "public_key": "mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==", "status": "pending" } $ Zone file upload and download CLI examples (uses BIND format files) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Refer to `Import DNS records `__ on API documentation for this feature. .. code:: bash $ cat zone.txt example.com. IN SOA somewhere.example.com. someone.example.com. ( 2017010101 3H 15 1w 3h ) record1.example.com. IN A 10.0.0.1 record2.example.com. IN AAAA 2001:d8b::2 record3.example.com. IN CNAME record1.example.com. record4.example.com. IN TXT "some text" $ $ cli4 --post file=@zone.txt /zones/:example.com/dns_records/import { "recs_added": 4, "total_records_parsed": 4 } $ The following is documented within the **Advanced** option of the DNS page within the Cloudflare portal. :: $ cli4 /zones/:example.com/dns_records/export | egrep -v '^;;|^$' $ORIGIN . @ 3600 IN SOA example.com. root.example.com. ( 2025552311 ; serial 7200 ; refresh 3600 ; retry 86400 ; expire 3600) ; minimum example.com. 300 IN NS REPLACE&ME$WITH^YOUR@NAMESERVER. record4.example.com. 300 IN TXT "some text" record3.example.com. 300 IN CNAME record1.example.com. record1.example.com. 300 IN A 10.0.0.1 record2.example.com. 300 IN AAAA 2001:d8b::2 $ The egrep is used for documentation brevity. This can also be done via Python code with the following example. :: #!/usr/bin/env python import sys import CloudFlare def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() zones = cf.zones.get(params={'name': zone_name}) zone_id = zones[0]['id'] dns_records = cf.zones.dns_records.export.get(zone_id) for l in dns_records.splitlines(): if len(l) == 0 or l[0] == ';': continue print l exit(0) if __name__ == '__main__': main() Cloudflare Workers ~~~~~~~~~~~~~~~~~~ Cloudflare Workers are described on the Cloudflare blog at `here `__ and `here `__, with the beta release announced `here `__. The Python libraries now support the Cloudflare Workers API calls. The following javascript is lifted from https://cloudflareworkers.com/ and slightly modified. :: $ cat modify-body.js addEventListener("fetch", event => { event.respondWith(fetchAndModify(event.request)); }); async function fetchAndModify(request) { console.log("got a request:", request); // Send the request on to the origin server. const response = await fetch(request); // Read response body. const text = await response.text(); // Modify it. const modified = text.replace( "", ""); // Return modified response. return new Response(modified, { status: response.status, statusText: response.statusText, headers: response.headers }); } $ Here's the website with it's simple ```` statement :: $ curl -sS https://example.com/ | fgrep ' $ Now lets add the script. Looking above, you will see that it's simple action is to modify the ```` statement and make the background yellow. :: $ cli4 --put =@- /zones/:example.com/workers/script < modify-body.js { "etag": "1234567890123456789012345678901234567890123456789012345678901234", "id": "example-com", "modified_on": "2018-02-15T00:00:00.000000Z", "script": "addEventListener(\"fetch\", event => {\n event.respondWith(fetchAndModify(event.request));\n});\n\nasync function fetchAndModify(request) {\n console.log(\"got a request:\", request);\n\n // Send the request on to the origin server.\n const response = await fetch(request);\n\n // Read response body.\n const text = await response.text();\n\n // Modify it.\n const modified = text.replace(\n \"\",\n \"\");\n\n // Return modified response.\n return new Response(modified, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers\n });\n}\n", "size": 603 } $ The following call checks that the script is associated with the zone. In this case, it's the only script added by this user. :: $ python3 -m cli4 /user/workers/scripts [ { "created_on": "2018-02-15T00:00:00.000000Z", "etag": "1234567890123456789012345678901234567890123456789012345678901234", "id": "example-com", "modified_on": "2018-02-15T00:00:00.000000Z" } ] $ Next step is to make sure a route is added for that script on that zone. :: $ cli4 --post pattern="example.com/*" script="example-com" /zones/:example.com/workers/routes { "id": "12345678901234567890123456789012" } $ $ cli4 /zones/:example.com/workers/routes [ { "id": "12345678901234567890123456789012", "pattern": "example.com/*", "script": "example-com" } ] $ With that script added to the zone and the route added, we can now see the the website has been modified because of the Cloudflare Worker. :: $ curl -sS https://example.com/ | fgrep ' $ All this can be removed; hence bringing the website back to its initial state. :: $ cli4 --delete /zones/:example.com/workers/script 12345678901234567890123456789012 $ cli4 --delete /zones/:example.com/workers/routes/:12345678901234567890123456789012 true $ $ curl -sS https://example.com/ | fgrep ' $ Refer to the Cloudflare Workers API documentation for more information. Implemented API calls --------------------- The **--dump** argument to cli4 will produce a list of all the call implemented within the library. .. code:: bash $ cli4 --dump /certificates /ips /organizations ... /zones/ssl/analyze /zones/ssl/certificate_packs /zones/ssl/verification $ Table of commands ~~~~~~~~~~~~~~~~~ +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | ``POST`` | ``PATCH`` | ``DELETE`` | API call | +===========+===========+============+=============+==============+===============================================================+ | ``GET`` | | ``POST`` | | ``DELETE`` | /certificates | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /ips | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /organizations | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /organizations/:identifier/firewall/access\_rules/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | | | ``PATCH`` | | /organizations/:identifier/invite | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | | ``DELETE`` | /organizations/:identifier/invites | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | ``DELETE`` | /organizations/:identifier/members | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /organizations/:identifier/railguns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /organizations/:identifier/railguns/:identifier/zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /organizations/:identifier/roles | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /organizations/:identifier/virtual\_dns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /railguns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /railguns/:identifier/zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /user | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/history | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/profile | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/subscriptions/apps | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/subscriptions/zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /user/firewall/access\_rules/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /user/invites | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | ``DELETE`` | /user/organizations | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /user/virtual\_dns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | ``PUT`` | | | | /zones/:identifier/activation\_check | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/analytics/colos | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/analytics/dashboard | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/available\_plans | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | ``PUT`` | | | | /zones/:identifier/custom\_certificates/prioritize | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/custom\_certificates | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | | | | /zones/:identifier/custom\_pages | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | ``POST`` | | ``DELETE`` | /zones/:identifier/dns\_records | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/firewall/waf/packages/:identifier/groups | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/firewall/waf/packages/:identifier/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/firewall/waf/packages | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/firewall/access\_rules/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/keyless\_certificates | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/pagerules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | | | | ``DELETE`` | /zones/:identifier/purge\_cache | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/railguns/:identifier/diagnose | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/railguns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/settings/advanced\_ddos | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/always\_online | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/browser\_cache\_ttl | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/browser\_check | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/cache\_level | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/challenge\_ttl | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/development\_mode | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/email\_obfuscation | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/hotlink\_protection | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/ip\_geolocation | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/ipv6 | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/minify | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/mirage | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/mobile\_redirect | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/origin\_error\_page\_pass\_thru | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/polish | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/prefetch\_preload | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/response\_buffering | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/rocket\_loader | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/security\_header | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/security\_level | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/server\_side\_exclude | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/sort\_query\_string\_for\_cache | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/ssl | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/tls\_1\_2\_only | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/tls\_client\_auth | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/true\_client\_ip\_header | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/waf | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ Adding extra API calls manually ------------------------------- Extra API calls can be added via the configuration file .. code:: bash $ cat ~/.cloudflare/cloudflare.cfg [CloudFlare] extras = /client/v4/command /client/v4/command/:command_identifier /client/v4/command/:command_identifier/settings $ While it's easy to call anything within Cloudflare's API, it's not very useful to add items in here as they will simply return API URL errors. Technically, this is only useful for internal testing within Cloudflare. Issues ------ The following error can be caused by an out of date SSL/TLS library and/or out of date Python. :: /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/util/ssl_.py:318: SNIMissingWarning: An HTTPS request has been made, but the SNI (Subject Name Indication) extension to TLS is not available on this platform. This may cause the server to present an incorrect TLS certificate, which can cause validation failures. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#snimissingwarning. SNIMissingWarning /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/util/ssl_.py:122: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning. InsecurePlatformWarning The solution can be found `here `__ and/or `here `__. Python 2.x vs 3.x support ------------------------- As of May/June 2016 the code is now tested against pylint. This was required in order to move the codebase into Python 3.x. The motivation for this came from `Danielle Madeley (danni) `__. While the codebase has been edited to run on Python 3.x, there's not been enough Python 3.x testing performed. If you can help in this regard; please contact the maintainers. Credit ------ This is based on work by `Felix Wong (gnowxilef) `__ found `here `__. It has been seriously expanded upon. Copyright --------- Portions copyright `Felix Wong (gnowxilef) `__ 2015 and Cloudflare 2016. Keywords: cloudflare Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 cloudflare-2.0.4/README.rst0000644000076500000240000014446313241223331015747 0ustar martinstaff00000000000000cloudflare-python ================= Installation ------------ Two methods are provided to install this software. Use PyPi (see `package `__ details) or GitHub (see `package `__ details). Via PyPI ~~~~~~~~ .. code:: bash $ sudo pip install cloudflare $ Yes - that simple! (the sudo may not be needed in some cases). Via github ~~~~~~~~~~ .. code:: bash $ git clone https://github.com/cloudflare/python-cloudflare $ cd python-cloudflare $ ./setup.py build $ sudo ./setup.py install $ Or whatever variance of that you want to use. There is a Makefile included. Cloudflare name change - dropping the capital F ----------------------------------------------- In Sepember/October 2016 the company modified its company name and dropped the capital F. However, for now (and for backward compatibility reasons) the class name stays the same. Cloudflare API version 4 ------------------------ The Cloudflare API can be found `here `__. Each API call is provided via a similarly named function within the **CloudFlare** class. A full list is provided below. Example code ------------ All example code is available on GitHub (see `package `__ in the `examples `__ folder. Blog ---- This package was initially introduced `here `__ via Cloudflare's `blog `__. Getting Started --------------- A very simple listing of zones within your account; including the IPv6 status of the zone. .. code:: python import CloudFlare def main(): cf = CloudFlare.CloudFlare() zones = cf.zones.get() for zone in zones: zone_id = zone['id'] zone_name = zone['name'] print zone_id, zone_name if __name__ == '__main__': main() This example works when there are less than 50 zones (50 is the default number of values returned from a query like this). Now lets expand on that and add code to show the IPv6 and SSL status of the zones. Lets also query 100 zones. .. code:: python import CloudFlare def main(): cf = CloudFlare.CloudFlare() zones = cf.zones.get(params = {'per_page':100}) for zone in zones: zone_id = zone['id'] zone_name = zone['name'] settings_ssl = cf.zones.settings.ssl.get(zone_id) ssl_status = settings_ssl['value'] settings_ipv6 = cf.zones.settings.ipv6.get(zone_id) ipv6_status = settings_ipv6['value'] print zone_id, zone_name, ssl_status, ipv6_status if __name__ == '__main__': main() In order to query more than a single page of zones, we would have to use the raw mode (decribed more below). We can loop over many get calls and pass the page paramater to facilitate the paging. Raw mode is only needed when a get request has the possibility of returning many items. .. code:: python import CloudFlare def main(): cf = CloudFlare.CloudFlare(raw=True) page_number = 0 while True: page_number += 1 raw_results = cf.zones.get(params={'per_page':5,'page':page_number}) zones = raw_results['result'] for zone in zones: zone_id = zone['id'] zone_name = zone['name'] print zone_id, zone_name total_pages = raw_results['result_info']['total_pages'] if page_number == total_pages: break if __name__ == '__main__': main() A more complex example follows. .. code:: python import CloudFlare def main(): zone_name = 'example.com' cf = CloudFlare.CloudFlare() # query for the zone name and expect only one value back try: zones = cf.zones.get(params = {'name':zone_name,'per_page':1}) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones.get %d %s - api call failed' % (e, e)) except Exception as e: exit('/zones.get - %s - api call failed' % (e)) if len(zones) == 0: exit('No zones found') # extract the zone_id which is needed to process that zone zone = zones[0] zone_id = zone['id'] # request the DNS records from that zone try: dns_records = cf.zones.dns_records.get(zone_id) except CloudFlare.exceptions.CloudFlareAPIError as e: exit('/zones/dns_records.get %d %s - api call failed' % (e, e)) # print the results - first the zone name print zone_id, zone_name # then all the DNS records for that zone for dns_record in dns_records: r_name = dns_record['name'] r_type = dns_record['type'] r_value = dns_record['content'] r_id = dns_record['id'] print '\t', r_id, r_name, r_type, r_value exit(0) if __name__ == '__main__': main() Providing Cloudflare Username and API Key ----------------------------------------- When you create a **CloudFlare** class you can pass up to four paramaters. - Account email - Account API key - Optional Origin-CA Certificate Token - Optional Debug flag (True/False) .. code:: python import CloudFlare # A minimal call - reading values from environment variables or configuration file cf = CloudFlare.CloudFlare() # A minimal call with debug enabled cf = CloudFlare.CloudFlare(debug=True)) # A full blown call with passed basic account information cf = CloudFlare.CloudFlare(email='user@example.com', token='00000000000000000000000000000000') # A full blown call with passed basic account information and CA-Origin info cf = CloudFlare.CloudFlare(email='user@example.com', token='00000000000000000000000000000000', certtoken='v1.0-...') If the account email and API key are not passed when you create the class, then they are retreived from either the users exported shell environment variables or the .cloudflare.cfg or ~/.cloudflare.cfg or ~/.cloudflare/cloudflare.cfg files, in that order. There is one call that presently doesn't need any email or token certification (the */ips* call); hence you can test without any values saved away. Using shell environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash $ export CF_API_EMAIL='user@example.com' $ export CF_API_KEY='00000000000000000000000000000000' $ export CF_API_CERTKEY='v1.0-...' $ These are optional environment variables; however, they do override the values set within a configuration file. Using configuration file to store email and keys ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: bash $ cat ~/.cloudflare/cloudflare.cfg [CloudFlare] email = user@example.com token = 00000000000000000000000000000000 certtoken = v1.0-... extras = $ The *CF\_API\_CERTKEY* or *certtoken* values are used for the Origin-CA */certificates* API calls. You can leave *certtoken* in the configuration with a blank value (or omit the option variable fully). The *extras* values are used when adding API calls outside of the core codebase. Technically, this is only useful for internal testing within Cloudflare. You can leave *extras* in the configuration with a blank value (or omit the option variable fully). Exceptions and return values ---------------------------- Response data ~~~~~~~~~~~~~ The response is build from the JSON in the API call. It contains the **results** values; but does not contain the paging values. You can return all the paging values by calling the class with raw=True. Here's an example without paging. .. code:: python #!/usr/bin/env python import json import CloudFlare def main(): cf = CloudFlare.CloudFlare() zones = cf.zones.get(params={'per_page':5}) print len(zones) if __name__ == '__main__': main() The results are as follows. :: 5 When you add the raw option; the APIs full structure is returned. This means the paging values can be seen. .. code:: python #!/usr/bin/env python import json import CloudFlare def main(): cf = CloudFlare.CloudFlare(raw=True) zones = cf.zones.get(params={'per_page':5}) print zones.length() print json.dumps(zones, indent=4, sort_keys=True) if __name__ == '__main__': main() This produces. :: 5 { "result": [ ... ], "result_info": { "count": 5, "page": 1, "per_page": 5, "total_count": 31, "total_pages": 7 } } A full example of paging is provided below. Exceptions ~~~~~~~~~~ The library will raise **CloudFlareAPIError** when the API call fails. The exception returns both an integer and textual message in one value. .. code:: python import CloudFlare ... try r = ... except CloudFlare.exceptions.CloudFlareAPIError as e: exit('api error: %d %s' % (e, e)) ... The other raised response is **CloudFlareInternalError** which can happen when calling an invalid method. In some cases more than one error is returned. In this case the return value **e** is also an array. You can itterate over that array to see the additional error. .. code:: python import sys import CloudFlare ... try r = ... except CloudFlare.exceptions.CloudFlareAPIError as e: if len(e) > 0: sys.stderr.write('api error - more than one error value returned!\n') for x in e: sys.stderr.write('api error: %d %s\n' % (x, x)) exit('api error: %d %s' % (e, e)) ... Exception examples ~~~~~~~~~~~~~~~~~~ Here's examples using the CLI command cli4 of the responses passed back in exceptions. First a simple get with a clean (non-error) response. :: $ cli4 /zones/:example.com/dns_records | jq -c '.[]|{"name":.name,"type":.type,"content":.content}' {"name":"example.com","type":"MX","content":"something.example.com"} {"name":"something.example.com","type":"A","content":"10.10.10.10"} $ Next a simple/single error response. This is simulated by providing incorrect authentication information. :: $ CF_API_EMAIL='someone@example.com' cli4 /zones/ cli4: /zones - 9103 Unknown X-Auth-Key or X-Auth-Email $ Finally, a command that provides more than one error response. This is simulated by passing an invalid IPv4 address to a DNS record creation. :: $ cli4 --post name='foo' type=A content="1" /zones/:example.com/dns_records cli4: /zones/:example.com/dns_records - 9005 Content for A record is invalid. Must be a valid IPv4 address cli4: /zones/:example.com/dns_records - 1004 DNS Validation Error $ Included example code --------------------- The `examples `__ folder contains many examples in both simple and verbose formats. A DNS zone code example ----------------------- .. code:: python #!/usr/bin/env python import sys import CloudFlare def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() zone_info = cf.zones.post(data={'jump_start':False, 'name': zone_name}) zone_id = zone_info['id'] dns_records = [ {'name':'foo', 'type':'AAAA', 'content':'2001:d8b::1'}, {'name':'foo', 'type':'A', 'content':'192.168.0.1'}, {'name':'duh', 'type':'A', 'content':'10.0.0.1', 'ttl':120}, {'name':'bar', 'type':'CNAME', 'content':'foo'}, {'name':'shakespeare', 'type':'TXT', 'content':"What's in a name? That which we call a rose by any other name ..."} ] for dns_record in dns_records: r = cf.zones.dns_records.post(zone_id, data=dns_record) exit(0) if __name__ == '__main__': main() A DNS zone delete code example (be careful) ------------------------------------------- .. code:: python #!/usr/bin/env python import sys import CloudFlare def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() zone_info = cf.zones.get(param={'name': zone_name}) zone_id = zone_info['id'] dns_name = sys.argv[2] dns_records = cf.zones.dns_records.get(zone_id, params={'name':dns_name + '.' + zone_name}) for dns_record in dns_records: dns_record_id = dns_record['id'] r = cf.zones.dns_records.delete(zone_id, dns_record_id) exit(0) if __name__ == '__main__': main() CLI --- All API calls can be called from the command line. The command will convert domain names on-the-fly into zone\_identifier's. .. code:: bash $ cli4 [-V|--version] [-h|--help] [-v|--verbose] [-q|--quiet] [-j|--json] [-y|--yaml] [-r|--raw] [-d|--dump] [--get|--patch|--post|--put|--delete] [item=value ...] /command... CLI paramaters for POST/PUT/PATCH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For API calls that need to pass data or parameters there is various formats to use. The simplest form is ``item=value``. This passes the value as a string within the APIs JSON data. If you need a numeric value passed then **==** can be used to force the value to be treated as a numeric value within the APIs JSON data. For example: ``item==value``. if you need to pass a list of items; then **[]** can be used. For example: :: pool_id1="11111111111111111111111111111111" pool_id2="22222222222222222222222222222222" pool_id3="33333333333333333333333333333333" cli4 --post global_pools="[ ${pool_id1}, ${pool_id2}, ${pool_id3} ]" region_pools="[ ]" /user/load_balancers/maps Data or parameters can be either named or unnamed. It can not be both. Named is the majority format; as described above. Unnamed parameters simply don't have anything before the **=** sign, as in ``=value``. This format is presently only used by the Cloudflare Load Balancer API calls. For example: :: cli4 --put ="00000000000000000000000000000000" /user/load_balancers/maps/:00000000000000000000000000000000/region/:WNAM Data can also be uploaded from file contents. Using the ``item=@filename`` format will open the file and the contents uploaded in the POST. CLI output ~~~~~~~~~~ The output from the CLI command is in JSON or YAML format (and human readable). This is controled by the **--yaml** or **--json** flags (JSON is the default). Simple CLI examples ~~~~~~~~~~~~~~~~~~~ - ``cli4 /user/billing/profile`` - ``cli4 /user/invites`` - ``cli4 /zones/:example.com`` - ``cli4 /zones/:example.com/dnssec`` - ``cli4 /zones/:example.com/settings/ipv6`` - ``cli4 --put /zones/:example.com/activation_check`` - ``cli4 /zones/:example.com/keyless_certificates`` - ``cli4 /zones/:example.com/analytics/dashboard`` More complex CLI examples ~~~~~~~~~~~~~~~~~~~~~~~~~ Here is the creation of a DNS entry, followed by a listing of that entry and then the deletion of that entry. .. code:: bash $ $ cli4 --post name="test" type="A" content="10.0.0.1" /zones/:example.com/dns_records { "id": "00000000000000000000000000000000", "name": "test.example.com", "type": "A", "content": "10.0.0.1", ... } $ $ cli4 /zones/:example.com/dns_records/:test.example.com | jq '{"id":.id,"name":.name,"type":.type,"content":.content}' { "id": "00000000000000000000000000000000", "name": "test.example.com", "type": "A", "content": "10.0.0.1" } $ cli4 --delete /zones/:example.com/dns_records/:test.example.com | jq -c . {"id":"00000000000000000000000000000000"} $ There's the ability to handle dns entries with multiple values. This produces more than one API call within the command. :: $ cli4 /zones/:example.com/dns_records/:test.example.com | jq -c '.[]|{"id":.id,"name":.name,"type":.type,"content":.content}' {"id":"00000000000000000000000000000000","name":"test.example.com","type":"A","content":"192.168.0.1"} {"id":"00000000000000000000000000000000","name":"test.example.com","type":"AAAA","content":"2001:d8b::1"} $ Here are the cache purging commands. .. code:: bash $ cli4 --delete purge_everything=true /zones/:example.com/purge_cache | jq -c . {"id":"00000000000000000000000000000000"} $ $ cli4 --delete files='[http://example.com/css/styles.css]' /zones/:example.com/purge_cache | jq -c . {"id":"00000000000000000000000000000000"} $ $ cli4 --delete files='[http://example.com/css/styles.css,http://example.com/js/script.js]' /zones/:example.com/purge_cache | jq -c . {"id":"00000000000000000000000000000000"} $ $ cli4 --delete tags='[tag1,tag2,tag3]' /zones/:example.com/purge_cache | jq -c . cli4: /zones/:example.com/purge_cache - 1107 Only enterprise zones can purge by tag. $ A somewhat useful listing of available plans for a specific zone. .. code:: bash $ cli4 /zones/:example.com/available_plans | jq -c '.[]|{"id":.id,"name":.name}' {"id":"00000000000000000000000000000000","name":"Pro Website"} {"id":"00000000000000000000000000000000","name":"Business Website"} {"id":"00000000000000000000000000000000","name":"Enterprise Website"} {"id":"0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee","name":"Free Website"} $ Cloudflare CA CLI examples ~~~~~~~~~~~~~~~~~~~~~~~~~~ Here's some Cloudflare CA examples. Note the need of the zone\_id= paramater with the basic **/certificates** call. .. code:: bash $ cli4 /zones/:example.com | jq -c '.|{"id":.id,"name":.name}' {"id":"12345678901234567890123456789012","name":"example.com"} $ $ cli4 zone_id=12345678901234567890123456789012 /certificates | jq -c '.[]|{"id":.id,"expires_on":.expires_on,"hostnames":.hostnames,"certificate":.certificate}' {"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-29 22:36:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "} {"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-28 23:23:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "} {"id":"123456789012345678901234567890123456789012345678","expires_on":"2032-01-28 23:20:00 +0000 UTC","hostnames":["*.example.com","example.com"],"certificate":"-----BEGIN CERTIFICATE-----\n ... "} $ A certificate can be viewed via a simple GET request. .. code:: bash $ cli4 /certificates/:123456789012345678901234567890123456789012345678 { "certificate": "-----BEGIN CERTIFICATE-----\n ... ", "expires_on": "2032-01-29 22:36:00 +0000 UTC", "hostnames": [ "*.example.com", "example.com" ], "id": "123456789012345678901234567890123456789012345678", "request_type": "origin-rsa" } $ Creating a certificate. This is done with a **POST** request. Note the use of **==** in order to pass a decimal number (vs. string) in JSON. The CSR is not shown for simplicity sake. .. code:: bash $ CSR=`cat example.com.csr` $ cli4 --post hostnames='["example.com","*.example.com"]' requested_validity==365 request_type="origin-ecc" csr="$CSR" /certificates { "certificate": "-----BEGIN CERTIFICATE-----\n ... ", "csr": "-----BEGIN CERTIFICATE REQUEST-----\n ... ", "expires_on": "2018-09-27 21:47:00 +0000 UTC", "hostnames": [ "*.example.com", "example.com" ], "id": "123456789012345678901234567890123456789012345678", "request_type": "origin-ecc", "requested_validity": 365 } $ Deleting a certificate can be done with a **DELETE** call. .. code:: bash $ cli4 --delete /certificates/:123456789012345678901234567890123456789012345678 { "id": "123456789012345678901234567890123456789012345678", "revoked_at": "0000-00-00T00:00:00Z" } $ Paging CLI examples ~~~~~~~~~~~~~~~~~~~ The **--raw** command provides access to the paging returned values. See the API documentation for all the info. Here's an example of how to page thru a list of zones (it's included in the examples folder as **example\_paging\_thru\_zones.sh**). .. code:: bash : tmp=/tmp/$$_ trap "rm ${tmp}; exit 0" 0 1 2 15 PAGE=0 while true do cli4 --raw per_page=5 page=${PAGE} /zones > ${tmp} domains=`jq -c '.|.result|.[]|.name' < ${tmp} | tr -d '"'` result_info=`jq -c '.|.result_info' < ${tmp}` COUNT=` echo "${result_info}" | jq .count` PAGE=` echo "${result_info}" | jq .page` PER_PAGE=` echo "${result_info}" | jq .per_page` TOTAL_COUNT=`echo "${result_info}" | jq .total_count` TOTAL_PAGES=`echo "${result_info}" | jq .total_pages` echo COUNT=${COUNT} PAGE=${PAGE} PER_PAGE=${PER_PAGE} TOTAL_COUNT=${TOTAL_COUNT} TOTAL_PAGES=${TOTAL_PAGES} -- ${domains} if [ "${PAGE}" == "${TOTAL_PAGES}" ] then ## last section break fi # grab the next page PAGE=`expr ${PAGE} + 1` done It produces the following results. :: COUNT=5 PAGE=1 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- accumsan.example auctor.example consectetur.example dapibus.example elementum.example COUNT=5 PAGE=2 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- felis.example iaculis.example ipsum.example justo.example lacus.example COUNT=5 PAGE=3 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- lectus.example lobortis.example maximus.example morbi.example pharetra.example COUNT=5 PAGE=4 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- porttitor.example potenti.example pretium.example purus.example quisque.example COUNT=5 PAGE=5 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- sagittis.example semper.example sollicitudin.example suspendisse.example tortor.example COUNT=1 PAGE=7 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- varius.example vehicula.example velit.example velit.example vitae.example COUNT=5 PAGE=6 PER_PAGE=5 TOTAL_COUNT=31 TOTAL_PAGES=7 -- vivamus.example DNSSEC CLI examples ~~~~~~~~~~~~~~~~~~~ .. code:: bash $ cli4 /zones/:example.com/dnssec | jq -c '{"status":.status}' {"status":"disabled"} $ $ cli4 --patch status=active /zones/:example.com/dnssec | jq -c '{"status":.status}' {"status":"pending"} $ $ cli4 /zones/:example.com/dnssec { "algorithm": "13", "digest": "41600621c65065b09230ebc9556ced937eb7fd86e31635d0025326ccf09a7194", "digest_algorithm": "SHA256", "digest_type": "2", "ds": "example.com. 3600 IN DS 2371 13 2 41600621c65065b09230ebc9556ced937eb7fd86e31635d0025326ccf09a7194", "flags": 257, "key_tag": 2371, "key_type": "ECDSAP256SHA256", "modified_on": "2016-05-01T22:42:15.591158Z", "public_key": "mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==", "status": "pending" } $ Zone file upload and download CLI examples (uses BIND format files) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Refer to `Import DNS records `__ on API documentation for this feature. .. code:: bash $ cat zone.txt example.com. IN SOA somewhere.example.com. someone.example.com. ( 2017010101 3H 15 1w 3h ) record1.example.com. IN A 10.0.0.1 record2.example.com. IN AAAA 2001:d8b::2 record3.example.com. IN CNAME record1.example.com. record4.example.com. IN TXT "some text" $ $ cli4 --post file=@zone.txt /zones/:example.com/dns_records/import { "recs_added": 4, "total_records_parsed": 4 } $ The following is documented within the **Advanced** option of the DNS page within the Cloudflare portal. :: $ cli4 /zones/:example.com/dns_records/export | egrep -v '^;;|^$' $ORIGIN . @ 3600 IN SOA example.com. root.example.com. ( 2025552311 ; serial 7200 ; refresh 3600 ; retry 86400 ; expire 3600) ; minimum example.com. 300 IN NS REPLACE&ME$WITH^YOUR@NAMESERVER. record4.example.com. 300 IN TXT "some text" record3.example.com. 300 IN CNAME record1.example.com. record1.example.com. 300 IN A 10.0.0.1 record2.example.com. 300 IN AAAA 2001:d8b::2 $ The egrep is used for documentation brevity. This can also be done via Python code with the following example. :: #!/usr/bin/env python import sys import CloudFlare def main(): zone_name = sys.argv[1] cf = CloudFlare.CloudFlare() zones = cf.zones.get(params={'name': zone_name}) zone_id = zones[0]['id'] dns_records = cf.zones.dns_records.export.get(zone_id) for l in dns_records.splitlines(): if len(l) == 0 or l[0] == ';': continue print l exit(0) if __name__ == '__main__': main() Cloudflare Workers ~~~~~~~~~~~~~~~~~~ Cloudflare Workers are described on the Cloudflare blog at `here `__ and `here `__, with the beta release announced `here `__. The Python libraries now support the Cloudflare Workers API calls. The following javascript is lifted from https://cloudflareworkers.com/ and slightly modified. :: $ cat modify-body.js addEventListener("fetch", event => { event.respondWith(fetchAndModify(event.request)); }); async function fetchAndModify(request) { console.log("got a request:", request); // Send the request on to the origin server. const response = await fetch(request); // Read response body. const text = await response.text(); // Modify it. const modified = text.replace( "", ""); // Return modified response. return new Response(modified, { status: response.status, statusText: response.statusText, headers: response.headers }); } $ Here's the website with it's simple ```` statement :: $ curl -sS https://example.com/ | fgrep ' $ Now lets add the script. Looking above, you will see that it's simple action is to modify the ```` statement and make the background yellow. :: $ cli4 --put =@- /zones/:example.com/workers/script < modify-body.js { "etag": "1234567890123456789012345678901234567890123456789012345678901234", "id": "example-com", "modified_on": "2018-02-15T00:00:00.000000Z", "script": "addEventListener(\"fetch\", event => {\n event.respondWith(fetchAndModify(event.request));\n});\n\nasync function fetchAndModify(request) {\n console.log(\"got a request:\", request);\n\n // Send the request on to the origin server.\n const response = await fetch(request);\n\n // Read response body.\n const text = await response.text();\n\n // Modify it.\n const modified = text.replace(\n \"\",\n \"\");\n\n // Return modified response.\n return new Response(modified, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers\n });\n}\n", "size": 603 } $ The following call checks that the script is associated with the zone. In this case, it's the only script added by this user. :: $ python3 -m cli4 /user/workers/scripts [ { "created_on": "2018-02-15T00:00:00.000000Z", "etag": "1234567890123456789012345678901234567890123456789012345678901234", "id": "example-com", "modified_on": "2018-02-15T00:00:00.000000Z" } ] $ Next step is to make sure a route is added for that script on that zone. :: $ cli4 --post pattern="example.com/*" script="example-com" /zones/:example.com/workers/routes { "id": "12345678901234567890123456789012" } $ $ cli4 /zones/:example.com/workers/routes [ { "id": "12345678901234567890123456789012", "pattern": "example.com/*", "script": "example-com" } ] $ With that script added to the zone and the route added, we can now see the the website has been modified because of the Cloudflare Worker. :: $ curl -sS https://example.com/ | fgrep ' $ All this can be removed; hence bringing the website back to its initial state. :: $ cli4 --delete /zones/:example.com/workers/script 12345678901234567890123456789012 $ cli4 --delete /zones/:example.com/workers/routes/:12345678901234567890123456789012 true $ $ curl -sS https://example.com/ | fgrep ' $ Refer to the Cloudflare Workers API documentation for more information. Implemented API calls --------------------- The **--dump** argument to cli4 will produce a list of all the call implemented within the library. .. code:: bash $ cli4 --dump /certificates /ips /organizations ... /zones/ssl/analyze /zones/ssl/certificate_packs /zones/ssl/verification $ Table of commands ~~~~~~~~~~~~~~~~~ +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | ``POST`` | ``PATCH`` | ``DELETE`` | API call | +===========+===========+============+=============+==============+===============================================================+ | ``GET`` | | ``POST`` | | ``DELETE`` | /certificates | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /ips | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /organizations | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /organizations/:identifier/firewall/access\_rules/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | | | ``PATCH`` | | /organizations/:identifier/invite | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | | ``DELETE`` | /organizations/:identifier/invites | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | ``DELETE`` | /organizations/:identifier/members | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /organizations/:identifier/railguns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /organizations/:identifier/railguns/:identifier/zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /organizations/:identifier/roles | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /organizations/:identifier/virtual\_dns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /railguns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /railguns/:identifier/zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /user | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/history | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/profile | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/subscriptions/apps | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /user/billing/subscriptions/zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /user/firewall/access\_rules/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /user/invites | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | ``DELETE`` | /user/organizations | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /user/virtual\_dns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | ``PUT`` | | | | /zones/:identifier/activation\_check | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/analytics/colos | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/analytics/dashboard | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/available\_plans | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | ``PUT`` | | | | /zones/:identifier/custom\_certificates/prioritize | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/custom\_certificates | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | | | | /zones/:identifier/custom\_pages | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | ``POST`` | | ``DELETE`` | /zones/:identifier/dns\_records | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/firewall/waf/packages/:identifier/groups | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/firewall/waf/packages/:identifier/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/firewall/waf/packages | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/firewall/access\_rules/rules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/keyless\_certificates | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | ``PUT`` | ``POST`` | ``PATCH`` | ``DELETE`` | /zones/:identifier/pagerules | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | | | | | ``DELETE`` | /zones/:identifier/purge\_cache | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/railguns/:identifier/diagnose | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/railguns | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | | | /zones/:identifier/settings/advanced\_ddos | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/always\_online | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/browser\_cache\_ttl | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/browser\_check | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/cache\_level | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/challenge\_ttl | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/development\_mode | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/email\_obfuscation | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/hotlink\_protection | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/ip\_geolocation | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/ipv6 | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/minify | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/mirage | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/mobile\_redirect | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/origin\_error\_page\_pass\_thru | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/polish | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/prefetch\_preload | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/response\_buffering | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/rocket\_loader | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/security\_header | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/security\_level | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/server\_side\_exclude | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/sort\_query\_string\_for\_cache | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/ssl | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/tls\_1\_2\_only | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/tls\_client\_auth | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/true\_client\_ip\_header | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ | ``GET`` | | | ``PATCH`` | | /zones/:identifier/settings/waf | +-----------+-----------+------------+-------------+--------------+---------------------------------------------------------------+ Adding extra API calls manually ------------------------------- Extra API calls can be added via the configuration file .. code:: bash $ cat ~/.cloudflare/cloudflare.cfg [CloudFlare] extras = /client/v4/command /client/v4/command/:command_identifier /client/v4/command/:command_identifier/settings $ While it's easy to call anything within Cloudflare's API, it's not very useful to add items in here as they will simply return API URL errors. Technically, this is only useful for internal testing within Cloudflare. Issues ------ The following error can be caused by an out of date SSL/TLS library and/or out of date Python. :: /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/util/ssl_.py:318: SNIMissingWarning: An HTTPS request has been made, but the SNI (Subject Name Indication) extension to TLS is not available on this platform. This may cause the server to present an incorrect TLS certificate, which can cause validation failures. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#snimissingwarning. SNIMissingWarning /usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/util/ssl_.py:122: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. You can upgrade to a newer version of Python to solve this. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning. InsecurePlatformWarning The solution can be found `here `__ and/or `here `__. Python 2.x vs 3.x support ------------------------- As of May/June 2016 the code is now tested against pylint. This was required in order to move the codebase into Python 3.x. The motivation for this came from `Danielle Madeley (danni) `__. While the codebase has been edited to run on Python 3.x, there's not been enough Python 3.x testing performed. If you can help in this regard; please contact the maintainers. Credit ------ This is based on work by `Felix Wong (gnowxilef) `__ found `here `__. It has been seriously expanded upon. Copyright --------- Portions copyright `Felix Wong (gnowxilef) `__ 2015 and Cloudflare 2016. cloudflare-2.0.4/setup.cfg0000644000076500000240000000007313241223516016072 0ustar martinstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 cloudflare-2.0.4/setup.py0000755000076500000240000000415713147246402016001 0ustar martinstaff00000000000000#!/usr/bin/env python """Cloudflare API code - setup.py file""" import re from setuptools import setup, find_packages _version_re = re.compile(r"__version__\s=\s'(.*)'") def main(): """Cloudflare API code - setup.py file""" with open('README.rst') as read_me: long_description = read_me.read() with open('CloudFlare/__init__.py', 'r') as f: version = _version_re.search(f.read()).group(1) setup( name='cloudflare', version=version, description='Python wrapper for the Cloudflare v4 API', long_description=long_description, author='Martin J. Levy', author_email='martin@cloudflare.com', # maintainer='Martin J. Levy', # maintainer_email='martin@cloudflare.com', url='https://github.com/cloudflare/python-cloudflare', license='MIT', packages=['cli4', 'examples']+find_packages(), #package_dir={'CloudFlare': 'lib'} #package_dir={'CloudFlare/examples': 'examples'}, #package_data={'cloudflare-examples': ["examples/*"]}, include_package_data=True, #data_files = [('man/man1', ['cli4/cli4.man'])], install_requires=['requests', 'future', 'pyyaml'], keywords='cloudflare', entry_points={ 'console_scripts': [ 'cli4=cli4.__main__:main' ] }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries :: Python Modules', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6' ] ) if __name__ == '__main__': main()