Skip to content

Commit

Permalink
Release v1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
joelvh committed Sep 14, 2018
0 parents commit d8a159a
Show file tree
Hide file tree
Showing 21 changed files with 1,033 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

# rspec failure tracking
.rspec_status

Gemfile.lock
*.gem
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper
43 changes: 43 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
AllCops:
TargetRubyVersion: 2.5.1
# Rails: true
# Include:
# - '**/Rakefile'
# - '**/config.ru'
Exclude:
- 'doc/**/*'
- 'tmp/**/*'
- 'bin/**/*'
- 'db/**/*'
- 'test/**/*'
- 'config/**/*'
- 'script/**/*'
- 'vendor/**/*'
- 'spec/**/*'
- !ruby/regexp /old_and_unused\.rb$/

# Style/WhileUntilModifier:
# MaxLineLength: 160

# Style/IfUnlessModifier:
# MaxLineLength: 160

Metrics/LineLength:
Max: 160

Metrics/AbcSize:
Max: 120

Metrics/MethodLength:
CountComments: false # count full line comments?
Max: 120

NumericPredicate:
EnforcedStyle: comparison

Style/Documentation:
Enabled: false

Metrics/ModuleLength:
Exclude:
- 'lib/rack/cloudflare/countries.rb'
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
sudo: false
language: ruby
rvm:
- 2.5.1
before_install: gem install bundler -v 1.16.2
8 changes: 8 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

source 'https://rubygems.org'

git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

# Specify your gem's dependencies in rack-cloudflare.gemspec
gemspec
144 changes: 144 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Rack::Cloudflare

