diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34e0a27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +t/ +Gemfile* +*.gem +tags +.byebug_history +.rspec_status diff --git a/README.md b/README.md index f687930..039b8dc 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,29 @@ puts records.first.record[:name] puts records ``` +Get firewall rules: + +``` ruby +all_rules = zones.first.firewall_rules.all +block_rules = zones.first.firewall_rules.all("block") # or "whitelist" or "challenge" +``` + +Get blocked ips: + +``` ruby +block_rules = zones.first.firewall_rules.all("block") +blocked_ips = zones.first.firewall_rules.firewalled_ips(block_rules) +``` + +Block an ip: + +``` ruby +# ip = "nnn.nnn.nnn.nnn" +# note: "some note about the block" +data = {"mode":"block","configuration":{"target":"ip","value":"#{ip}"},"notes":"#{note} #{Time.now.strftime("%m/%d/%y")} "} +response = zones.first.firewall_rules.post(data.to_json, content_type: 'application/json') + +``` ## Contributing 1. Fork it @@ -83,7 +106,7 @@ puts records Released under the MIT license. -Copyright, 2012, 2014, by [Marcin Prokop](https://github.com/b4k3r). +Copyright, 2012, 2014, by [Marcin Prokop](https://github.com/b4k3r). Copyright, 2017, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). Permission is hereby granted, free of charge, to any person obtaining a copy @@ -103,3 +126,6 @@ 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. + + + diff --git a/lib/cloudflare/connection.rb b/lib/cloudflare/connection.rb index fd4743d..57a08bf 100644 --- a/lib/cloudflare/connection.rb +++ b/lib/cloudflare/connection.rb @@ -29,27 +29,44 @@ module Cloudflare DEFAULT_URL = "https://api.cloudflare.com/client/v4/" TIMEOUT = 10 # Default is 5 seconds - + class Resource < RestClient::Resource + include Enumerable # @param api_key [String] `X-Auth-Key` or `X-Auth-User-Service-Key` if no email provided. # @param email [String] `X-Auth-Email`, your email address for the account. def initialize(url = DEFAULT_URL, key: nil, email: nil, **options) headers = options[:headers] || {} - + if email.nil? headers['X-Auth-User-Service-Key'] = key else headers['X-Auth-Key'] = key headers['X-Auth-Email'] = email end - + # Convert HTTP API responses to our own internal response class: super(url, headers: headers, accept: 'application/json', **options) do |response| Response.new(response.request.url, response.body) end end + + def paginate(obj, url, url_args = "") + page = 1 + page_size = 100 + results = [] + + loop do # fetch and aggregate all pages + rules = obj.new(concat_urls(url, "?scope_type=organization#{url_args}&per_page=#{page_size}&page=#{page}"), self, **options) + results += rules.get.results + break if results.size % page_size != 0 + page += 1 + end + results end - + + + end + class Connection < Resource end end diff --git a/lib/cloudflare/version.rb b/lib/cloudflare/version.rb index 24392ba..abc2b82 100644 --- a/lib/cloudflare/version.rb +++ b/lib/cloudflare/version.rb @@ -20,5 +20,5 @@ # THE SOFTWARE. module Cloudflare - VERSION = '3.0.0' + VERSION = '3.1.3' end diff --git a/lib/cloudflare/zone.rb b/lib/cloudflare/zone.rb index 5918247..51e3f37 100644 --- a/lib/cloudflare/zone.rb +++ b/lib/cloudflare/zone.rb @@ -19,6 +19,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# added firewall rules support: david rosenbloom|davidr@artifactory.com|artifactory +# require_relative 'connection' module Cloudflare @@ -27,82 +29,153 @@ def zones @zones ||= Zones.new(concat_urls(url, 'zones'), options) end end - + class DNSRecord < Resource def initialize(url, record = nil, **options) super(url, **options) - + @record = record || self.get.result end - + + + def update_content(content) + response = self.put({type: @record[:type], name: @record[:name], content: content}.to_json, content_type: 'application/json') + response.successful? + end + attr :record - + def to_s "#{@record[:name]} #{@record[:type]} #{@record[:content]}" end end - + class DNSRecords < Resource def initialize(url, zone, **options) super(url, **options) - + @zone = zone end - + attr :zone - + def all - self.get.results.map{|record| DNSRecord.new(concat_urls(url, record[:id]), record, **options)} + results = paginate(DNSRecords, url) + results.map{|record| DNSRecord.new(concat_urls(url, record[:id]), record, **options)} end - + def find_by_name(name) response = self.get(params: {name: name}) - + unless response.empty? record = response.results.first - + DNSRecord.new(concat_urls(url, record[:id]), record, **options) end end - + def find_by_id(id) DNSRecord.new(concat_urls(url, id), **options) end end - + + class FirewallRule < Resource + def initialize(url, record = nil, **options) + super(url, **options) + + @record = record || self.get.result + end + + attr :record + + def to_s + "#{@record[:configuration][:value]} - #{@record[:mode]} - #{@record[:notes]}" + end + end + + class FirewallRules < Resource + def initialize(url, zone, **options) + super(url, **options) + + @zone = zone + end + + attr :zone + + def all(mode = nil, ip = nil, notes = nil) + url_args = "" + url_args.concat("&mode=#{mode}") if mode + url_args.concat("&configuration_value=#{ip}") if ip + url_args.concat("¬es=#{notes}") if notes + + results = paginate(FirewallRules, url, url_args) + results.map{|record| FirewallRule.new(concat_urls(url, record[:id]), record, **options)} + end + + def firewalled_ips(rules) + rules.collect {|r| r.record[:configuration][:value]} + end + + def blocked_ips + firewalled_ips(all("block")) + end + + def set(mode, ip, note) + data = {"mode":"#{mode.to_s}","configuration":{"target":"ip","value":"#{ip}"},"notes":"cloudflare gem firewall_rules [#{mode}] #{note} #{Time.now.strftime("%m/%d/%y")} "} + post(data.to_json, content_type: 'application/json') + end + + def unset(mode, value) + rule = send("find_by_#{mode}", value) + rule.delete + end + + def find_by_id(id) + FirewallRule.new(concat_urls(url, id), **options) + end + + def find_by_ip(ip) + rule = FirewallRule.new(concat_urls(url, "?configuration_value=#{ip}"), **options) + FirewallRule.new(concat_urls(url, rule.record.first[:id]), **options) + end + end + class Zone < Resource def initialize(url, record = nil, **options) super(url, **options) - @record = record || self.get.result end - + attr :record - + def dns_records @dns_records ||= DNSRecords.new(concat_urls(url, 'dns_records'), self, **options) end - + + def firewall_rules + @firewall_rules ||= FirewallRules.new(concat_urls(url, "firewall/access_rules/rules"), self, **options) + end + def to_s @record[:name] end end - + class Zones < Resource def all self.get.results.map{|record| Zone.new(concat_urls(url, record[:id]), record, **options)} end - + def find_by_name(name) - record = self.get(params: {name: name}).result - + response = self.get(params: {name: name}) + unless response.empty? record = response.results.first - + Zone.new(concat_urls(url, record[:id]), record, **options) end end - + def find_by_id(id) Zone.new(concat_urls(url, id), **options) end diff --git a/spec/cloudflare/zone_spec.rb b/spec/cloudflare/zone_spec.rb index 8d7a90e..64e70aa 100644 --- a/spec/cloudflare/zone_spec.rb +++ b/spec/cloudflare/zone_spec.rb @@ -1,41 +1,91 @@ RSpec.describe "Cloudflare DNS Zones" do - include_context Cloudflare::RSpec::Connection - - it "should list zones" do - zones = connection.zones.all - - expect(zones).to be_any - end - - describe Cloudflare::DNSRecords, order: :defined do - let(:zone) {connection.zones.all.first} - let(:name) {"test"} - - it "should create dns record" do - response = zone.dns_records.post({ - type: "A", - name: name, - content: "127.0.0.1", - ttl: 240, - proxied: false - }.to_json, content_type: 'application/json') - - expect(response).to be_successful - - result = response.result - expect(result).to include(:id, :type, :name, :content, :ttl) - end - - it "should delete dns record" do - dns_records = zone.dns_records.all - - expect(dns_records).to be_any - - dns_records.each do |record| - response = record.delete - expect(response).to be_successful - end - end - end + include_context Cloudflare::RSpec::Connection + + it "should list zones" do + zones = connection.zones.all + + expect(zones).to be_any + end + + describe Cloudflare::DNSRecords, order: :defined do + let(:zone) {connection.zones.all.first} + let(:name) {"test"} + let(:ip) {"123.123.123.123"} + record = nil + + it "should get all records" do + result = zone.dns_records.all + + puts "===> #{result.size} records returned" + expect(result.size).to be > 0 + end + + + it "should create dns record" do + response = zone.dns_records.post({ + type: "A", + name: name, + content: ip, + ttl: 240, + proxied: false + }.to_json, content_type: 'application/json') + + expect(response).to be_successful + + result = response.result + expect(result).to include(:id, :type, :name, :content, :ttl) + record = result + end + + it "should delete dns record" do + dns_record = zone.dns_records.find_by_id(record[:id]) + response = dns_record.delete + expect(response).to be_successful + end + end + + describe Cloudflare::FirewallRules, order: :defined do + let(:zone) {connection.zones.all.first} + let(:name) {"test"} + let(:ip) {'123.123.123.123'} + let(:ip2) {'123.123.123.124'} + let(:notes) {"gemtest"} + record = nil + before do + response = zone.firewall_rules.set('block', ip2, notes) + end + + it "should get all rules" do + result = zone.firewall_rules.all + + puts "===> #{result.size} records returned" + expect(result.size).to be > 0 + end + + it "should create firewall rules for 'block', 'challenge', 'whitelist'" do + + [:block, :challenge, :whitelist].each do |mode| + response = zone.firewall_rules.set(mode, ip, notes) + expect(response).to be_successful + + result = response.result + expect(result).to include(:id, :mode, :notes, :configuration) + expect(result[:mode]).to eq mode.to_s + record = result + end + end + + it "should delete firewall rule by record" do + response = zone.firewall_rules.unset('id', record[:id]) + + expect(response).to be_successful + end + + it "should delete firewall rule by ip" do + response = zone.firewall_rules.unset('ip', ip2) + + expect(response).to be_successful + end + end end