geo_redirect 0.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +16 -0
- data/README.md +48 -26
- data/Rakefile +2 -2
- data/geo_redirect.gemspec +15 -16
- data/lib/geo_redirect.rb +1 -4
- data/lib/geo_redirect/middleware.rb +47 -38
- data/lib/geo_redirect/version.rb +1 -1
- data/lib/tasks/geo_redirect.rake +3 -3
- data/spec/geo_redirect_spec.rb +123 -202
- data/spec/spec_helper.rb +7 -7
- data/spec/support/geo_redirect.rb +9 -10
- data/spec/support/rake.rb +5 -5
- metadata +20 -55
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cc53d6e770a29fb9a25ad601803bc220e9de5595
|
4
|
+
data.tar.gz: ce7e9473fb1a032807256e361c72811ef06856c8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b82b53e72d1ac2f59562433e6f4f30033115692484049253991507aa3871c4030751f382020976fda8b945425c9df93d7957a58534fa1f065b5e7f13b9b998e
|
7
|
+
data.tar.gz: 467773cacba033e14b1f0ee811fc420095e9f650d7239db8caf10a6bc0ebf4948fb967a5fde8e6096b8d8223624d08d4c732047e2194efa5198ab85dbf8c5326
|
data/.rubocop.yml
ADDED
data/README.md
CHANGED
@@ -12,9 +12,15 @@ to redirect users to:
|
|
12
12
|
* [biz.waze.com](http://biz.waze.com) for US and Canada incoming traffic.
|
13
13
|
* [biz-world.waze.com](http://biz-world.waze.com/) for any other sources.
|
14
14
|
|
15
|
-
The server stores a session variable with the server it decided on, for future
|
15
|
+
The server stores a session variable with the server it decided on, for future
|
16
|
+
traffic from the same client.
|
16
17
|
|
17
|
-
In addition, you can override these redirects by adding `?redirect=1` to any
|
18
|
+
In addition, you can override these redirects by adding `?redirect=1` to any
|
19
|
+
URL, and by that forcing the server to host from the current domain (and saving
|
20
|
+
that domain to the user's session variable).
|
21
|
+
|
22
|
+
To skip geo-redirection completely, pass a `?skip_geo=true` argument (this would
|
23
|
+
avoid saving any session value and/or HTTP redirects).
|
18
24
|
|
19
25
|
## Installation
|
20
26
|
|
@@ -45,36 +51,44 @@ This should be a YML file representing your redirection rules.
|
|
45
51
|
|
46
52
|
Here's a template that we use for the setup described above:
|
47
53
|
|
48
|
-
|
49
|
-
:
|
50
|
-
|
51
|
-
:countries: ['US', 'CA']
|
54
|
+
:us:
|
55
|
+
:host: 'biz.waze.com'
|
56
|
+
:countries: ['US', 'CA']
|
52
57
|
|
53
|
-
:il:
|
54
|
-
|
55
|
-
|
58
|
+
:il:
|
59
|
+
:host: 'biz.waze.co.il'
|
60
|
+
:countries: ['IL']
|
56
61
|
|
57
|
-
:world: &default
|
58
|
-
|
62
|
+
:world: &default
|
63
|
+
:host: 'biz-world.waze.com'
|
59
64
|
|
60
|
-
:default: *default
|
61
|
-
```
|
65
|
+
:default: *default
|
62
66
|
|
63
67
|
Note that:
|
64
68
|
|
65
69
|
1. Every main item is a location, and must have a `host` configured.
|
66
|
-
2. A location can have a `countries` array. This will cause a redirect to this
|
67
|
-
|
70
|
+
2. A location can have a `countries` array. This will cause a redirect to this
|
71
|
+
location for users from that country code. For available country codes, see
|
72
|
+
[ISO 3166 Country Codes list](http://www.maxmind.com/en/iso3166) from MaxMind
|
73
|
+
(the Geo IP provider `GeoRedirect` uses).
|
74
|
+
3. There must be a `default` location that would be used in case the client
|
75
|
+
can't be geo-located.
|
68
76
|
|
69
77
|
### 2. GeoIP Countries database
|
70
78
|
|
71
|
-
`GeoRedirect` uses the [`geoip`](http://geoip.rubyforge.org/) gem for its
|
79
|
+
`GeoRedirect` uses the [`geoip`](http://geoip.rubyforge.org/) gem for its
|
80
|
+
geo-location functionality. In particular, it requires the `GeoLite country`
|
81
|
+
free database from [MaxMind](http://www.maxmind.com/).
|
72
82
|
|
73
|
-
You can download the database file [directly from
|
83
|
+
You can download the database file [directly from
|
84
|
+
MaxMind](http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz)
|
85
|
+
and unzip it into `db/` in your project, **or** you could use the following
|
86
|
+
`rake` task designed just for that:
|
74
87
|
|
75
88
|
$ rake geo_redirect:fetch_db
|
76
89
|
|
77
|
-
It'd be a good idea to use this task on your (Capistrano or whatever) deployment
|
90
|
+
It'd be a good idea to use this task on your (Capistrano or whatever) deployment
|
91
|
+
scripts.
|
78
92
|
|
79
93
|
### Custom paths
|
80
94
|
|
@@ -83,33 +97,41 @@ The default paths for these files are:
|
|
83
97
|
1. `config/georedirect.yml`
|
84
98
|
2. `db/GeoIP.dat`
|
85
99
|
|
86
|
-
If that doesn't suit you, you can customize these when adding `GeoRedirect` to
|
100
|
+
If that doesn't suit you, you can customize these when adding `GeoRedirect` to
|
101
|
+
your project:
|
87
102
|
|
88
103
|
Rails.application.middleware.use GeoRedirect::Middleware, {
|
89
|
-
:
|
90
|
-
:
|
104
|
+
db: 'db/geo_database.dat',
|
105
|
+
config: 'geo_cfg.yml'
|
91
106
|
}
|
92
107
|
|
93
108
|
### Debugging
|
94
109
|
|
95
|
-
You can add a `logfile` path string when adding the middleware if you want it to
|
110
|
+
You can add a `logfile` path string when adding the middleware if you want it to
|
111
|
+
log some of its decision process into the file.
|
96
112
|
This is useful when working on your configuration YAML.
|
97
113
|
|
98
|
-
Rails.application.middleware.use GeoRedirect::Middleware, :
|
114
|
+
Rails.application.middleware.use GeoRedirect::Middleware, logfile: 'log/geo_redirect.log'
|
99
115
|
|
100
116
|
`GeoRedirect`'s log messages will always be prefixed with `[GeoRedirect]`.
|
101
117
|
|
102
118
|
### Accessing discovered country
|
103
119
|
|
104
|
-
The country code discovered for the current user is available for your
|
120
|
+
The country code discovered for the current user is available for your
|
121
|
+
convenience, under `session['geo_redirect.country']`.
|
105
122
|
You can use it to make content decisions, or whatever.
|
106
123
|
|
107
124
|
## Known Issues
|
108
125
|
|
109
126
|
A couple issues I know about but haven't had the time to fix:
|
110
127
|
|
111
|
-
1. Cross-domain session var is required. In particular, if your stubborn user
|
112
|
-
|
128
|
+
1. Cross-domain session var is required. In particular, if your stubborn user
|
129
|
+
goes to more than 1 server with `?redirect=1`, all of these servers will
|
130
|
+
never redirect them again (until the session is expired).
|
131
|
+
2. When a client accesses your site from an unknown hostname (one that was not
|
132
|
+
configured in the `yml` file) with `?redirect=1`, they will stay in that
|
133
|
+
hostname for the current session, but in the future would be redirected to
|
134
|
+
the configured default hostname (because it was saved on their session var).
|
113
135
|
|
114
136
|
|
115
137
|
## Contributing
|
data/Rakefile
CHANGED
data/geo_redirect.gemspec
CHANGED
@@ -4,25 +4,24 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'geo_redirect/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |gem|
|
7
|
-
gem.name =
|
7
|
+
gem.name = 'geo_redirect'
|
8
8
|
gem.version = GeoRedirect::VERSION
|
9
|
-
gem.authors = [
|
10
|
-
gem.email = [
|
11
|
-
gem.description =
|
12
|
-
gem.summary =
|
13
|
-
gem.homepage =
|
9
|
+
gem.authors = ['Sagie Maoz']
|
10
|
+
gem.email = ['sagie@waze.com']
|
11
|
+
gem.description = 'Geo-location based redirector'
|
12
|
+
gem.summary = 'Rack middleware to redirect clients to hostnames based on geo-location'
|
13
|
+
gem.homepage = ''
|
14
14
|
|
15
|
-
gem.add_dependency
|
16
|
-
gem.add_dependency
|
15
|
+
gem.add_dependency 'rake'
|
16
|
+
gem.add_dependency 'geoip'
|
17
17
|
|
18
|
-
gem.add_development_dependency
|
19
|
-
gem.add_development_dependency
|
20
|
-
gem.add_development_dependency
|
21
|
-
gem.add_development_dependency
|
22
|
-
gem.add_development_dependency "simplecov", "~> 0.7.1"
|
18
|
+
gem.add_development_dependency 'rspec', '~> 3.1.0'
|
19
|
+
gem.add_development_dependency 'rack', '~> 1.6.0'
|
20
|
+
gem.add_development_dependency 'rack-test', '~> 0.6.3'
|
21
|
+
gem.add_development_dependency 'simplecov', '~> 0.9.1'
|
23
22
|
|
24
|
-
gem.files = `git ls-files`.split(
|
25
|
-
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
23
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
24
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
26
25
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
27
|
-
gem.require_paths = [
|
26
|
+
gem.require_paths = ['lib']
|
28
27
|
end
|
data/lib/geo_redirect.rb
CHANGED
@@ -16,13 +16,16 @@ module GeoRedirect
|
|
16
16
|
@db = init_db(options[:db])
|
17
17
|
@config = init_config(options[:config])
|
18
18
|
|
19
|
-
|
19
|
+
log 'Initialized middleware'
|
20
20
|
end
|
21
21
|
|
22
22
|
def call(env)
|
23
23
|
@request = Rack::Request.new(env)
|
24
24
|
|
25
|
-
if
|
25
|
+
if skip_redirect?
|
26
|
+
@app.call(env)
|
27
|
+
|
28
|
+
elsif force_redirect?
|
26
29
|
handle_force
|
27
30
|
|
28
31
|
elsif session_exists?
|
@@ -36,7 +39,7 @@ module GeoRedirect
|
|
36
39
|
def session_exists?
|
37
40
|
host = @request.session['geo_redirect']
|
38
41
|
if host && @config[host].nil? # Invalid var, remove it
|
39
|
-
|
42
|
+
log 'Invalid session var, forgetting'
|
40
43
|
forget_host(host)
|
41
44
|
host = nil
|
42
45
|
end
|
@@ -46,7 +49,7 @@ module GeoRedirect
|
|
46
49
|
|
47
50
|
def handle_session
|
48
51
|
host = @request.session['geo_redirect']
|
49
|
-
|
52
|
+
log "Handling session var: #{host}"
|
50
53
|
redirect_request(host)
|
51
54
|
end
|
52
55
|
|
@@ -55,10 +58,15 @@ module GeoRedirect
|
|
55
58
|
Rack::Utils.parse_query(url.query).key? 'redirect'
|
56
59
|
end
|
57
60
|
|
61
|
+
def skip_redirect?
|
62
|
+
url = URI.parse(@request.url)
|
63
|
+
Rack::Utils.parse_query(url.query).key? 'skip_geo'
|
64
|
+
end
|
65
|
+
|
58
66
|
def handle_force
|
59
67
|
url = URI.parse(@request.url)
|
60
68
|
host = host_by_hostname(url.host)
|
61
|
-
|
69
|
+
log "Handling force flag: #{host}"
|
62
70
|
remember_host(host)
|
63
71
|
redirect_request(url.host, true)
|
64
72
|
end
|
@@ -66,28 +74,28 @@ module GeoRedirect
|
|
66
74
|
def handle_geoip
|
67
75
|
country = country_from_request rescue nil
|
68
76
|
@request.session['geo_redirect.country'] = country
|
69
|
-
|
77
|
+
log "GeoIP match: country code #{country}"
|
70
78
|
|
71
|
-
|
79
|
+
if country.nil?
|
80
|
+
@app.call(@request.env)
|
81
|
+
else
|
72
82
|
host = host_by_country(country) # desired host
|
73
|
-
|
83
|
+
log "GeoIP host match: #{host}"
|
74
84
|
remember_host(host)
|
75
85
|
|
76
86
|
redirect_request(host)
|
77
|
-
else
|
78
|
-
@app.call(@request.env)
|
79
87
|
end
|
80
88
|
end
|
81
89
|
|
82
|
-
def redirect_request(host=nil, same_host=false)
|
90
|
+
def redirect_request(host = nil, same_host = false)
|
83
91
|
hostname = hostname_by_host(host)
|
84
92
|
|
85
93
|
if should_redirect?(hostname, same_host)
|
86
94
|
url = redirect_url(hostname)
|
87
95
|
|
88
|
-
|
96
|
+
log "Redirecting to #{url}"
|
89
97
|
[301,
|
90
|
-
{'Location' => url.to_s, 'Content-Type' => 'text/plain'},
|
98
|
+
{ 'Location' => url.to_s, 'Content-Type' => 'text/plain' },
|
91
99
|
['Moved Permanently\n']]
|
92
100
|
else
|
93
101
|
@app.call(@request.env)
|
@@ -95,12 +103,12 @@ module GeoRedirect
|
|
95
103
|
end
|
96
104
|
|
97
105
|
def host_by_country(country)
|
98
|
-
hosts = @config.select { |
|
106
|
+
hosts = @config.select { |_k, v| Array(v[:countries]).include?(country) }
|
99
107
|
hosts.keys.first || :default
|
100
108
|
end
|
101
109
|
|
102
110
|
def host_by_hostname(hostname)
|
103
|
-
hosts = @config.select { |
|
111
|
+
hosts = @config.select { |_k, v| v[:host] == hostname }
|
104
112
|
hosts.keys.first || :default
|
105
113
|
end
|
106
114
|
|
@@ -109,17 +117,18 @@ module GeoRedirect
|
|
109
117
|
end
|
110
118
|
|
111
119
|
def remember_host(host)
|
112
|
-
|
120
|
+
log "Remembering: #{host}"
|
113
121
|
@request.session['geo_redirect'] = host
|
114
122
|
end
|
115
123
|
|
116
124
|
def forget_host(host)
|
117
|
-
|
125
|
+
log "Forgetting: #{host}"
|
118
126
|
remember_host(nil)
|
119
127
|
end
|
120
128
|
|
121
129
|
protected
|
122
|
-
|
130
|
+
|
131
|
+
def log(message, level = :debug)
|
123
132
|
@logger.send(level, "[GeoRedirect] #{message}") unless @logger.nil?
|
124
133
|
end
|
125
134
|
|
@@ -134,35 +143,38 @@ module GeoRedirect
|
|
134
143
|
rescue Errno::EINVAL, Errno::ENOENT
|
135
144
|
message = <<-ERROR
|
136
145
|
Could not load GeoIP database file.
|
137
|
-
Please make sure you have a valid one and add its name to
|
138
|
-
|
146
|
+
Please make sure you have a valid one and add its name to
|
147
|
+
the GeoRedirect middleware.
|
148
|
+
Alternatively, use `rake georedirect:fetch_db` to fetch it
|
149
|
+
to the default location (under db/).
|
139
150
|
ERROR
|
140
|
-
|
151
|
+
log(message, :error)
|
141
152
|
end
|
142
153
|
|
143
154
|
def init_config(path)
|
144
|
-
YAML.load_file(path) ||
|
155
|
+
YAML.load_file(path) || fail(Errno::EINVAL)
|
145
156
|
rescue Errno::EINVAL, Errno::ENOENT, Psych::SyntaxError, SyntaxError
|
146
157
|
message = <<-ERROR
|
147
158
|
Could not load GeoRedirect config YML file.
|
148
|
-
Please make sure you have a valid YML file and pass its name
|
159
|
+
Please make sure you have a valid YML file and pass its name
|
160
|
+
when adding the GeoRedirect middlware.
|
149
161
|
ERROR
|
150
|
-
|
162
|
+
log(message, :error)
|
151
163
|
end
|
152
164
|
|
153
165
|
def request_ip
|
154
|
-
ip_address =
|
166
|
+
ip_address =
|
167
|
+
@request.env['HTTP_X_FORWARDED_FOR'] || @request.env['REMOTE_ADDR']
|
155
168
|
# take only the first given ip
|
156
169
|
ip_address.split(',').first.strip
|
157
170
|
end
|
158
171
|
|
159
|
-
|
160
172
|
def country_from_request
|
161
173
|
ip = request_ip
|
162
|
-
|
174
|
+
log "Handling GeoIP lookup: IP #{ip}"
|
163
175
|
|
164
|
-
res
|
165
|
-
code
|
176
|
+
res = @db.country(ip)
|
177
|
+
code = res[:country_code]
|
166
178
|
|
167
179
|
res[:country_code2] unless code.nil? || code.zero?
|
168
180
|
end
|
@@ -173,9 +185,9 @@ module GeoRedirect
|
|
173
185
|
url.host = hostname if hostname
|
174
186
|
|
175
187
|
# Remove force flag from GET arguments
|
176
|
-
query_hash = Rack::Utils.parse_query(url.query).tap
|
188
|
+
query_hash = Rack::Utils.parse_query(url.query).tap do |u|
|
177
189
|
u.delete('redirect')
|
178
|
-
|
190
|
+
end
|
179
191
|
|
180
192
|
# Copy query
|
181
193
|
url.query = URI.encode_www_form(query_hash)
|
@@ -185,13 +197,10 @@ module GeoRedirect
|
|
185
197
|
end
|
186
198
|
|
187
199
|
def should_redirect?(hostname, same_host)
|
188
|
-
|
189
|
-
hostname_ends_with = %r{#{hostname.gsub(".", "\.")}$}
|
190
|
-
(@request.host =~ hostname_ends_with).nil?
|
191
|
-
else
|
192
|
-
true
|
193
|
-
end
|
194
|
-
end
|
200
|
+
return true if hostname.nil? || same_host
|
195
201
|
|
202
|
+
hostname_ends_with = %r{#{hostname.gsub(".", "\.")}$}
|
203
|
+
(@request.host =~ hostname_ends_with).nil?
|
204
|
+
end
|
196
205
|
end
|
197
206
|
end
|
data/lib/geo_redirect/version.rb
CHANGED
data/lib/tasks/geo_redirect.rake
CHANGED
@@ -5,9 +5,9 @@ require 'zlib'
|
|
5
5
|
namespace :geo_redirect do
|
6
6
|
DB_URI = 'http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz'
|
7
7
|
|
8
|
-
desc
|
9
|
-
task :fetch_db, :db_path do |
|
10
|
-
args.with_defaults(:
|
8
|
+
desc 'Fetches an updated copy of the GeoIP countries DB from MaxMind'
|
9
|
+
task :fetch_db, :db_path do |_t, args|
|
10
|
+
args.with_defaults(db_path: GeoRedirect::DEFAULT_DB_PATH)
|
11
11
|
|
12
12
|
# Fetches DB copy and gunzips it
|
13
13
|
# Thx http://stackoverflow.com/a/2014317/107085
|
data/spec/geo_redirect_spec.rb
CHANGED
@@ -1,141 +1,130 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'logger'
|
4
4
|
|
5
|
-
describe
|
5
|
+
describe 'geo_redirect' do
|
6
6
|
include GeoRedirect::Support
|
7
7
|
include Rack::Test::Methods
|
8
8
|
|
9
9
|
def session
|
10
|
-
last_request.env['rack.session']
|
10
|
+
last_request.env['rack.session'] || {}
|
11
11
|
end
|
12
12
|
|
13
13
|
def url_scheme
|
14
14
|
last_request.env['rack.url_scheme']
|
15
15
|
end
|
16
16
|
|
17
|
-
|
18
|
-
@config = YAML.load_file(fixture_path("config.yml"))
|
19
|
-
end
|
17
|
+
let(:config) { YAML.load_file(fixture_path('config.yml')) }
|
20
18
|
|
21
|
-
describe
|
22
|
-
it
|
19
|
+
describe '#load_config' do
|
20
|
+
it 'reads a config file' do
|
23
21
|
mock_app
|
24
|
-
|
25
|
-
@app.config.should_not be_nil
|
26
|
-
@app.config.should eq(@config)
|
22
|
+
expect(@app.config).to eq(config)
|
27
23
|
end
|
28
24
|
|
29
|
-
it
|
30
|
-
mock_app :
|
31
|
-
log_should_include(
|
32
|
-
log_should_include(
|
25
|
+
it 'errors on not-found config file' do
|
26
|
+
mock_app config: nonexisting_file_path
|
27
|
+
log_should_include('ERROR')
|
28
|
+
log_should_include('Could not load GeoRedirect config YML file')
|
33
29
|
end
|
34
30
|
|
35
|
-
it
|
36
|
-
mock_app :
|
37
|
-
log_should_include(
|
38
|
-
log_should_include(
|
31
|
+
it 'errors on a mal-formatted config file' do
|
32
|
+
mock_app config: fixture_path('config.bad.yml')
|
33
|
+
log_should_include('ERROR')
|
34
|
+
log_should_include('Could not load GeoRedirect config YML file')
|
39
35
|
end
|
40
36
|
end
|
41
37
|
|
42
|
-
describe
|
43
|
-
it
|
38
|
+
describe '#load_db' do
|
39
|
+
it 'reads a db file' do
|
44
40
|
mock_app
|
45
|
-
|
46
|
-
@app.db.should_not be_nil
|
47
|
-
@app.db.should be_a_kind_of GeoIP
|
41
|
+
expect(@app.db).to be_a(GeoIP)
|
48
42
|
end
|
49
43
|
|
50
|
-
it
|
51
|
-
mock_app :
|
52
|
-
log_should_include(
|
53
|
-
log_should_include(
|
44
|
+
it 'errors on not-found db file' do
|
45
|
+
mock_app db: nonexisting_file_path
|
46
|
+
log_should_include('ERROR')
|
47
|
+
log_should_include('Could not load GeoIP database file.')
|
54
48
|
end
|
55
49
|
|
56
50
|
# this example is disabled, as it seems that
|
57
51
|
# GeoIP does not let me know if a db file is proper.
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
end
|
65
|
-
=end
|
52
|
+
# it "errors on mal-formatted db file" do
|
53
|
+
# pending "GeoIP does not raise on bad files"
|
54
|
+
# mock_app :db => fixture_path("config.yml")
|
55
|
+
# log_should_include("ERROR")
|
56
|
+
# log_should_include("Could not load GeoIP database file.")
|
57
|
+
# end
|
66
58
|
end
|
67
59
|
|
68
|
-
describe
|
69
|
-
describe
|
70
|
-
before
|
71
|
-
mock_app
|
72
|
-
end
|
60
|
+
describe '#log' do
|
61
|
+
describe 'with valid logfile path' do
|
62
|
+
before { mock_app }
|
73
63
|
|
74
|
-
it
|
64
|
+
it 'initiates a log file' do
|
75
65
|
@app.instance_variable_get(:"@logger").should be_kind_of Logger
|
76
66
|
end
|
77
67
|
|
78
|
-
it
|
79
|
-
message =
|
68
|
+
it 'prints to log file' do
|
69
|
+
message = 'Testing GeoRedirect logger'
|
80
70
|
@app.send(:log, [message])
|
81
71
|
log_should_include(message)
|
82
72
|
end
|
83
73
|
end
|
84
74
|
|
85
|
-
it
|
86
|
-
mock_app :
|
87
|
-
@app.instance_variable_get(:"@logger").
|
75
|
+
it 'ignores invalid logfile path' do
|
76
|
+
mock_app logfile: '/no_such_file'
|
77
|
+
expect(@app.instance_variable_get(:"@logger")).to be_nil
|
88
78
|
end
|
89
79
|
end
|
90
80
|
|
91
|
-
describe
|
92
|
-
before
|
93
|
-
|
94
|
-
end
|
81
|
+
describe '#host_by_country' do
|
82
|
+
before { mock_app }
|
83
|
+
subject { @app.host_by_country(country) }
|
95
84
|
|
96
|
-
|
97
|
-
|
98
|
-
|
85
|
+
context 'when country is valid' do
|
86
|
+
let(:country) { 'US' }
|
87
|
+
it { is_expected.to eq(:us) }
|
99
88
|
end
|
100
89
|
|
101
|
-
|
102
|
-
|
90
|
+
context 'when country is invalid' do
|
91
|
+
let(:country) { 'WHATEVER' }
|
92
|
+
it { is_expected.to eq(:default) }
|
103
93
|
end
|
104
94
|
end
|
105
95
|
|
106
|
-
describe
|
107
|
-
before
|
108
|
-
|
109
|
-
end
|
96
|
+
describe 'host_by_hostname' do
|
97
|
+
before { mock_app }
|
98
|
+
subject { @app.host_by_hostname(hostname) }
|
110
99
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
@app.host_by_hostname("biz.world.waze.com").should eq(:world)
|
100
|
+
context 'when hostname is valid' do
|
101
|
+
let(:hostname) { 'biz.waze.co.il' }
|
102
|
+
it { is_expected.to eq(:il) }
|
115
103
|
end
|
116
104
|
|
117
|
-
|
118
|
-
|
105
|
+
context 'when hostname is invalid' do
|
106
|
+
let(:hostname) { 'something.else.org' }
|
107
|
+
it { is_expected.to eq(:default) }
|
119
108
|
end
|
120
109
|
end
|
121
110
|
|
122
|
-
describe
|
111
|
+
describe 'redirect logic' do
|
123
112
|
before :each do
|
124
113
|
mock_app
|
125
114
|
end
|
126
115
|
|
127
|
-
def mock_request_from(code, options={})
|
128
|
-
ip =
|
116
|
+
def mock_request_from(code, options = {})
|
117
|
+
ip = '5.5.5.5'
|
129
118
|
|
130
119
|
if code.nil?
|
131
120
|
country = nil
|
132
121
|
else
|
133
|
-
country = GeoIP::Country.stub(
|
134
|
-
|
122
|
+
country = GeoIP::Country.stub(country_code2: code,
|
123
|
+
country_code: 5)
|
135
124
|
end
|
136
125
|
@app.db.stub(:country).with(ip).and_return(country)
|
137
126
|
|
138
|
-
env = {
|
127
|
+
env = { 'REMOTE_ADDR' => ip, 'HTTP_HOST' => 'biz.waze.co.il' }
|
139
128
|
|
140
129
|
if options[:session]
|
141
130
|
env['rack.session'] ||= {}
|
@@ -144,23 +133,22 @@ describe "geo_redirect" do
|
|
144
133
|
end
|
145
134
|
|
146
135
|
args = {}
|
147
|
-
if options[:force]
|
148
|
-
|
149
|
-
end
|
136
|
+
args[:redirect] = 1 if options[:force]
|
137
|
+
args[:skip_geo] = true if options[:skip]
|
150
138
|
|
151
|
-
get
|
139
|
+
get '/', args, env
|
152
140
|
end
|
153
141
|
|
154
142
|
def should_redirect_to(host)
|
155
|
-
last_response.body.should include(
|
143
|
+
last_response.body.should include('Moved Permanently')
|
156
144
|
last_response.status.should eq(301)
|
157
|
-
last_response.headers.should have_key(
|
158
|
-
url = "#{url_scheme}://#{
|
159
|
-
last_response.headers[
|
145
|
+
last_response.headers.should have_key('Location')
|
146
|
+
url = "#{url_scheme}://#{config[host][:host]}"
|
147
|
+
last_response.headers['Location'].should start_with(url)
|
160
148
|
end
|
161
149
|
|
162
150
|
def should_not_redirect
|
163
|
-
last_response.body.should include(
|
151
|
+
last_response.body.should include('Hello world!')
|
164
152
|
last_response.should be_ok
|
165
153
|
end
|
166
154
|
|
@@ -172,149 +160,82 @@ describe "geo_redirect" do
|
|
172
160
|
session['geo_redirect.country'].should eq(country)
|
173
161
|
end
|
174
162
|
|
175
|
-
describe
|
176
|
-
describe
|
177
|
-
before
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
it "redirects to destination" do
|
182
|
-
should_redirect_to :us
|
183
|
-
end
|
184
|
-
|
185
|
-
it "stores decision in session" do
|
186
|
-
should_remember :us
|
187
|
-
end
|
188
|
-
|
189
|
-
it "stores discovered country in session" do
|
190
|
-
should_remember_country "US"
|
191
|
-
end
|
163
|
+
describe 'without session memory' do
|
164
|
+
describe 'for a foreign source' do
|
165
|
+
before { mock_request_from 'US' }
|
166
|
+
it { should_redirect_to :us }
|
167
|
+
it { should_remember :us }
|
168
|
+
it { should_remember_country 'US' }
|
192
169
|
end
|
193
170
|
|
194
|
-
describe
|
195
|
-
before
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
it "does not redirect" do
|
200
|
-
should_not_redirect
|
201
|
-
end
|
202
|
-
|
203
|
-
it "stores decision in session" do
|
204
|
-
should_remember :il
|
205
|
-
end
|
206
|
-
|
207
|
-
it "stores discovered country in session" do
|
208
|
-
should_remember_country "IL"
|
209
|
-
end
|
171
|
+
describe 'for a local source' do
|
172
|
+
before { mock_request_from 'IL' }
|
173
|
+
it { should_not_redirect }
|
174
|
+
it { should_remember :il }
|
175
|
+
it { should_remember_country 'IL' }
|
210
176
|
end
|
211
177
|
|
212
|
-
describe
|
213
|
-
before
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
it "redirects to default" do
|
218
|
-
should_redirect_to :default
|
219
|
-
end
|
220
|
-
|
221
|
-
it "stores decision in session" do
|
222
|
-
should_remember :default
|
223
|
-
end
|
224
|
-
|
225
|
-
it "stores discovered country in session" do
|
226
|
-
should_remember_country "SOMEWHERE OVER THE RAINBOW"
|
227
|
-
end
|
178
|
+
describe 'for an unknown source' do
|
179
|
+
before { mock_request_from 'SOMEWHERE OVER THE RAINBOW' }
|
180
|
+
it { should_redirect_to :default }
|
181
|
+
it { should_remember :default }
|
182
|
+
it { should_remember_country 'SOMEWHERE OVER THE RAINBOW' }
|
228
183
|
end
|
229
184
|
end
|
230
185
|
|
231
|
-
describe
|
232
|
-
before :
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
it "redirects to remembered destination" do
|
237
|
-
should_redirect_to :default
|
238
|
-
end
|
239
|
-
|
240
|
-
it "leaves session as is" do
|
241
|
-
should_remember :default
|
242
|
-
end
|
243
|
-
|
244
|
-
it "remembers discovered country" do
|
245
|
-
should_remember_country "US"
|
246
|
-
end
|
186
|
+
describe 'with valid session memory' do
|
187
|
+
before { mock_request_from 'US', session: :default }
|
188
|
+
it { should_redirect_to :default }
|
189
|
+
it { should_remember :default }
|
190
|
+
it { should_remember_country 'US' }
|
247
191
|
end
|
248
192
|
|
249
|
-
describe
|
250
|
-
before :
|
251
|
-
mock_request_from "US", :session => "foo"
|
252
|
-
end
|
253
|
-
|
254
|
-
it "removes invalid session data" do
|
255
|
-
session['geo_redirect'].should_not eq("foo")
|
256
|
-
end
|
257
|
-
|
258
|
-
it "redirects to destination" do
|
259
|
-
should_redirect_to :us
|
260
|
-
end
|
193
|
+
describe 'with invalid session memory' do
|
194
|
+
before { mock_request_from 'US', session: 'foo' }
|
261
195
|
|
262
|
-
it
|
263
|
-
|
196
|
+
it 'removes invalid session data' do
|
197
|
+
expect(session['geo_redirect']).not_to eq('foo')
|
264
198
|
end
|
265
199
|
|
266
|
-
it
|
267
|
-
|
268
|
-
|
200
|
+
it { should_redirect_to :us }
|
201
|
+
it { should_remember :us }
|
202
|
+
it { should_remember_country 'US' }
|
269
203
|
end
|
270
204
|
|
271
|
-
describe
|
272
|
-
before :
|
273
|
-
mock_request_from "US", :force => true
|
274
|
-
end
|
205
|
+
describe 'with forced redirect flag' do
|
206
|
+
before { mock_request_from 'US', force: true }
|
275
207
|
|
276
|
-
it
|
277
|
-
|
278
|
-
last_response.headers[
|
208
|
+
it { should_redirect_to :il }
|
209
|
+
it 'rewrites the flag out' do
|
210
|
+
expect(last_response.headers['Location']).not_to include('redirect=1')
|
279
211
|
end
|
280
212
|
|
281
|
-
it
|
282
|
-
|
283
|
-
end
|
284
|
-
|
285
|
-
it "does not store discovered country in session" do
|
286
|
-
should_remember_country nil
|
287
|
-
end
|
213
|
+
it { should_remember :il }
|
214
|
+
it { should_remember_country nil }
|
288
215
|
end
|
289
216
|
|
290
|
-
describe
|
291
|
-
before :
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
it "does not redirect" do
|
296
|
-
should_not_redirect
|
297
|
-
end
|
298
|
-
|
299
|
-
it "does not store session" do
|
300
|
-
should_remember nil
|
301
|
-
end
|
302
|
-
|
303
|
-
it "does not store discovered country in session" do
|
304
|
-
should_remember_country nil
|
305
|
-
end
|
217
|
+
describe 'with skip flag' do
|
218
|
+
before { mock_request_from 'US', skip: true }
|
219
|
+
it { should_not_redirect }
|
220
|
+
it { should_remember nil }
|
221
|
+
it { should_remember_country nil }
|
306
222
|
end
|
307
223
|
|
224
|
+
describe 'with no recognizable IP' do
|
225
|
+
before { mock_request_from nil }
|
226
|
+
it { should_not_redirect }
|
227
|
+
it { should_remember nil }
|
228
|
+
it { should_remember_country nil }
|
229
|
+
end
|
308
230
|
end
|
309
231
|
end
|
310
232
|
|
311
|
-
describe
|
312
|
-
include_context
|
233
|
+
describe 'geo_redirect:fetch_db' do
|
234
|
+
include_context 'rake'
|
313
235
|
|
314
|
-
it
|
315
|
-
|
316
|
-
subject.invoke(
|
317
|
-
|
318
|
-
@dbfile.size.should be > 0
|
236
|
+
it 'downloads a GeoIP db to a location' do
|
237
|
+
dbfile = Tempfile.new('db')
|
238
|
+
subject.invoke(dbfile.path)
|
239
|
+
expect(dbfile.size).to be > 0
|
319
240
|
end
|
320
241
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
|
-
require
|
1
|
+
require 'simplecov'
|
2
2
|
SimpleCov.start
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
4
|
+
require 'rspec'
|
5
|
+
require 'rack'
|
6
|
+
require 'rack/test'
|
7
7
|
|
8
8
|
current_dir = File.dirname(__FILE__)
|
9
9
|
$LOAD_PATH.unshift(File.join(current_dir, '..', 'lib'))
|
10
10
|
$LOAD_PATH.unshift(current_dir)
|
11
|
-
require
|
11
|
+
require 'geo_redirect'
|
12
12
|
|
13
|
-
Dir[File.join(current_dir,
|
13
|
+
Dir[File.join(current_dir, 'support/**/*.rb')].each { |f| require f }
|
14
14
|
|
15
15
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
16
16
|
RSpec.configure do |config|
|
@@ -18,5 +18,5 @@ RSpec.configure do |config|
|
|
18
18
|
config.run_all_when_everything_filtered = true
|
19
19
|
config.filter_run :focus
|
20
20
|
|
21
|
-
config.order =
|
21
|
+
config.order = 'random'
|
22
22
|
end
|
@@ -5,7 +5,7 @@ module GeoRedirect
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def nonexisting_file_path
|
8
|
-
|
8
|
+
'/no_such_file'
|
9
9
|
end
|
10
10
|
|
11
11
|
def app
|
@@ -14,16 +14,16 @@ module GeoRedirect
|
|
14
14
|
|
15
15
|
def mock_app(options = {})
|
16
16
|
# Simple HTTP server that always returns 'Hello world!'
|
17
|
-
main_app = lambda
|
17
|
+
main_app = lambda do |env|
|
18
18
|
Rack::Request.new(env)
|
19
|
-
headers = {
|
20
|
-
[200, headers, [
|
21
|
-
|
19
|
+
headers = { 'Content-Type' => 'text/html' }
|
20
|
+
[200, headers, ['Hello world!']]
|
21
|
+
end
|
22
22
|
|
23
|
-
@logfile = Tempfile.new(
|
24
|
-
options = { :
|
25
|
-
:
|
26
|
-
:
|
23
|
+
@logfile = Tempfile.new('log')
|
24
|
+
options = { config: fixture_path('config.yml'),
|
25
|
+
db: fixture_path('GeoIP.dat'),
|
26
|
+
logfile: @logfile.path
|
27
27
|
}.merge(options)
|
28
28
|
|
29
29
|
builder = Rack::Builder.new
|
@@ -38,4 +38,3 @@ module GeoRedirect
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
41
|
-
|
data/spec/support/rake.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
# thanks http://robots.thoughtbot.com/post/11957424161/test-rake-tasks-like-a-boss
|
2
2
|
|
3
|
-
require
|
3
|
+
require 'rake'
|
4
4
|
|
5
|
-
shared_context
|
5
|
+
shared_context 'rake' do
|
6
6
|
let(:rake) { Rake::Application.new }
|
7
7
|
let(:task_name) { self.class.top_level_description }
|
8
|
-
let(:task_path) { "lib/tasks/#{task_name.split(
|
8
|
+
let(:task_path) { "lib/tasks/#{task_name.split(':').first}" }
|
9
9
|
subject { rake[task_name] }
|
10
10
|
|
11
11
|
before do
|
12
12
|
Rake.application = rake
|
13
|
-
Rake.application
|
14
|
-
[File.join(File.dirname(__FILE__),
|
13
|
+
Rake.application
|
14
|
+
.rake_require(task_path, [File.join(File.dirname(__FILE__), '..', '..')])
|
15
15
|
|
16
16
|
Rake::Task.define_task(:environment)
|
17
17
|
end
|
metadata
CHANGED
@@ -1,128 +1,99 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: geo_redirect
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
5
|
-
prerelease:
|
4
|
+
version: '0.4'
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Sagie Maoz
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2015-01-12 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: rake
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - '>='
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: '0'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- -
|
24
|
+
- - '>='
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '0'
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: geoip
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- -
|
31
|
+
- - '>='
|
36
32
|
- !ruby/object:Gem::Version
|
37
33
|
version: '0'
|
38
34
|
type: :runtime
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- -
|
38
|
+
- - '>='
|
44
39
|
- !ruby/object:Gem::Version
|
45
40
|
version: '0'
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: rspec
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
45
|
- - ~>
|
52
46
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
47
|
+
version: 3.1.0
|
54
48
|
type: :development
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
52
|
- - ~>
|
60
53
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
54
|
+
version: 3.1.0
|
62
55
|
- !ruby/object:Gem::Dependency
|
63
56
|
name: rack
|
64
57
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
58
|
requirements:
|
67
59
|
- - ~>
|
68
60
|
- !ruby/object:Gem::Version
|
69
|
-
version: 1.
|
61
|
+
version: 1.6.0
|
70
62
|
type: :development
|
71
63
|
prerelease: false
|
72
64
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
65
|
requirements:
|
75
66
|
- - ~>
|
76
67
|
- !ruby/object:Gem::Version
|
77
|
-
version: 1.
|
68
|
+
version: 1.6.0
|
78
69
|
- !ruby/object:Gem::Dependency
|
79
70
|
name: rack-test
|
80
71
|
requirement: !ruby/object:Gem::Requirement
|
81
|
-
none: false
|
82
72
|
requirements:
|
83
73
|
- - ~>
|
84
74
|
- !ruby/object:Gem::Version
|
85
|
-
version: 0.6.
|
75
|
+
version: 0.6.3
|
86
76
|
type: :development
|
87
77
|
prerelease: false
|
88
78
|
version_requirements: !ruby/object:Gem::Requirement
|
89
|
-
none: false
|
90
79
|
requirements:
|
91
80
|
- - ~>
|
92
81
|
- !ruby/object:Gem::Version
|
93
|
-
version: 0.6.
|
94
|
-
- !ruby/object:Gem::Dependency
|
95
|
-
name: debugger
|
96
|
-
requirement: !ruby/object:Gem::Requirement
|
97
|
-
none: false
|
98
|
-
requirements:
|
99
|
-
- - ~>
|
100
|
-
- !ruby/object:Gem::Version
|
101
|
-
version: 1.5.0
|
102
|
-
type: :development
|
103
|
-
prerelease: false
|
104
|
-
version_requirements: !ruby/object:Gem::Requirement
|
105
|
-
none: false
|
106
|
-
requirements:
|
107
|
-
- - ~>
|
108
|
-
- !ruby/object:Gem::Version
|
109
|
-
version: 1.5.0
|
82
|
+
version: 0.6.3
|
110
83
|
- !ruby/object:Gem::Dependency
|
111
84
|
name: simplecov
|
112
85
|
requirement: !ruby/object:Gem::Requirement
|
113
|
-
none: false
|
114
86
|
requirements:
|
115
87
|
- - ~>
|
116
88
|
- !ruby/object:Gem::Version
|
117
|
-
version: 0.
|
89
|
+
version: 0.9.1
|
118
90
|
type: :development
|
119
91
|
prerelease: false
|
120
92
|
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
none: false
|
122
93
|
requirements:
|
123
94
|
- - ~>
|
124
95
|
- !ruby/object:Gem::Version
|
125
|
-
version: 0.
|
96
|
+
version: 0.9.1
|
126
97
|
description: Geo-location based redirector
|
127
98
|
email:
|
128
99
|
- sagie@waze.com
|
@@ -132,6 +103,7 @@ extra_rdoc_files: []
|
|
132
103
|
files:
|
133
104
|
- .gitignore
|
134
105
|
- .rspec
|
106
|
+
- .rubocop.yml
|
135
107
|
- .travis.yml
|
136
108
|
- Gemfile
|
137
109
|
- LICENSE.txt
|
@@ -152,33 +124,26 @@ files:
|
|
152
124
|
- spec/support/rake.rb
|
153
125
|
homepage: ''
|
154
126
|
licenses: []
|
127
|
+
metadata: {}
|
155
128
|
post_install_message:
|
156
129
|
rdoc_options: []
|
157
130
|
require_paths:
|
158
131
|
- lib
|
159
132
|
required_ruby_version: !ruby/object:Gem::Requirement
|
160
|
-
none: false
|
161
133
|
requirements:
|
162
|
-
- -
|
134
|
+
- - '>='
|
163
135
|
- !ruby/object:Gem::Version
|
164
136
|
version: '0'
|
165
|
-
segments:
|
166
|
-
- 0
|
167
|
-
hash: 3031125964627560618
|
168
137
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
169
|
-
none: false
|
170
138
|
requirements:
|
171
|
-
- -
|
139
|
+
- - '>='
|
172
140
|
- !ruby/object:Gem::Version
|
173
141
|
version: '0'
|
174
|
-
segments:
|
175
|
-
- 0
|
176
|
-
hash: 3031125964627560618
|
177
142
|
requirements: []
|
178
143
|
rubyforge_project:
|
179
|
-
rubygems_version:
|
144
|
+
rubygems_version: 2.4.5
|
180
145
|
signing_key:
|
181
|
-
specification_version:
|
146
|
+
specification_version: 4
|
182
147
|
summary: Rack middleware to redirect clients to hostnames based on geo-location
|
183
148
|
test_files:
|
184
149
|
- spec/fixtures/GeoIP.dat
|