utopia 1.3.2 → 1.4.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.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/lib/utopia.rb +2 -3
- data/lib/utopia/exceptions.rb +27 -0
- data/lib/utopia/exceptions/handler.rb +99 -0
- data/lib/utopia/exceptions/mailer.rb +138 -0
- data/lib/utopia/redirection.rb +176 -0
- data/lib/utopia/version.rb +1 -1
- data/setup/site/config.ru +16 -12
- data/setup/site/{pages → public}/_static/icon.png +0 -0
- data/setup/site/{pages → public}/_static/site.css +0 -0
- data/setup/site/{pages → public}/_static/utopia-background.svg +0 -0
- data/setup/site/public/_static/utopia.svg +1 -0
- data/setup/site/public/readme.txt +1 -1
- data/spec/utopia/{exception_handler_spec.rb → exceptions/handler_spec.rb} +17 -25
- data/spec/utopia/exceptions/handler_spec.ru +6 -0
- data/spec/utopia/{exception_handler_spec → exceptions/handler_spec}/controller.rb +0 -0
- data/spec/utopia/{redirector_spec.rb → redirection_spec.rb} +27 -5
- data/spec/utopia/redirection_spec.ru +25 -0
- metadata +20 -20
- data/lib/utopia/exception_handler.rb +0 -104
- data/lib/utopia/redirector.rb +0 -164
- data/setup/site/pages/_static/utopia.svg +0 -1
- data/setup/site/pages/errors/controller.rb +0 -9
- data/setup/site/public/_static +0 -1
- data/spec/utopia/exception_handler_spec.ru +0 -6
- data/spec/utopia/redirector_spec.ru +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0fb046791da67c397e853162649620dc2b530d3e
|
4
|
+
data.tar.gz: 390a488937297cec03a267206d4a096d4c74a497
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9bd0a3820ebe9422484bb3c1bf048c99b7f7d0e9907eb0cdfccf292ecb6c55e8927cbdeacf87f96b4ad759b7047290f7622639438997d2fa53c96ced3fd24e26
|
7
|
+
data.tar.gz: 9cca58be165173f26f72cee8bde2b7b5b1052018c2a1fff7946476228cc09c63eb48e4d51a85da50893e59894b33c504afede627b5cfeb51dd15c466af3b94fa
|
data/README.md
CHANGED
@@ -79,7 +79,7 @@ Then, Nginx is configured like so:
|
|
79
79
|
Utopia builds on top of Rack with the following middleware:
|
80
80
|
|
81
81
|
- `Utopia::Static`: Serve static files efficiently.
|
82
|
-
- `Utopia::
|
82
|
+
- `Utopia::Redirection`: Redirect URL patterns and status codes.
|
83
83
|
- `Utopia::Localization`: Non-intrusive localization of resources.
|
84
84
|
- `Utopia::Controller`: Dynamic behaviour with recursive execution.
|
85
85
|
- `Utopia::Content`: XML-style template engine with powerful tag behaviours.
|
@@ -89,9 +89,9 @@ Utopia builds on top of Rack with the following middleware:
|
|
89
89
|
|
90
90
|
This middleware serves static files using the `mime-types` library. By default, it works with `Rack::Sendfile` and `Rack::Cache` and supports `ETag` based caching.
|
91
91
|
|
92
|
-
###
|
92
|
+
### Redirection
|
93
93
|
|
94
|
-
A
|
94
|
+
A set of flexible URI rewriting middleware which includes support for string mappings, regular expressions and status codes (e.g. 404 errors).
|
95
95
|
|
96
96
|
### Localization
|
97
97
|
|
data/lib/utopia.rb
CHANGED
@@ -22,10 +22,9 @@ require_relative 'utopia/version'
|
|
22
22
|
|
23
23
|
require_relative 'utopia/content'
|
24
24
|
require_relative 'utopia/controller'
|
25
|
-
require_relative 'utopia/exception_handler'
|
26
25
|
require_relative 'utopia/localization'
|
27
|
-
require_relative 'utopia/
|
28
|
-
require_relative 'utopia/
|
26
|
+
require_relative 'utopia/exceptions'
|
27
|
+
require_relative 'utopia/redirection'
|
29
28
|
require_relative 'utopia/static'
|
30
29
|
|
31
30
|
require_relative 'utopia/tags/deferred'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
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.
|
20
|
+
|
21
|
+
require_relative 'exceptions/handler'
|
22
|
+
require_relative 'exceptions/mailer'
|
23
|
+
|
24
|
+
module Utopia
|
25
|
+
module Exceptions
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
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.
|
20
|
+
|
21
|
+
module Utopia
|
22
|
+
module Exceptions
|
23
|
+
# Catches exceptions and performs an internal redirect.
|
24
|
+
class Handler
|
25
|
+
def initialize(app, location = '/errors/exception')
|
26
|
+
@app = app
|
27
|
+
|
28
|
+
@location = location
|
29
|
+
end
|
30
|
+
|
31
|
+
def freeze
|
32
|
+
@location.freeze
|
33
|
+
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generate a very simple fatal error response. This function should be unlikely to fail. Additionally, it generates a lowest common denominator response which should be suitable as a response to any kind of request. Ideally, this response is also not good or useful for any kind of higher level browser or API client, as this is not a normal error path but one that represents broken behaviour.
|
38
|
+
def fatal_error(env, exception)
|
39
|
+
body = StringIO.new
|
40
|
+
|
41
|
+
write_exception_to_stream(body, env, exception)
|
42
|
+
body.rewind
|
43
|
+
|
44
|
+
return [500, {HTTP::CONTENT_TYPE => "text/plain"}, body]
|
45
|
+
end
|
46
|
+
|
47
|
+
def log_exception(env, exception)
|
48
|
+
# An error has occurred, log it:
|
49
|
+
output = env['rack.errors'] || $stderr
|
50
|
+
write_exception_to_stream(output, env, exception, true)
|
51
|
+
end
|
52
|
+
|
53
|
+
def call(env)
|
54
|
+
begin
|
55
|
+
return @app.call(env)
|
56
|
+
rescue Exception => exception
|
57
|
+
log_exception(env, exception)
|
58
|
+
|
59
|
+
# If the error occurred while accessing the error handler, we finish with a fatal error:
|
60
|
+
if env[Rack::PATH_INFO] == @location
|
61
|
+
return fatal_error(env, exception)
|
62
|
+
else
|
63
|
+
begin
|
64
|
+
# We do an internal redirection to the error location:
|
65
|
+
error_request = env.merge(Rack::PATH_INFO => @location, Rack::REQUEST_METHOD => Rack::GET)
|
66
|
+
error_response = @app.call(error_request)
|
67
|
+
|
68
|
+
return [500, error_response[1], error_response[2]]
|
69
|
+
rescue Exception
|
70
|
+
# If redirection fails, we also finish with a fatal error:
|
71
|
+
return fatal_error(env, exception)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private def write_exception_to_stream(stream, env, exception, include_backtrace = false)
|
78
|
+
buffer = []
|
79
|
+
|
80
|
+
buffer << "While requesting resource #{env[Rack::PATH_INFO].inspect}, a fatal error occurred:"
|
81
|
+
|
82
|
+
while exception != nil
|
83
|
+
buffer << "\t#{exception.class.name}: #{exception.to_s}"
|
84
|
+
|
85
|
+
if include_backtrace
|
86
|
+
exception.backtrace.each do |line|
|
87
|
+
buffer << "\t\t#{line}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
exception = exception.cause
|
92
|
+
end
|
93
|
+
|
94
|
+
# We do this in one go so that lines don't get mixed up.
|
95
|
+
stream.puts buffer.join("\n")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
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.
|
20
|
+
|
21
|
+
require 'net/smtp'
|
22
|
+
require 'mail'
|
23
|
+
|
24
|
+
module Utopia
|
25
|
+
module Exceptions
|
26
|
+
# Catches all exceptions raised from the app it wraps and sends a useful email with the exception, stacktrace, and contents of the environment.
|
27
|
+
class Mailer
|
28
|
+
# A basic local non-authenticated SMTP server.
|
29
|
+
LOCAL_SMTP = [:smtp, {
|
30
|
+
:address => "localhost",
|
31
|
+
:port => 25,
|
32
|
+
:enable_starttls_auto => false
|
33
|
+
}]
|
34
|
+
|
35
|
+
def initialize(app, config = {})
|
36
|
+
@app = app
|
37
|
+
|
38
|
+
@to = config[:to] || "postmaster"
|
39
|
+
@from = config.fetch(:from) {(ENV['USER'] || 'rack') + "@localhost"}
|
40
|
+
@subject = config[:subject] || '%{exception} [PID %{pid} : %{cwd}]'
|
41
|
+
@delivery_method = config.fetch(:delivery_method, LOCAL_SMTP)
|
42
|
+
|
43
|
+
@dump_environment = config.fetch(:dump_environment, false)
|
44
|
+
end
|
45
|
+
|
46
|
+
def call(env)
|
47
|
+
begin
|
48
|
+
return @app.call(env)
|
49
|
+
rescue => exception
|
50
|
+
send_notification exception, env
|
51
|
+
|
52
|
+
raise
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
REQUEST_KEYS = [:ip, :referrer, :path, :user_agent]
|
59
|
+
|
60
|
+
def generate_body(exception, env)
|
61
|
+
io = StringIO.new
|
62
|
+
|
63
|
+
# Dump out useful rack environment variables:
|
64
|
+
request = Rack::Request.new(env)
|
65
|
+
|
66
|
+
io.puts "#{request.request_method} #{request.url}"
|
67
|
+
|
68
|
+
io.puts
|
69
|
+
|
70
|
+
REQUEST_KEYS.each do |key|
|
71
|
+
value = request.send(key)
|
72
|
+
io.puts "request.#{key}: #{value.inspect}"
|
73
|
+
end
|
74
|
+
|
75
|
+
request.params.each do |key, value|
|
76
|
+
io.puts "request.params.#{key}: #{value.inspect}"
|
77
|
+
end
|
78
|
+
|
79
|
+
io.puts
|
80
|
+
|
81
|
+
io.puts "#{exception.class.name}: #{exception.to_s}"
|
82
|
+
|
83
|
+
if exception.respond_to?(:backtrace)
|
84
|
+
io.puts exception.backtrace
|
85
|
+
else
|
86
|
+
io.puts exception.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
return io.string
|
90
|
+
end
|
91
|
+
|
92
|
+
def generate_mail(exception, env)
|
93
|
+
attributes = {
|
94
|
+
exception: exception.class.name,
|
95
|
+
pid: $$,
|
96
|
+
cwd: Dir.getwd,
|
97
|
+
}
|
98
|
+
|
99
|
+
mail = Mail.new(
|
100
|
+
:from => @from,
|
101
|
+
:to => @to,
|
102
|
+
:subject => @subject % attributes
|
103
|
+
)
|
104
|
+
|
105
|
+
mail.text_part = Mail::Part.new
|
106
|
+
mail.text_part.body = generate_body(exception, env)
|
107
|
+
|
108
|
+
if body = extract_body(env) and body.size > 0
|
109
|
+
mail.attachments['body.bin'] = body
|
110
|
+
end
|
111
|
+
|
112
|
+
if @dump_environment
|
113
|
+
mail.attachments['environment.yaml'] = YAML::dump(env)
|
114
|
+
end
|
115
|
+
|
116
|
+
return mail
|
117
|
+
end
|
118
|
+
|
119
|
+
def send_notification(exception, env)
|
120
|
+
mail = generate_mail(exception, env)
|
121
|
+
|
122
|
+
mail.delivery_method(*@delivery_method) if @delivery_method
|
123
|
+
|
124
|
+
mail.deliver
|
125
|
+
rescue => mail_exception
|
126
|
+
$stderr.puts mail_exception.to_s
|
127
|
+
$stderr.puts mail_exception.backtrace
|
128
|
+
end
|
129
|
+
|
130
|
+
def extract_body(env)
|
131
|
+
if io = env['rack.input']
|
132
|
+
io.rewind if io.respond_to?(:rewind)
|
133
|
+
io.read
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
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.
|
20
|
+
|
21
|
+
require_relative 'middleware'
|
22
|
+
|
23
|
+
module Utopia
|
24
|
+
module Redirection
|
25
|
+
class RequestFailure < StandardError
|
26
|
+
def initialize(resource_path, resource_status, error_path, error_status)
|
27
|
+
@resource_path = resource_path
|
28
|
+
@resource_status = resource_status
|
29
|
+
|
30
|
+
@error_path = error_path
|
31
|
+
@error_status = error_status
|
32
|
+
|
33
|
+
super "Requested resource #{@resource_path} resulted in a #{@resource_status} error. Requested error handler #{@error_path} resulted in a #{@error_status} error."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Errors
|
38
|
+
# Maps an error code to a given string
|
39
|
+
def initialize(app, codes = {})
|
40
|
+
@codes = codes
|
41
|
+
|
42
|
+
@app = app
|
43
|
+
end
|
44
|
+
|
45
|
+
def freeze
|
46
|
+
@codes.freeze
|
47
|
+
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def call(env)
|
52
|
+
response = @app.call(env)
|
53
|
+
|
54
|
+
if response[0] >= 400 and location = @codes[response[0]]
|
55
|
+
error_request = env.merge(Rack::PATH_INFO => location, Rack::REQUEST_METHOD => Rack::GET)
|
56
|
+
error_response = @app.call(error_request)
|
57
|
+
|
58
|
+
if error_response[0] >= 400
|
59
|
+
raise RequestFailure.new(env[Rack::PATH_INFO], response[0], location, error_response[0])
|
60
|
+
else
|
61
|
+
# Feed the error code back with the error document:
|
62
|
+
error_response[0] = response[0]
|
63
|
+
return error_response
|
64
|
+
end
|
65
|
+
else
|
66
|
+
return response
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class FileNotFound < Errors
|
72
|
+
def initialize(uri = '/errors/file-not-found')
|
73
|
+
super(404 => uri)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# We cache 301 redirects for 24 hours.
|
78
|
+
DEFAULT_MAX_AGE = 3600*24
|
79
|
+
|
80
|
+
class Redirection
|
81
|
+
def initialize(app, status: 307, max_age: DEFAULT_MAX_AGE)
|
82
|
+
@app = app
|
83
|
+
@status = status
|
84
|
+
@max_age = max_age
|
85
|
+
end
|
86
|
+
|
87
|
+
def freeze
|
88
|
+
@status.freeze
|
89
|
+
@max_age.freeze
|
90
|
+
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
attr :status
|
95
|
+
attr :max_age
|
96
|
+
|
97
|
+
def cache_control
|
98
|
+
# http://jacquesmattheij.com/301-redirects-a-dangerous-one-way-street
|
99
|
+
"max-age=#{self.max_age}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def headers(location)
|
103
|
+
{HTTP::LOCATION => location, HTTP::CACHE_CONTROL => self.cache_control}
|
104
|
+
end
|
105
|
+
|
106
|
+
def redirect(location)
|
107
|
+
return [self.status, self.headers(location), []]
|
108
|
+
end
|
109
|
+
|
110
|
+
def [] path
|
111
|
+
false
|
112
|
+
end
|
113
|
+
|
114
|
+
def call(env)
|
115
|
+
path = env[Rack::PATH_INFO]
|
116
|
+
|
117
|
+
if redirection = self[path]
|
118
|
+
return redirection
|
119
|
+
end
|
120
|
+
|
121
|
+
return @app.call(env)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class DirectoryIndex < Redirection
|
126
|
+
def initialize(app, index = 'index')
|
127
|
+
@app = app
|
128
|
+
@index = 'index'
|
129
|
+
|
130
|
+
super(app)
|
131
|
+
end
|
132
|
+
|
133
|
+
def [] path
|
134
|
+
if path.end_with?('/')
|
135
|
+
return redirect(path + @index)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class Rewrite < Redirection
|
141
|
+
def initialize(app, patterns, status: 301)
|
142
|
+
@patterns = patterns
|
143
|
+
|
144
|
+
super(app, status: status)
|
145
|
+
end
|
146
|
+
|
147
|
+
def [] path
|
148
|
+
if location = @patterns[path]
|
149
|
+
return redirect(location)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class Moved < Redirection
|
155
|
+
def initialize(app, pattern, prefix, status: 301, flatten: false)
|
156
|
+
@app = app
|
157
|
+
|
158
|
+
@pattern = pattern
|
159
|
+
@prefix = prefix
|
160
|
+
@flatten = flatten
|
161
|
+
|
162
|
+
super(app, status: status)
|
163
|
+
end
|
164
|
+
|
165
|
+
def [] path
|
166
|
+
if path.start_with?(@pattern)
|
167
|
+
if @flatten
|
168
|
+
return redirect(@prefix)
|
169
|
+
else
|
170
|
+
return redirect(path.sub(@pattern, @prefix))
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
data/lib/utopia/version.rb
CHANGED
data/setup/site/config.ru
CHANGED
@@ -14,10 +14,11 @@ require 'utopia'
|
|
14
14
|
require 'rack/cache'
|
15
15
|
|
16
16
|
if RACK_ENV == :production
|
17
|
-
use Utopia::
|
18
|
-
use Utopia::
|
17
|
+
use Utopia::Exceptions::Handler
|
18
|
+
use Utopia::Exceptions::Mailer
|
19
19
|
elsif RACK_ENV == :development
|
20
20
|
use Rack::ShowExceptions
|
21
|
+
use Utopia::Static, root: 'public'
|
21
22
|
end
|
22
23
|
|
23
24
|
use Rack::Sendfile
|
@@ -31,16 +32,13 @@ end
|
|
31
32
|
|
32
33
|
use Rack::ContentLength
|
33
34
|
|
34
|
-
use Utopia::
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
errors: {
|
42
|
-
404 => "/errors/file-not-found"
|
43
|
-
}
|
35
|
+
use Utopia::Redirection::Rewrite,
|
36
|
+
'/' => '/welcome/index'
|
37
|
+
|
38
|
+
use Utopia::Redirection::DirectoryIndex
|
39
|
+
|
40
|
+
use Utopia::Redirection::Errors,
|
41
|
+
404 => '/errors/file-not-found'
|
44
42
|
|
45
43
|
use Utopia::Localization,
|
46
44
|
:default_locale => 'en',
|
@@ -52,6 +50,12 @@ use Utopia::Controller,
|
|
52
50
|
|
53
51
|
use Utopia::Static
|
54
52
|
|
53
|
+
if RACK_ENV != :production
|
54
|
+
# Serve static files from public/ when not running in a production environment:
|
55
|
+
use Utopia::Static, root: 'public'
|
56
|
+
end
|
57
|
+
|
58
|
+
# Serve dynamic content
|
55
59
|
use Utopia::Content,
|
56
60
|
cache_templates: (RACK_ENV == :production),
|
57
61
|
tags: {
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 420 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><rect x="0" y="0" width="420" height="56" style="fill:#f79433;"/><rect x="0" y="56" width="420" height="24" style="fill:#4e8dd8;"/><g><path d="M75.145,70.819c2.37,-3.097 4.173,-6.921 5.111,-11.365c0.91,-4.318 1.498,-9.261 1.498,-14.692l0,-44.762l-62.754,0l0,44.762c0,2.628 0.244,5.333 0.407,8.035c0.168,2.782 0.674,5.515 1.345,8.118c0.68,2.644 1.739,5.173 3.067,7.517c1.363,2.405 3.263,4.526 5.609,6.303c2.319,1.755 5.245,3.163 8.677,4.172c1.617,0.478 3.416,1.093 5.354,1.093l13.856,0c3.071,0 5.797,-1.058 8.131,-2.001c4.042,-1.631 7.305,-4.049 9.699,-7.18Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M151.481,18.701l0,-18.701l-62.754,-0.022l0,18.723l22.246,0l0.02,61.299l17.93,0l-0.02,-61.299l22.578,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M229.926,39.999c0,-22.051 -16.979,-39.992 -37.852,-39.992c-20.872,0 -37.851,17.942 -37.851,39.992c0,22.054 16.979,39.994 37.851,39.994c20.873,0 37.852,-17.94 37.852,-39.994Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M269.238,50.909c9.717,0 17.181,-2.066 22.183,-6.395c5.087,-4.399 7.667,-10.942 7.667,-19.575c0,-3.257 -0.393,-5.962 -1.167,-8.476c-0.778,-2.528 -1.883,-4.934 -3.281,-6.814c-1.401,-1.882 -3.098,-3.458 -5.045,-4.703c-1.895,-1.21 -4.003,-2.198 -6.264,-2.943c-2.239,-0.737 -4.64,-1.263 -7.139,-1.56c-2.464,-0.292 -5.016,-0.443 -7.587,-0.443l-29.468,0l0,80l17.93,0l0,-29.091l12.171,0Z" style="fill:#fff;fill-rule:nonzero;"/><rect x="304.879" y="0" width="17.93" height="80" style="fill:#fff;"/><path d="M362.589,0l-29.477,80l75.888,0l-31.247,-80l-15.164,0Z" style="fill:#fff;fill-rule:nonzero;"/></g></g></svg>
|
@@ -1 +1 @@
|
|
1
|
-
This directory is required by Apache/Phusion Passenger.
|
1
|
+
This directory is required by Apache/Phusion Passenger and contains static assets that are typically served using sendfile.
|
@@ -20,33 +20,25 @@
|
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
21
|
# THE SOFTWARE.
|
22
22
|
|
23
|
-
|
24
|
-
require '
|
23
|
+
require_relative '../rack_helper'
|
24
|
+
require 'utopia/exceptions'
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
let(:app) {Rack::Builder.parse_file(File.expand_path('exception_handler_spec.ru', __dir__)).first}
|
26
|
+
RSpec.describe Utopia::Exceptions::Handler do
|
27
|
+
include_context "rack app", File.expand_path("handler_spec.ru", __dir__)
|
28
|
+
|
29
|
+
it "should successfully call the controller method" do
|
30
|
+
# This request will raise an exception, and then redirect to the /exception url which will fail again, and cause a fatal error.
|
31
|
+
get "/blow?fatal=true"
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
expect(last_response.body).to be_include 'fatal error'
|
43
|
-
end
|
33
|
+
expect(last_response.status).to be == 500
|
34
|
+
expect(last_response.headers['Content-Type']).to be == 'text/plain'
|
35
|
+
expect(last_response.body).to be_include 'fatal error'
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should fail with a 500 error" do
|
39
|
+
get "/blow"
|
44
40
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
expect(last_response.status).to be == 500
|
49
|
-
expect(last_response.body).to be_include 'Error Will Robertson'
|
50
|
-
end
|
41
|
+
expect(last_response.status).to be == 500
|
42
|
+
expect(last_response.body).to be_include 'Error Will Robertson'
|
51
43
|
end
|
52
44
|
end
|
File without changes
|
@@ -19,10 +19,18 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require_relative 'rack_helper'
|
22
|
-
require 'utopia/
|
22
|
+
require 'utopia/redirection'
|
23
23
|
|
24
|
-
RSpec.describe Utopia::
|
25
|
-
include_context "rack app", "
|
24
|
+
RSpec.describe Utopia::Redirection do
|
25
|
+
include_context "rack app", "redirection_spec.ru"
|
26
|
+
|
27
|
+
it "should redirect directory to index" do
|
28
|
+
get "/welcome/"
|
29
|
+
|
30
|
+
expect(last_response.status).to be == 307
|
31
|
+
expect(last_response.headers['Location']).to be == '/welcome/index'
|
32
|
+
expect(last_response.headers['Cache-Control']).to include("max-age=86400")
|
33
|
+
end
|
26
34
|
|
27
35
|
it "should be permanently moved" do
|
28
36
|
get "/a"
|
@@ -36,7 +44,7 @@ RSpec.describe Utopia::Redirector do
|
|
36
44
|
get "/"
|
37
45
|
|
38
46
|
expect(last_response.status).to be == 301
|
39
|
-
expect(last_response.headers['Location']).to be == '/
|
47
|
+
expect(last_response.headers['Location']).to be == '/welcome/index'
|
40
48
|
expect(last_response.headers['Cache-Control']).to include("max-age=86400")
|
41
49
|
end
|
42
50
|
|
@@ -48,6 +56,20 @@ RSpec.describe Utopia::Redirector do
|
|
48
56
|
end
|
49
57
|
|
50
58
|
it "should blow up if internal error redirect also fails" do
|
51
|
-
expect{get "/teapot"}.to raise_error Utopia::
|
59
|
+
expect{get "/teapot"}.to raise_error Utopia::Redirection::RequestFailure
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should redirect deep url to top" do
|
63
|
+
get "/hierarchy/a/b/c/d/e"
|
64
|
+
|
65
|
+
expect(last_response.status).to be == 301
|
66
|
+
expect(last_response.headers['Location']).to be == '/hierarchy'
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should get a weird status" do
|
70
|
+
get "/weird"
|
71
|
+
|
72
|
+
expect(last_response.status).to be == 333
|
73
|
+
expect(last_response.headers['Location']).to be == '/status'
|
52
74
|
end
|
53
75
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
use Utopia::Redirection::Rewrite, "/" => "/welcome/index"
|
3
|
+
|
4
|
+
use Utopia::Redirection::DirectoryIndex
|
5
|
+
|
6
|
+
use Utopia::Redirection::Errors,
|
7
|
+
404 => '/error',
|
8
|
+
418 => '/teapot'
|
9
|
+
|
10
|
+
use Utopia::Redirection::Moved, "/a", "/b"
|
11
|
+
use Utopia::Redirection::Moved, "/hierarchy/", "/hierarchy", flatten: true
|
12
|
+
use Utopia::Redirection::Moved, "/weird", "/status", status: 333
|
13
|
+
|
14
|
+
def error_handler(env)
|
15
|
+
request = Rack::Request.new(env)
|
16
|
+
if request.path_info == "/error"
|
17
|
+
[200, {}, ["File not found :("]]
|
18
|
+
elsif request.path_info == "/teapot"
|
19
|
+
[418, {}, ["I'm a teapot!"]]
|
20
|
+
else
|
21
|
+
[404, {}, []]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
run self.method(:error_handler)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: utopia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: trenni
|
@@ -199,7 +199,9 @@ files:
|
|
199
199
|
- lib/utopia/controller/respond.rb
|
200
200
|
- lib/utopia/controller/rewrite.rb
|
201
201
|
- lib/utopia/controller/variables.rb
|
202
|
-
- lib/utopia/
|
202
|
+
- lib/utopia/exceptions.rb
|
203
|
+
- lib/utopia/exceptions/handler.rb
|
204
|
+
- lib/utopia/exceptions/mailer.rb
|
203
205
|
- lib/utopia/extensions/array.rb
|
204
206
|
- lib/utopia/extensions/date.rb
|
205
207
|
- lib/utopia/extensions/rack.rb
|
@@ -210,7 +212,7 @@ files:
|
|
210
212
|
- lib/utopia/middleware.rb
|
211
213
|
- lib/utopia/path.rb
|
212
214
|
- lib/utopia/path/matcher.rb
|
213
|
-
- lib/utopia/
|
215
|
+
- lib/utopia/redirection.rb
|
214
216
|
- lib/utopia/session.rb
|
215
217
|
- lib/utopia/session/encrypted_cookie.rb
|
216
218
|
- lib/utopia/session/lazy_hash.rb
|
@@ -234,16 +236,14 @@ files:
|
|
234
236
|
- setup/site/lib/readme.txt
|
235
237
|
- setup/site/pages/_heading.xnode
|
236
238
|
- setup/site/pages/_page.xnode
|
237
|
-
- setup/site/pages/_static/icon.png
|
238
|
-
- setup/site/pages/_static/site.css
|
239
|
-
- setup/site/pages/_static/utopia-background.svg
|
240
|
-
- setup/site/pages/_static/utopia.svg
|
241
|
-
- setup/site/pages/errors/controller.rb
|
242
239
|
- setup/site/pages/errors/exception.xnode
|
243
240
|
- setup/site/pages/errors/file-not-found.xnode
|
244
241
|
- setup/site/pages/links.yaml
|
245
242
|
- setup/site/pages/welcome/index.xnode
|
246
|
-
- setup/site/public/_static
|
243
|
+
- setup/site/public/_static/icon.png
|
244
|
+
- setup/site/public/_static/site.css
|
245
|
+
- setup/site/public/_static/utopia-background.svg
|
246
|
+
- setup/site/public/_static/utopia.svg
|
247
247
|
- setup/site/public/readme.txt
|
248
248
|
- setup/site/tmp/readme.txt
|
249
249
|
- spec/utopia/content/link_spec.rb
|
@@ -291,9 +291,9 @@ files:
|
|
291
291
|
- spec/utopia/controller/rewrite_spec.rb
|
292
292
|
- spec/utopia/controller/sequence_spec.rb
|
293
293
|
- spec/utopia/controller/variables_spec.rb
|
294
|
-
- spec/utopia/
|
295
|
-
- spec/utopia/
|
296
|
-
- spec/utopia/
|
294
|
+
- spec/utopia/exceptions/handler_spec.rb
|
295
|
+
- spec/utopia/exceptions/handler_spec.ru
|
296
|
+
- spec/utopia/exceptions/handler_spec/controller.rb
|
297
297
|
- spec/utopia/extensions_spec.rb
|
298
298
|
- spec/utopia/http/status_spec.rb
|
299
299
|
- spec/utopia/locale_spec.rb
|
@@ -316,8 +316,8 @@ files:
|
|
316
316
|
- spec/utopia/path_spec.rb
|
317
317
|
- spec/utopia/rack_helper.rb
|
318
318
|
- spec/utopia/rack_spec.rb
|
319
|
-
- spec/utopia/
|
320
|
-
- spec/utopia/
|
319
|
+
- spec/utopia/redirection_spec.rb
|
320
|
+
- spec/utopia/redirection_spec.ru
|
321
321
|
- spec/utopia/session_spec.rb
|
322
322
|
- spec/utopia/session_spec.ru
|
323
323
|
- spec/utopia/static_spec.rb
|
@@ -392,9 +392,9 @@ test_files:
|
|
392
392
|
- spec/utopia/controller/rewrite_spec.rb
|
393
393
|
- spec/utopia/controller/sequence_spec.rb
|
394
394
|
- spec/utopia/controller/variables_spec.rb
|
395
|
-
- spec/utopia/
|
396
|
-
- spec/utopia/
|
397
|
-
- spec/utopia/
|
395
|
+
- spec/utopia/exceptions/handler_spec.rb
|
396
|
+
- spec/utopia/exceptions/handler_spec.ru
|
397
|
+
- spec/utopia/exceptions/handler_spec/controller.rb
|
398
398
|
- spec/utopia/extensions_spec.rb
|
399
399
|
- spec/utopia/http/status_spec.rb
|
400
400
|
- spec/utopia/locale_spec.rb
|
@@ -417,8 +417,8 @@ test_files:
|
|
417
417
|
- spec/utopia/path_spec.rb
|
418
418
|
- spec/utopia/rack_helper.rb
|
419
419
|
- spec/utopia/rack_spec.rb
|
420
|
-
- spec/utopia/
|
421
|
-
- spec/utopia/
|
420
|
+
- spec/utopia/redirection_spec.rb
|
421
|
+
- spec/utopia/redirection_spec.ru
|
422
422
|
- spec/utopia/session_spec.rb
|
423
423
|
- spec/utopia/session_spec.ru
|
424
424
|
- spec/utopia/static_spec.rb
|
@@ -1,104 +0,0 @@
|
|
1
|
-
# Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
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.
|
20
|
-
|
21
|
-
require_relative 'middleware'
|
22
|
-
|
23
|
-
require 'trenni/strings'
|
24
|
-
|
25
|
-
module Utopia
|
26
|
-
class ExceptionHandler
|
27
|
-
def initialize(app, location)
|
28
|
-
@app = app
|
29
|
-
|
30
|
-
@location = location
|
31
|
-
|
32
|
-
self.freeze
|
33
|
-
end
|
34
|
-
|
35
|
-
def freeze
|
36
|
-
@location.freeze
|
37
|
-
|
38
|
-
super
|
39
|
-
end
|
40
|
-
|
41
|
-
private def write_exception_to_stream(stream, env, exception, include_backtrace = false)
|
42
|
-
buffer = []
|
43
|
-
|
44
|
-
buffer << "While requesting resource #{env[Rack::PATH_INFO].inspect}, a fatal error occurred:"
|
45
|
-
|
46
|
-
while exception != nil
|
47
|
-
buffer << "\t#{exception.class.name}: #{exception.to_s}"
|
48
|
-
|
49
|
-
if include_backtrace
|
50
|
-
exception.backtrace.each do |line|
|
51
|
-
buffer << "\t\t#{line}"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
exception = exception.cause
|
56
|
-
end
|
57
|
-
|
58
|
-
# We do this in one go so that lines don't get mixed up.
|
59
|
-
stream.puts buffer.join("\n")
|
60
|
-
end
|
61
|
-
|
62
|
-
# Generate a very simple fatal error response. This function should be unlikely to fail. Additionally, it generates a lowest common denominator response which should be suitable as a response to any kind of request. Ideally, this response is also not good or useful for any kind of higher level browser or API client, as this is not a normal error path but one that represents broken behaviour.
|
63
|
-
def fatal_error(env, exception)
|
64
|
-
body = StringIO.new
|
65
|
-
|
66
|
-
write_exception_to_stream(body, env, exception)
|
67
|
-
body.rewind
|
68
|
-
|
69
|
-
return [500, {HTTP::CONTENT_TYPE => "text/plain"}, body]
|
70
|
-
end
|
71
|
-
|
72
|
-
def log_exception(env, exception)
|
73
|
-
# An error has occurred, log it:
|
74
|
-
output = env['rack.errors'] || $stderr
|
75
|
-
write_exception_to_stream(output, env, exception, true)
|
76
|
-
end
|
77
|
-
|
78
|
-
def redirect(env, exception)
|
79
|
-
response = @app.call(env.merge(Rack::PATH_INFO => @location, Rack::REQUEST_METHOD => Rack::GET))
|
80
|
-
|
81
|
-
return [500, response[1], response[2]]
|
82
|
-
end
|
83
|
-
|
84
|
-
def call(env)
|
85
|
-
begin
|
86
|
-
return @app.call(env)
|
87
|
-
rescue Exception => exception
|
88
|
-
log_exception(env, exception)
|
89
|
-
|
90
|
-
# If the error occurred while accessing the error handler, we finish with a fatal error:
|
91
|
-
if env[Rack::PATH_INFO] == @location
|
92
|
-
return fatal_error(env, exception)
|
93
|
-
else
|
94
|
-
# If redirection fails, we also finish with a fatal error:
|
95
|
-
begin
|
96
|
-
return redirect(env, exception)
|
97
|
-
rescue
|
98
|
-
return fatal_error(env, exception)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
data/lib/utopia/redirector.rb
DELETED
@@ -1,164 +0,0 @@
|
|
1
|
-
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
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.
|
20
|
-
|
21
|
-
require_relative 'middleware'
|
22
|
-
|
23
|
-
module Utopia
|
24
|
-
class FailedRequestError < StandardError
|
25
|
-
def initialize(resource_path, resource_status, error_path, error_status)
|
26
|
-
@resource_path = resource_path
|
27
|
-
@resource_status = resource_status
|
28
|
-
|
29
|
-
@error_path = error_path
|
30
|
-
@error_status = error_status
|
31
|
-
end
|
32
|
-
|
33
|
-
def to_s
|
34
|
-
"Requested resource #{@resource_path} resulted in a #{@resource_status} error. Requested error handler #{@error_path} resulted in a #{@error_status} error."
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
class Redirector
|
39
|
-
# This redirects directories to the directory + 'index'
|
40
|
-
DIRECTORY_INDEX = [/^(.*)\/$/, lambda{|prefix| [307, {HTTP::LOCATION => "#{prefix}index"}, []]}].freeze
|
41
|
-
|
42
|
-
# Redirects a whole source tree to a destination tree, given by the roots.
|
43
|
-
def self.moved(source_root, destination_root, max_age = 3600*24)
|
44
|
-
return [/^#{Regexp.escape(source_root)}(.*)$/, lambda{|match| destination_root + match[1]}]
|
45
|
-
end
|
46
|
-
|
47
|
-
def self.starts_with(source_root, destination_uri)
|
48
|
-
return [/^#{Regexp.escape(source_root)}/, destination_uri]
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def normalize_strings(strings)
|
54
|
-
normalized = {}
|
55
|
-
|
56
|
-
strings.each_pair do |key, value|
|
57
|
-
if Array === key
|
58
|
-
key.each { |s| normalized[s] = value }
|
59
|
-
else
|
60
|
-
normalized[key] = value
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
return normalized
|
65
|
-
end
|
66
|
-
|
67
|
-
def normalize_patterns(patterns)
|
68
|
-
normalized = []
|
69
|
-
|
70
|
-
patterns.each do |pattern|
|
71
|
-
*keys, uri = *pattern
|
72
|
-
|
73
|
-
keys.each do |key|
|
74
|
-
normalized.push([key, uri])
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
return normalized
|
79
|
-
end
|
80
|
-
|
81
|
-
public
|
82
|
-
|
83
|
-
def initialize(app, **options)
|
84
|
-
@app = app
|
85
|
-
|
86
|
-
@strings = options[:strings] || {}
|
87
|
-
@patterns = options[:patterns] || []
|
88
|
-
|
89
|
-
@patterns.collect! do |rule|
|
90
|
-
if Symbol === rule[0]
|
91
|
-
self.class.send(*rule)
|
92
|
-
else
|
93
|
-
rule
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
@strings = normalize_strings(@strings)
|
98
|
-
@patterns = normalize_patterns(@patterns)
|
99
|
-
|
100
|
-
@errors = options[:errors]
|
101
|
-
|
102
|
-
self.freeze
|
103
|
-
end
|
104
|
-
|
105
|
-
def freeze
|
106
|
-
@strings.freeze
|
107
|
-
@patterns.freeze
|
108
|
-
@errors.freeze
|
109
|
-
|
110
|
-
super
|
111
|
-
end
|
112
|
-
|
113
|
-
def cache_control(max_age)
|
114
|
-
# http://jacquesmattheij.com/301-redirects-a-dangerous-one-way-street
|
115
|
-
"max-age=#{max_age}"
|
116
|
-
end
|
117
|
-
|
118
|
-
# We cache 301 redirects for 24 hours.
|
119
|
-
DEFAULT_MAX_AGE = 3600*24
|
120
|
-
|
121
|
-
def redirect(uri, match_data, status: 301, max_age: DEFAULT_MAX_AGE)
|
122
|
-
cache_control = self.cache_control(max_age)
|
123
|
-
|
124
|
-
if uri.respond_to? :call
|
125
|
-
uri = uri.call(match_data)
|
126
|
-
end
|
127
|
-
|
128
|
-
return [status, {HTTP::LOCATION => uri.to_s, HTTP::CACHE_CONTROL => cache_control}, []]
|
129
|
-
end
|
130
|
-
|
131
|
-
def call(env)
|
132
|
-
base_path = env[Rack::PATH_INFO]
|
133
|
-
|
134
|
-
if uri = @strings[base_path]
|
135
|
-
return redirect(@strings[base_path], base_path)
|
136
|
-
end
|
137
|
-
|
138
|
-
@patterns.each do |pattern, uri|
|
139
|
-
if match_data = pattern.match(base_path)
|
140
|
-
result = redirect(uri, match_data)
|
141
|
-
|
142
|
-
return result if result != nil
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
response = @app.call(env)
|
147
|
-
|
148
|
-
if @errors && response[0] >= 400 && uri = @errors[response[0]]
|
149
|
-
error_request = env.merge(Rack::PATH_INFO => uri, Rack::REQUEST_METHOD => Rack::GET)
|
150
|
-
error_response = @app.call(error_request)
|
151
|
-
|
152
|
-
if error_response[0] >= 400
|
153
|
-
raise FailedRequestError.new(env[Rack::PATH_INFO], response[0], uri, error_response[0])
|
154
|
-
else
|
155
|
-
# Feed the error code back with the error document
|
156
|
-
error_response[0] = response[0]
|
157
|
-
return error_response
|
158
|
-
end
|
159
|
-
else
|
160
|
-
return response
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
end
|
@@ -1 +0,0 @@
|
|
1
|
-
../../../../materials/utopia.svg
|
data/setup/site/public/_static
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
../pages/_static
|
@@ -1,26 +0,0 @@
|
|
1
|
-
|
2
|
-
use Utopia::Redirector,
|
3
|
-
patterns: [
|
4
|
-
Utopia::Redirector::DIRECTORY_INDEX,
|
5
|
-
[:moved, "/a", "/b"],
|
6
|
-
],
|
7
|
-
strings: {
|
8
|
-
'/' => '/c',
|
9
|
-
},
|
10
|
-
errors: {
|
11
|
-
404 => "/error",
|
12
|
-
418 => "/teapot",
|
13
|
-
}
|
14
|
-
|
15
|
-
def error_handler(env)
|
16
|
-
request = Rack::Request.new(env)
|
17
|
-
if request.path_info == "/error"
|
18
|
-
[200, {}, ["File not found :("]]
|
19
|
-
elsif request.path_info == "/teapot"
|
20
|
-
[418, {}, ["I'm a teapot!"]]
|
21
|
-
else
|
22
|
-
[404, {}, []]
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
run self.method(:error_handler)
|