rack-cloudflare 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|