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.
@@ -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
@@ -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")
@@ -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")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ eval File.read "gemfiles/common.gemfile" # rubocop:disable Security/Eval
2
+
3
+ gem "rails", "~> 4.1.0"
4
+ gem "maxmind_geoip2"
@@ -0,0 +1,4 @@
1
+ eval File.read "gemfiles/common.gemfile" # rubocop:disable Security/Eval
2
+
3
+ gem "rails", "~> 4.2.0"
4
+ gem "maxmind_geoip2"
@@ -0,0 +1,4 @@
1
+ eval File.read "gemfiles/common.gemfile" # rubocop:disable Security/Eval
2
+
3
+ gem "rails", "~> 5.0.0"
4
+ gem "maxmind_geoip2"
@@ -0,0 +1,4 @@
1
+ eval File.read "gemfiles/common.gemfile" # rubocop:disable Security/Eval
2
+
3
+ gem "rails", "~> 5.1.0"
4
+ gem "maxmind_geoip2"
@@ -0,0 +1,4 @@
1
+ eval File.read "gemfiles/common.gemfile" # rubocop:disable Security/Eval
2
+
3
+ gem "rails", github: "rails/rails"
4
+ gem "maxmind_geoip2"
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec path: ".."
4
+
5
+ gem "codecov", require: false
6
+ gem "simplecov", require: false
@@ -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,18 @@
1
+ # (c) Copyright 2017 Ribose Inc.
2
+ #
3
+
4
+ module RequestInfo
5
+ class Configuration
6
+ attr_accessor :geoip2_db_path
7
+
8
+ def initialize
9
+ set_defaults
10
+ end
11
+
12
+ private
13
+
14
+ def set_defaults
15
+ self.geoip2_db_path = nil
16
+ end
17
+ end
18
+ 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