request_info 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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