cloudflare_client_rb 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|