otto 1.2.0 → 1.3.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/.gitignore +1 -0
- data/.rubocop.yml +364 -20
- data/Gemfile +1 -1
- data/Gemfile.lock +76 -46
- data/examples/basic/app.rb +20 -20
- data/examples/basic/config.ru +1 -1
- data/examples/dynamic_pages/app.rb +30 -30
- data/examples/dynamic_pages/config.ru +1 -1
- data/examples/security_features/app.rb +90 -87
- data/examples/security_features/config.ru +19 -17
- data/lib/otto/design_system.rb +22 -22
- data/lib/otto/helpers/request.rb +3 -5
- data/lib/otto/helpers/response.rb +10 -38
- data/lib/otto/route.rb +12 -12
- data/lib/otto/security/config.rb +34 -38
- data/lib/otto/security/csrf.rb +23 -27
- data/lib/otto/security/validator.rb +33 -30
- data/lib/otto/static.rb +1 -1
- data/lib/otto/version.rb +1 -25
- data/lib/otto.rb +97 -60
- data/otto.gemspec +9 -10
- metadata +1 -23
- data/.rubocop_todo.yml +0 -152
- data/VERSION.yml +0 -5
@@ -5,31 +5,32 @@ require 'cgi'
|
|
5
5
|
|
6
6
|
class Otto
|
7
7
|
module Security
|
8
|
+
# ValidationMiddleware provides input validation and sanitization for web requests
|
8
9
|
class ValidationMiddleware
|
9
10
|
# Character validation patterns
|
10
|
-
INVALID_CHARACTERS = /[\x00-\x1f\x7f-\xff]/n
|
11
|
-
NULL_BYTE
|
11
|
+
INVALID_CHARACTERS = /[\x00-\x1f\x7f-\xff]/n
|
12
|
+
NULL_BYTE = /\0/
|
12
13
|
|
13
14
|
DANGEROUS_PATTERNS = [
|
14
15
|
/<script[^>]*>/i, # Script tags
|
15
16
|
/javascript:/i, # JavaScript protocol
|
16
17
|
/data:.*base64/i, # Data URLs with base64
|
17
|
-
/on\w+\s*=/i,
|
18
|
+
/on\w+\s*=/i, # Event handlers
|
18
19
|
/expression\s*\(/i, # CSS expressions
|
19
|
-
/url\s*\(/i,
|
20
|
-
NULL_BYTE,
|
21
|
-
INVALID_CHARACTERS # Control characters and extended ASCII
|
20
|
+
/url\s*\(/i, # CSS url() functions
|
21
|
+
NULL_BYTE, # Null bytes
|
22
|
+
INVALID_CHARACTERS, # Control characters and extended ASCII
|
22
23
|
].freeze
|
23
24
|
|
24
25
|
SQL_INJECTION_PATTERNS = [
|
25
26
|
/('|(\\')|(;)|(\\)|(--)|(%27)|(%3B)|(%3D))/i,
|
26
27
|
/(union|select|insert|update|delete|drop|create|alter|exec|execute)/i,
|
27
28
|
/(or|and)\s+\w+\s*=\s*\w+/i,
|
28
|
-
/\d+\s*(=|>|<|>=|<=|<>|!=)\s*\d+/i
|
29
|
+
/\d+\s*(=|>|<|>=|<=|<>|!=)\s*\d+/i,
|
29
30
|
].freeze
|
30
31
|
|
31
32
|
def initialize(app, config = nil)
|
32
|
-
@app
|
33
|
+
@app = app
|
33
34
|
@config = config || Otto::Security::Config.new
|
34
35
|
end
|
35
36
|
|
@@ -46,17 +47,21 @@ class Otto
|
|
46
47
|
validate_content_type(request)
|
47
48
|
|
48
49
|
# Validate and sanitize parameters
|
49
|
-
|
50
|
+
begin
|
51
|
+
validate_parameters(request) if request.params
|
52
|
+
rescue Rack::QueryParser::QueryLimitError => ex
|
53
|
+
# Handle Rack's built-in query parsing limits
|
54
|
+
raise Otto::Security::ValidationError, "Parameter structure too complex: #{ex.message}"
|
55
|
+
end
|
50
56
|
|
51
57
|
# Validate headers
|
52
58
|
validate_headers(request)
|
53
59
|
|
54
60
|
@app.call(env)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
return request_too_large_response(e.message)
|
61
|
+
rescue Otto::Security::ValidationError => ex
|
62
|
+
validation_error_response(ex.message)
|
63
|
+
rescue Otto::Security::RequestTooLargeError => ex
|
64
|
+
request_too_large_response(ex.message)
|
60
65
|
end
|
61
66
|
end
|
62
67
|
|
@@ -76,7 +81,7 @@ class Otto
|
|
76
81
|
'application/x-shockwave-flash',
|
77
82
|
'application/x-silverlight-app',
|
78
83
|
'text/vbscript',
|
79
|
-
'application/vbscript'
|
84
|
+
'application/vbscript',
|
80
85
|
]
|
81
86
|
|
82
87
|
if dangerous_types.any? { |type| content_type.downcase.include?(type) }
|
@@ -148,14 +153,14 @@ class Otto
|
|
148
153
|
# Check for dangerous patterns
|
149
154
|
DANGEROUS_PATTERNS.each do |pattern|
|
150
155
|
if value.match?(pattern)
|
151
|
-
raise Otto::Security::ValidationError,
|
156
|
+
raise Otto::Security::ValidationError, 'Dangerous content detected in parameter'
|
152
157
|
end
|
153
158
|
end
|
154
159
|
|
155
160
|
# Check for SQL injection patterns
|
156
161
|
SQL_INJECTION_PATTERNS.each do |pattern|
|
157
162
|
if value.match?(pattern)
|
158
|
-
raise Otto::Security::ValidationError,
|
163
|
+
raise Otto::Security::ValidationError, 'Potential SQL injection detected'
|
159
164
|
end
|
160
165
|
end
|
161
166
|
|
@@ -169,9 +174,7 @@ class Otto
|
|
169
174
|
|
170
175
|
# Additional sanitization for common attack vectors
|
171
176
|
sanitized = sanitized.gsub(/<!--.*?-->/m, '') # Remove HTML comments
|
172
|
-
sanitized
|
173
|
-
|
174
|
-
sanitized
|
177
|
+
sanitized.gsub(/<!\[CDATA\[.*?\]\]>/m, '') # Remove CDATA sections
|
175
178
|
end
|
176
179
|
|
177
180
|
def validate_headers(request)
|
@@ -197,13 +200,13 @@ class Otto
|
|
197
200
|
# Validate User-Agent length
|
198
201
|
user_agent = request.env['HTTP_USER_AGENT']
|
199
202
|
if user_agent && user_agent.length > 1000
|
200
|
-
raise Otto::Security::ValidationError,
|
203
|
+
raise Otto::Security::ValidationError, 'User-Agent header too long'
|
201
204
|
end
|
202
205
|
|
203
206
|
# Validate Referer header
|
204
207
|
referer = request.env['HTTP_REFERER']
|
205
208
|
if referer && referer.length > 2000
|
206
|
-
raise Otto::Security::ValidationError,
|
209
|
+
raise Otto::Security::ValidationError, 'Referer header too long'
|
207
210
|
end
|
208
211
|
end
|
209
212
|
|
@@ -212,9 +215,9 @@ class Otto
|
|
212
215
|
400,
|
213
216
|
{
|
214
217
|
'content-type' => 'application/json',
|
215
|
-
'content-length' => validation_error_body(message).bytesize.to_s
|
218
|
+
'content-length' => validation_error_body(message).bytesize.to_s,
|
216
219
|
},
|
217
|
-
[validation_error_body(message)]
|
220
|
+
[validation_error_body(message)],
|
218
221
|
]
|
219
222
|
end
|
220
223
|
|
@@ -223,9 +226,9 @@ class Otto
|
|
223
226
|
413,
|
224
227
|
{
|
225
228
|
'content-type' => 'application/json',
|
226
|
-
'content-length' => request_too_large_body(message).bytesize.to_s
|
229
|
+
'content-length' => request_too_large_body(message).bytesize.to_s,
|
227
230
|
},
|
228
|
-
[request_too_large_body(message)]
|
231
|
+
[request_too_large_body(message)],
|
229
232
|
]
|
230
233
|
end
|
231
234
|
|
@@ -233,7 +236,7 @@ class Otto
|
|
233
236
|
require 'json'
|
234
237
|
{
|
235
238
|
error: 'Validation failed',
|
236
|
-
message: message
|
239
|
+
message: message,
|
237
240
|
}.to_json
|
238
241
|
end
|
239
242
|
|
@@ -241,7 +244,7 @@ class Otto
|
|
241
244
|
require 'json'
|
242
245
|
{
|
243
246
|
error: 'Request too large',
|
244
|
-
message: message
|
247
|
+
message: message,
|
245
248
|
}.to_json
|
246
249
|
end
|
247
250
|
end
|
@@ -261,7 +264,7 @@ class Otto
|
|
261
264
|
unless allow_html
|
262
265
|
ValidationMiddleware::DANGEROUS_PATTERNS.each do |pattern|
|
263
266
|
if input_str.match?(pattern)
|
264
|
-
raise Otto::Security::ValidationError,
|
267
|
+
raise Otto::Security::ValidationError, 'Dangerous content detected'
|
265
268
|
end
|
266
269
|
end
|
267
270
|
end
|
@@ -269,7 +272,7 @@ class Otto
|
|
269
272
|
# Always check for SQL injection
|
270
273
|
ValidationMiddleware::SQL_INJECTION_PATTERNS.each do |pattern|
|
271
274
|
if input_str.match?(pattern)
|
272
|
-
raise Otto::Security::ValidationError,
|
275
|
+
raise Otto::Security::ValidationError, 'Potential SQL injection detected'
|
273
276
|
end
|
274
277
|
end
|
275
278
|
|
data/lib/otto/static.rb
CHANGED
data/lib/otto/version.rb
CHANGED
@@ -1,29 +1,5 @@
|
|
1
1
|
# lib/otto/version.rb
|
2
2
|
|
3
3
|
class Otto
|
4
|
-
|
5
|
-
#
|
6
|
-
module VERSION
|
7
|
-
def self.to_a
|
8
|
-
load_config
|
9
|
-
version = [@version[:MAJOR], @version[:MINOR], @version[:PATCH]]
|
10
|
-
version << @version[:PRE] unless @version.fetch(:PRE, nil).to_s.empty?
|
11
|
-
version
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.to_s
|
15
|
-
to_a.join('.')
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.inspect
|
19
|
-
to_s
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.load_config
|
23
|
-
return if @version
|
24
|
-
|
25
|
-
require 'yaml'
|
26
|
-
@version = YAML.load_file(File.join(__dir__, '..', '..', 'VERSION.yml'))
|
27
|
-
end
|
28
|
-
end
|
4
|
+
VERSION = '1.3.0'.freeze
|
29
5
|
end
|
data/lib/otto.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
+
require 'json'
|
1
2
|
require 'logger'
|
2
3
|
require 'securerandom'
|
4
|
+
require 'uri'
|
3
5
|
|
4
6
|
require 'rack/request'
|
5
7
|
require 'rack/response'
|
6
8
|
require 'rack/utils'
|
7
|
-
require 'addressable/uri'
|
8
9
|
|
9
10
|
require_relative 'otto/route'
|
10
11
|
require_relative 'otto/static'
|
@@ -39,23 +40,23 @@ require_relative 'otto/security/validator'
|
|
39
40
|
class Otto
|
40
41
|
LIB_HOME = __dir__ unless defined?(Otto::LIB_HOME)
|
41
42
|
|
42
|
-
@debug
|
43
|
+
@debug = ENV['OTTO_DEBUG'] == 'true'
|
43
44
|
@logger = Logger.new($stdout, Logger::INFO)
|
44
45
|
|
45
46
|
attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option, :static_route, :security_config
|
46
47
|
attr_accessor :not_found, :server_error, :middleware_stack
|
47
48
|
|
48
49
|
def initialize(path = nil, opts = {})
|
49
|
-
@routes_static
|
50
|
-
@routes
|
51
|
-
@routes_literal
|
50
|
+
@routes_static = { GET: {} }
|
51
|
+
@routes = { GET: [] }
|
52
|
+
@routes_literal = { GET: {} }
|
52
53
|
@route_definitions = {}
|
53
|
-
@option
|
54
|
+
@option = {
|
54
55
|
public: nil,
|
55
|
-
locale: 'en'
|
56
|
+
locale: 'en',
|
56
57
|
}.merge(opts)
|
57
|
-
@security_config
|
58
|
-
@middleware_stack
|
58
|
+
@security_config = Otto::Security::Config.new
|
59
|
+
@middleware_stack = []
|
59
60
|
|
60
61
|
# Configure security based on options
|
61
62
|
configure_security(opts)
|
@@ -72,15 +73,15 @@ class Otto
|
|
72
73
|
|
73
74
|
raw = File.readlines(path).select { |line| line =~ /^\w/ }.collect { |line| line.strip.split(/\s+/) }
|
74
75
|
raw.each do |entry|
|
75
|
-
verb, path, definition
|
76
|
-
route
|
77
|
-
route.otto
|
78
|
-
path_clean
|
79
|
-
@route_definitions[route.definition]
|
76
|
+
verb, path, definition = *entry
|
77
|
+
route = Otto::Route.new verb, path, definition
|
78
|
+
route.otto = self
|
79
|
+
path_clean = path.gsub(%r{/$}, '')
|
80
|
+
@route_definitions[route.definition] = route
|
80
81
|
Otto.logger.debug "route: #{route.pattern}" if Otto.debug
|
81
|
-
@routes[route.verb]
|
82
|
+
@routes[route.verb] ||= []
|
82
83
|
@routes[route.verb] << route
|
83
|
-
@routes_literal[route.verb]
|
84
|
+
@routes_literal[route.verb] ||= {}
|
84
85
|
@routes_literal[route.verb][path_clean] = route
|
85
86
|
rescue StandardError
|
86
87
|
Otto.logger.error "Bad route in #{path}: #{entry}"
|
@@ -97,7 +98,7 @@ class Otto
|
|
97
98
|
return false unless File.directory?(public_dir)
|
98
99
|
|
99
100
|
# Clean the requested path - remove null bytes and normalize
|
100
|
-
clean_path = path.
|
101
|
+
clean_path = path.delete("\0").strip
|
101
102
|
return false if clean_path.empty?
|
102
103
|
|
103
104
|
# Join and expand to get the full resolved path
|
@@ -111,13 +112,13 @@ class Otto
|
|
111
112
|
File.readable?(requested_path) &&
|
112
113
|
!File.directory?(requested_path) &&
|
113
114
|
(File.owned?(requested_path) || File.grpowned?(requested_path))
|
114
|
-
end
|
115
|
+
end
|
115
116
|
|
116
117
|
def safe_dir?(path)
|
117
118
|
return false if path.nil? || path.empty?
|
118
119
|
|
119
120
|
# Clean and expand the path
|
120
|
-
clean_path = path.
|
121
|
+
clean_path = path.delete("\0").strip
|
121
122
|
return false if clean_path.empty?
|
122
123
|
|
123
124
|
expanded_path = File.expand_path(clean_path)
|
@@ -131,9 +132,9 @@ end
|
|
131
132
|
def add_static_path(path)
|
132
133
|
return unless safe_file?(path)
|
133
134
|
|
134
|
-
base_path
|
135
|
+
base_path = File.split(path).first
|
135
136
|
# Files in the root directory can refer to themselves
|
136
|
-
base_path
|
137
|
+
base_path = path if base_path == '/'
|
137
138
|
File.join(option[:public], base_path)
|
138
139
|
Otto.logger.debug "new static route: #{base_path} (#{path})" if Otto.debug
|
139
140
|
routes_static[:GET][base_path] = base_path
|
@@ -141,46 +142,46 @@ end
|
|
141
142
|
|
142
143
|
def call(env)
|
143
144
|
# Apply middleware stack
|
144
|
-
app =
|
145
|
-
@middleware_stack.
|
145
|
+
app = ->(e) { handle_request(e) }
|
146
|
+
@middleware_stack.reverse_each do |middleware|
|
146
147
|
app = middleware.new(app, @security_config)
|
147
148
|
end
|
148
149
|
|
149
150
|
begin
|
150
151
|
app.call(env)
|
151
|
-
rescue StandardError =>
|
152
|
-
handle_error(
|
152
|
+
rescue StandardError => ex
|
153
|
+
handle_error(ex, env)
|
153
154
|
end
|
154
155
|
end
|
155
156
|
|
156
157
|
def handle_request(env)
|
157
|
-
locale
|
158
|
+
locale = determine_locale env
|
158
159
|
env['rack.locale'] = locale
|
159
|
-
@static_route
|
160
|
-
path_info
|
161
|
-
path_info
|
160
|
+
@static_route ||= Rack::Files.new(option[:public]) if option[:public] && safe_dir?(option[:public])
|
161
|
+
path_info = Rack::Utils.unescape(env['PATH_INFO'])
|
162
|
+
path_info = '/' if path_info.to_s.empty?
|
162
163
|
|
163
164
|
begin
|
164
165
|
path_info_clean = path_info
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
rescue ArgumentError =>
|
166
|
+
.encode(
|
167
|
+
'UTF-8', # Target encoding
|
168
|
+
invalid: :replace, # Replace invalid byte sequences
|
169
|
+
undef: :replace, # Replace characters undefined in UTF-8
|
170
|
+
replace: '', # Use empty string for replacement
|
171
|
+
)
|
172
|
+
.gsub(%r{/$}, '') # Remove trailing slash, if present
|
173
|
+
rescue ArgumentError => ex
|
173
174
|
# Log the error but don't expose details
|
174
|
-
Otto.logger.error
|
175
|
-
Otto.logger.debug "[Otto.handle_request] Error details: #{
|
175
|
+
Otto.logger.error '[Otto.handle_request] Path encoding error'
|
176
|
+
Otto.logger.debug "[Otto.handle_request] Error details: #{ex.message}" if Otto.debug
|
176
177
|
# Set a default value or use the original path_info
|
177
178
|
path_info_clean = path_info
|
178
179
|
end
|
179
180
|
|
180
|
-
base_path
|
181
|
+
base_path = File.split(path_info).first
|
181
182
|
# Files in the root directory can refer to themselves
|
182
|
-
base_path
|
183
|
-
http_verb
|
183
|
+
base_path = path_info if base_path == '/'
|
184
|
+
http_verb = env['REQUEST_METHOD'].upcase.to_sym
|
184
185
|
literal_routes = routes_literal[http_verb] || {}
|
185
186
|
literal_routes.merge! routes_literal[:GET] if http_verb == :HEAD
|
186
187
|
if static_route && http_verb == :GET && routes_static[:GET].member?(base_path)
|
@@ -195,15 +196,15 @@ end
|
|
195
196
|
routes_static[:GET][base_path] = base_path
|
196
197
|
static_route.call(env)
|
197
198
|
else
|
198
|
-
extra_params
|
199
|
-
found_route
|
200
|
-
valid_routes
|
199
|
+
extra_params = {}
|
200
|
+
found_route = nil
|
201
|
+
valid_routes = routes[http_verb] || []
|
201
202
|
valid_routes.push(*routes[:GET]) if http_verb == :HEAD
|
202
203
|
valid_routes.each do |route|
|
203
204
|
# Otto.logger.debug " request: #{http_verb} #{path_info} (trying route: #{route.verb} #{route.pattern})"
|
204
205
|
next unless (match = route.pattern.match(path_info))
|
205
206
|
|
206
|
-
values
|
207
|
+
values = match.captures.to_a
|
207
208
|
# The first capture returned is the entire matched string b/c
|
208
209
|
# we wrapped the entire regex in parens. We don't need it to
|
209
210
|
# the full match.
|
@@ -222,7 +223,7 @@ end
|
|
222
223
|
else
|
223
224
|
{}
|
224
225
|
end
|
225
|
-
found_route
|
226
|
+
found_route = route
|
226
227
|
break
|
227
228
|
end
|
228
229
|
found_route ||= literal_routes['/404']
|
@@ -245,7 +246,7 @@ end
|
|
245
246
|
return if route.nil?
|
246
247
|
|
247
248
|
local_params = params.clone
|
248
|
-
local_path
|
249
|
+
local_path = route.path.clone
|
249
250
|
|
250
251
|
local_params.each_pair do |k, v|
|
251
252
|
next unless local_path.match(":#{k}")
|
@@ -253,16 +254,19 @@ end
|
|
253
254
|
local_path.gsub!(":#{k}", v.to_s)
|
254
255
|
local_params.delete(k)
|
255
256
|
end
|
256
|
-
|
257
|
-
uri
|
258
|
-
|
257
|
+
|
258
|
+
uri = URI::HTTP.new(nil, nil, nil, nil, nil, local_path, nil, nil, nil)
|
259
|
+
unless local_params.empty?
|
260
|
+
query_string = local_params.map { |k, v| "#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" }.join('&')
|
261
|
+
uri.query = query_string
|
262
|
+
end
|
259
263
|
uri.to_s
|
260
264
|
end
|
261
265
|
|
262
266
|
def determine_locale(env)
|
263
267
|
accept_langs = env['HTTP_ACCEPT_LANGUAGE']
|
264
268
|
accept_langs = option[:locale] if accept_langs.to_s.empty?
|
265
|
-
locales
|
269
|
+
locales = []
|
266
270
|
unless accept_langs.empty?
|
267
271
|
locales = accept_langs.split(',').map do |l|
|
268
272
|
l += ';q=1.0' unless /;q=\d+(?:\.\d+)?$/.match?(l)
|
@@ -282,7 +286,7 @@ end
|
|
282
286
|
# @param middleware [Class] The middleware class to add
|
283
287
|
# @param args [Array] Additional arguments for the middleware
|
284
288
|
# @param block [Proc] Optional block for middleware configuration
|
285
|
-
def use(middleware,
|
289
|
+
def use(middleware, *, &)
|
286
290
|
@middleware_stack << middleware
|
287
291
|
end
|
288
292
|
|
@@ -343,7 +347,7 @@ end
|
|
343
347
|
# @param include_subdomains [Boolean] Apply to all subdomains (default: true)
|
344
348
|
# @example
|
345
349
|
# otto.enable_hsts!(max_age: 86400, include_subdomains: false)
|
346
|
-
def enable_hsts!(max_age:
|
350
|
+
def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
|
347
351
|
@security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
|
348
352
|
end
|
349
353
|
|
@@ -396,8 +400,12 @@ end
|
|
396
400
|
Otto.logger.error "[#{error_id}] #{error.class}: #{error.message}"
|
397
401
|
Otto.logger.debug "[#{error_id}] Backtrace: #{error.backtrace.join("\n")}" if Otto.debug
|
398
402
|
|
399
|
-
#
|
400
|
-
|
403
|
+
# Parse request for content negotiation
|
404
|
+
begin
|
405
|
+
Rack::Request.new(env)
|
406
|
+
rescue StandardError
|
407
|
+
nil
|
408
|
+
end
|
401
409
|
literal_routes = @routes_literal[:GET] || {}
|
402
410
|
|
403
411
|
# Try custom 500 route first
|
@@ -405,11 +413,17 @@ end
|
|
405
413
|
begin
|
406
414
|
env['otto.error_id'] = error_id
|
407
415
|
return found_route.call(env)
|
408
|
-
rescue StandardError =>
|
409
|
-
Otto.logger.error "[#{error_id}] Error in custom error handler: #{
|
416
|
+
rescue StandardError => ex
|
417
|
+
Otto.logger.error "[#{error_id}] Error in custom error handler: #{ex.message}"
|
410
418
|
end
|
411
419
|
end
|
412
420
|
|
421
|
+
# Content negotiation for built-in error response
|
422
|
+
accept_header = env['HTTP_ACCEPT'].to_s
|
423
|
+
if accept_header.include?('application/json')
|
424
|
+
return json_error_response(error_id)
|
425
|
+
end
|
426
|
+
|
413
427
|
# Fallback to built-in error response
|
414
428
|
@server_error || secure_error_response(error_id)
|
415
429
|
end
|
@@ -418,12 +432,35 @@ end
|
|
418
432
|
body = if Otto.env?(:dev, :development)
|
419
433
|
"Server error (ID: #{error_id}). Check logs for details."
|
420
434
|
else
|
421
|
-
|
435
|
+
'An error occurred. Please try again later.'
|
422
436
|
end
|
423
437
|
|
424
438
|
headers = {
|
425
439
|
'content-type' => 'text/plain',
|
426
|
-
'content-length' => body.bytesize.to_s
|
440
|
+
'content-length' => body.bytesize.to_s,
|
441
|
+
}.merge(@security_config.security_headers)
|
442
|
+
|
443
|
+
[500, headers, [body]]
|
444
|
+
end
|
445
|
+
|
446
|
+
def json_error_response(error_id)
|
447
|
+
error_data = if Otto.env?(:dev, :development)
|
448
|
+
{
|
449
|
+
error: 'Internal Server Error',
|
450
|
+
message: 'Server error occurred. Check logs for details.',
|
451
|
+
error_id: error_id,
|
452
|
+
}
|
453
|
+
else
|
454
|
+
{
|
455
|
+
error: 'Internal Server Error',
|
456
|
+
message: 'An error occurred. Please try again later.',
|
457
|
+
}
|
458
|
+
end
|
459
|
+
|
460
|
+
body = JSON.generate(error_data)
|
461
|
+
headers = {
|
462
|
+
'content-type' => 'application/json',
|
463
|
+
'content-length' => body.bytesize.to_s,
|
427
464
|
}.merge(@security_config.security_headers)
|
428
465
|
|
429
466
|
[500, headers, [body]]
|
data/otto.gemspec
CHANGED
@@ -3,17 +3,17 @@
|
|
3
3
|
require_relative 'lib/otto/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name
|
7
|
-
spec.version
|
8
|
-
spec.summary
|
9
|
-
spec.description
|
10
|
-
spec.email
|
11
|
-
spec.authors
|
12
|
-
spec.license
|
13
|
-
spec.files
|
6
|
+
spec.name = 'otto'
|
7
|
+
spec.version = Otto::VERSION.to_s
|
8
|
+
spec.summary = 'Auto-define your rack-apps in plaintext.'
|
9
|
+
spec.description = "Otto: #{spec.summary}"
|
10
|
+
spec.email = 'gems@solutious.com'
|
11
|
+
spec.authors = ['Delano Mandelbaum']
|
12
|
+
spec.license = 'MIT'
|
13
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
14
14
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
15
15
|
end
|
16
|
-
spec.homepage
|
16
|
+
spec.homepage = 'https://github.com/delano/otto'
|
17
17
|
spec.require_paths = ['lib']
|
18
18
|
|
19
19
|
spec.required_ruby_version = ['>= 3.4', '< 4.0']
|
@@ -21,7 +21,6 @@ Gem::Specification.new do |spec|
|
|
21
21
|
# https://github.com/delano/otto/security/dependabot/5
|
22
22
|
spec.add_dependency 'rexml', '>= 3.3.6'
|
23
23
|
|
24
|
-
spec.add_dependency 'addressable', '~> 2.2', '< 3'
|
25
24
|
spec.add_dependency 'rack', '~> 3.1', '< 4.0'
|
26
25
|
spec.add_dependency 'rack-parser', '~> 0.7'
|
27
26
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: otto
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
@@ -23,26 +23,6 @@ dependencies:
|
|
23
23
|
- - ">="
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: 3.3.6
|
26
|
-
- !ruby/object:Gem::Dependency
|
27
|
-
name: addressable
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
29
|
-
requirements:
|
30
|
-
- - "~>"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '2.2'
|
33
|
-
- - "<"
|
34
|
-
- !ruby/object:Gem::Version
|
35
|
-
version: '3'
|
36
|
-
type: :runtime
|
37
|
-
prerelease: false
|
38
|
-
version_requirements: !ruby/object:Gem::Requirement
|
39
|
-
requirements:
|
40
|
-
- - "~>"
|
41
|
-
- !ruby/object:Gem::Version
|
42
|
-
version: '2.2'
|
43
|
-
- - "<"
|
44
|
-
- !ruby/object:Gem::Version
|
45
|
-
version: '3'
|
46
26
|
- !ruby/object:Gem::Dependency
|
47
27
|
name: rack
|
48
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,12 +66,10 @@ files:
|
|
86
66
|
- ".gitignore"
|
87
67
|
- ".rspec"
|
88
68
|
- ".rubocop.yml"
|
89
|
-
- ".rubocop_todo.yml"
|
90
69
|
- Gemfile
|
91
70
|
- Gemfile.lock
|
92
71
|
- LICENSE.txt
|
93
72
|
- README.md
|
94
|
-
- VERSION.yml
|
95
73
|
- examples/basic/app.rb
|
96
74
|
- examples/basic/config.ru
|
97
75
|
- examples/basic/routes
|