lib/geo_redirect/middleware.rb
require 'yaml'
require 'geoip'
module GeoRedirect
class Middleware
attr_accessor :db, :config
def initialize(app, options = {})
@app = app
@options = options
@logger = init_logger(options[:logfile])
@db = init_db(options[:db] || DEFAULT_DB_PATH)
@config = init_config(options[:config] || DEFAULT_CONFIG_PATH)
@include_paths = Array(options[:include])
@exclude_paths = Array(options[:exclude])
log 'Initialized middleware'
end
def call(env)
request = Rack::Request.new(env)
url = URI.parse(request.url)
if skip_redirect?(request, url)
if @options[:remember_when_skipping]
remember_host(request, request_host(url))
end
@app.call(env)
elsif force_redirect?(url)
handle_force(request, url)
elsif session_exists?(request)
handle_session(request)
else
handle_geoip(request)
end
end
def session_exists?(request)
host = request.session['geo_redirect']
host = host.to_sym if host && host.respond_to?(:to_sym)
if host && @config[host].nil? # Invalid var, remove it
log 'Invalid session var, forgetting'
forget_host(request, host)
host = nil
end
!host.nil?
end
def handle_session(request)
host = request.session['geo_redirect']
host = host.is_a?(Symbol) ? host : host.to_sym if host
log "Handling session var: #{host}"
redirect_request(request, host)
end
def force_redirect?(url)
Rack::Utils.parse_query(url.query).key? 'redirect'
end
def skip_redirect?(request, url)
query_includes_skip_geo?(url) ||
path_not_whitelisted?(url) ||
path_blacklisted?(url) ||
skipped_by_block?(request)
end
def query_includes_skip_geo?(url)
Rack::Utils.parse_query(url.query).key? 'skip_geo'
end
def path_not_whitelisted?(url)
!@include_paths.empty? &&
@include_paths.none? { |include| url.path == include }
end
def path_blacklisted?(url)
@exclude_paths.any? { |exclude| url.path == exclude }
end
def skipped_by_block?(request)
@options[:skip_if] && @options[:skip_if].call(request)
end
def handle_force(request, url)
log 'Handling force flag'
remember_host(request, request_host(url))
redirect_request(request, url.host, true)
end
def handle_geoip(request)
country = country_from_request(request) rescue nil
request.session['geo_redirect.country'] = country
log "GeoIP match: country code #{country.inspect}"
if country.nil?
@app.call(request.env)
else
host = host_by_country(country) # desired host
log "GeoIP host match: #{host}"
remember_host(request, host)
redirect_request(request, host)
end
end
def redirect_request(request, host = nil, same_host = false)
hostname = hostname_by_host(host)
if should_redirect?(request, hostname, same_host)
url = redirect_url(request, hostname)
log "Redirecting to #{url}"
[301,
{ 'Location' => url.to_s, 'Content-Type' => 'text/plain' },
['Moved Permanently\n']]
else
@app.call(request.env)
end
end
def host_by_country(country)
hosts = @config.select { |_k, v| Array(v[:countries]).include?(country) }
hosts.keys.first || :default
end
def host_by_hostname(hostname)
hosts = @config.select { |_k, v| v[:host] == hostname }
hosts.keys.first || :default
end
def hostname_by_host(host)
host.is_a?(Symbol) ? @config[host][:host] : host
end
def remember_host(request, host)
log "Remembering: #{host}"
request.session['geo_redirect'] = host
end
def forget_host(request, host)
log "Forgetting: #{host}"
remember_host(request, nil)
end
protected
def log(message, level = :debug)
@logger.send(level, "[GeoRedirect] #{message}") unless @logger.nil?
end
def init_logger(path)
Logger.new(path) if path
rescue Errno::EINVAL, Errno::EACCES
nil
end
def init_db(path)
GeoIP.new(path)
rescue Errno::EINVAL, Errno::ENOENT
message = <<-ERROR
Could not load GeoIP database file.
Please make sure you have a valid one and add its name to
the GeoRedirect middleware.
Alternatively, use `rake georedirect:fetch_db` to fetch it
to the default location (under db/).
ERROR
log(message, :error)
end
def init_config(path)
YAML.load_file(path) || raise(Errno::EINVAL)
rescue Errno::EINVAL, Errno::ENOENT, Psych::SyntaxError, SyntaxError
message = <<-ERROR
Could not load GeoRedirect config YML file.
Please make sure you have a valid YML file and pass its name
when adding the GeoRedirect middlware.
ERROR
log(message, :error)
end
def request_ip(request)
ip_address =
request.env['HTTP_X_FORWARDED_FOR'] || request.env['REMOTE_ADDR']
# take only the first given ip
ip_address.split(',').first.strip
end
def request_host(url)
host_by_hostname(url.host)
end
def country_from_request(request)
ip = request_ip(request)
log "Handling GeoIP lookup: IP #{ip}"
country = @db.country(ip)
code = country[:country_code]
country[:country_code2] unless code.nil? || code.zero?
end
def redirect_url(request, hostname)
url = URI.parse(request.url)
url.port = nil
url.host = hostname if hostname
# Remove force flag from GET arguments
query_hash = Rack::Utils.parse_query(url.query).tap do |u|
u.delete('redirect')
end
# Copy query
url.query = URI.encode_www_form(query_hash)
url.query = nil if url.query.empty?
url
end
def should_redirect?(request, hostname, same_host)
return true if hostname.nil? || same_host
hostname_ends_with = %r{#{hostname.tr('.', '\.')}$}
(request.host =~ hostname_ends_with).nil?
end
end
end