rack-cloudflare 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +43 -0
- data/Gemfile +2 -0
- data/README.md +116 -7
- data/Rakefile +26 -1
- data/data/ips_v4.txt +14 -0
- data/data/ips_v6.txt +6 -0
- data/lib/rack/cloudflare.rb +17 -4
- data/lib/rack/cloudflare/countries.rb +268 -0
- data/lib/rack/cloudflare/headers.rb +131 -0
- data/lib/rack/cloudflare/ips.rb +46 -0
- data/lib/rack/cloudflare/middleware/access_control.rb +34 -0
- data/lib/rack/cloudflare/middleware/rewrite_headers.rb +23 -0
- data/lib/rack/cloudflare/version.rb +4 -2
- data/rack-cloudflare-0.1.0.gem +0 -0
- data/rack-cloudflare.gemspec +4 -0
- metadata +39 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c8a5b0439022d396fd56721e79b97a96c4cf830b0e37c27a4a9f6f2db2f8bd0
|
4
|
+
data.tar.gz: d3866b5cfcce96211b25ae277b5e5280f3f38e0eb5f78d291c23d2a08cf84715
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ea482f40489690a5f368f0e21a9aad7fbc59dc76517b351b635ff53064ac8a1339ecc4c3129047967d5bdbafa259aa11a4b79314ea52ebe78b63d3bf031e9f4
|
7
|
+
data.tar.gz: 40709bddc477ccd842dd50318827c4af50bbc7cf5a0b8ee58304e0e0d0d736b5fdd04b8b6f81692eb45f82b9a04492c4cb63001e6089490ab8e1cb8ee217b2a4
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.5.1
|
3
|
+
# Rails: true
|
4
|
+
# Include:
|
5
|
+
# - '**/Rakefile'
|
6
|
+
# - '**/config.ru'
|
7
|
+
Exclude:
|
8
|
+
- 'doc/**/*'
|
9
|
+
- 'tmp/**/*'
|
10
|
+
- 'bin/**/*'
|
11
|
+
- 'db/**/*'
|
12
|
+
- 'test/**/*'
|
13
|
+
- 'config/**/*'
|
14
|
+
- 'script/**/*'
|
15
|
+
- 'vendor/**/*'
|
16
|
+
- 'spec/**/*'
|
17
|
+
- !ruby/regexp /old_and_unused\.rb$/
|
18
|
+
|
19
|
+
# Style/WhileUntilModifier:
|
20
|
+
# MaxLineLength: 160
|
21
|
+
|
22
|
+
# Style/IfUnlessModifier:
|
23
|
+
# MaxLineLength: 160
|
24
|
+
|
25
|
+
Metrics/LineLength:
|
26
|
+
Max: 160
|
27
|
+
|
28
|
+
Metrics/AbcSize:
|
29
|
+
Max: 120
|
30
|
+
|
31
|
+
Metrics/MethodLength:
|
32
|
+
CountComments: false # count full line comments?
|
33
|
+
Max: 120
|
34
|
+
|
35
|
+
NumericPredicate:
|
36
|
+
EnforcedStyle: comparison
|
37
|
+
|
38
|
+
Style/Documentation:
|
39
|
+
Enabled: false
|
40
|
+
|
41
|
+
Metrics/ModuleLength:
|
42
|
+
Exclude:
|
43
|
+
- 'lib/rack/cloudflare/countries.rb'
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Rack::Cloudflare
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
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.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -22,13 +20,124 @@ Or install it yourself as:
|
|
22
20
|
|
23
21
|
## Usage
|
24
22
|
|
25
|
-
|
23
|
+
### Whitelist Cloudflare IP addresses
|
24
|
+
|
25
|
+
You can block access to non-Cloudflare networks using `Rack::Cloudflare::Middleware::AccessControl`.
|
26
|
+
|
27
|
+
require 'rack/cloudflare'
|
28
|
+
|
29
|
+
# In config.ru
|
30
|
+
use Rack::Cloudflare::Middleware::AccessControl
|
31
|
+
|
32
|
+
# In Rails config/application.rb
|
33
|
+
config.middleware.use Rack::Cloudflare::Middleware::AccessControl
|
34
|
+
|
35
|
+
# Configure custom blocked message (defaults to "Forbidden")
|
36
|
+
Rack::Cloudflare::Middleware::AccessControl.blocked_message = "You don't belong here..."
|
37
|
+
|
38
|
+
# Fully customize the Rack response (such as making it a redirect)
|
39
|
+
Rack::Cloudflare::Middleware::AccessControl.blocked_response = lambda do |_env|
|
40
|
+
[301, { 'Location' => 'https://somewhere.else.xyz' }, ["Redirecting...\n"]]
|
41
|
+
end
|
42
|
+
|
43
|
+
Alternatively, using [`Rack::Attack`](https://github.com/kickstarter/rack-attack) you can easily add a "safelist" rule.
|
44
|
+
|
45
|
+
Rack::Attack.safelist('Only allow requests through the Cloudflare network') do |request|
|
46
|
+
Rack::Cloudflare::Headers.trusted?(request.env)
|
47
|
+
end
|
48
|
+
|
49
|
+
Utilizing the `trusted?` helper method, you can implement a similar check using other middleware.
|
50
|
+
|
51
|
+
See _Toolkits: Detect Cloudflare Requests_ for alternative uses.
|
52
|
+
|
53
|
+
### Rewrite Cloudflare Remote/Client IP address
|
54
|
+
|
55
|
+
You can set `REMOTE_ADDR` to the correct remote IP using `Rack::Cloudflare::Middleware::RewriteHeaders`.
|
56
|
+
|
57
|
+
require 'rack/cloudflare'
|
58
|
+
|
59
|
+
# In config.ru
|
60
|
+
use Rack::Cloudflare::Middleware::RewriteHeaders
|
61
|
+
|
62
|
+
# In Rails config/application.rb
|
63
|
+
config.middleware.use Rack::Cloudflare::Middleware::RewriteHeaders
|
64
|
+
|
65
|
+
You can customize whether rewritten headers should be backed up and what names to use.
|
66
|
+
|
67
|
+
# Toggle header backups
|
68
|
+
Rack::Cloudflare::Headers.backup = false
|
69
|
+
|
70
|
+
# Rename backed up headers (defaults: "ORIGINAL_REMOTE_ADDR", "ORIGINAL_FORWARDED_FOR")
|
71
|
+
Rack::Cloudflare::Headers.original_remote_addr = 'BACKUP_REMOTE_ADDR'
|
72
|
+
Rack::Cloudflare::Headers.original_forwarded_for = 'BACKUP_FORWARDED_FOR'
|
73
|
+
|
74
|
+
See _Toolkits: Rewrite Headers_ for alternative uses.
|
75
|
+
|
76
|
+
### Logging
|
77
|
+
|
78
|
+
You can enable logging to see what requests are blocked or headers are rewritten.
|
79
|
+
|
80
|
+
Rack::Cloudflare.logger = Logger.new(STDOUT)
|
81
|
+
|
82
|
+
Log levels used are INFO, DEBUG and WARN.
|
83
|
+
|
84
|
+
## Toolkits
|
85
|
+
|
86
|
+
### Detect Cloudflare Requests
|
87
|
+
|
88
|
+
You can very easily check your HTTP headers to see if the request came from a Cloudflare network.
|
89
|
+
|
90
|
+
# Your headers are in a `Hash` format
|
91
|
+
# e.g. { 'REMOTE_ADDR' => '0.0.0.0', ... }
|
92
|
+
# Verifies the remote address
|
93
|
+
Rack::Cloudflare::Headers.trusted?(headers)
|
94
|
+
|
95
|
+
Note that we can only trust the `REMOTE_ADDR` header to verify a request came from Cloudflare.
|
96
|
+
The `HTTP_X_FORWARDED_FOR` header can be modified and therefore not trusted.
|
97
|
+
|
98
|
+
Make sure your web server does not modify `REMOTE_ADDR` because it could cause security holes.
|
99
|
+
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)
|
100
|
+
|
101
|
+
### Rewrite Headers
|
102
|
+
|
103
|
+
We can easily rewrite `REMOTE_ADDR` and add `HTTP_X_FORWARDED_FOR` based on verifying the request comes from a Cloudflare network.
|
104
|
+
|
105
|
+
# Get a list of headers relevant to Cloudflare (unmodified)
|
106
|
+
headers = Rack::Cloudflare::Headers.new(headers).target_headers
|
107
|
+
|
108
|
+
# Get a list of headers that will be rewritten (modified)
|
109
|
+
headers = Rack::Cloudflare::Headers.new(headers).rewritten_headers
|
110
|
+
|
111
|
+
# Get a list of headers relevant to Cloudflare with rewritten values
|
112
|
+
headers = Rack::Cloudflare::Headers.new(headers).rewritten_target_headers
|
113
|
+
|
114
|
+
# Update original headers with rewritten ones
|
115
|
+
headers = Rack::Cloudflare::Headers.new(headers).rewrite
|
116
|
+
|
117
|
+
### Up-to-date Cloudflare IP addresses
|
118
|
+
|
119
|
+
Cloudflare provides a [list of IP addresses](https://www.cloudflare.com/ips/) that are important to keep up-to-date.
|
120
|
+
|
121
|
+
A copy of the IPs are kept in [/data](./data/). The list is converted to a `IPAddr` list and is accessible as:
|
122
|
+
|
123
|
+
# Configurable list of IPs
|
124
|
+
# Defaults to Rack::Cloudflare::IPs::DEFAULTS
|
125
|
+
Rack::Cloudflare::IPs.list
|
126
|
+
|
127
|
+
The list can be updated to Cloudflare's latest published IP lists in-memory:
|
128
|
+
|
129
|
+
# Fetches Rack::Cloudflare::IPs::V4_URL and Rack::Cloudflare::IPs::V6_URL
|
130
|
+
Rack::Cloudflare::IPs.refresh!
|
131
|
+
|
132
|
+
# Updates cached list in-memory
|
133
|
+
Rack::Cloudflare::IPs.list
|
26
134
|
|
27
|
-
##
|
135
|
+
## Credits
|
28
136
|
|
29
|
-
|
137
|
+
Inspired by:
|
30
138
|
|
31
|
-
|
139
|
+
* https://github.com/tatey/rack-cloudflare
|
140
|
+
* https://github.com/rikas/cloudflare_localizable
|
32
141
|
|
33
142
|
## Contributing
|
34
143
|
|
data/Rakefile
CHANGED
@@ -1,6 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bundler/gem_tasks'
|
2
4
|
require 'rspec/core/rake_task'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
require 'rubycritic/rake_task'
|
7
|
+
|
8
|
+
RuboCop::RakeTask.new do |task|
|
9
|
+
task.requires << 'rubocop-rspec'
|
10
|
+
end
|
11
|
+
|
12
|
+
RubyCritic::RakeTask.new do |task|
|
13
|
+
# # Name of RubyCritic task. Defaults to :rubycritic.
|
14
|
+
# task.name = 'something_special'
|
15
|
+
|
16
|
+
# # Glob pattern to match source files. Defaults to FileList['.'].
|
17
|
+
task.paths = FileList['apps/**/*.rb', 'lib/**/*.rb']
|
18
|
+
|
19
|
+
# # You can pass all the options here in that are shown by "rubycritic -h" except for
|
20
|
+
# # "-p / --path" since that is set separately. Defaults to ''.
|
21
|
+
# task.options = '--mode-ci --format json'
|
22
|
+
# # task.options = '--no-browser'
|
23
|
+
|
24
|
+
# # Defaults to false
|
25
|
+
task.verbose = true
|
26
|
+
end
|
3
27
|
|
4
28
|
RSpec::Core::RakeTask.new(:spec)
|
5
29
|
|
6
|
-
task default: :spec
|
30
|
+
# task default: %w[rubocop:auto_correct rubycritic spec]
|
31
|
+
task default: %w[rubocop:auto_correct spec]
|
data/data/ips_v4.txt
ADDED
data/data/ips_v6.txt
ADDED
data/lib/rack/cloudflare.rb
CHANGED
@@ -1,8 +1,21 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'cloudflare/version'
|
4
|
+
require_relative 'cloudflare/countries'
|
5
|
+
require_relative 'cloudflare/ips'
|
6
|
+
require_relative 'cloudflare/headers'
|
7
|
+
|
8
|
+
require_relative 'cloudflare/middleware/access_control'
|
9
|
+
require_relative 'cloudflare/middleware/rewrite_headers'
|
2
10
|
|
3
11
|
module Rack
|
4
|
-
|
5
|
-
|
6
|
-
|
12
|
+
class Cloudflare
|
13
|
+
class << self
|
14
|
+
attr_accessor :logger
|
15
|
+
|
16
|
+
%i[info debug warn error].each do |m|
|
17
|
+
define_method(m) { |*args| logger&.__send__(m, *args) }
|
18
|
+
end
|
19
|
+
end
|
7
20
|
end
|
8
21
|
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Cloudflare
|
5
|
+
module Countries
|
6
|
+
def self.[](abbr)
|
7
|
+
LIST.fetch(abbr, UNKNOWN)
|
8
|
+
end
|
9
|
+
|
10
|
+
DEFAULT = 'XX'
|
11
|
+
UNKNOWN = 'Unknown'
|
12
|
+
|
13
|
+
LIST = {
|
14
|
+
DEFAULT => UNKNOWN,
|
15
|
+
|
16
|
+
'AD' => 'Andorra',
|
17
|
+
'AE' => 'United Arab Emirates',
|
18
|
+
'AF' => 'Afghanistan',
|
19
|
+
'AG' => 'Antigua and Barbuda',
|
20
|
+
'AI' => 'Anguilla',
|
21
|
+
'AL' => 'Albania',
|
22
|
+
'AM' => 'Armenia',
|
23
|
+
'AO' => 'Angola',
|
24
|
+
'AQ' => 'Antarctica',
|
25
|
+
'AR' => 'Argentina',
|
26
|
+
'AS' => 'American Samoa',
|
27
|
+
'AT' => 'Austria',
|
28
|
+
'AU' => 'Australia',
|
29
|
+
'AW' => 'Aruba',
|
30
|
+
'AX' => 'Aland Islands',
|
31
|
+
'AZ' => 'Azerbaijan',
|
32
|
+
'BA' => 'Bosnia and Herzegovina',
|
33
|
+
'BB' => 'Barbados',
|
34
|
+
'BD' => 'Bangladesh',
|
35
|
+
'BE' => 'Belgium',
|
36
|
+
'BF' => 'Burkina Faso',
|
37
|
+
'BG' => 'Bulgaria',
|
38
|
+
'BH' => 'Bahrain',
|
39
|
+
'BI' => 'Burundi',
|
40
|
+
'BJ' => 'Benin',
|
41
|
+
'BL' => 'Saint Barthélemy',
|
42
|
+
'BM' => 'Bermuda',
|
43
|
+
'BN' => 'Brunei',
|
44
|
+
'BO' => 'Bolivia',
|
45
|
+
'BQ' => 'Caribbean Netherlands',
|
46
|
+
'BR' => 'Brazil',
|
47
|
+
'BS' => 'The Bahamas',
|
48
|
+
'BT' => 'Bhutan',
|
49
|
+
'BV' => 'Bouvet Island',
|
50
|
+
'BW' => 'Botswana',
|
51
|
+
'BY' => 'Belarus',
|
52
|
+
'BZ' => 'Belize',
|
53
|
+
'CA' => 'Canada',
|
54
|
+
'CC' => 'Cocos (Keeling) Islands',
|
55
|
+
'CD' => 'Democratic Republic of the Congo',
|
56
|
+
'CF' => 'Central African Republic',
|
57
|
+
'CG' => 'Republic of the Congo',
|
58
|
+
'CH' => 'Switzerland',
|
59
|
+
'CI' => "Cote d'Ivoire",
|
60
|
+
'CK' => 'Cook Islands',
|
61
|
+
'CL' => 'Chile',
|
62
|
+
'CM' => 'Cameroon',
|
63
|
+
'CN' => 'China',
|
64
|
+
'CO' => 'Colombia',
|
65
|
+
'CR' => 'Costa Rica',
|
66
|
+
'CU' => 'Cuba',
|
67
|
+
'CV' => 'Cape Verde',
|
68
|
+
'CW' => 'Curaçao',
|
69
|
+
'CX' => 'Christmas Island',
|
70
|
+
'CY' => 'Cyprus',
|
71
|
+
'CZ' => 'Czech Republic',
|
72
|
+
'DE' => 'Germany',
|
73
|
+
'DJ' => 'Djibouti',
|
74
|
+
'DK' => 'Denmark',
|
75
|
+
'DM' => 'Dominica',
|
76
|
+
'DO' => 'Dominican Republic',
|
77
|
+
'DZ' => 'Algeria',
|
78
|
+
'EC' => 'Ecuador',
|
79
|
+
'EE' => 'Estonia',
|
80
|
+
'EG' => 'Egypt',
|
81
|
+
'EH' => 'Western Sahara',
|
82
|
+
'ER' => 'Eritrea',
|
83
|
+
'ES' => 'Spain',
|
84
|
+
'ET' => 'Ethiopia',
|
85
|
+
'FI' => 'Finland',
|
86
|
+
'FJ' => 'Fiji',
|
87
|
+
'FK' => 'Falkland Islands',
|
88
|
+
'FM' => 'Federated States of Micronesia',
|
89
|
+
'FO' => 'Faroe Islands',
|
90
|
+
'FR' => 'France',
|
91
|
+
'GA' => 'Gabon',
|
92
|
+
'GB' => 'United Kingdom',
|
93
|
+
'GD' => 'Grenada',
|
94
|
+
'GE' => 'Georgia',
|
95
|
+
'GF' => 'French Guiana',
|
96
|
+
'GG' => 'Guernsey',
|
97
|
+
'GH' => 'Ghana',
|
98
|
+
'GI' => 'Gibraltar',
|
99
|
+
'GL' => 'Greenland',
|
100
|
+
'GM' => 'The Gambia',
|
101
|
+
'GN' => 'Guinea',
|
102
|
+
'GP' => 'Guadeloupe',
|
103
|
+
'GQ' => 'Equatorial Guinea',
|
104
|
+
'GR' => 'Greece',
|
105
|
+
'GS' => 'South Georgia and the South Sandwich Islands',
|
106
|
+
'GT' => 'Guatemala',
|
107
|
+
'GU' => 'Guam',
|
108
|
+
'GW' => 'Guinea-Bissau',
|
109
|
+
'GY' => 'Guyana',
|
110
|
+
'HK' => 'Hong Kong',
|
111
|
+
'HM' => 'Heard Island and McDonald Islands',
|
112
|
+
'HN' => 'Honduras',
|
113
|
+
'HR' => 'Croatia',
|
114
|
+
'HT' => 'Haiti',
|
115
|
+
'HU' => 'Hungary',
|
116
|
+
'ID' => 'Indonesia',
|
117
|
+
'IE' => 'Republic of Ireland',
|
118
|
+
'IL' => 'Israel',
|
119
|
+
'IM' => 'Isle of Man',
|
120
|
+
'IN' => 'India',
|
121
|
+
'IO' => 'British Indian Ocean Territory',
|
122
|
+
'IQ' => 'Iraq',
|
123
|
+
'IR' => 'Iran',
|
124
|
+
'IS' => 'Iceland',
|
125
|
+
'IT' => 'Italy',
|
126
|
+
'JE' => 'Jersey',
|
127
|
+
'JM' => 'Jamaica',
|
128
|
+
'JO' => 'Jordan',
|
129
|
+
'JP' => 'Japan',
|
130
|
+
'KE' => 'Kenya',
|
131
|
+
'KG' => 'Kyrgyzstan',
|
132
|
+
'KH' => 'Cambodia',
|
133
|
+
'KI' => 'Kiribati',
|
134
|
+
'KM' => 'Comoros',
|
135
|
+
'KN' => 'Saint Kitts and Nevis',
|
136
|
+
'KP' => 'North Korea',
|
137
|
+
'KR' => 'South Korea',
|
138
|
+
'KW' => 'Kuwait',
|
139
|
+
'KY' => 'Cayman Islands',
|
140
|
+
'KZ' => 'Kazakhstan',
|
141
|
+
'LA' => 'Laos',
|
142
|
+
'LB' => 'Lebanon',
|
143
|
+
'LC' => 'Saint Lucia',
|
144
|
+
'LI' => 'Liechtenstein',
|
145
|
+
'LK' => 'Sri Lanka',
|
146
|
+
'LR' => 'Liberia',
|
147
|
+
'LS' => 'Lesotho',
|
148
|
+
'LT' => 'Lithuania',
|
149
|
+
'LU' => 'Luxembourg',
|
150
|
+
'LV' => 'Latvia',
|
151
|
+
'LY' => 'Libya',
|
152
|
+
'MA' => 'Morocco',
|
153
|
+
'MC' => 'Monaco',
|
154
|
+
'MD' => 'Moldova',
|
155
|
+
'ME' => 'Montenegro',
|
156
|
+
'MF' => 'Collectivity of Saint Martin',
|
157
|
+
'MG' => 'Madagascar',
|
158
|
+
'MH' => 'Marshall Islands',
|
159
|
+
'MK' => 'Republic of Macedonia',
|
160
|
+
'ML' => 'Mali',
|
161
|
+
'MM' => 'Myanmar',
|
162
|
+
'MN' => 'Mongolia',
|
163
|
+
'MO' => 'Macau',
|
164
|
+
'MP' => 'Northern Mariana Islands',
|
165
|
+
'MQ' => 'Martinique',
|
166
|
+
'MR' => 'Mauritania',
|
167
|
+
'MS' => 'Montserrat',
|
168
|
+
'MT' => 'Malta',
|
169
|
+
'MU' => 'Mauritius',
|
170
|
+
'MV' => 'Maldives',
|
171
|
+
'MW' => 'Malawi',
|
172
|
+
'MX' => 'Mexico',
|
173
|
+
'MY' => 'Malaysia',
|
174
|
+
'MZ' => 'Mozambique',
|
175
|
+
'NA' => 'Namibia',
|
176
|
+
'NC' => 'New Caledonia',
|
177
|
+
'NE' => 'Niger',
|
178
|
+
'NF' => 'Norfolk Island',
|
179
|
+
'NG' => 'Nigeria',
|
180
|
+
'NI' => 'Nicaragua',
|
181
|
+
'NL' => 'Netherlands',
|
182
|
+
'NO' => 'Norway',
|
183
|
+
'NP' => 'Nepal',
|
184
|
+
'NR' => 'Nauru',
|
185
|
+
'NU' => 'Niue',
|
186
|
+
'NZ' => 'New Zealand',
|
187
|
+
'OM' => 'Oman',
|
188
|
+
'PA' => 'Panama',
|
189
|
+
'PE' => 'Peru',
|
190
|
+
'PF' => 'French Polynesia',
|
191
|
+
'PG' => 'Papua New Guinea',
|
192
|
+
'PH' => 'Philippines',
|
193
|
+
'PK' => 'Pakistan',
|
194
|
+
'PL' => 'Poland',
|
195
|
+
'PM' => 'Saint Pierre and Miquelon',
|
196
|
+
'PN' => 'Pitcairn Islands',
|
197
|
+
'PR' => 'Puerto Rico',
|
198
|
+
'PS' => 'State of Palestine',
|
199
|
+
'PT' => 'Portugal',
|
200
|
+
'PW' => 'Palau',
|
201
|
+
'PY' => 'Paraguay',
|
202
|
+
'QA' => 'Qatar',
|
203
|
+
'RE' => 'Reunion',
|
204
|
+
'RO' => 'Romania',
|
205
|
+
'RS' => 'Serbia',
|
206
|
+
'RU' => 'Russia',
|
207
|
+
'RW' => 'Rwanda',
|
208
|
+
'SA' => 'Saudi Arabia',
|
209
|
+
'SB' => 'Solomon Islands',
|
210
|
+
'SC' => 'Seychelles',
|
211
|
+
'SD' => 'Sudan',
|
212
|
+
'SE' => 'Sweden',
|
213
|
+
'SG' => 'Singapore',
|
214
|
+
'SH' => 'Saint Helena, Ascension and Tristan da Cunha',
|
215
|
+
'SI' => 'Slovenia',
|
216
|
+
'SJ' => 'Svalbard and Jan Mayen',
|
217
|
+
'SK' => 'Slovakia',
|
218
|
+
'SL' => 'Sierra Leone',
|
219
|
+
'SM' => 'San Marino',
|
220
|
+
'SN' => 'Senegal',
|
221
|
+
'SO' => 'Somalia',
|
222
|
+
'SR' => 'Suriname',
|
223
|
+
'SS' => 'South Sudan',
|
224
|
+
'ST' => 'São Tomé and Príncipe',
|
225
|
+
'SV' => 'El Salvador',
|
226
|
+
'SX' => 'Sint Maarten',
|
227
|
+
'SY' => 'Syria',
|
228
|
+
'SZ' => 'Swaziland',
|
229
|
+
'TC' => 'Turks and Caicos Islands',
|
230
|
+
'TD' => 'Chad',
|
231
|
+
'TF' => 'French Southern and Antarctic Lands',
|
232
|
+
'TG' => 'Togo',
|
233
|
+
'TH' => 'Thailand',
|
234
|
+
'TJ' => 'Tajikistan',
|
235
|
+
'TK' => 'Tokelau',
|
236
|
+
'TL' => 'East Timor',
|
237
|
+
'TM' => 'Turkmenistan',
|
238
|
+
'TN' => 'Tunisia',
|
239
|
+
'TO' => 'Tonga',
|
240
|
+
'TR' => 'Turkey',
|
241
|
+
'TT' => 'Trinidad and Tobago',
|
242
|
+
'TV' => 'Tuvalu',
|
243
|
+
'TW' => 'Taiwan',
|
244
|
+
'TZ' => 'Tanzania',
|
245
|
+
'UA' => 'Ukraine',
|
246
|
+
'UG' => 'Uganda',
|
247
|
+
'UM' => 'United States Minor Outlying Islands',
|
248
|
+
'US' => 'United States',
|
249
|
+
'UY' => 'Uruguay',
|
250
|
+
'UZ' => 'Uzbekistan',
|
251
|
+
'VA' => 'Vatican City',
|
252
|
+
'VC' => 'Saint Vincent and the Grenadines',
|
253
|
+
'VE' => 'Venezuela',
|
254
|
+
'VG' => 'British Virgin Islands',
|
255
|
+
'VI' => 'United States Virgin Islands',
|
256
|
+
'VN' => 'Vietnam',
|
257
|
+
'VU' => 'Vanuatu',
|
258
|
+
'WF' => 'Wallis and Futuna',
|
259
|
+
'WS' => 'Samoa',
|
260
|
+
'YE' => 'Yemen',
|
261
|
+
'YT' => 'Mayotte',
|
262
|
+
'ZA' => 'South Africa',
|
263
|
+
'ZM' => 'Zambia',
|
264
|
+
'ZW' => 'Zimbabwe'
|
265
|
+
}.freeze
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class Cloudflare
|
7
|
+
class Headers
|
8
|
+
# See: https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
|
9
|
+
NAMES = %w[
|
10
|
+
HTTP_CF_IPCOUNTRY
|
11
|
+
HTTP_CF_CONNECTING_IP
|
12
|
+
HTTP_CF_RAY
|
13
|
+
HTTP_CF_VISITOR
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
STANDARD = %w[
|
17
|
+
HTTP_X_FORWARDED_FOR
|
18
|
+
HTTP_X_FORWARDED_PROTO
|
19
|
+
REMOTE_ADDR
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
ALL = (NAMES + STANDARD).freeze
|
23
|
+
|
24
|
+
# Create constants for each header
|
25
|
+
ALL.map { |h| const_set h, h.to_s.freeze }.freeze
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_accessor :backup, :original_remote_addr, :original_forwarded_for
|
29
|
+
|
30
|
+
def trusted?(headers)
|
31
|
+
Headers.new(headers).trusted?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
self.backup = true
|
36
|
+
self.original_remote_addr = 'ORIGINAL_REMOTE_ADDR'
|
37
|
+
self.original_forwarded_for = 'ORIGINAL_FORWARDED_FOR'
|
38
|
+
|
39
|
+
def initialize(headers)
|
40
|
+
@headers = headers
|
41
|
+
end
|
42
|
+
|
43
|
+
# "Cf-Ipcountry: US"
|
44
|
+
def ip_country
|
45
|
+
@headers.fetch(HTTP_CF_IPCOUNTRY, 'XX')
|
46
|
+
end
|
47
|
+
|
48
|
+
# "CF-Connecting-IP: A.B.C.D"
|
49
|
+
def connecting_ip
|
50
|
+
@connecting_ip ||= IPs.parse(@headers[HTTP_CF_CONNECTING_IP]).first
|
51
|
+
end
|
52
|
+
|
53
|
+
# "X-Forwarded-For: A.B.C.D"
|
54
|
+
# "X-Forwarded-For: A.B.C.D[,X.X.X.X,Y.Y.Y.Y,]"
|
55
|
+
def forwarded_for
|
56
|
+
@forwarded_for ||= IPs.parse(@headers[HTTP_X_FORWARDED_FOR])
|
57
|
+
end
|
58
|
+
|
59
|
+
# "X-Forwarded-Proto: https"
|
60
|
+
def forwarded_proto
|
61
|
+
@headers[HTTP_X_FORWARDED_PROTO]
|
62
|
+
end
|
63
|
+
|
64
|
+
# "Cf-Ray: 230b030023ae2822-SJC"
|
65
|
+
def ray
|
66
|
+
@headers[HTTP_CF_RAY]
|
67
|
+
end
|
68
|
+
|
69
|
+
# "Cf-Visitor: { \"scheme\":\"https\"}"
|
70
|
+
def visitor
|
71
|
+
return unless has?(HTTP_CF_VISITOR)
|
72
|
+
JSON.parse @headers[HTTP_CF_VISITOR]
|
73
|
+
end
|
74
|
+
|
75
|
+
def remote_addr
|
76
|
+
@remote_addr ||= IPs.parse(@headers[REMOTE_ADDR]).first
|
77
|
+
end
|
78
|
+
|
79
|
+
# Indicates if the headers passed through Cloudflare
|
80
|
+
def trusted?
|
81
|
+
IPs.list.any? { |range| range.include? remote_addr }
|
82
|
+
end
|
83
|
+
|
84
|
+
def backup_headers
|
85
|
+
return {} unless Headers.backup
|
86
|
+
|
87
|
+
{}.tap do |headers|
|
88
|
+
headers[Headers.original_remote_addr] = @headers[REMOTE_ADDR]
|
89
|
+
headers[Headers.original_forwarded_for] = @headers[HTTP_X_FORWARDED_FOR]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def rewritten_headers
|
94
|
+
# Only rewrites headers if it's a Cloudflare request
|
95
|
+
return {} unless trusted?
|
96
|
+
|
97
|
+
{}.tap do |headers|
|
98
|
+
headers.merge! backup_headers
|
99
|
+
|
100
|
+
# Overwrite the original remote IP based on
|
101
|
+
# Cloudflare's specified original remote IP
|
102
|
+
headers[REMOTE_ADDR] = connecting_ip.to_s if connecting_ip
|
103
|
+
|
104
|
+
# Add HTTP_X_FORWARDED_FOR if it wasn't present.
|
105
|
+
# Cloudflare will already have modified the header if
|
106
|
+
# it was present in the original request.
|
107
|
+
# See: https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
|
108
|
+
headers[HTTP_X_FORWARDED_FOR] = "#{connecting_ip}, #{remote_addr}" if forwarded_for.none?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Headers that relate to Cloudflare
|
113
|
+
# See: https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
|
114
|
+
def target_headers
|
115
|
+
@headers.select { |k, _| ALL.include? k }
|
116
|
+
end
|
117
|
+
|
118
|
+
def rewritten_target_headers
|
119
|
+
target_headers.merge(rewritten_headers)
|
120
|
+
end
|
121
|
+
|
122
|
+
def rewrite
|
123
|
+
@headers.merge(rewritten_headers)
|
124
|
+
end
|
125
|
+
|
126
|
+
def has?(header)
|
127
|
+
@headers.key?(header)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
class Cloudflare
|
8
|
+
module IPs
|
9
|
+
# See: https://www.cloudflare.com/ips/
|
10
|
+
V4_URL = 'https://www.cloudflare.com/ips-v4'
|
11
|
+
V6_URL = 'https://www.cloudflare.com/ips-v6'
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# List of IPs to reference
|
15
|
+
attr_accessor :list
|
16
|
+
|
17
|
+
# Refresh list of IPs in case local copy is outdated
|
18
|
+
def refresh!
|
19
|
+
self.list = fetch(V4_URL) + fetch(V6_URL)
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch(url)
|
23
|
+
parse Net::HTTP.get(URI(url))
|
24
|
+
end
|
25
|
+
|
26
|
+
def read(filename)
|
27
|
+
parse File.read(filename)
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse(string)
|
31
|
+
return [] if string.to_s.strip.empty?
|
32
|
+
string.split(/[,\s]+/).map { |ip| IPAddr.new(ip.strip) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
V4 = read("#{__dir__}/../../../data/ips_v4.txt")
|
37
|
+
V6 = read("#{__dir__}/../../../data/ips_v6.txt")
|
38
|
+
|
39
|
+
DEFAULTS = V4 + V6
|
40
|
+
|
41
|
+
### Configure
|
42
|
+
|
43
|
+
self.list = DEFAULTS
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Cloudflare
|
5
|
+
module Middleware
|
6
|
+
class AccessControl
|
7
|
+
class << self
|
8
|
+
attr_accessor :blocked_response, :blocked_message
|
9
|
+
end
|
10
|
+
|
11
|
+
self.blocked_message = 'Forbidden'
|
12
|
+
self.blocked_response = lambda do |_env|
|
13
|
+
[403, { 'Content-Type' => 'text/plain' }, ["#{blocked_message.strip}\n"]]
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(app)
|
17
|
+
@app = app
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
headers = Headers.new(env)
|
22
|
+
|
23
|
+
if headers.trusted?
|
24
|
+
Cloudflare.info "[#{self.class.name}] Trusted Network (REMOTE_ADDR): #{headers.target_headers}"
|
25
|
+
@app.call(env)
|
26
|
+
else
|
27
|
+
Cloudflare.warn "[#{self.class.name}] Untrusted Network (REMOTE_ADDR): #{headers.target_headers}"
|
28
|
+
AccessControl.blocked_response.call(env)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Cloudflare
|
5
|
+
module Middleware
|
6
|
+
class RewriteHeaders
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
headers = Headers.new(env)
|
13
|
+
|
14
|
+
Cloudflare.warn "[#{self.class.name}] Untrusted Network (REMOTE_ADDR): #{headers.target_headers}" unless headers.trusted?
|
15
|
+
Cloudflare.debug "[#{self.class.name}] Target Headers: #{headers.target_headers}"
|
16
|
+
Cloudflare.debug "[#{self.class.name}] Rewritten Headers: #{headers.rewritten_target_headers}"
|
17
|
+
|
18
|
+
@app.call(headers.rewrite)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
Binary file
|
data/rack-cloudflare.gemspec
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
lib = File.expand_path('lib', __dir__)
|
2
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
5
|
require 'rack/cloudflare/version'
|
@@ -24,4 +26,6 @@ Gem::Specification.new do |spec|
|
|
24
26
|
spec.add_development_dependency 'bundler', '~> 1.16'
|
25
27
|
spec.add_development_dependency 'rake', '~> 10.0'
|
26
28
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
29
|
+
spec.add_development_dependency 'rubocop'
|
30
|
+
spec.add_development_dependency 'rubycritic'
|
27
31
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-cloudflare
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joel Van Horn
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-09-
|
11
|
+
date: 2018-09-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,34 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubycritic
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
55
83
|
description: Deal with Cloudflare features in Rack-based apps.
|
56
84
|
email:
|
57
85
|
- joel@joelvanhorn.com
|
@@ -61,14 +89,23 @@ extra_rdoc_files: []
|
|
61
89
|
files:
|
62
90
|
- ".gitignore"
|
63
91
|
- ".rspec"
|
92
|
+
- ".rubocop.yml"
|
64
93
|
- ".travis.yml"
|
65
94
|
- Gemfile
|
66
95
|
- README.md
|
67
96
|
- Rakefile
|
68
97
|
- bin/console
|
69
98
|
- bin/setup
|
99
|
+
- data/ips_v4.txt
|
100
|
+
- data/ips_v6.txt
|
70
101
|
- lib/rack/cloudflare.rb
|
102
|
+
- lib/rack/cloudflare/countries.rb
|
103
|
+
- lib/rack/cloudflare/headers.rb
|
104
|
+
- lib/rack/cloudflare/ips.rb
|
105
|
+
- lib/rack/cloudflare/middleware/access_control.rb
|
106
|
+
- lib/rack/cloudflare/middleware/rewrite_headers.rb
|
71
107
|
- lib/rack/cloudflare/version.rb
|
108
|
+
- rack-cloudflare-0.1.0.gem
|
72
109
|
- rack-cloudflare.gemspec
|
73
110
|
homepage: https://github.com/joelvh/rack-cloudflare
|
74
111
|
licenses: []
|