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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d361e00dd386a7ee04f14083033478613234cf3595d3fcd2305293dda78f43d2
4
- data.tar.gz: 2d51e3ee954d38f99db080f28d82206a6d3156b1f8de7f1a4dd8e44328979c16
3
+ metadata.gz: 6c8a5b0439022d396fd56721e79b97a96c4cf830b0e37c27a4a9f6f2db2f8bd0
4
+ data.tar.gz: d3866b5cfcce96211b25ae277b5e5280f3f38e0eb5f78d291c23d2a08cf84715
5
5
  SHA512:
6
- metadata.gz: '0318856bf3ad9182b7a22de5a5cb14a8b79ae55c7cb40c68329ba044820a1dcd69b3103ae47b6f4ac727a0f90e405d810a0d61c8b523e41a0b74170791cc300d'
7
- data.tar.gz: 3d871d3ad152afed37a1ca788602e570f601e25d301d73a9c141cfc12464988b080972a935e4f51b5620c6ea710a471f9f57de975285768980b24e56c5d5df6e
6
+ metadata.gz: 8ea482f40489690a5f368f0e21a9aad7fbc59dc76517b351b635ff53064ac8a1339ecc4c3129047967d5bdbafa259aa11a4b79314ea52ebe78b63d3bf031e9f4
7
+ data.tar.gz: 40709bddc477ccd842dd50318827c4af50bbc7cf5a0b8ee58304e0e0d0d736b5fdd04b8b6f81692eb45f82b9a04492c4cb63001e6089490ab8e1cb8ee217b2a4
data/.gitignore CHANGED
@@ -9,3 +9,5 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+
13
+ Gemfile.lock
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Rack::Cloudflare
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rack/cloudflare`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Write usage instructions here
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
- ## Development
135
+ ## Credits
28
136
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
137
+ Inspired by:
30
138
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
@@ -0,0 +1,14 @@
1
+ 103.21.244.0/22
2
+ 103.22.200.0/22
3
+ 103.31.4.0/22
4
+ 104.16.0.0/12
5
+ 108.162.192.0/18
6
+ 131.0.72.0/22
7
+ 141.101.64.0/18
8
+ 162.158.0.0/15
9
+ 172.64.0.0/13
10
+ 173.245.48.0/20
11
+ 188.114.96.0/20
12
+ 190.93.240.0/20
13
+ 197.234.240.0/22
14
+ 198.41.128.0/17
data/data/ips_v6.txt ADDED
@@ -0,0 +1,6 @@
1
+ 2400:cb00::/32
2
+ 2405:b500::/32
3
+ 2606:4700::/32
4
+ 2803:f800::/32
5
+ 2c0f:f248::/32
6
+ 2a06:98c0::/29
@@ -1,8 +1,21 @@
1
- require 'rack/cloudflare/version'
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
- # Documentation goes here...
5
- module Cloudflare
6
- # Your code goes here...
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
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
- module Cloudflare
3
- VERSION = '0.1.0'.freeze
4
+ class Cloudflare
5
+ VERSION = '1.0.0'
4
6
  end
5
7
  end
Binary file
@@ -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: 0.1.0
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-13 00:00:00.000000000 Z
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: []