cloudflare_client_rb 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/cloudflare_client.rb +211 -0
- data/lib/cloudflare_client/certificate.rb +40 -0
- data/lib/cloudflare_client/organization.rb +26 -0
- data/lib/cloudflare_client/organization/access_rule.rb +76 -0
- data/lib/cloudflare_client/organization/invite.rb +53 -0
- data/lib/cloudflare_client/organization/member.rb +37 -0
- data/lib/cloudflare_client/organization/railgun.rb +79 -0
- data/lib/cloudflare_client/organization/role.rb +19 -0
- data/lib/cloudflare_client/railgun.rb +63 -0
- data/lib/cloudflare_client/version.rb +3 -0
- data/lib/cloudflare_client/virtual_dns_cluster.rb +133 -0
- data/lib/cloudflare_client/virtual_dns_cluster/analytic.rb +38 -0
- data/lib/cloudflare_client/zone.rb +129 -0
- data/lib/cloudflare_client/zone/analytics.rb +56 -0
- data/lib/cloudflare_client/zone/base.rb +9 -0
- data/lib/cloudflare_client/zone/custom_hostname.rb +86 -0
- data/lib/cloudflare_client/zone/custom_page.rb +28 -0
- data/lib/cloudflare_client/zone/custom_ssl.rb +62 -0
- data/lib/cloudflare_client/zone/dns.rb +66 -0
- data/lib/cloudflare_client/zone/firewall.rb +3 -0
- data/lib/cloudflare_client/zone/firewall/access_rule.rb +87 -0
- data/lib/cloudflare_client/zone/firewall/waf_package.rb +46 -0
- data/lib/cloudflare_client/zone/firewall/waf_package/base.rb +9 -0
- data/lib/cloudflare_client/zone/firewall/waf_package/rule.rb +46 -0
- data/lib/cloudflare_client/zone/firewall/waf_package/rule_group.rb +42 -0
- data/lib/cloudflare_client/zone/keyless_ssl.rb +56 -0
- data/lib/cloudflare_client/zone/log.rb +51 -0
- data/lib/cloudflare_client/zone/page_rule.rb +64 -0
- data/lib/cloudflare_client/zone/railgun_connections.rb +43 -0
- data/lib/cloudflare_client/zone/rate_limit.rb +73 -0
- data/lib/cloudflare_client/zone/ssl.rb +28 -0
- data/lib/cloudflare_client/zone/ssl/certificate_pack.rb +32 -0
- data/lib/cloudflare_client/zone/subscription.rb +55 -0
- metadata +76 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 53055d6f6789ba960ef43045e6b102ffb4e97baf
|
4
|
+
data.tar.gz: 3ef8b8314e81ae120c8c3711b75997d56e5f0e0e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ba0a7b3a325441795cbfe180f351a8385bf5770b79c9b8b6008770438f9fbe4497af207ded9bc20f207482769c6418dee161e1b4311e669a2632031a60f4d468
|
7
|
+
data.tar.gz: f26ff45885f8efb43349310c4ea943522742b0dbbd320154e531104c2e2846b0c1b339af79c605656bc09c9e148beb779eb687e415a9640f6b73df897dd19000
|
@@ -0,0 +1,211 @@
|
|
1
|
+
##
|
2
|
+
# General class for the client
|
3
|
+
class CloudflareClient
|
4
|
+
require 'json'
|
5
|
+
require 'faraday'
|
6
|
+
require 'date'
|
7
|
+
require 'byebug'
|
8
|
+
Dir[File.expand_path('../cloudflare_client/*.rb', __FILE__)].each { |f| require f }
|
9
|
+
|
10
|
+
API_BASE = 'https://api.cloudflare.com/client/v4'.freeze
|
11
|
+
VALID_BUNDLE_METHODS = %w[ubiquitous optimal force].freeze
|
12
|
+
VALID_DIRECTIONS = %w[asc desc].freeze
|
13
|
+
VALID_MATCHES = %w[any all].freeze
|
14
|
+
|
15
|
+
POSSIBLE_API_SETTINGS = %w[
|
16
|
+
advanced_ddos
|
17
|
+
always_online
|
18
|
+
automatic_https_rewrites
|
19
|
+
browser_cache_ttl
|
20
|
+
browser_check
|
21
|
+
cache_level
|
22
|
+
challenge_ttl
|
23
|
+
development_mode
|
24
|
+
email_obfuscation
|
25
|
+
hotlink_protection
|
26
|
+
ip_geolocation
|
27
|
+
ipv6
|
28
|
+
minify
|
29
|
+
mobile_redirect
|
30
|
+
mirage
|
31
|
+
origin_error_page_pass_thru
|
32
|
+
opportunistic_encryption
|
33
|
+
polish
|
34
|
+
webp
|
35
|
+
prefetch_preload
|
36
|
+
response_buffering
|
37
|
+
rocket_loader
|
38
|
+
security_header
|
39
|
+
security_level
|
40
|
+
server_side_exclude
|
41
|
+
sort_query_string_for_cache
|
42
|
+
ssl
|
43
|
+
tls_1_2_only
|
44
|
+
tls_1_3
|
45
|
+
tls_client_auth
|
46
|
+
true_client_ip_header
|
47
|
+
waf
|
48
|
+
http2
|
49
|
+
pseudo_ipv4
|
50
|
+
websockets
|
51
|
+
].freeze
|
52
|
+
|
53
|
+
def initialize(auth_key: nil, email: nil)
|
54
|
+
raise('Missing auth_key') if auth_key.nil?
|
55
|
+
raise('missing email') if email.nil?
|
56
|
+
@cf_client ||= build_client(auth_key: auth_key, email: email)
|
57
|
+
end
|
58
|
+
|
59
|
+
#TODO: add the time based stuff
|
60
|
+
|
61
|
+
#TODO: cloudflare IPs
|
62
|
+
#TODO: AML
|
63
|
+
#TODO: load balancer monitors
|
64
|
+
#TODO: load balancer pools
|
65
|
+
#TODO: org load balancer monitors
|
66
|
+
#TODO: org load balancer pools
|
67
|
+
#TODO: load balancers
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def bundle_method_check(bundle_method)
|
72
|
+
unless VALID_BUNDLE_METHODS.include?(bundle_method)
|
73
|
+
raise("valid bundle methods are #{VALID_BUNDLE_METHODS.flatten}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def date_rfc3339?(ts)
|
78
|
+
begin
|
79
|
+
DateTime.rfc3339(ts)
|
80
|
+
rescue ArgumentError
|
81
|
+
return false
|
82
|
+
end
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
def iso8601_check(name, ts)
|
87
|
+
DateTime.iso8601(ts)
|
88
|
+
rescue ArgumentError
|
89
|
+
raise "#{name} must be a valid iso8601 timestamp"
|
90
|
+
end
|
91
|
+
|
92
|
+
def timestamp_check(name, ts)
|
93
|
+
Time.at(ts).to_datetime
|
94
|
+
rescue TypeError
|
95
|
+
raise "#{name} must be a valid unix timestamp"
|
96
|
+
end
|
97
|
+
|
98
|
+
def id_check(name, id)
|
99
|
+
raise "#{name} required" if id.nil?
|
100
|
+
end
|
101
|
+
|
102
|
+
def valid_value_check(name, value, valid_values)
|
103
|
+
raise "#{name} must be one of #{valid_values}" unless valid_values.include?(value)
|
104
|
+
end
|
105
|
+
|
106
|
+
def non_empty_array_check(name, array)
|
107
|
+
raise "#{name} must be an array of #{name}" unless array.is_a?(Array) && !array.empty?
|
108
|
+
end
|
109
|
+
|
110
|
+
def non_empty_hash_check(name, hash)
|
111
|
+
raise "#{name} must be an hash of #{name}" unless hash.is_a?(Hash) && !hash.empty?
|
112
|
+
end
|
113
|
+
|
114
|
+
def basic_type_check(name, value, type)
|
115
|
+
raise "#{name} must be a #{type}" unless value.is_a?(type)
|
116
|
+
end
|
117
|
+
|
118
|
+
def max_length_check(name, value, max_length=32)
|
119
|
+
raise "the length of #{name} must not exceed #{max_length}" unless value.length <= max_length
|
120
|
+
end
|
121
|
+
|
122
|
+
def range_check(name, value, min=nil, max=nil)
|
123
|
+
if min && max
|
124
|
+
raise "#{name} must be between #{min} and #{max}" unless value >= min && value <= max
|
125
|
+
elsif min
|
126
|
+
raise "#{name} must be equal or larger than #{min}" unless value >= min
|
127
|
+
elsif max
|
128
|
+
raise "#{name} must be equal or less than #{max}" unless value <= max
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def build_client(params)
|
133
|
+
raise('Missing auth_key') if params[:auth_key].nil?
|
134
|
+
raise('Missing auth email') if params[:email].nil?
|
135
|
+
# we need multipart form encoding for some of these operations
|
136
|
+
client = Faraday.new(url: API_BASE) do |conn|
|
137
|
+
conn.request :multipart
|
138
|
+
conn.request :url_encoded
|
139
|
+
conn.adapter :net_http
|
140
|
+
end
|
141
|
+
client.headers['X-Auth-Key'] = params[:auth_key]
|
142
|
+
client.headers['X-Auth-User-Service-Key '] = params[:auth_key] #FIXME, is this always the same?
|
143
|
+
client.headers['X-Auth-Email'] = params[:email]
|
144
|
+
client.headers['Content-Type'] = 'application/json'
|
145
|
+
client
|
146
|
+
end
|
147
|
+
|
148
|
+
def cf_post(path: nil, data: {})
|
149
|
+
raise('No data to post') if data.empty?
|
150
|
+
result = @cf_client.post do |request|
|
151
|
+
request.url(API_BASE + path) unless path.nil?
|
152
|
+
request.body = data.to_json
|
153
|
+
end
|
154
|
+
raise(JSON.parse(result.body).dig('errors').first.to_s) unless result.status == 200
|
155
|
+
JSON.parse(result.body, symbolize_names: true)
|
156
|
+
end
|
157
|
+
|
158
|
+
def cf_get(path: nil, params: {}, raw: nil, extra_headers: {})
|
159
|
+
result = @cf_client.get do |request|
|
160
|
+
request.headers.merge!(extra_headers) unless extra_headers.empty?
|
161
|
+
request.url(API_BASE + path) unless path.nil?
|
162
|
+
unless params.nil?
|
163
|
+
request.params = params if params.values.any? { |i| !i.nil? }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
raise(JSON.parse(result.body).dig('errors').first.to_s) unless result.status == 200
|
167
|
+
unless raw.nil?
|
168
|
+
return result.body
|
169
|
+
end
|
170
|
+
# we ask for compressed logs. uncompress if we get them
|
171
|
+
# as the user can always ask for raw stuff
|
172
|
+
if result.headers["content-encoding"] == 'gzip'
|
173
|
+
return Zlib::GzipReader.new(StringIO.new(result.body.to_s)).read
|
174
|
+
end
|
175
|
+
JSON.parse(result.body, symbolize_names: true)
|
176
|
+
end
|
177
|
+
|
178
|
+
def cf_put(path: nil, data: nil)
|
179
|
+
result = @cf_client.put do |request|
|
180
|
+
request.url(API_BASE + path) unless path.nil?
|
181
|
+
request.body = data.to_json unless data.nil?
|
182
|
+
end
|
183
|
+
raise(JSON.parse(result.body).dig('errors').first.to_s) unless result.status == 200
|
184
|
+
JSON.parse(result.body, symbolize_names: true)
|
185
|
+
end
|
186
|
+
|
187
|
+
def cf_patch(path: nil, data: {})
|
188
|
+
valid_response_codes = [200, 202]
|
189
|
+
result = @cf_client.patch do |request|
|
190
|
+
request.url(API_BASE + path) unless path.nil?
|
191
|
+
request.body = data.to_json unless data.empty?
|
192
|
+
end
|
193
|
+
raise(JSON.parse(result.body).dig('errors').first.to_s) unless valid_response_codes.include?(result.status)
|
194
|
+
JSON.parse(result.body, symbolize_names: true)
|
195
|
+
end
|
196
|
+
|
197
|
+
def cf_delete(path: nil, data: {})
|
198
|
+
result = @cf_client.delete do |request|
|
199
|
+
request.url(API_BASE + path) unless path.nil?
|
200
|
+
request.body = data.to_json unless data.empty?
|
201
|
+
end
|
202
|
+
raise(JSON.parse(result.body).dig('errors').first.to_s) unless result.status == 200
|
203
|
+
JSON.parse(result.body, symbolize_names: true)
|
204
|
+
end
|
205
|
+
|
206
|
+
def valid_setting?(name = nil)
|
207
|
+
return false if name.nil?
|
208
|
+
return false unless POSSIBLE_API_SETTINGS.include?(name)
|
209
|
+
true
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class CloudflareClient::Certificate < CloudflareClient
|
2
|
+
VALID_REQUESTED_VALIDITIES = [7, 30, 90, 365, 730, 1095, 5475].freeze
|
3
|
+
VALID_REQUEST_TYPES = %w[origin-rsa origin-ecc keyless-certificate].freeze
|
4
|
+
|
5
|
+
##
|
6
|
+
# cloudflare CA
|
7
|
+
|
8
|
+
##
|
9
|
+
# list certificates
|
10
|
+
def list(zone_id: nil)
|
11
|
+
cf_get(path: '/certificates', params: {zone_id: zone_id})
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# create a certificate
|
16
|
+
def create(hostnames:, requested_validity: 5475, request_type: 'origin-rsa', csr: nil)
|
17
|
+
non_empty_array_check(:hostnames, hostnames)
|
18
|
+
valid_value_check(:requested_validity, requested_validity, VALID_REQUESTED_VALIDITIES)
|
19
|
+
valid_value_check(:request_type, request_type, VALID_REQUEST_TYPES)
|
20
|
+
|
21
|
+
data = {hostnames: hostnames, requested_validity: requested_validity, request_type: request_type}
|
22
|
+
data[:csr] = csr unless csr.nil?
|
23
|
+
|
24
|
+
cf_post(path: '/certificates', data: data)
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# details of a certificate
|
29
|
+
def show(id:)
|
30
|
+
id_check(:id, id)
|
31
|
+
cf_get(path: "/certificates/#{id}")
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# revoke a cert
|
36
|
+
def revoke(id:)
|
37
|
+
id_check(:id, id)
|
38
|
+
cf_delete(path: "/certificates/#{id}")
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class CloudflareClient::Organization < CloudflareClient
|
2
|
+
Dir[File.expand_path('../organization/*.rb', __FILE__)].each {|f| require f}
|
3
|
+
|
4
|
+
attr_reader :org_id
|
5
|
+
|
6
|
+
##
|
7
|
+
# Organization based operations
|
8
|
+
def initialize(args)
|
9
|
+
@org_id = args.delete(:org_id)
|
10
|
+
id_check(:org_id, org_id)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# get an org's details
|
16
|
+
def show
|
17
|
+
cf_get(path: "/organizations/#{org_id}")
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# update a given org (only supports name)
|
22
|
+
def update(name: nil)
|
23
|
+
data = name.nil? ? {} : {name: name}
|
24
|
+
cf_patch(path: "/organizations/#{org_id}", data: data)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class CloudflareClient::Organization::AccessRule < CloudflareClient::Organization
|
2
|
+
VALID_MODES = %w[block challenge whitelist].freeze
|
3
|
+
VALID_CONFIG_TARGETS = %w[ip ip_range country_code].freeze
|
4
|
+
VALID_ORDERS = %w[configuration_target configuration_value mode].freeze
|
5
|
+
|
6
|
+
##
|
7
|
+
# org level firewall rules
|
8
|
+
#
|
9
|
+
|
10
|
+
##
|
11
|
+
# list access rules
|
12
|
+
def list(notes: nil, mode: nil, match: 'all', configuration_value: nil, order: nil, page: 1, per_page: 50, configuration_target: nil, direction: 'desc')
|
13
|
+
params = {page: page, per_page: per_page}
|
14
|
+
|
15
|
+
unless notes.nil?
|
16
|
+
basic_type_check(:notes, notes, String)
|
17
|
+
params[:notes] = notes
|
18
|
+
end
|
19
|
+
|
20
|
+
unless mode.nil?
|
21
|
+
valid_value_check(:mode, mode, VALID_MODES)
|
22
|
+
params[:mode] = mode
|
23
|
+
end
|
24
|
+
|
25
|
+
unless match.nil?
|
26
|
+
valid_value_check(:match, match, VALID_MATCHES)
|
27
|
+
params[:match] = match
|
28
|
+
end
|
29
|
+
|
30
|
+
params[:configuration_value] = configuration_value unless configuration_value.nil?
|
31
|
+
|
32
|
+
#FIXME: check this against the api
|
33
|
+
unless order.nil?
|
34
|
+
valid_value_check(:order, order, VALID_ORDERS)
|
35
|
+
params[:order] = order
|
36
|
+
end
|
37
|
+
|
38
|
+
unless configuration_target.nil?
|
39
|
+
valid_value_check(:configuration_target, configuration_target, VALID_CONFIG_TARGETS)
|
40
|
+
params[:configuration_target] = configuration_target
|
41
|
+
end
|
42
|
+
|
43
|
+
unless direction.nil?
|
44
|
+
valid_value_check(:direction, direction, VALID_DIRECTIONS)
|
45
|
+
params[:direction] = direction
|
46
|
+
end
|
47
|
+
|
48
|
+
cf_get(path: "/organizations/#{org_id}/firewall/access_rules/rules", params: params)
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# create access rule
|
53
|
+
def create(mode:, configuration:, notes: nil)
|
54
|
+
non_empty_hash_check(:configuration, configuration)
|
55
|
+
valid_value_check(:mode, mode, VALID_MODES)
|
56
|
+
|
57
|
+
#TODO: validate config objects?
|
58
|
+
data = {mode: mode, configuration: configuration}
|
59
|
+
data[:notes] = notes unless notes.nil?
|
60
|
+
|
61
|
+
cf_post(path: "/organizations/#{org_id}/firewall/access_rules/rules", data: data)
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# update access rule
|
66
|
+
# def update
|
67
|
+
#
|
68
|
+
# end
|
69
|
+
|
70
|
+
##
|
71
|
+
# delete org access rule
|
72
|
+
def delete(id:)
|
73
|
+
id_check('id', id)
|
74
|
+
cf_delete(path: "/organizations/#{org_id}/firewall/access_rules/rules/#{id}")
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class CloudflareClient::Organization::Invite < CloudflareClient::Organization
|
2
|
+
##
|
3
|
+
# org invites
|
4
|
+
|
5
|
+
##
|
6
|
+
# create an org invite
|
7
|
+
def create(email:, roles:, auto_accept: nil)
|
8
|
+
basic_type_check(:email, email, String)
|
9
|
+
max_length_check(:email, email, 90)
|
10
|
+
non_empty_array_check(:roles, roles)
|
11
|
+
|
12
|
+
data = {invited_member_email: email, roles: roles}
|
13
|
+
|
14
|
+
unless auto_accept.nil?
|
15
|
+
valid_value_check(:auto_accept, auto_accept, [true, false])
|
16
|
+
data[:auto_accept] = auto_accept
|
17
|
+
end
|
18
|
+
|
19
|
+
cf_post(path: "/organizations/#{org_id}/invites", data: data)
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# org invites
|
24
|
+
def list
|
25
|
+
cf_get(path: "/organizations/#{org_id}/invites")
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# org invite details
|
30
|
+
def show(id:)
|
31
|
+
id_check(:id, id)
|
32
|
+
|
33
|
+
cf_get(path: "/organizations/#{org_id}/invites/#{id}")
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# update an organization invites roles
|
38
|
+
def update(id:, roles:)
|
39
|
+
id_check(:id, id)
|
40
|
+
non_empty_array_check(:roles, roles)
|
41
|
+
|
42
|
+
data = {roles: roles}
|
43
|
+
|
44
|
+
cf_patch(path: "/organizations/#{org_id}/invites/#{id}", data: data)
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# cancel an organization invite
|
49
|
+
def delete(id:)
|
50
|
+
id_check(:id, id)
|
51
|
+
cf_delete(path: "/organizations/#{org_id}/invites/#{id}")
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class CloudflareClient::Organization::Member < CloudflareClient::Organization
|
2
|
+
##
|
3
|
+
# org members
|
4
|
+
|
5
|
+
##
|
6
|
+
# list org members
|
7
|
+
def list
|
8
|
+
cf_get(path: "/organizations/#{org_id}/members")
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# org member details
|
13
|
+
def show(id:)
|
14
|
+
id_check(:id, id)
|
15
|
+
|
16
|
+
cf_get(path: "/organizations/#{org_id}/members/#{id}")
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# update org member roles
|
21
|
+
def update(id:, roles:)
|
22
|
+
id_check(:id, id)
|
23
|
+
non_empty_array_check(:roles, roles)
|
24
|
+
|
25
|
+
data = {roles: roles}
|
26
|
+
|
27
|
+
cf_patch(path: "/organizations/#{org_id}/members/#{id}", data: data)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# remove org member
|
32
|
+
def delete(id:)
|
33
|
+
id_check(:id, id)
|
34
|
+
|
35
|
+
cf_delete(path: "/organizations/#{org_id}/members/#{id}")
|
36
|
+
end
|
37
|
+
end
|