File: //lib/python3.6/site-packages/CloudFlare/cloudflare.py
""" 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
# NDJSON is returned by Enterprise Log Share i.e. /zones/:id/logs/received
if hasattr(response_data, 'decode'):
response_data = response_data.decode('utf-8')
try:
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:
# Lets see if it's NDJSON data
# NDJSON is a series of JSON elements with newlines between each element
try:
r = []
for l in response_data.splitlines():
r.append(json.loads(l))
response_data = r
except:
# While this should not happen; it's always possible
if self.logger:
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
if hasattr(response_data, 'decode'):
response_data = response_data.decode('utf-8')
try:
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)}
elif response_type == 'text/html':
# used by media for preview
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 'messages' in response_data:
errors['error_chain'] = response_data['messages']
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, p1, p2=None, p3=None):
""" Cloudflare v4 API"""
self._base = base
self._parts_unused = [p1, p2, p3]
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, files=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, p1, p2=None, p3=None):
""" Cloudflare v4 API"""
self._base = base
self._parts = [p1, p2, p3]
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, files=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, p1, p2=None, p3=None):
""" Cloudflare v4 API"""
self._base = base
self._parts = [p1, p2, p3]
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, p1, p2=None, p3=None):
""" Cloudflare v4 API"""
self._base = base
self._parts = [p1, p2, p3]
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, p1, p2=None, p3=None):
""" Cloudflare v4 API"""
self._base = base
self._parts = [p1, p2, p3]
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 add(self, t, p1, p2=None, p3=None):
"""add api call to class"""
a = []
if p1:
a += p1.split('/')
if p2:
a += p2.split('/')
if p3:
a += p3.split('/')
branch = self
for element in a[0:-1]:
try:
branch = getattr(branch, element)
except:
# should never happen
raise CloudFlareAPIError(0, 'api load name failed')
name = a[-1]
if t == 'VOID':
f = self._add_unused(self._base, p1, p2, p3)
elif t == 'OPEN':
f = self._add_noauth(self._base, p1, p2, p3)
elif t == 'AUTH':
f = self._add_with_auth(self._base, p1, p2, p3)
elif t == 'CERT':
f = self._add_with_cert_auth(self._base, p1, p2, p3)
elif t == 'AUTH_UNWRAPPED':
f = self._add_with_auth_unwrapped(self._base, p1, p2, p3)
else:
# should never happen
raise CloudFlareAPIError(0, 'api load type mismatch')
setattr(branch, name, f)
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
)