turnout2024 3.0.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,119 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+
4
+ module Turnout
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 = Turnout.config.default_reason
14
+ @allowed_paths = Turnout.config.default_allowed_paths
15
+ @allowed_ips = Turnout.config.default_allowed_ips
16
+ @response_code = Turnout.config.default_response_code
17
+ @retry_after = Turnout.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
+ Turnout.config.named_maintenance_file_paths
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,79 @@
1
+ module Turnout
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 ||= Turnout.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(Turnout.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 Turnout
8
+ module MaintenancePage
9
+ class Erb < Turnout::MaintenancePage::HTML
10
+
11
+ def content
12
+ Turnout::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 Turnout
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 Turnout
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
@@ -0,0 +1,24 @@
1
+ require 'pathname'
2
+ module Turnout
3
+ module MaintenancePage
4
+ require 'rack/accept'
5
+
6
+ def self.all
7
+ @all ||= []
8
+ end
9
+
10
+ def self.best_for(env)
11
+ request = Rack::Accept::Request.new(env)
12
+
13
+ all_types = all.map(&:media_types).flatten
14
+ best_type = request.best_media_type(all_types)
15
+ best = all.find { |page| page.media_types.include?(best_type) && File.exist?(page.new.custom_path) }
16
+ best || Turnout.config.default_maintenance_page
17
+ end
18
+
19
+ require 'turnout/maintenance_page/base'
20
+ require 'turnout/maintenance_page/erb'
21
+ require 'turnout/maintenance_page/html'
22
+ require 'turnout/maintenance_page/json'
23
+ end
24
+ end
@@ -0,0 +1,98 @@
1
+ module Turnout
2
+ class OrderedOptions < Hash
3
+ alias_method :_get, :[] # preserve the original #[] method
4
+ protected :_get # make it protected
5
+
6
+ def initialize(constructor = {}, &block)
7
+ if constructor.respond_to?(:to_hash)
8
+ super()
9
+ update(constructor, &block)
10
+ hash = constructor.to_hash
11
+
12
+ self.default = hash.default if hash.default
13
+ self.default_proc = hash.default_proc if hash.default_proc
14
+ else
15
+ super()
16
+ end
17
+ end
18
+
19
+ def update(other_hash)
20
+ if other_hash.is_a? Hash
21
+ super(other_hash)
22
+ else
23
+ other_hash.to_hash.each_pair do |key, value|
24
+ if block_given?
25
+ value = yield(key, value)
26
+ end
27
+ self[key] = value
28
+ end
29
+ self
30
+ end
31
+ end
32
+
33
+ def []=(key, value)
34
+ super(key.to_sym, value)
35
+ end
36
+
37
+ def [](key)
38
+ super(key.to_sym)
39
+ end
40
+
41
+ def method_missing(name, *args)
42
+ name_string = name.to_s
43
+ if name_string.chomp!('=')
44
+ self[name_string] = args.first
45
+ else
46
+ bangs = name_string.chomp!('!')
47
+
48
+ if bangs
49
+ value = fetch(name_string.to_sym)
50
+ raise(RuntimeError.new("#{name_string} is blank.")) if value.nil? || value.empty?
51
+ value
52
+ else
53
+ self[name_string]
54
+ end
55
+ end
56
+ end
57
+
58
+ def except(*keys)
59
+ dup.except!(*keys)
60
+ end
61
+
62
+ def except!(*keys)
63
+ keys.each { |key| delete(key) }
64
+ self
65
+ end
66
+
67
+
68
+ def respond_to_missing?(name, include_private)
69
+ true
70
+ end
71
+ end
72
+
73
+
74
+ # +InheritableOptions+ provides a constructor to build an +OrderedOptions+
75
+ # hash inherited from another hash.
76
+ #
77
+ # Use this if you already have some hash and you want to create a new one based on it.
78
+ #
79
+ # h = ActiveSupport::InheritableOptions.new({ girl: 'Mary', boy: 'John' })
80
+ # h.girl # => 'Mary'
81
+ # h.boy # => 'John'
82
+ class InheritableOptions < Turnout::OrderedOptions
83
+ def initialize(parent = nil)
84
+ if parent.kind_of?(Turnout::OrderedOptions)
85
+ # use the faster _get when dealing with OrderedOptions
86
+ super(parent) {|key,value| parent._get(key) }
87
+ elsif parent
88
+ super(parent) { |key, value| parent[key] }
89
+ else
90
+ super(parent)
91
+ end
92
+ end
93
+
94
+ def inheritable_copy
95
+ self.class.new(self)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,3 @@
1
+ # This file is meant to be used to include rake tasks in a Rakefile by adding
2
+ # require 'turnout/rake_tasks'
3
+ Dir[File.expand_path('../../tasks/*.rake', __FILE__)].each { |ext| import ext }
@@ -0,0 +1,35 @@
1
+ require 'ipaddr'
2
+
3
+ module Turnout
4
+ class Request
5
+ def initialize(env)
6
+ @rack_request = Rack::Request.new(env)
7
+ end
8
+
9
+ def allowed?(settings)
10
+ path_allowed?(settings.allowed_paths) || ip_allowed?(settings.allowed_ips)
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :rack_request
16
+
17
+ def path_allowed?(allowed_paths)
18
+ allowed_paths.any? do |allowed_path|
19
+ rack_request.path =~ Regexp.new(allowed_path)
20
+ end
21
+ end
22
+
23
+ def ip_allowed?(allowed_ips)
24
+ begin
25
+ ip = IPAddr.new(rack_request.ip.to_s)
26
+ rescue ArgumentError
27
+ return false
28
+ end
29
+
30
+ allowed_ips.any? do |allowed_ip|
31
+ IPAddr.new(allowed_ip).include? ip
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Turnout
2
+ VERSION = '3.0.0'.freeze
3
+ end
data/lib/turnout.rb ADDED
@@ -0,0 +1,15 @@
1
+ module Turnout
2
+ require 'turnout/configuration'
3
+ require 'turnout/maintenance_file'
4
+ require 'turnout/maintenance_page'
5
+ require 'turnout/request'
6
+ require 'turnout/engine' if defined? Rails
7
+
8
+ def self.configure
9
+ yield config
10
+ end
11
+
12
+ def self.config
13
+ @config ||= Configuration.new
14
+ end
15
+ end
@@ -0,0 +1,69 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Down for Maintenance</title>
6
+
7
+ <style type="text/css">
8
+
9
+ *{
10
+ font-family: Arial, Helvetica, sans-serif;
11
+ }
12
+
13
+ body{
14
+ margin: 0;
15
+ background-color: #fff;
16
+ }
17
+
18
+ #page{
19
+ position: relative;
20
+ width: 550px;
21
+ margin: 200px auto;
22
+ padding: 75px 0;
23
+ text-align: center;
24
+ background-color: #eaeaea;
25
+ border: solid 1px #ccc;
26
+ border-top: solid 10px #666;
27
+ -moz-box-shadow: inset 0 2px 10px #ccc;
28
+ -webkit-box-shadow: inset 0 2px 10px #ccc;
29
+ box-shadow: inset 0 2px 10px #ccc;
30
+ }
31
+
32
+ header, #body{
33
+ width: 400px;
34
+ margin: 0 auto;
35
+ }
36
+
37
+ h1{
38
+ margin: 0;
39
+ color: #CC3601;
40
+ font-size: 26pt;
41
+ border-bottom: solid 4px #666;
42
+ }
43
+
44
+ #reason{
45
+ margin: 10px 0;
46
+ color: #333;
47
+ }
48
+
49
+ </style>
50
+
51
+ </head>
52
+ <body>
53
+
54
+ <section id="page">
55
+
56
+ <header>
57
+ <h1>Down for Maintenance</h1>
58
+ </header>
59
+
60
+ <section id="body">
61
+ <div>
62
+ {{ reason }}
63
+ </div>
64
+ </section>
65
+
66
+ </section>
67
+
68
+ </body>
69
+ </html>
@@ -0,0 +1,69 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Down for Maintenance</title>
6
+
7
+ <style type="text/css">
8
+
9
+ *{
10
+ font-family: Arial, Helvetica, sans-serif;
11
+ }
12
+
13
+ body{
14
+ margin: 0;
15
+ background-color: #fff;
16
+ }
17
+
18
+ #page{
19
+ position: relative;
20
+ width: 550px;
21
+ margin: 200px auto;
22
+ padding: 75px 0;
23
+ text-align: center;
24
+ background-color: #eaeaea;
25
+ border: solid 1px #ccc;
26
+ border-top: solid 10px #666;
27
+ -moz-box-shadow: inset 0 2px 10px #ccc;
28
+ -webkit-box-shadow: inset 0 2px 10px #ccc;
29
+ box-shadow: inset 0 2px 10px #ccc;
30
+ }
31
+
32
+ header, #body{
33
+ width: 400px;
34
+ margin: 0 auto;
35
+ }
36
+
37
+ h1{
38
+ margin: 0;
39
+ color: #CC3601;
40
+ font-size: 26pt;
41
+ border-bottom: solid 4px #666;
42
+ }
43
+
44
+ #reason{
45
+ margin: 10px 0;
46
+ color: #333;
47
+ }
48
+
49
+ </style>
50
+
51
+ </head>
52
+ <body>
53
+
54
+ <section id="page">
55
+
56
+ <header>
57
+ <h1>Down for Maintenance</h1>
58
+ </header>
59
+
60
+ <section id="body">
61
+ <div>
62
+ <%= reason %>
63
+ </div>
64
+ </section>
65
+
66
+ </section>
67
+
68
+ </body>
69
+ </html>
@@ -0,0 +1 @@
1
+ {"error":"Service Unavailable","message":{{ reason }}}