request_info 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +15 -0
- data/.gitignore +170 -0
- data/.hound.yml +2 -0
- data/.rubocop.ribose.yml +65 -0
- data/.rubocop.tb.yml +640 -0
- data/.rubocop.yml +15 -0
- data/.travis.yml +45 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.adoc +178 -0
- data/Rakefile +6 -0
- data/bin/bundle +105 -0
- data/bin/console +14 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/gemfiles/Rails-4.1.gemfile +4 -0
- data/gemfiles/Rails-4.2.gemfile +4 -0
- data/gemfiles/Rails-5.0.gemfile +4 -0
- data/gemfiles/Rails-5.1.gemfile +4 -0
- data/gemfiles/Rails-head.gemfile +4 -0
- data/gemfiles/common.gemfile +6 -0
- data/lib/request_info.rb +47 -0
- data/lib/request_info/configuration.rb +18 -0
- data/lib/request_info/detector_app.rb +39 -0
- data/lib/request_info/detectors/browser_detector.rb +23 -0
- data/lib/request_info/detectors/ip_detector.rb +52 -0
- data/lib/request_info/detectors/locale_detector.rb +74 -0
- data/lib/request_info/detectors/timezone_detector.rb +48 -0
- data/lib/request_info/env_analyzer.rb +18 -0
- data/lib/request_info/geoip.rb +51 -0
- data/lib/request_info/railtie.rb +24 -0
- data/lib/request_info/results.rb +10 -0
- data/lib/request_info/version.rb +6 -0
- data/request_info.gemspec +38 -0
- metadata +220 -0
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "request_info"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
12
|
+
|
13
|
+
# require "irb"
|
14
|
+
# IRB.start
|
data/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rspec' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/setup
ADDED
data/lib/request_info.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# (c) Copyright 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require "request_info/version"
|
5
|
+
|
6
|
+
require "request_info/configuration"
|
7
|
+
require "request_info/detector_app"
|
8
|
+
require "request_info/env_analyzer"
|
9
|
+
require "request_info/results"
|
10
|
+
|
11
|
+
# Optionally use railtie if Rails is available
|
12
|
+
require "request_info/railtie" if defined?(Rails)
|
13
|
+
|
14
|
+
module RequestInfo
|
15
|
+
CONFIGURATION_MUTEX = Mutex.new
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Get detection results
|
19
|
+
def results
|
20
|
+
Thread.current[:request_info_results] ||=
|
21
|
+
RequestInfo::Results.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Set results
|
25
|
+
def results=(value)
|
26
|
+
Thread.current[:request_info_results] = value
|
27
|
+
end
|
28
|
+
|
29
|
+
def configure
|
30
|
+
CONFIGURATION_MUTEX.synchronize do
|
31
|
+
@mutable_configuration ||= Configuration.new
|
32
|
+
yield @mutable_configuration if block_given?
|
33
|
+
@configuration = @mutable_configuration.dup.tap(&:freeze)
|
34
|
+
end
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def configuration
|
39
|
+
configure if @configuration.nil?
|
40
|
+
@configuration
|
41
|
+
end
|
42
|
+
|
43
|
+
def preload
|
44
|
+
GeoIP.instance
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# (c) Copyright 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require "request_info/detectors/browser_detector"
|
5
|
+
require "request_info/detectors/ip_detector"
|
6
|
+
require "request_info/detectors/timezone_detector"
|
7
|
+
require "request_info/detectors/locale_detector"
|
8
|
+
|
9
|
+
module RequestInfo
|
10
|
+
# Rack middleware to process all specified detectors and sets results for the
|
11
|
+
# current thread
|
12
|
+
class DetectorApp
|
13
|
+
class << self
|
14
|
+
attr_accessor :detectors
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :analyzer, :app
|
18
|
+
|
19
|
+
# TODO: make this list of detectors available for others to add/change
|
20
|
+
DEFAULT_DETECTORS = [
|
21
|
+
RequestInfo::Detectors::IpDetector,
|
22
|
+
RequestInfo::Detectors::BrowserDetector,
|
23
|
+
RequestInfo::Detectors::TimezoneDetector,
|
24
|
+
RequestInfo::Detectors::LocaleDetector,
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
def initialize(app)
|
28
|
+
@app = app
|
29
|
+
@analyzer = EnvAnalyzer.new(self.class.detectors || DEFAULT_DETECTORS)
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(env)
|
33
|
+
analyzer.analyze(env)
|
34
|
+
analyzer.wrap_app do
|
35
|
+
app.call(env)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# (c) Copyright 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require "browser/browser"
|
5
|
+
|
6
|
+
module RequestInfo
|
7
|
+
module Detectors
|
8
|
+
module BrowserDetector
|
9
|
+
def analyze(env)
|
10
|
+
super
|
11
|
+
RequestInfo.results.browser = detect_browser(env)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def detect_browser(env)
|
17
|
+
ua = env["HTTP_USER_AGENT"]
|
18
|
+
lang = env["HTTP_ACCEPT_LANGUAGE"]
|
19
|
+
Browser.new(ua, accept_language: lang)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# (c) Copyright 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require "request_info/geoip"
|
5
|
+
|
6
|
+
# Detects IP related information
|
7
|
+
module RequestInfo
|
8
|
+
module Detectors
|
9
|
+
# TODO Write some notes on configuration & security in README.
|
10
|
+
module IpDetector
|
11
|
+
def analyze(env)
|
12
|
+
super
|
13
|
+
|
14
|
+
results = RequestInfo.results
|
15
|
+
ip = request_ip(env)
|
16
|
+
|
17
|
+
results.ip = ip
|
18
|
+
results.ipinfo = RequestInfo::GeoIP.instance.lookup(ip)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Extracts the IP address from env
|
24
|
+
#
|
25
|
+
def request_ip(env)
|
26
|
+
obtain_ip_from_rails(env) || obtain_ip_from_rack(env)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Obtain client's IP address from +ActionDispatch::RemoteIp+ middleware
|
30
|
+
# provided by Rails.
|
31
|
+
#
|
32
|
+
# This is preferred over using +Rack::Request+ because
|
33
|
+
# +ActionDispatch::RemoteIp+ middleware must be enabled purposely, and
|
34
|
+
# is more customizable (proxies whitelisting is customizable).
|
35
|
+
#
|
36
|
+
# Please read security notes before enabling +ActionDispatch::RemoteIp+
|
37
|
+
# middleware in your application. It may do harm if used incorrectly:
|
38
|
+
# http://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html
|
39
|
+
def obtain_ip_from_rails(env)
|
40
|
+
env["action_dispatch.remote_ip"].try(:to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Obtain client's IP address from +Rack::Request+. May return proxy
|
44
|
+
# address if it adds a non-private IP address to +X-Forwarded-For+ header.
|
45
|
+
#
|
46
|
+
# https://github.com/rack/rack/blob/d1363a66ab217/lib/rack/request.rb#L420
|
47
|
+
def obtain_ip_from_rack(env)
|
48
|
+
Rack::Request.new(env).ip
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# (c) Copyright 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require "country_to_locales_mapping"
|
5
|
+
require "i18n"
|
6
|
+
|
7
|
+
module RequestInfo
|
8
|
+
module Detectors
|
9
|
+
module LocaleDetector
|
10
|
+
def analyze(env)
|
11
|
+
super
|
12
|
+
RequestInfo.results.locale = detect_locale
|
13
|
+
end
|
14
|
+
|
15
|
+
def wrap_app
|
16
|
+
detected_locale = RequestInfo.results.locale
|
17
|
+
previous_locale = ::I18n.locale
|
18
|
+
::I18n.locale = detected_locale
|
19
|
+
|
20
|
+
status, headers, body = super
|
21
|
+
|
22
|
+
# Set header language back to the client
|
23
|
+
headers["Content-Language"] = RequestInfo.results.locale
|
24
|
+
|
25
|
+
# Reset our modifications after app is finished
|
26
|
+
::I18n.locale = previous_locale
|
27
|
+
|
28
|
+
[status, headers, body]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def detect_locale
|
34
|
+
available_locales = ::I18n.available_locales.map(&:to_s)
|
35
|
+
user_preference.detect { |l| available_locales.include?(l) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns enumerator which yields locales which are preferred by user,
|
39
|
+
# starting with the best matching one. The user preference is defined as
|
40
|
+
# concatenation of locales in Accept-Language HTTP header (already sorted
|
41
|
+
# according to respective weights), locales matching the user's location
|
42
|
+
# (guessed from the IP address), and finally the application's default
|
43
|
+
# locale.
|
44
|
+
#
|
45
|
+
# It is not guaranteed that these locales are available in I18n.
|
46
|
+
def user_preference
|
47
|
+
Enumerator.new do |y|
|
48
|
+
browser_locales.each { |l| y << l }
|
49
|
+
ip_locales.each { |l| y << l }
|
50
|
+
y << default_locale
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Locales preferred by user according to Accept-Language HTTP header.
|
55
|
+
def browser_locales
|
56
|
+
locales_arr = RequestInfo.results.browser.try(:accept_language) || []
|
57
|
+
locales_arr.flat_map { |l| [l.full, l.code] }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Guessing of locales preferred by user basing on his location.
|
61
|
+
def ip_locales
|
62
|
+
ipinfo = RequestInfo.results.ipinfo || {}
|
63
|
+
country_code = ipinfo["country_code"]
|
64
|
+
return [] unless country_code
|
65
|
+
locales = CountryToLocalesMapping.country_code_locales(country_code)
|
66
|
+
locales + locales.map { |l| l.split(/\W/, 2).first }
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_locale
|
70
|
+
::I18n.default_locale.to_s
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# (c) Copyright 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
require "active_support/time"
|
5
|
+
|
6
|
+
module RequestInfo
|
7
|
+
module Detectors
|
8
|
+
# Detects Timezone related information
|
9
|
+
module TimezoneDetector
|
10
|
+
def analyze(_env)
|
11
|
+
super
|
12
|
+
|
13
|
+
results = RequestInfo.results
|
14
|
+
tzinfo_id, tzinfo = get_tzinfo_from_ipinfo(results.ipinfo)
|
15
|
+
return unless tzinfo_id && tzinfo
|
16
|
+
|
17
|
+
results.timezone = tzinfo
|
18
|
+
results.timezone_id = tzinfo_id
|
19
|
+
results.timezone_offset = calculate_utc_offset(tzinfo)
|
20
|
+
results.timezone_desc = tz_description(tzinfo)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Return time zone identifier and object basing on what has been found by
|
26
|
+
# GeoIP.
|
27
|
+
def get_tzinfo_from_ipinfo(ipinfo)
|
28
|
+
tzinfo_id = ipinfo && ipinfo["time_zone"]
|
29
|
+
tzinfo = tzinfo_id && TZInfo::Timezone.get(tzinfo_id)
|
30
|
+
tzinfo ? [tzinfo_id, tzinfo] : nil
|
31
|
+
rescue TZInfo::InvalidTimezoneIdentifier
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# Total offset is UTC + DST
|
36
|
+
def calculate_utc_offset(tzinfo)
|
37
|
+
tzinfo.current_period.utc_total_offset / 3600.0
|
38
|
+
end
|
39
|
+
|
40
|
+
# TODO: i18n this
|
41
|
+
def tz_description(tzinfo)
|
42
|
+
offset = calculate_utc_offset(tzinfo)
|
43
|
+
offset_string = "#{offset > 0 ? '+' : ''}#{offset}"
|
44
|
+
"GMT(#{offset_string}) #{tzinfo.friendly_identifier}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# (c) Copyright 2017 Ribose Inc.
|
2
|
+
#
|
3
|
+
|
4
|
+
module RequestInfo
|
5
|
+
class EnvAnalyzer
|
6
|
+
def initialize(detectors)
|
7
|
+
detectors.each { |d| extend(d) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def analyze(_env)
|
11
|
+
RequestInfo.results = RequestInfo::Results.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def wrap_app
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|