Skip to content

Commit

Permalink
Fixes Cloudflare IP lookup when on private network
Browse files Browse the repository at this point in the history
  • Loading branch information
joelvh committed Sep 15, 2018
1 parent 3423f9d commit d2eb3a1
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 26 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions data/ips_private.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
37 changes: 21 additions & 16 deletions lib/rack/cloudflare/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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?
Expand All @@ -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
Expand All @@ -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
23 changes: 15 additions & 8 deletions lib/rack/cloudflare/ips.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require 'ipaddr'
require 'net/http'
require 'open-uri'

module Rack
class Cloudflare
Expand All @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/rack/cloudflare/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Rack
class Cloudflare
VERSION = '1.0.1'
VERSION = '1.0.2'
end
end
17 changes: 17 additions & 0 deletions spec/rack/cloudflare_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit d2eb3a1

Please sign in to comment.