Deal with Cloudflare features in your Ruby app using Rack middleware. Also provides a Ruby toolkit to deal with Cloudflare in other contexts if you'd like.

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'rack-cloudflare'
```

And then execute:

$ bundle

Or install it yourself as:

$ gem install rack-cloudflare

## Usage

### Whitelist Cloudflare IP addresses

You can block access to non-Cloudflare networks using `Rack::Cloudflare::Middleware::AccessControl`.

require 'rack/cloudflare'

# In config.ru
use Rack::Cloudflare::Middleware::AccessControl

# In Rails config/application.rb
config.middleware.use Rack::Cloudflare::Middleware::AccessControl

# Configure custom blocked message (defaults to "Forbidden")
Rack::Cloudflare::Middleware::AccessControl.blocked_message = "You don't belong here..."

# Fully customize the Rack response (such as making it a redirect)
Rack::Cloudflare::Middleware::AccessControl.blocked_response = lambda do |_env|
[301, { 'Location' => 'https://somewhere.else.xyz' }, ["Redirecting...\n"]]
end

Alternatively, using [`Rack::Attack`](https://github.com/kickstarter/rack-attack) you can easily add a "safelist" rule.

Rack::Attack.safelist('Only allow requests through the Cloudflare network') do |request|
Rack::Cloudflare::Headers.trusted?(request.env)
end

Utilizing the `trusted?` helper method, you can implement a similar check using other middleware.

See _Toolkits: Detect Cloudflare Requests_ for alternative uses.

### Rewrite Cloudflare Remote/Client IP address

You can set `REMOTE_ADDR` to the correct remote IP using `Rack::Cloudflare::Middleware::RewriteHeaders`.

require 'rack/cloudflare'

# In config.ru
use Rack::Cloudflare::Middleware::RewriteHeaders

# In Rails config/application.rb
config.middleware.use Rack::Cloudflare::Middleware::RewriteHeaders

You can customize whether rewritten headers should be backed up and what names to use.

# Toggle header backups
Rack::Cloudflare::Headers.backup = false

# Rename backed up headers (defaults: "ORIGINAL_REMOTE_ADDR", "ORIGINAL_FORWARDED_FOR")
Rack::Cloudflare::Headers.original_remote_addr = 'BACKUP_REMOTE_ADDR'
Rack::Cloudflare::Headers.original_forwarded_for = 'BACKUP_FORWARDED_FOR'

See _Toolkits: Rewrite Headers_ for alternative uses.

### Logging

You can enable logging to see what requests are blocked or headers are rewritten.

Rack::Cloudflare.logger = Logger.new(STDOUT)

Log levels used are INFO, DEBUG and WARN.

## Toolkits

### Detect Cloudflare Requests

You can very easily check your HTTP headers to see if the request came from a Cloudflare network.

# Your headers are in a `Hash` format
# e.g. { 'REMOTE_ADDR' => '0.0.0.0', ... }
# Verifies the remote address
Rack::Cloudflare::Headers.trusted?(headers)

Note that we can only trust the `REMOTE_ADDR` header to verify a request came from Cloudflare.
The `HTTP_X_FORWARDED_FOR` header can be modified and therefore not trusted.

Make sure your web server does not modify `REMOTE_ADDR` because it could cause security holes.
Read this article, for example: [Anatomy of an Attack: How I Hacked StackOverflow](https://blog.ircmaxell.com/2012/11/anatomy-of-attack-how-i-hacked.html)

### Rewrite Headers

We can easily rewrite `REMOTE_ADDR` and add `HTTP_X_FORWARDED_FOR` based on verifying the request comes from a Cloudflare network.

# Get a list of headers relevant to Cloudflare (unmodified)
headers = Rack::Cloudflare::Headers.new(headers).target_headers

# Get a list of headers that will be rewritten (modified)
headers = Rack::Cloudflare::Headers.new(headers).rewritten_headers

# Get a list of headers relevant to Cloudflare with rewritten values
headers = Rack::Cloudflare::Headers.new(headers).rewritten_target_headers

# Update original headers with rewritten ones
headers = Rack::Cloudflare::Headers.new(headers).rewrite

### Up-to-date Cloudflare IP addresses

Cloudflare provides a [list of IP addresses](https://www.cloudflare.com/ips/) that are important to keep up-to-date.

A copy of the IPs are kept in [/data](./data/). The list is converted to a `IPAddr` list and is accessible as:

# Configurable list of IPs
# Defaults to Rack::Cloudflare::IPs::DEFAULTS
Rack::Cloudflare::IPs.list

The list can be updated to Cloudflare's latest published IP lists in-memory:

# Fetches Rack::Cloudflare::IPs::V4_URL and Rack::Cloudflare::IPs::V6_URL
Rack::Cloudflare::IPs.refresh!

# Updates cached list in-memory
Rack::Cloudflare::IPs.list

## Credits

Inspired by:

* https://github.com/tatey/rack-cloudflare
* https://github.com/rikas/cloudflare_localizable

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/joelvh/rack-cloudflare.
31 changes: 31 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rubocop/rake_task'
require 'rubycritic/rake_task'

RuboCop::RakeTask.new do |task|
task.requires << 'rubocop-rspec'
end

RubyCritic::RakeTask.new do |task|
# # Name of RubyCritic task. Defaults to :rubycritic.
# task.name = 'something_special'

# # Glob pattern to match source files. Defaults to FileList['.'].
task.paths = FileList['apps/**/*.rb', 'lib/**/*.rb']

# # You can pass all the options here in that are shown by "rubycritic -h" except for
# # "-p / --path" since that is set separately. Defaults to ''.
# task.options = '--mode-ci --format json'
# # task.options = '--no-browser'

# # Defaults to false
task.verbose = true
end

RSpec::Core::RakeTask.new(:spec)

# task default: %w[rubocop:auto_correct rubycritic spec]
task default: %w[rubocop:auto_correct spec]
14 changes: 14 additions & 0 deletions bin/console
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env ruby

require 'bundler/setup'
require 'rack/cloudflare'

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.

# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start

require 'irb'
IRB.start(__FILE__)
8 changes: 8 additions & 0 deletions bin/setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx

bundle install

# Do any other automated setup that you need to do here
14 changes: 14 additions & 0 deletions data/ips_v4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
103.21.244.0/22
103.22.200.0/22
103.31.4.0/22
104.16.0.0/12
108.162.192.0/18
131.0.72.0/22
141.101.64.0/18
162.158.0.0/15
172.64.0.0/13
173.245.48.0/20
188.114.96.0/20
190.93.240.0/20
197.234.240.0/22
198.41.128.0/17
6 changes: 6 additions & 0 deletions data/ips_v6.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
2400:cb00::/32
2405:b500::/32
2606:4700::/32
2803:f800::/32
2c0f:f248::/32
2a06:98c0::/29
21 changes: 21 additions & 0 deletions lib/rack/cloudflare.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require_relative 'cloudflare/version'
require_relative 'cloudflare/countries'
require_relative 'cloudflare/ips'
require_relative 'cloudflare/headers'

require_relative 'cloudflare/middleware/access_control'
require_relative 'cloudflare/middleware/rewrite_headers'

module Rack
class Cloudflare
class << self
attr_accessor :logger

%i[info debug warn error].each do |m|
define_method(m) { |*args| logger&.__send__(m, *args) }
end
end
end
end
Loading

0 comments on commit d8a159a

Please sign in to comment.