diff --git a/README.md b/README.md index 3e761ab..3708e0a 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ The list can be updated to Cloudflare's latest published IP lists in-memory: ```ruby # Fetches Rack::Cloudflare::IPs::V4_URL and Rack::Cloudflare::IPs::V6_URL -Rack::Cloudflare::IPs.refresh! +Rack::Cloudflare::IPs.update! # Updates cached list in-memory Rack::Cloudflare::IPs.list diff --git a/data/ips_private.txt b/data/ips_private.txt new file mode 100644 index 0000000..668e954 --- /dev/null +++ b/data/ips_private.txt @@ -0,0 +1,3 @@ +10.0.0.0/8 +172.16.0.0/12 +192.168.0.0/16 \ No newline at end of file diff --git a/lib/rack/cloudflare/headers.rb b/lib/rack/cloudflare/headers.rb index 7bc16f0..5ebdd5b 100644 --- a/lib/rack/cloudflare/headers.rb +++ b/lib/rack/cloudflare/headers.rb @@ -32,10 +32,6 @@ def trusted?(headers) end end - self.backup = true - self.original_remote_addr = 'ORIGINAL_REMOTE_ADDR' - self.original_forwarded_for = 'ORIGINAL_FORWARDED_FOR' - def initialize(headers) @headers = headers end @@ -68,17 +64,15 @@ def ray # "Cf-Visitor: { \"scheme\":\"https\"}" def visitor - return unless has?(HTTP_CF_VISITOR) - ::JSON.parse @headers[HTTP_CF_VISITOR] + @visitor ||= ::JSON.parse @headers[HTTP_CF_VISITOR] if has?(HTTP_CF_VISITOR) end def remote_addr @remote_addr ||= IPs.parse(@headers[REMOTE_ADDR]).first end - # Indicates if the headers passed through Cloudflare - def trusted? - IPs.list.any? { |range| range.include? remote_addr } + def cloudflare_ip + @cloudflare_ip ||= IPs.private?(remote_addr) ? forwarded_for.last : remote_addr end def backup_headers @@ -90,6 +84,12 @@ def backup_headers end end + # Headers that relate to Cloudflare + # See: https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- + def target_headers + @headers.select { |k, _| ALL.include? k } + end + def rewritten_headers # Only rewrites headers if it's a Cloudflare request return {} unless trusted? @@ -105,16 +105,10 @@ def rewritten_headers # Cloudflare will already have modified the header if # it was present in the original request. # See: https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- - headers[HTTP_X_FORWARDED_FOR] = "#{connecting_ip}, #{remote_addr}" if forwarded_for.none? + headers[HTTP_X_FORWARDED_FOR] = "#{connecting_ip}, #{cloudflare_ip}" if forwarded_for.none? end end - # Headers that relate to Cloudflare - # See: https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- - def target_headers - @headers.select { |k, _| ALL.include? k } - end - def rewritten_target_headers target_headers.merge(rewritten_headers) end @@ -123,9 +117,20 @@ def rewrite @headers.merge(rewritten_headers) end + # Indicates if the headers passed through Cloudflare + def trusted? + @trusted ||= IPs.list.any? { |range| range.include? cloudflare_ip } + end + def has?(header) @headers.key?(header) end + + ### Configure + + self.backup = true + self.original_remote_addr = 'ORIGINAL_REMOTE_ADDR' + self.original_forwarded_for = 'ORIGINAL_FORWARDED_FOR' end end end diff --git a/lib/rack/cloudflare/ips.rb b/lib/rack/cloudflare/ips.rb index e3f4413..039cd4e 100644 --- a/lib/rack/cloudflare/ips.rb +++ b/lib/rack/cloudflare/ips.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'ipaddr' -require 'net/http' +require 'open-uri' module Rack class Cloudflare @@ -14,13 +14,20 @@ class << self # List of IPs to reference attr_accessor :list - # Refresh list of IPs in case local copy is outdated - def refresh! + def private?(ip) + PRIVATE.any? { |range| range.include? ip } + end + + # Update list of IPs in-memory in case local copy is outdated + def update! self.list = fetch(V4_URL) + fetch(V6_URL) end def fetch(url) - parse ::Net::HTTP.get(URI(url)) + parse URI(url).read + rescue OpenURI::HTTPError => ex + Cloudflare.error "[#{name}] #{ex.class.name} fetching #{url.inspect}: #{ex.message}" + [] end def read(filename) @@ -29,13 +36,13 @@ def read(filename) def parse(string) return [] if string.to_s.strip.empty? - string.split(/[,\s]+/).map { |ip| ::IPAddr.new(ip.strip) } + string.strip.split(/[,\s]+/).map { |ip| ::IPAddr.new(ip.strip) } end end - V4 = read("#{__dir__}/../../../data/ips_v4.txt") - V6 = read("#{__dir__}/../../../data/ips_v6.txt") - + V4 = read("#{__dir__}/../../../data/ips_v4.txt") + V6 = read("#{__dir__}/../../../data/ips_v6.txt") + PRIVATE = parse('10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16') DEFAULTS = V4 + V6 ### Configure diff --git a/lib/rack/cloudflare/version.rb b/lib/rack/cloudflare/version.rb index 95fd404..ef4a84e 100644 --- a/lib/rack/cloudflare/version.rb +++ b/lib/rack/cloudflare/version.rb @@ -2,6 +2,6 @@ module Rack class Cloudflare - VERSION = '1.0.1' + VERSION = '1.0.2' end end diff --git a/spec/rack/cloudflare_spec.rb b/spec/rack/cloudflare_spec.rb index 646f15c..93cc79a 100644 --- a/spec/rack/cloudflare_spec.rb +++ b/spec/rack/cloudflare_spec.rb @@ -165,4 +165,21 @@ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4, 103.21.244.1' ) end + + it 'gets the Cloudflare IP from HTTP_X_FORWARDED_FOR when REMOTE_ADDR is the local network' do + env = { + 'HTTP_X_FORWARDED_FOR' => '74.64.167.164, 173.245.52.147', + 'HTTP_CF_CONNECTING_IP' => '74.64.167.164', + 'REMOTE_ADDR' => '10.81.159.108' + } + middleware = Rack::Cloudflare::Middleware::RewriteHeaders.new(->(e) { e }) + + expect(middleware.call(env)).to eq( + 'ORIGINAL_FORWARDED_FOR' => '74.64.167.164, 173.245.52.147', + 'ORIGINAL_REMOTE_ADDR' => '10.81.159.108', + 'REMOTE_ADDR' => '74.64.167.164', + 'HTTP_CF_CONNECTING_IP' => '74.64.167.164', + 'HTTP_X_FORWARDED_FOR' => '74.64.167.164, 173.245.52.147' + ) + end end