geo_redirect 0.2.4 → 0.3
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.
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/LICENSE.txt +2 -2
- data/README.md +7 -1
- data/Rakefile +5 -0
- data/geo_redirect.gemspec +7 -0
- data/lib/geo_redirect/middleware.rb +197 -0
- data/lib/geo_redirect/railtie.rb +1 -1
- data/lib/geo_redirect/version.rb +1 -1
- data/lib/geo_redirect.rb +7 -165
- data/lib/tasks/{geo_redirect.rb → geo_redirect.rake} +7 -5
- data/spec/fixtures/GeoIP.dat +0 -0
- data/spec/fixtures/config.bad.yml +7 -0
- data/spec/fixtures/config.yml +12 -0
- data/spec/geo_redirect_spec.rb +320 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/geo_redirect.rb +41 -0
- data/spec/support/rake.rb +18 -0
- metadata +123 -4
data/.rspec
ADDED
data/.travis.yml
ADDED
data/LICENSE.txt
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2012
|
1
|
+
Copyright (c) 2012 Sagie Maoz
|
2
2
|
|
3
3
|
MIT License
|
4
4
|
|
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# Waze GeoRedirect
|
2
|
+
[](http://travis-ci.org/wazeHQ/geo_redirect) [](https://codeclimate.com/github/wazeHQ/geo_redirect) [](http://badge.fury.io/rb/geo_redirect)
|
2
3
|
|
3
4
|
`GeoRedirect` is a Rack middleware that can be configured to
|
4
5
|
redirect incoming clients to different hosts based on their
|
@@ -71,7 +72,7 @@ Note that:
|
|
71
72
|
|
72
73
|
You can download the database file [directly from MaxMind](http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz) and unzip it into `db/` in your project, **or** you could use the following `rake` task designed just for that:
|
73
74
|
|
74
|
-
$ rake
|
75
|
+
$ rake geo_redirect:fetch_db
|
75
76
|
|
76
77
|
It'd be a good idea to use this task on your (Capistrano or whatever) deployment scripts.
|
77
78
|
|
@@ -98,6 +99,11 @@ This is useful when working on your configuration YAML.
|
|
98
99
|
|
99
100
|
`GeoRedirect`'s log messages will always be prefixed with `[GeoRedirect]`.
|
100
101
|
|
102
|
+
### Accessing discovered country
|
103
|
+
|
104
|
+
The country code discovered for the current user is available for your convenience, under `session['geo_redirect.country']`.
|
105
|
+
You can use it to make content decisions, or whatever.
|
106
|
+
|
101
107
|
## Known Issues
|
102
108
|
|
103
109
|
A couple issues I know about but haven't had the time to fix:
|
data/Rakefile
CHANGED
data/geo_redirect.gemspec
CHANGED
@@ -12,8 +12,15 @@ Gem::Specification.new do |gem|
|
|
12
12
|
gem.summary = %q{Rack middleware to redirect clients to hostnames based on geo-location}
|
13
13
|
gem.homepage = ""
|
14
14
|
|
15
|
+
gem.add_dependency "rake"
|
15
16
|
gem.add_dependency "geoip"
|
16
17
|
|
18
|
+
gem.add_development_dependency "rspec", "~> 2.13.0"
|
19
|
+
gem.add_development_dependency "rack", "~> 1.5.2"
|
20
|
+
gem.add_development_dependency "rack-test", "~> 0.6.2"
|
21
|
+
gem.add_development_dependency "debugger", "~> 1.5.0"
|
22
|
+
gem.add_development_dependency "simplecov", "~> 0.7.1"
|
23
|
+
|
17
24
|
gem.files = `git ls-files`.split($/)
|
18
25
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
26
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'geoip'
|
3
|
+
|
4
|
+
module GeoRedirect
|
5
|
+
class Middleware
|
6
|
+
attr_accessor :db, :config
|
7
|
+
|
8
|
+
def initialize(app, options = {})
|
9
|
+
# Some defaults
|
10
|
+
options[:db] ||= DEFAULT_DB_PATH
|
11
|
+
options[:config] ||= DEFAULT_CONFIG_PATH
|
12
|
+
|
13
|
+
@app = app
|
14
|
+
|
15
|
+
@logger = init_logger(options[:logfile]) if options[:logfile]
|
16
|
+
@db = init_db(options[:db])
|
17
|
+
@config = init_config(options[:config])
|
18
|
+
|
19
|
+
self.log "Initialized middleware"
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(env)
|
23
|
+
@request = Rack::Request.new(env)
|
24
|
+
|
25
|
+
if force_redirect?
|
26
|
+
handle_force
|
27
|
+
|
28
|
+
elsif session_exists?
|
29
|
+
handle_session
|
30
|
+
|
31
|
+
else
|
32
|
+
handle_geoip
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def session_exists?
|
37
|
+
host = @request.session['geo_redirect']
|
38
|
+
if host && @config[host].nil? # Invalid var, remove it
|
39
|
+
self.log "Invalid session var, forgetting"
|
40
|
+
forget_host(host)
|
41
|
+
host = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
!host.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def handle_session
|
48
|
+
host = @request.session['geo_redirect']
|
49
|
+
self.log "Handling session var: #{host}"
|
50
|
+
redirect_request(host)
|
51
|
+
end
|
52
|
+
|
53
|
+
def force_redirect?
|
54
|
+
url = URI.parse(@request.url)
|
55
|
+
Rack::Utils.parse_query(url.query).key? 'redirect'
|
56
|
+
end
|
57
|
+
|
58
|
+
def handle_force
|
59
|
+
url = URI.parse(@request.url)
|
60
|
+
host = host_by_hostname(url.host)
|
61
|
+
self.log "Handling force flag: #{host}"
|
62
|
+
remember_host(host)
|
63
|
+
redirect_request(url.host, true)
|
64
|
+
end
|
65
|
+
|
66
|
+
def handle_geoip
|
67
|
+
country = country_from_request rescue nil
|
68
|
+
@request.session['geo_redirect.country'] = country
|
69
|
+
self.log "GeoIP match: country code #{country}"
|
70
|
+
|
71
|
+
unless country.nil?
|
72
|
+
host = host_by_country(country) # desired host
|
73
|
+
self.log "GeoIP host match: #{host}"
|
74
|
+
remember_host(host)
|
75
|
+
|
76
|
+
redirect_request(host)
|
77
|
+
else
|
78
|
+
@app.call(@request.env)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def redirect_request(host=nil, same_host=false)
|
83
|
+
hostname = hostname_by_host(host)
|
84
|
+
|
85
|
+
if should_redirect?(hostname, same_host)
|
86
|
+
url = redirect_url(hostname)
|
87
|
+
|
88
|
+
self.log "Redirecting to #{url}"
|
89
|
+
[301,
|
90
|
+
{'Location' => url.to_s, 'Content-Type' => 'text/plain'},
|
91
|
+
['Moved Permanently\n']]
|
92
|
+
else
|
93
|
+
@app.call(@request.env)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def host_by_country(country)
|
98
|
+
hosts = @config.select { |k, v| Array(v[:countries]).include?(country) }
|
99
|
+
hosts.keys.first || :default
|
100
|
+
end
|
101
|
+
|
102
|
+
def host_by_hostname(hostname)
|
103
|
+
hosts = @config.select { |k, v| v[:host] == hostname }
|
104
|
+
hosts.keys.first || :default
|
105
|
+
end
|
106
|
+
|
107
|
+
def hostname_by_host(host)
|
108
|
+
host.is_a?(Symbol) ? @config[host][:host] : host
|
109
|
+
end
|
110
|
+
|
111
|
+
def remember_host(host)
|
112
|
+
self.log "Remembering: #{host}"
|
113
|
+
@request.session['geo_redirect'] = host
|
114
|
+
end
|
115
|
+
|
116
|
+
def forget_host(host)
|
117
|
+
self.log "Forgetting: #{host}"
|
118
|
+
remember_host(nil)
|
119
|
+
end
|
120
|
+
|
121
|
+
protected
|
122
|
+
def log(message, level=:debug)
|
123
|
+
@logger.send(level, "[GeoRedirect] #{message}") unless @logger.nil?
|
124
|
+
end
|
125
|
+
|
126
|
+
def init_logger(path)
|
127
|
+
Logger.new(path)
|
128
|
+
rescue Errno::EINVAL, Errno::EACCES
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
|
132
|
+
def init_db(path)
|
133
|
+
GeoIP.new(path)
|
134
|
+
rescue Errno::EINVAL, Errno::ENOENT
|
135
|
+
message = <<-ERROR
|
136
|
+
Could not load GeoIP database file.
|
137
|
+
Please make sure you have a valid one and add its name to the GeoRedirect middleware.
|
138
|
+
Alternatively, use `rake georedirect:fetch_db` to fetch it to the default location (under db/).
|
139
|
+
ERROR
|
140
|
+
self.log(message, :error)
|
141
|
+
end
|
142
|
+
|
143
|
+
def init_config(path)
|
144
|
+
YAML.load_file(path) || raise(Errno::EINVAL)
|
145
|
+
rescue Errno::EINVAL, Errno::ENOENT, Psych::SyntaxError, SyntaxError
|
146
|
+
message = <<-ERROR
|
147
|
+
Could not load GeoRedirect config YML file.
|
148
|
+
Please make sure you have a valid YML file and pass its name when adding the GeoRedirect middlware.
|
149
|
+
ERROR
|
150
|
+
self.log(message, :error)
|
151
|
+
end
|
152
|
+
|
153
|
+
def request_ip
|
154
|
+
ip_address = @request.env['HTTP_X_FORWARDED_FOR'] || @request.env['REMOTE_ADDR']
|
155
|
+
# take only the first given ip
|
156
|
+
ip_address.split(',').first.strip
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
def country_from_request
|
161
|
+
ip = request_ip
|
162
|
+
self.log "Handling GeoIP lookup: IP #{ip}"
|
163
|
+
|
164
|
+
res = @db.country(ip)
|
165
|
+
code = res[:country_code]
|
166
|
+
|
167
|
+
res[:country_code2] unless code.nil? || code.zero?
|
168
|
+
end
|
169
|
+
|
170
|
+
def redirect_url(hostname)
|
171
|
+
url = URI.parse(@request.url)
|
172
|
+
url.port = nil
|
173
|
+
url.host = hostname if hostname
|
174
|
+
|
175
|
+
# Remove force flag from GET arguments
|
176
|
+
query_hash = Rack::Utils.parse_query(url.query).tap{ |u|
|
177
|
+
u.delete('redirect')
|
178
|
+
}
|
179
|
+
|
180
|
+
# Copy query
|
181
|
+
url.query = URI.encode_www_form(query_hash)
|
182
|
+
url.query = nil if url.query.empty?
|
183
|
+
|
184
|
+
url
|
185
|
+
end
|
186
|
+
|
187
|
+
def should_redirect?(hostname, same_host)
|
188
|
+
unless hostname.nil? || same_host
|
189
|
+
hostname_ends_with = %r{#{hostname.gsub(".", "\.")}$}
|
190
|
+
(@request.host =~ hostname_ends_with).nil?
|
191
|
+
else
|
192
|
+
true
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
data/lib/geo_redirect/railtie.rb
CHANGED
data/lib/geo_redirect/version.rb
CHANGED
data/lib/geo_redirect.rb
CHANGED
@@ -1,171 +1,13 @@
|
|
1
|
-
require '
|
2
|
-
require 'geoip'
|
1
|
+
require 'geo_redirect/middleware'
|
3
2
|
require 'geo_redirect/version'
|
4
3
|
|
5
4
|
module GeoRedirect
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
# Rack middleware
|
10
|
-
class Middleware
|
11
|
-
def initialize(app, options = {})
|
12
|
-
# Some defaults
|
13
|
-
options[:db] ||= 'db/GeoIP.dat'
|
14
|
-
options[:config] ||= 'config/geo_redirect.yml'
|
15
|
-
@logfile = options[:logfile] || nil
|
16
|
-
|
17
|
-
@app = app
|
18
|
-
|
19
|
-
# Load GeoIP database
|
20
|
-
begin
|
21
|
-
@db = GeoIP.new(options[:db])
|
22
|
-
rescue Errno::EINVAL, Errno::ENOENT => e
|
23
|
-
puts "Could not load GeoIP database file."
|
24
|
-
puts "Please make sure you have a valid one and"
|
25
|
-
puts "add its name to the GeoRedirect middleware."
|
26
|
-
puts "Alternatively, use `rake georedirect:fetch_db`"
|
27
|
-
puts "to fetch it to the default location (under db/)."
|
28
|
-
raise e
|
29
|
-
end
|
30
|
-
|
31
|
-
# Load config object
|
32
|
-
begin
|
33
|
-
@config = YAML.load_file('config/geo_redirect.yml')
|
34
|
-
raise Errno::EINVAL unless @config
|
35
|
-
rescue Errno::EINVAL, Errno::ENOENT => e
|
36
|
-
puts "Could not load GeoRedirect config YML file."
|
37
|
-
puts "Please make sure you have a valid YML file"
|
38
|
-
puts "and pass its name when adding the"
|
39
|
-
puts "GeoRedirect middlware."
|
40
|
-
raise e
|
41
|
-
end
|
42
|
-
|
43
|
-
self.log "Initialized middleware"
|
44
|
-
end
|
45
|
-
|
46
|
-
def call(env)
|
47
|
-
@request = Rack::Request.new(env)
|
48
|
-
|
49
|
-
if force_redirect?
|
50
|
-
handle_force
|
51
|
-
|
52
|
-
elsif session_exists?
|
53
|
-
handle_session
|
54
|
-
|
55
|
-
else
|
56
|
-
handle_geoip
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def session_exists?
|
61
|
-
host = @request.session['geo_redirect']
|
62
|
-
if host.present? && @config[host].nil? # Invalid var, remove it
|
63
|
-
self.log "Invalid session var, forgetting"
|
64
|
-
forget_host
|
65
|
-
host = nil
|
66
|
-
end
|
67
|
-
|
68
|
-
host.present?
|
69
|
-
end
|
70
|
-
|
71
|
-
def handle_session
|
72
|
-
host = @request.session['geo_redirect']
|
73
|
-
self.log "Handling session var: #{host}"
|
74
|
-
redirect_request(host)
|
75
|
-
end
|
76
|
-
|
77
|
-
def force_redirect?
|
78
|
-
url = URI.parse(@request.url)
|
79
|
-
Rack::Utils.parse_query(url.query).key? 'redirect'
|
80
|
-
end
|
5
|
+
DEFAULT_DB_PATH = 'db/GeoIP.dat'
|
6
|
+
DEFAULT_CONFIG_PATH = 'config/geo_redirect.yml'
|
81
7
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
self.log "Handling force flag: #{host}"
|
86
|
-
remember_host(host)
|
87
|
-
redirect_request(url.host, true)
|
88
|
-
end
|
89
|
-
|
90
|
-
def handle_geoip
|
91
|
-
# Fetch country code
|
92
|
-
begin
|
93
|
-
ip_address = @request.env['HTTP_X_FORWARDED_FOR'] || @request.env['REMOTE_ADDR']
|
94
|
-
ip_address = ip_address.split(',').first.chop # take only the first given ip
|
95
|
-
self.log "Handling GeoIP lookup: IP #{ip_address}"
|
96
|
-
res = @db.country(ip_address)
|
97
|
-
code = res.try(:country_code)
|
98
|
-
country = res.try(:country_code2) unless code.nil? || code.zero?
|
99
|
-
rescue
|
100
|
-
country = nil
|
101
|
-
end
|
102
|
-
|
103
|
-
self.log "GeoIP match: country code #{country}"
|
104
|
-
|
105
|
-
unless country.nil?
|
106
|
-
host = host_by_country(country) # desired host
|
107
|
-
self.log "GeoIP host match: #{host}"
|
108
|
-
remember_host(host)
|
109
|
-
|
110
|
-
redirect_request(host)
|
111
|
-
else
|
112
|
-
@app.call(@request.env)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def redirect_request(host=nil, same_host=false)
|
117
|
-
redirect = true
|
118
|
-
unless host.nil?
|
119
|
-
hostname = host.is_a?(Symbol) ? @config[host][:host] : host
|
120
|
-
redirect = hostname.present?
|
121
|
-
redirect &&= !@request.host.ends_with?(hostname) unless same_host
|
122
|
-
end
|
123
|
-
|
124
|
-
if redirect
|
125
|
-
url = URI.parse(@request.url)
|
126
|
-
url.port = nil
|
127
|
-
url.host = hostname if host
|
128
|
-
# Remove 'redirect' GET arg
|
129
|
-
url.query = Rack::Utils.parse_query(url.query).tap{ |u|
|
130
|
-
u.delete('redirect')
|
131
|
-
}.to_param
|
132
|
-
url.query = nil if url.query.empty?
|
133
|
-
|
134
|
-
self.log "Redirecting to #{url}"
|
135
|
-
[301,
|
136
|
-
{'Location' => url.to_s, 'Content-Type' => 'text/plain'},
|
137
|
-
['Moved Permanently\n']]
|
138
|
-
else
|
139
|
-
@app.call(@request.env)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def host_by_country(country)
|
144
|
-
hosts = @config.select { |k, v| Array(v[:countries]).include?(country) }
|
145
|
-
hosts.keys.first || :default
|
146
|
-
end
|
147
|
-
|
148
|
-
def host_by_hostname(hostname)
|
149
|
-
hosts = @config.select { |k, v| v[:host] == hostname }
|
150
|
-
hosts.keys.first || :default
|
151
|
-
end
|
152
|
-
|
153
|
-
def remember_host(host)
|
154
|
-
self.log "Remembering: #{host}"
|
155
|
-
@request.session['geo_redirect'] = host
|
156
|
-
end
|
157
|
-
|
158
|
-
def forget_host
|
159
|
-
self.log "Forgetting: #{host}"
|
160
|
-
remember_host(nil)
|
161
|
-
end
|
162
|
-
|
163
|
-
protected
|
164
|
-
def log(message)
|
165
|
-
unless @logfile.nil?
|
166
|
-
@logger ||= Logger.new(@logfile)
|
167
|
-
@logger.debug("[GeoRedirect] #{message}")
|
168
|
-
end
|
169
|
-
end
|
8
|
+
# Load rake tasks
|
9
|
+
if defined? Rails
|
10
|
+
require 'geo_redirect/railtie'
|
170
11
|
end
|
12
|
+
|
171
13
|
end
|
@@ -1,11 +1,14 @@
|
|
1
|
-
require '
|
1
|
+
require 'geo_redirect'
|
2
2
|
require 'open-uri'
|
3
|
+
require 'zlib'
|
3
4
|
|
4
|
-
namespace :
|
5
|
+
namespace :geo_redirect do
|
5
6
|
DB_URI = 'http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz'
|
6
7
|
|
7
8
|
desc "Fetches an updated copy of the GeoIP countries DB from MaxMind"
|
8
|
-
task :fetch_db do
|
9
|
+
task :fetch_db, :db_path do |t, args|
|
10
|
+
args.with_defaults(:db_path => GeoRedirect::DEFAULT_DB_PATH)
|
11
|
+
|
9
12
|
# Fetches DB copy and gunzips it
|
10
13
|
# Thx http://stackoverflow.com/a/2014317/107085
|
11
14
|
source = open(DB_URI)
|
@@ -13,7 +16,6 @@ namespace :georedirect do
|
|
13
16
|
result = gz.read
|
14
17
|
|
15
18
|
# Write to file
|
16
|
-
|
17
|
-
File.open(filename, 'w') { |f| f.write(result) }
|
19
|
+
File.open(args[:db_path], 'w') { |f| f.write(result) }
|
18
20
|
end
|
19
21
|
end
|
Binary file
|
@@ -0,0 +1,320 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "tempfile"
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
describe "geo_redirect" do
|
6
|
+
include GeoRedirect::Support
|
7
|
+
include Rack::Test::Methods
|
8
|
+
|
9
|
+
def session
|
10
|
+
last_request.env['rack.session']
|
11
|
+
end
|
12
|
+
|
13
|
+
def url_scheme
|
14
|
+
last_request.env['rack.url_scheme']
|
15
|
+
end
|
16
|
+
|
17
|
+
before :each do
|
18
|
+
@config = YAML.load_file(fixture_path("config.yml"))
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#load_config" do
|
22
|
+
it "reads a config file" do
|
23
|
+
mock_app
|
24
|
+
|
25
|
+
@app.config.should_not be_nil
|
26
|
+
@app.config.should eq(@config)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "errors on not-found config file" do
|
30
|
+
mock_app :config => nonexisting_file_path
|
31
|
+
log_should_include("ERROR")
|
32
|
+
log_should_include("Could not load GeoRedirect config YML file")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "errors on a mal-formatted config file" do
|
36
|
+
mock_app :config => fixture_path("config.bad.yml")
|
37
|
+
log_should_include("ERROR")
|
38
|
+
log_should_include("Could not load GeoRedirect config YML file")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#load_db" do
|
43
|
+
it "reads a db file" do
|
44
|
+
mock_app
|
45
|
+
|
46
|
+
@app.db.should_not be_nil
|
47
|
+
@app.db.should be_a_kind_of GeoIP
|
48
|
+
end
|
49
|
+
|
50
|
+
it "errors on not-found db file" do
|
51
|
+
mock_app :db => nonexisting_file_path
|
52
|
+
log_should_include("ERROR")
|
53
|
+
log_should_include("Could not load GeoIP database file.")
|
54
|
+
end
|
55
|
+
|
56
|
+
# this example is disabled, as it seems that
|
57
|
+
# GeoIP does not let me know if a db file is proper.
|
58
|
+
=begin
|
59
|
+
it "errors on mal-formatted db file" do
|
60
|
+
pending "GeoIP does not raise on bad files"
|
61
|
+
mock_app :db => fixture_path("config.yml")
|
62
|
+
log_should_include("ERROR")
|
63
|
+
log_should_include("Could not load GeoIP database file.")
|
64
|
+
end
|
65
|
+
=end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#log" do
|
69
|
+
describe "with valid logfile path" do
|
70
|
+
before :each do
|
71
|
+
mock_app
|
72
|
+
end
|
73
|
+
|
74
|
+
it "initiates a log file" do
|
75
|
+
@app.instance_variable_get(:"@logger").should be_kind_of Logger
|
76
|
+
end
|
77
|
+
|
78
|
+
it "prints to log file" do
|
79
|
+
message = "Testing GeoRedirect logger"
|
80
|
+
@app.send(:log, [message])
|
81
|
+
log_should_include(message)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "ignores invalid logfile path" do
|
86
|
+
mock_app :logfile => '/no_such_file'
|
87
|
+
@app.instance_variable_get(:"@logger").should be_nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#host_by_country" do
|
92
|
+
before :each do
|
93
|
+
mock_app
|
94
|
+
end
|
95
|
+
|
96
|
+
it "fetches host by country" do
|
97
|
+
@app.host_by_country("US").should eq(:us)
|
98
|
+
@app.host_by_country("IL").should eq(:il)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "falls back to default" do
|
102
|
+
@app.host_by_country(:foo).should eq(:default)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "host_by_hostname" do
|
107
|
+
before :each do
|
108
|
+
mock_app
|
109
|
+
end
|
110
|
+
|
111
|
+
it "fetches host by hostname" do
|
112
|
+
@app.host_by_hostname("biz.waze.com").should eq(:us)
|
113
|
+
@app.host_by_hostname("biz.waze.co.il").should eq(:il)
|
114
|
+
@app.host_by_hostname("biz.world.waze.com").should eq(:world)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "falls back to default" do
|
118
|
+
@app.host_by_hostname("foo").should eq(:default)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "redirect logic" do
|
123
|
+
before :each do
|
124
|
+
mock_app
|
125
|
+
end
|
126
|
+
|
127
|
+
def mock_request_from(code, options={})
|
128
|
+
ip = "5.5.5.5"
|
129
|
+
|
130
|
+
if code.nil?
|
131
|
+
country = nil
|
132
|
+
else
|
133
|
+
country = GeoIP::Country.stub({ :country_code2 => code,
|
134
|
+
:country_code => 5 })
|
135
|
+
end
|
136
|
+
@app.db.stub(:country).with(ip).and_return(country)
|
137
|
+
|
138
|
+
env = { "REMOTE_ADDR" => ip, "HTTP_HOST" => "biz.waze.co.il" }
|
139
|
+
|
140
|
+
if options[:session]
|
141
|
+
env['rack.session'] ||= {}
|
142
|
+
env['rack.session']['geo_redirect'] = options[:session]
|
143
|
+
env['rack.session']['geo_redirect.country'] = code
|
144
|
+
end
|
145
|
+
|
146
|
+
args = {}
|
147
|
+
if options[:force]
|
148
|
+
args[:redirect] = 1
|
149
|
+
end
|
150
|
+
|
151
|
+
get "/", args, env
|
152
|
+
end
|
153
|
+
|
154
|
+
def should_redirect_to(host)
|
155
|
+
last_response.body.should include("Moved Permanently")
|
156
|
+
last_response.status.should eq(301)
|
157
|
+
last_response.headers.should have_key("Location")
|
158
|
+
url = "#{url_scheme}://#{@config[host][:host]}"
|
159
|
+
last_response.headers["Location"].should start_with(url)
|
160
|
+
end
|
161
|
+
|
162
|
+
def should_not_redirect
|
163
|
+
last_response.body.should include("Hello world!")
|
164
|
+
last_response.should be_ok
|
165
|
+
end
|
166
|
+
|
167
|
+
def should_remember(host)
|
168
|
+
session['geo_redirect'].should eq(host)
|
169
|
+
end
|
170
|
+
|
171
|
+
def should_remember_country(country)
|
172
|
+
session['geo_redirect.country'].should eq(country)
|
173
|
+
end
|
174
|
+
|
175
|
+
describe "without session memory" do
|
176
|
+
describe "for a foreign source" do
|
177
|
+
before :each do
|
178
|
+
mock_request_from "US"
|
179
|
+
end
|
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
|
192
|
+
end
|
193
|
+
|
194
|
+
describe "for a local source" do
|
195
|
+
before :each do
|
196
|
+
mock_request_from "IL"
|
197
|
+
end
|
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
|
210
|
+
end
|
211
|
+
|
212
|
+
describe "for an unknown source" do
|
213
|
+
before :each do
|
214
|
+
mock_request_from "SOMEWHERE OVER THE RAINBOW"
|
215
|
+
end
|
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
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe "with valid session memory" do
|
232
|
+
before :each do
|
233
|
+
mock_request_from "US", :session => :default
|
234
|
+
end
|
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
|
247
|
+
end
|
248
|
+
|
249
|
+
describe "with invalid session memory" do
|
250
|
+
before :each do
|
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
|
261
|
+
|
262
|
+
it "stores decision in session" do
|
263
|
+
should_remember :us
|
264
|
+
end
|
265
|
+
|
266
|
+
it "stores discovered country in session" do
|
267
|
+
should_remember_country "US"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
describe "with forced redirect flag" do
|
272
|
+
before :each do
|
273
|
+
mock_request_from "US", :force => true
|
274
|
+
end
|
275
|
+
|
276
|
+
it "rewrites the flag out" do
|
277
|
+
should_redirect_to :il
|
278
|
+
last_response.headers["Location"].should_not include("redirect=1")
|
279
|
+
end
|
280
|
+
|
281
|
+
it "stores decision in session" do
|
282
|
+
should_remember :il
|
283
|
+
end
|
284
|
+
|
285
|
+
it "does not store discovered country in session" do
|
286
|
+
should_remember_country nil
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
describe "with no recognizable IP" do
|
291
|
+
before :each do
|
292
|
+
mock_request_from nil
|
293
|
+
end
|
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
|
306
|
+
end
|
307
|
+
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe "geo_redirect:fetch_db" do
|
312
|
+
include_context "rake"
|
313
|
+
|
314
|
+
it "downloads a GeoIP db to a location" do
|
315
|
+
@dbfile = Tempfile.new("db")
|
316
|
+
subject.invoke(@dbfile.path)
|
317
|
+
@dbfile.size.should_not be_nil
|
318
|
+
@dbfile.size.should be > 0
|
319
|
+
end
|
320
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "simplecov"
|
2
|
+
SimpleCov.start
|
3
|
+
|
4
|
+
require "rspec"
|
5
|
+
require "rack"
|
6
|
+
require "rack/test"
|
7
|
+
|
8
|
+
current_dir = File.dirname(__FILE__)
|
9
|
+
$LOAD_PATH.unshift(File.join(current_dir, '..', 'lib'))
|
10
|
+
$LOAD_PATH.unshift(current_dir)
|
11
|
+
require "geo_redirect"
|
12
|
+
|
13
|
+
Dir[File.join(current_dir, "support/**/*.rb")].each { |f| require f }
|
14
|
+
|
15
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
18
|
+
config.run_all_when_everything_filtered = true
|
19
|
+
config.filter_run :focus
|
20
|
+
|
21
|
+
config.order = "random"
|
22
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module GeoRedirect
|
2
|
+
module Support
|
3
|
+
def fixture_path(file)
|
4
|
+
"spec/fixtures/#{file}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def nonexisting_file_path
|
8
|
+
"/no_such_file"
|
9
|
+
end
|
10
|
+
|
11
|
+
def app
|
12
|
+
Rack::Lint.new(@app)
|
13
|
+
end
|
14
|
+
|
15
|
+
def mock_app(options = {})
|
16
|
+
# Simple HTTP server that always returns 'Hello world!'
|
17
|
+
main_app = lambda { |env|
|
18
|
+
Rack::Request.new(env)
|
19
|
+
headers = {"Content-Type" => "text/html"}
|
20
|
+
[200, headers, ["Hello world!"]]
|
21
|
+
}
|
22
|
+
|
23
|
+
@logfile = Tempfile.new("log")
|
24
|
+
options = { :config => fixture_path("config.yml"),
|
25
|
+
:db => fixture_path("GeoIP.dat"),
|
26
|
+
:logfile => @logfile.path
|
27
|
+
}.merge(options)
|
28
|
+
|
29
|
+
builder = Rack::Builder.new
|
30
|
+
builder.use GeoRedirect::Middleware, options
|
31
|
+
builder.run main_app
|
32
|
+
@app = builder.to_app
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_should_include(message)
|
36
|
+
@logfile.rewind
|
37
|
+
@logfile.read.should include(message)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# thanks http://robots.thoughtbot.com/post/11957424161/test-rake-tasks-like-a-boss
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
|
5
|
+
shared_context "rake" do
|
6
|
+
let(:rake) { Rake::Application.new }
|
7
|
+
let(:task_name) { self.class.top_level_description }
|
8
|
+
let(:task_path) { "lib/tasks/#{task_name.split(":").first}" }
|
9
|
+
subject { rake[task_name] }
|
10
|
+
|
11
|
+
before do
|
12
|
+
Rake.application = rake
|
13
|
+
Rake.application.rake_require(task_path,
|
14
|
+
[File.join(File.dirname(__FILE__), "..", "..")])
|
15
|
+
|
16
|
+
Rake::Task.define_task(:environment)
|
17
|
+
end
|
18
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: geo_redirect
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.3'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-07-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
14
30
|
- !ruby/object:Gem::Dependency
|
15
31
|
name: geoip
|
16
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -27,6 +43,86 @@ dependencies:
|
|
27
43
|
- - ! '>='
|
28
44
|
- !ruby/object:Gem::Version
|
29
45
|
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.13.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.13.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rack
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.5.2
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.5.2
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rack-test
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.6.2
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.6.2
|
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
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: simplecov
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.7.1
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 0.7.1
|
30
126
|
description: Geo-location based redirector
|
31
127
|
email:
|
32
128
|
- sagie@waze.com
|
@@ -35,15 +131,25 @@ extensions: []
|
|
35
131
|
extra_rdoc_files: []
|
36
132
|
files:
|
37
133
|
- .gitignore
|
134
|
+
- .rspec
|
135
|
+
- .travis.yml
|
38
136
|
- Gemfile
|
39
137
|
- LICENSE.txt
|
40
138
|
- README.md
|
41
139
|
- Rakefile
|
42
140
|
- geo_redirect.gemspec
|
43
141
|
- lib/geo_redirect.rb
|
142
|
+
- lib/geo_redirect/middleware.rb
|
44
143
|
- lib/geo_redirect/railtie.rb
|
45
144
|
- lib/geo_redirect/version.rb
|
46
|
-
- lib/tasks/geo_redirect.
|
145
|
+
- lib/tasks/geo_redirect.rake
|
146
|
+
- spec/fixtures/GeoIP.dat
|
147
|
+
- spec/fixtures/config.bad.yml
|
148
|
+
- spec/fixtures/config.yml
|
149
|
+
- spec/geo_redirect_spec.rb
|
150
|
+
- spec/spec_helper.rb
|
151
|
+
- spec/support/geo_redirect.rb
|
152
|
+
- spec/support/rake.rb
|
47
153
|
homepage: ''
|
48
154
|
licenses: []
|
49
155
|
post_install_message:
|
@@ -56,16 +162,29 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
162
|
- - ! '>='
|
57
163
|
- !ruby/object:Gem::Version
|
58
164
|
version: '0'
|
165
|
+
segments:
|
166
|
+
- 0
|
167
|
+
hash: 3031125964627560618
|
59
168
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
169
|
none: false
|
61
170
|
requirements:
|
62
171
|
- - ! '>='
|
63
172
|
- !ruby/object:Gem::Version
|
64
173
|
version: '0'
|
174
|
+
segments:
|
175
|
+
- 0
|
176
|
+
hash: 3031125964627560618
|
65
177
|
requirements: []
|
66
178
|
rubyforge_project:
|
67
179
|
rubygems_version: 1.8.24
|
68
180
|
signing_key:
|
69
181
|
specification_version: 3
|
70
182
|
summary: Rack middleware to redirect clients to hostnames based on geo-location
|
71
|
-
test_files:
|
183
|
+
test_files:
|
184
|
+
- spec/fixtures/GeoIP.dat
|
185
|
+
- spec/fixtures/config.bad.yml
|
186
|
+
- spec/fixtures/config.yml
|
187
|
+
- spec/geo_redirect_spec.rb
|
188
|
+
- spec/spec_helper.rb
|
189
|
+
- spec/support/geo_redirect.rb
|
190
|
+
- spec/support/rake.rb
|