request_info 0.3.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 +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
|