turnout2024 3.0.0

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