embargoed 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c4f9b6c54838896e47c825725907170322148c1bf40351d0e4607b5375037e98
4
+ data.tar.gz: 63e544b02169e811de9547fd1bf95a4008df0faaaa5e5fdf51efbdfb253b5d18
5
+ SHA512:
6
+ metadata.gz: 2a3a1062c73c4014abb13f1077587831b98a17e77675e806b567f165f6bf352538b929ab2a01c15a0958004f90c7716d7e74cf63b232da3e646aaa805cae7b6d
7
+ data.tar.gz: 4183a31830ffee4d535f8a35b49681318462d8afe416d330bf3a5d8cf01466cd60c6679c69a3689779584efe048335cf9e09fd32e1819a5a123e3e1e314b2e8d
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 by Biola University
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ Embargoed
2
+ =======
3
+ Embargoed is [Rack](http://rack.rubyforge.org/) middleware with a [Ruby on Rails](http://rubyonrails.org) engine that blocks all requests from Russia and displays a pro-Ukraine message instead.
4
+
5
+ Embargoed is a fork from [Turnout](https://github.com/biola/turnout).
6
+
7
+ ![Embargoed message displayed to Russian visitors](https://github.com/rameerez/embargoed/blob/main/public/embargoed-message.jpg?raw=true)
8
+
9
+
10
+ ## Installation
11
+
12
+ Add this to your `Gemfile`:
13
+
14
+ gem 'embargoed'
15
+
16
+ then run
17
+
18
+ bundle install
19
+
20
+ _Note that you'll need to restart your Rails server before it works_
21
+
22
+ That's it! Just by having `embargoed` on your Gemfile your Rails app will display this message to all requests from a Russian IP.
23
+
24
+
25
+ ## Collaborate
26
+
27
+ Please feel free to contact me [@rameerez](https://twitter.com/rameerez) or fork this to port it to other platforms, or make PRs to this repo to collaborate.
@@ -0,0 +1,60 @@
1
+ require_relative './ordered_options'
2
+ module Embargoed
3
+ class Configuration
4
+ SETTINGS = [
5
+ :app_root,
6
+ :named_maintenance_file_paths,
7
+ :maintenance_pages_path,
8
+ :default_maintenance_page,
9
+ :default_reason,
10
+ :default_allowed_ips,
11
+ :skip_middleware,
12
+ :default_allowed_paths,
13
+ :default_response_code,
14
+ :default_retry_after,
15
+ :i18n
16
+ ].freeze
17
+
18
+ SETTINGS.each do |setting|
19
+ attr_accessor setting
20
+ end
21
+
22
+ def initialize
23
+ @skip_middleware = false
24
+ @app_root = '.'
25
+ @named_maintenance_file_paths = {default: app_root.join('tmp', 'maintenance.yml').to_s}
26
+ @maintenance_pages_path = app_root.join('public').to_s
27
+ @default_maintenance_page = Embargoed::MaintenancePage::HTML
28
+ @default_reason = "The site is temporarily down for maintenance.\nPlease check back soon."
29
+ @default_allowed_paths = []
30
+ @default_allowed_ips = []
31
+ @default_response_code = 503
32
+ @default_retry_after = 7200 # 2 hours by default
33
+ @i18n = Embargoed::OrderedOptions.new
34
+ @i18n.railties_load_path = []
35
+ @i18n.load_path = []
36
+ @i18n.fallbacks = Embargoed::OrderedOptions.new
37
+ @i18n.enabled = false
38
+ @i18n.use_language_header = false
39
+ end
40
+
41
+ def app_root
42
+ Pathname.new(@app_root.to_s)
43
+ end
44
+
45
+ def named_maintenance_file_paths=(named_paths)
46
+ # Force keys to symbols
47
+ @named_maintenance_file_paths = Hash[named_paths.map { |k, v| [k.to_sym, v] }]
48
+ end
49
+
50
+ def update(settings_hash)
51
+ settings_hash.each do |setting, value|
52
+ unless SETTINGS.include? setting.to_sym
53
+ raise ArgumentError, "invalid setting: #{setting}"
54
+ end
55
+
56
+ self.public_send "#{setting}=", value
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,16 @@
1
+ require 'embargoed'
2
+ require 'rack/embargoed'
3
+ require 'rails'
4
+
5
+ # For Rails 3
6
+ if defined? Rails::Engine
7
+ module Embargoed
8
+ class Engine < Rails::Engine
9
+ initializer 'embargoed.add_to_middleware_stack' do |app|
10
+ unless Embargoed.config.skip_middleware
11
+ app.config.middleware.use(Rack::Embargoed)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,109 @@
1
+ module Embargoed
2
+ class AcceptLanguageParser
3
+ attr_accessor :header
4
+
5
+ def initialize(header)
6
+ @header = header
7
+ end
8
+
9
+ # Returns a sorted array based on user preference in HTTP_ACCEPT_LANGUAGE.
10
+ # Browsers send this HTTP header, so don't think this is holy.
11
+ #
12
+ # Example:
13
+ #
14
+ # request.user_preferred_languages
15
+ # # => [ 'nl-NL', 'nl-BE', 'nl', 'en-US', 'en' ]
16
+ #
17
+ def user_preferred_languages
18
+ return [] if header.to_s.strip.empty?
19
+ @user_preferred_languages ||= begin
20
+ header.to_s.gsub(/\s+/, '').split(',').map do |language|
21
+ locale, quality = language.split(';q=')
22
+ raise ArgumentError, 'Not correctly formatted' unless locale =~ /^[a-z\-0-9]+|\*$/i
23
+
24
+ locale = locale.downcase.gsub(/-[a-z0-9]+$/i, &:upcase) # Uppercase territory
25
+ locale = nil if locale == '*' # Ignore wildcards
26
+
27
+ quality = quality ? quality.to_f : 1.0
28
+
29
+ [locale, quality]
30
+ end.sort do |(_, left), (_, right)|
31
+ right <=> left
32
+ end.map(&:first).compact
33
+ rescue ArgumentError # Just rescue anything if the browser messed up badly.
34
+ []
35
+ end
36
+ end
37
+
38
+ # Sets the user languages preference, overriding the browser
39
+ #
40
+ def user_preferred_languages=(languages)
41
+ @user_preferred_languages = languages
42
+ end
43
+
44
+ # Finds the locale specifically requested by the browser.
45
+ #
46
+ # Example:
47
+ #
48
+ # request.preferred_language_from I18n.available_locales
49
+ # # => 'nl'
50
+ #
51
+ def preferred_language_from(array)
52
+ (user_preferred_languages & array.map(&:to_s)).first
53
+ end
54
+
55
+ # Returns the first of the user_preferred_languages that is compatible
56
+ # with the available locales. Ignores region.
57
+ #
58
+ # Example:
59
+ #
60
+ # request.compatible_language_from I18n.available_locales
61
+ #
62
+ def compatible_language_from(available_languages)
63
+ user_preferred_languages.map do |preferred| #en-US
64
+ preferred = preferred.downcase
65
+ preferred_language = preferred.split('-', 2).first
66
+
67
+ available_languages.find do |available| # en
68
+ available = available.to_s.downcase
69
+ preferred == available || preferred_language == available.split('-', 2).first
70
+ end
71
+ end.compact.first
72
+ end
73
+
74
+ # Returns a supplied list of available locals without any extra application info
75
+ # that may be attached to the locale for storage in the application.
76
+ #
77
+ # Example:
78
+ # [ja_JP-x1, en-US-x4, en_UK-x5, fr-FR-x3] => [ja-JP, en-US, en-UK, fr-FR]
79
+ #
80
+ def sanitize_available_locales(available_languages)
81
+ available_languages.map do |available|
82
+ available.to_s.split(/[_-]/).reject { |part| part.start_with?("x") }.join("-")
83
+ end
84
+ end
85
+
86
+ # Returns the first of the user preferred languages that is
87
+ # also found in available languages. Finds best fit by matching on
88
+ # primary language first and secondarily on region. If no matching region is
89
+ # found, return the first language in the group matching that primary language.
90
+ #
91
+ # Example:
92
+ #
93
+ # request.language_region_compatible(available_languages)
94
+ #
95
+ def language_region_compatible_from(available_languages)
96
+ available_languages = sanitize_available_locales(available_languages)
97
+ user_preferred_languages.map do |preferred| #en-US
98
+ preferred = preferred.downcase
99
+ preferred_language = preferred.split('-', 2).first
100
+
101
+ lang_group = available_languages.select do |available| # en
102
+ preferred_language == available.downcase.split('-', 2).first
103
+ end
104
+
105
+ lang_group.find { |lang| lang.downcase == preferred } || lang_group.first #en-US, en-UK
106
+ end.compact.first
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,141 @@
1
+ require 'i18n'
2
+ require 'i18n/backend/fallbacks'
3
+ require_relative './accept_language_parser'
4
+ require_relative '../ordered_options'
5
+
6
+ module Embargoed
7
+ class Internationalization
8
+ class << self
9
+ attr_reader :env
10
+ attr_writer :env
11
+
12
+ def initialize_i18n(env)
13
+ @env = env
14
+ setup_i18n_config
15
+ end
16
+
17
+ def i18n_config
18
+ @i18n_config = Embargoed.config.i18n
19
+ @i18n_config = @i18n_config.is_a?(Embargoed::OrderedOptions) ? @i18n_config : Embargoed::InheritableOptions.new(@i18n_config)
20
+ end
21
+
22
+ def embargoed_page
23
+ @embargoed_page ||= Embargoed.config.default_maintenance_page
24
+ end
25
+
26
+ def http_accept_language
27
+ language = (env.nil? || env.empty?) ? nil : env["HTTP_ACCEPT_LANGUAGE"]
28
+ @http_accept_language ||= Embargoed::AcceptLanguageParser.new(language)
29
+ end
30
+
31
+ def setup_additional_helpers
32
+ i18n_additional_helpers = i18n_config.delete(:additional_helpers)
33
+ i18n_additional_helpers = i18n_additional_helpers.is_a?(Array) ? i18n_additional_helpers : []
34
+
35
+ i18n_additional_helpers.each do |helper|
36
+ embargoed_page.send(:include, helper) if helper.is_a?(Module)
37
+ end
38
+ end
39
+
40
+ def expanded(path)
41
+ result = []
42
+ if File.directory?(path)
43
+ result.concat(Dir.glob(File.join(path, '**', '**')).map { |file| file }.sort)
44
+ else
45
+ result << path
46
+ end
47
+ result.uniq!
48
+ result
49
+ end
50
+
51
+ # Returns all expanded paths but only if they exist in the filesystem.
52
+ def existent(path)
53
+ expanded(path).select { |f| File.exist?(f) }
54
+ end
55
+
56
+ # Setup i18n configuration.
57
+ def setup_i18n_config
58
+ return unless i18n_config.enabled
59
+ setup_additional_helpers
60
+ fallbacks = i18n_config.delete(:fallbacks)
61
+
62
+
63
+ # Avoid issues with setting the default_locale by disabling available locales
64
+ # check while configuring.
65
+ enforce_available_locales = i18n_config.delete(:enforce_available_locales)
66
+ enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil?
67
+ I18n.enforce_available_locales = false
68
+
69
+ i18n_config.except(:enabled, :use_language_header).each do |setting, value|
70
+ case setting
71
+ when :railties_load_path
72
+ I18n.load_path.unshift(*value.map { |file| existent(file) }.flatten)
73
+ when :load_path
74
+ I18n.load_path += value
75
+ else
76
+ I18n.send("#{setting}=", value)
77
+ end
78
+ end
79
+
80
+
81
+ init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks)
82
+ I18n.backend.load_translations
83
+
84
+ # Restore available locales check so it will take place from now on.
85
+ I18n.enforce_available_locales = enforce_available_locales
86
+
87
+ begin
88
+ if i18n_config.use_language_header
89
+ I18n.locale = http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale
90
+ else
91
+ I18n.locale = I18n.default_locale
92
+ end
93
+ rescue
94
+ #nothing
95
+ end
96
+
97
+ end
98
+
99
+ def array_wrap(object)
100
+ if object.nil?
101
+ []
102
+ elsif object.respond_to?(:to_ary)
103
+ object.to_ary || [object]
104
+ else
105
+ [object]
106
+ end
107
+ end
108
+
109
+ def include_fallbacks_module
110
+ I18n.backend.class.send(:include, I18n::Backend::Fallbacks)
111
+ end
112
+
113
+ def init_fallbacks(fallbacks)
114
+ include_fallbacks_module
115
+
116
+ args = case fallbacks
117
+ when Embargoed::OrderedOptions
118
+ [*(fallbacks[:defaults] || []) << fallbacks[:map]].compact
119
+ when Hash, Array
120
+ array_wrap(fallbacks)
121
+ else # TrueClass
122
+ []
123
+ end
124
+
125
+ I18n.fallbacks = I18n::Locale::Fallbacks.new(*args)
126
+ end
127
+
128
+ def validate_fallbacks(fallbacks)
129
+ case fallbacks
130
+ when Embargoed::OrderedOptions
131
+ !fallbacks.empty?
132
+ when TrueClass, Array, Hash
133
+ true
134
+ else
135
+ raise "Unexpected fallback type #{fallbacks.inspect}"
136
+ end
137
+ end
138
+
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,29 @@
1
+ require 'maxmind/db'
2
+
3
+ module Embargoed
4
+ class IPLocator
5
+
6
+ def self.get_country_code(ip)
7
+ country_code = "none"
8
+
9
+ # Using an Open-Source IP db released under an Apache2 license: https://github.com/geoacumen/geoacumen-country
10
+ db = File.expand_path("../../../public/Geoacumen-Country.mmdb", __FILE__)
11
+ reader = MaxMind::DB.new(db, mode: MaxMind::DB::MODE_MEMORY)
12
+
13
+ begin
14
+ record = reader.get(ip)
15
+ rescue => error
16
+ puts error.inspect
17
+ end
18
+
19
+ if !record.nil?
20
+ country_code = record['country']['iso_code']
21
+ end
22
+
23
+ reader.close
24
+
25
+ return country_code
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,119 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+
4
+ module Embargoed
5
+ class MaintenanceFile
6
+ attr_reader :path
7
+
8
+ SETTINGS = [:reason, :allowed_paths, :allowed_ips, :response_code, :retry_after]
9
+ attr_reader(*SETTINGS)
10
+
11
+ def initialize(path)
12
+ @path = path
13
+ @reason = Embargoed.config.default_reason
14
+ @allowed_paths = Embargoed.config.default_allowed_paths
15
+ @allowed_ips = Embargoed.config.default_allowed_ips
16
+ @response_code = Embargoed.config.default_response_code
17
+ @retry_after = Embargoed.config.default_retry_after
18
+
19
+ import_yaml if exists?
20
+ end
21
+
22
+ def exists?
23
+ File.exist? path
24
+ end
25
+
26
+ def to_h
27
+ SETTINGS.each_with_object({}) do |att, hash|
28
+ hash[att] = send(att)
29
+ end
30
+ end
31
+
32
+ def to_yaml(key_mapper = :to_s)
33
+ to_h.each_with_object({}) { |(key, val), hash|
34
+ hash[key.send(key_mapper)] = val
35
+ }.to_yaml
36
+ end
37
+
38
+ def write
39
+ FileUtils.mkdir_p(dir_path) unless Dir.exist? dir_path
40
+
41
+ File.open(path, 'w') do |file|
42
+ file.write to_yaml
43
+ end
44
+ end
45
+
46
+ def delete
47
+ File.delete(path) if exists?
48
+ end
49
+
50
+ def import(hash)
51
+ SETTINGS.map(&:to_s).each do |att|
52
+ self.send(:"#{att}=", hash[att]) unless hash[att].nil?
53
+ end
54
+
55
+ true
56
+ end
57
+ alias :import_env_vars :import
58
+
59
+ # Find the first MaintenanceFile that exists
60
+ def self.find
61
+ path = named_paths.values.find { |p| File.exist? p }
62
+ self.new(path) if path
63
+ end
64
+
65
+ def self.named(name)
66
+ path = named_paths[name.to_sym]
67
+ self.new(path) unless path.nil?
68
+ end
69
+
70
+ def self.default
71
+ self.new(named_paths.values.first)
72
+ end
73
+
74
+ private
75
+
76
+ def retry_after=(value)
77
+ @retry_after = value
78
+ end
79
+
80
+ def reason=(reason)
81
+ @reason = reason.to_s
82
+ end
83
+
84
+ # Splits strings on commas for easier importing of environment variables
85
+ def allowed_paths=(paths)
86
+ if paths.is_a? String
87
+ # Grab everything between commas that aren't escaped with a backslash
88
+ paths = paths.to_s.split(/(?<!\\),\ ?/).map do |path|
89
+ path.strip.gsub('\,', ',') # remove the escape characters
90
+ end
91
+ end
92
+
93
+ @allowed_paths = paths
94
+ end
95
+
96
+ # Splits strings on commas for easier importing of environment variables
97
+ def allowed_ips=(ips)
98
+ ips = ips.to_s.split(',') if ips.is_a? String
99
+
100
+ @allowed_ips = ips
101
+ end
102
+
103
+ def response_code=(code)
104
+ @response_code = code.to_i
105
+ end
106
+
107
+ def dir_path
108
+ File.dirname(path)
109
+ end
110
+
111
+ def import_yaml
112
+ import YAML::load(File.open(path)) || {}
113
+ end
114
+
115
+ def self.named_paths
116
+ Embargoed.config.named_maintenance_file_paths
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,79 @@
1
+ module Embargoed
2
+ module MaintenancePage
3
+ class Base
4
+ attr_reader :reason
5
+
6
+ def initialize(reason = nil, options = {})
7
+ @options = options.is_a?(Hash) ? options : {}
8
+ @reason = reason
9
+ end
10
+
11
+ def rack_response(code = nil, retry_after = nil)
12
+ code ||= Embargoed.config.default_response_code
13
+ [code, headers(retry_after), body]
14
+ end
15
+
16
+ # Override with an array of media type strings. i.e. text/html
17
+ def self.media_types
18
+ raise NotImplementedError, '.media_types must be overridden in subclasses'
19
+ end
20
+ def media_types() self.class.media_types end
21
+
22
+ # Override with a file extension value like 'html' or 'json'
23
+ def self.extension
24
+ raise NotImplementedError, '.extension must be overridden in subclasses'
25
+ end
26
+ def extension() self.class.extension end
27
+
28
+ def custom_path
29
+ Pathname.new(Embargoed.config.maintenance_pages_path).join(filename)
30
+ end
31
+
32
+ protected
33
+
34
+ def self.inherited(subclass)
35
+ MaintenancePage.all << subclass
36
+ end
37
+
38
+ def headers(retry_after = nil)
39
+ headers = {'Content-Type' => media_types.first, 'Content-Length' => length}
40
+ # Include the Retry-After header unless it wasn't specified
41
+ headers['Retry-After'] = retry_after.to_s unless retry_after.nil?
42
+ headers
43
+ end
44
+
45
+ def length
46
+ content.bytesize.to_s
47
+ end
48
+
49
+ def body
50
+ [content]
51
+ end
52
+
53
+ def content
54
+ file_content.gsub(/{{\s?reason\s?}}/, reason)
55
+ end
56
+
57
+ def file_content
58
+ File.read(path)
59
+ end
60
+
61
+ def path
62
+ if File.exist? custom_path
63
+ custom_path
64
+ else
65
+ default_path
66
+ end
67
+ end
68
+
69
+ def default_path
70
+ File.expand_path("../../../../public/#{filename}", __FILE__)
71
+ end
72
+
73
+
74
+ def filename
75
+ "maintenance.#{extension}"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,22 @@
1
+ require 'erb'
2
+ require 'tilt'
3
+ require 'tilt/erb'
4
+ require_relative './html'
5
+ require_relative '../i18n/internationalization'
6
+
7
+ module Embargoed
8
+ module MaintenancePage
9
+ class Erb < Embargoed::MaintenancePage::HTML
10
+
11
+ def content
12
+ Embargoed::Internationalization.initialize_i18n(@options[:env])
13
+ Tilt.new(File.expand_path(path)).render(self, {reason: reason}.merge(@options))
14
+ end
15
+
16
+ def self.extension
17
+ 'html.erb'
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ require_relative './base'
2
+ module Embargoed
3
+ module MaintenancePage
4
+ class HTML < Base
5
+ def reason
6
+ super.to_s.split("\n").map{|txt| "<p>#{txt}</p>" }.join("\n")
7
+ end
8
+
9
+ def self.media_types
10
+ %w{
11
+ text/html
12
+ application/xhtml+xml
13
+ }
14
+ end
15
+
16
+ def self.extension
17
+ 'html'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ require 'json'
2
+
3
+ module Embargoed
4
+ module MaintenancePage
5
+ class JSON < Base
6
+ def reason
7
+ super.to_s.to_json
8
+ end
9
+
10
+ def self.media_types
11
+ %w{
12
+ application/json
13
+ text/json
14
+ application/x-javascript
15
+ text/javascript
16
+ text/x-javascript
17
+ text/x-json
18
+ }
19
+ end
20
+
21
+ def self.extension
22
+ 'json'
23
+ end
24
+ end
25
+ end
26
+ end