otto 2.0.0.pre8 → 2.0.0.pre10
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/.github/workflows/ci.yml +1 -1
- data/.github/workflows/claude-code-review.yml +1 -1
- data/.github/workflows/claude.yml +1 -1
- data/.github/workflows/code-smells.yml +2 -2
- data/CHANGELOG.rst +73 -35
- data/CLAUDE.md +44 -0
- data/Gemfile.lock +7 -7
- data/README.md +20 -0
- data/docs/.gitignore +2 -0
- data/docs/modern-authentication-authorization-landscape.md +558 -0
- data/docs/multi-strategy-authentication-design.md +1401 -0
- data/lib/otto/core/error_handler.rb +104 -10
- data/lib/otto/core/freezable.rb +0 -2
- data/lib/otto/core/helper_registry.rb +135 -0
- data/lib/otto/core/lifecycle_hooks.rb +63 -0
- data/lib/otto/core/middleware_management.rb +70 -0
- data/lib/otto/core/middleware_stack.rb +12 -8
- data/lib/otto/core/router.rb +25 -31
- data/lib/otto/core.rb +3 -0
- data/lib/otto/errors.rb +92 -0
- data/lib/otto/helpers.rb +2 -2
- data/lib/otto/locale/middleware.rb +1 -1
- data/lib/otto/mcp/core.rb +33 -0
- data/lib/otto/mcp/protocol.rb +1 -1
- data/lib/otto/mcp/rate_limiting.rb +6 -2
- data/lib/otto/mcp/schema_validation.rb +2 -2
- data/lib/otto/mcp.rb +1 -0
- data/lib/otto/privacy/core.rb +82 -0
- data/lib/otto/privacy.rb +1 -0
- data/lib/otto/{helpers/request.rb → request.rb} +17 -6
- data/lib/otto/{helpers/response.rb → response.rb} +20 -7
- data/lib/otto/response_handlers/json.rb +1 -3
- data/lib/otto/response_handlers/view.rb +1 -1
- data/lib/otto/route.rb +2 -4
- data/lib/otto/route_handlers/base.rb +88 -5
- data/lib/otto/route_handlers/class_method.rb +9 -67
- data/lib/otto/route_handlers/instance_method.rb +10 -57
- data/lib/otto/route_handlers/lambda.rb +2 -2
- data/lib/otto/route_handlers/logic_class.rb +85 -90
- data/lib/otto/security/authentication/auth_strategy.rb +2 -2
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +1 -1
- data/lib/otto/security/authentication/strategy_result.rb +9 -9
- data/lib/otto/security/authorization_error.rb +1 -1
- data/lib/otto/security/config.rb +3 -3
- data/lib/otto/security/core.rb +167 -0
- data/lib/otto/security/middleware/csrf_middleware.rb +1 -1
- data/lib/otto/security/middleware/validation_middleware.rb +1 -1
- data/lib/otto/security/rate_limiter.rb +7 -3
- data/lib/otto/security.rb +1 -0
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +29 -404
- metadata +12 -3
data/lib/otto.rb
CHANGED
|
@@ -11,12 +11,15 @@ require 'rack/request'
|
|
|
11
11
|
require 'rack/response'
|
|
12
12
|
require 'rack/utils'
|
|
13
13
|
|
|
14
|
+
require_relative 'otto/request'
|
|
15
|
+
require_relative 'otto/response'
|
|
14
16
|
require_relative 'otto/route_definition'
|
|
15
17
|
require_relative 'otto/route'
|
|
16
18
|
require_relative 'otto/static'
|
|
17
19
|
require_relative 'otto/helpers'
|
|
18
20
|
require_relative 'otto/response_handlers'
|
|
19
21
|
require_relative 'otto/route_handlers'
|
|
22
|
+
require_relative 'otto/errors'
|
|
20
23
|
require_relative 'otto/locale'
|
|
21
24
|
require_relative 'otto/mcp'
|
|
22
25
|
require_relative 'otto/core'
|
|
@@ -53,6 +56,12 @@ class Otto
|
|
|
53
56
|
include Otto::Core::Configuration
|
|
54
57
|
include Otto::Core::ErrorHandler
|
|
55
58
|
include Otto::Core::UriGenerator
|
|
59
|
+
include Otto::Core::HelperRegistry
|
|
60
|
+
include Otto::Core::MiddlewareManagement
|
|
61
|
+
include Otto::Core::LifecycleHooks
|
|
62
|
+
include Otto::Security::Core
|
|
63
|
+
include Otto::Privacy::Core
|
|
64
|
+
include Otto::MCP::Core
|
|
56
65
|
|
|
57
66
|
LIB_HOME = __dir__ unless defined?(Otto::LIB_HOME)
|
|
58
67
|
|
|
@@ -67,7 +76,7 @@ class Otto
|
|
|
67
76
|
attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option,
|
|
68
77
|
:static_route, :security_config, :locale_config, :auth_config,
|
|
69
78
|
:route_handler_factory, :mcp_server, :security, :middleware,
|
|
70
|
-
:error_handlers
|
|
79
|
+
:error_handlers, :request_class, :response_class
|
|
71
80
|
attr_accessor :not_found, :server_error
|
|
72
81
|
|
|
73
82
|
def initialize(path = nil, opts = {})
|
|
@@ -79,9 +88,10 @@ class Otto
|
|
|
79
88
|
load(path) unless path.nil?
|
|
80
89
|
super()
|
|
81
90
|
|
|
82
|
-
# Auto-register
|
|
83
|
-
# This allows Logic classes
|
|
84
|
-
|
|
91
|
+
# Auto-register all Otto framework error classes
|
|
92
|
+
# This allows Logic classes and framework code to raise appropriate errors
|
|
93
|
+
# without requiring manual registration in implementing projects
|
|
94
|
+
register_framework_errors
|
|
85
95
|
|
|
86
96
|
# Build the middleware app once after all initialization is complete
|
|
87
97
|
build_app!
|
|
@@ -114,7 +124,7 @@ class Otto
|
|
|
114
124
|
|
|
115
125
|
# Track request timing for lifecycle hooks
|
|
116
126
|
start_time = Otto::Utils.now_in_μs
|
|
117
|
-
request =
|
|
127
|
+
request = @request_class.new(env)
|
|
118
128
|
response_raw = nil
|
|
119
129
|
|
|
120
130
|
begin
|
|
@@ -127,9 +137,9 @@ class Otto
|
|
|
127
137
|
unless @request_complete_callbacks.empty?
|
|
128
138
|
begin
|
|
129
139
|
duration = Otto::Utils.now_in_μs - start_time
|
|
130
|
-
# Wrap response tuple in
|
|
131
|
-
# Otto's hook API should provide nice abstractions like
|
|
132
|
-
response =
|
|
140
|
+
# Wrap response tuple in Otto::Response for developer-friendly API
|
|
141
|
+
# Otto's hook API should provide nice abstractions like Otto::Request/Response
|
|
142
|
+
response = @response_class.new(response_raw[2], response_raw[0], response_raw[1])
|
|
133
143
|
@request_complete_callbacks.each do |callback|
|
|
134
144
|
callback.call(request, response, duration)
|
|
135
145
|
end
|
|
@@ -143,402 +153,8 @@ class Otto
|
|
|
143
153
|
response_raw
|
|
144
154
|
end
|
|
145
155
|
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
# IMPORTANT: If you have routes with auth requirements, you MUST add session
|
|
150
|
-
# middleware to your middleware stack BEFORE Otto processes requests.
|
|
151
|
-
#
|
|
152
|
-
# Session middleware is required for RouteAuthWrapper to correctly persist
|
|
153
|
-
# session changes during authentication. Common options include:
|
|
154
|
-
# - Rack::Session::Cookie (requires rack-session gem)
|
|
155
|
-
# - Rack::Session::Pool
|
|
156
|
-
# - Rack::Session::Memcache
|
|
157
|
-
# - Any Rack-compatible session middleware
|
|
158
|
-
#
|
|
159
|
-
# Example:
|
|
160
|
-
# use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
|
|
161
|
-
# otto = Otto.new('routes.txt')
|
|
162
|
-
#
|
|
163
|
-
def build_app!
|
|
164
|
-
base_app = method(:handle_request)
|
|
165
|
-
@app = @middleware.wrap(base_app, @security_config)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# Middleware Management
|
|
169
|
-
def use(middleware, ...)
|
|
170
|
-
ensure_not_frozen!
|
|
171
|
-
@middleware.add(middleware, ...)
|
|
172
|
-
|
|
173
|
-
# NOTE: If build_app! is triggered during a request (via use() or
|
|
174
|
-
# middleware_stack=), the @app instance variable could be swapped
|
|
175
|
-
# mid-request in a multi-threaded environment.
|
|
176
|
-
|
|
177
|
-
build_app! if @app # Rebuild app if already initialized
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Compatibility method for existing tests
|
|
181
|
-
def middleware_stack
|
|
182
|
-
@middleware.middleware_list
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Compatibility method for existing tests
|
|
186
|
-
def middleware_stack=(stack)
|
|
187
|
-
@middleware.clear!
|
|
188
|
-
Array(stack).each { |middleware| @middleware.add(middleware) }
|
|
189
|
-
build_app! if @app # Rebuild app if already initialized
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Compatibility method for middleware detection
|
|
193
|
-
def middleware_enabled?(middleware_class)
|
|
194
|
-
@middleware.includes?(middleware_class)
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Security Configuration Methods
|
|
198
|
-
|
|
199
|
-
# Enable CSRF protection for POST, PUT, DELETE, and PATCH requests.
|
|
200
|
-
# This will automatically add CSRF tokens to HTML forms and validate
|
|
201
|
-
# them on unsafe HTTP methods.
|
|
202
|
-
#
|
|
203
|
-
# @example
|
|
204
|
-
# otto.enable_csrf_protection!
|
|
205
|
-
def enable_csrf_protection!
|
|
206
|
-
ensure_not_frozen!
|
|
207
|
-
return if @middleware.includes?(Otto::Security::Middleware::CSRFMiddleware)
|
|
208
|
-
|
|
209
|
-
@security_config.enable_csrf_protection!
|
|
210
|
-
use Otto::Security::Middleware::CSRFMiddleware
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Enable request validation including input sanitization, size limits,
|
|
214
|
-
# and protection against XSS and SQL injection attacks.
|
|
215
|
-
#
|
|
216
|
-
# @example
|
|
217
|
-
# otto.enable_request_validation!
|
|
218
|
-
def enable_request_validation!
|
|
219
|
-
ensure_not_frozen!
|
|
220
|
-
return if @middleware.includes?(Otto::Security::Middleware::ValidationMiddleware)
|
|
221
|
-
|
|
222
|
-
@security_config.input_validation = true
|
|
223
|
-
use Otto::Security::Middleware::ValidationMiddleware
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Enable rate limiting to protect against abuse and DDoS attacks.
|
|
227
|
-
# This will automatically add rate limiting rules based on client IP.
|
|
228
|
-
#
|
|
229
|
-
# @param options [Hash] Rate limiting configuration options
|
|
230
|
-
# @option options [Integer] :requests_per_minute Maximum requests per minute per IP (default: 100)
|
|
231
|
-
# @option options [Hash] :custom_rules Custom rate limiting rules
|
|
232
|
-
# @example
|
|
233
|
-
# otto.enable_rate_limiting!(requests_per_minute: 50)
|
|
234
|
-
def enable_rate_limiting!(options = {})
|
|
235
|
-
ensure_not_frozen!
|
|
236
|
-
return if @middleware.includes?(Otto::Security::Middleware::RateLimitMiddleware)
|
|
237
|
-
|
|
238
|
-
@security.configure_rate_limiting(options)
|
|
239
|
-
use Otto::Security::Middleware::RateLimitMiddleware
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
# Add a custom rate limiting rule.
|
|
243
|
-
#
|
|
244
|
-
# @param name [String, Symbol] Rule name
|
|
245
|
-
# @param options [Hash] Rule configuration
|
|
246
|
-
# @option options [Integer] :limit Maximum requests
|
|
247
|
-
# @option options [Integer] :period Time period in seconds (default: 60)
|
|
248
|
-
# @option options [Proc] :condition Optional condition proc that receives request
|
|
249
|
-
# @example
|
|
250
|
-
# otto.add_rate_limit_rule('uploads', limit: 5, period: 300, condition: ->(req) { req.post? && req.path.include?('upload') })
|
|
251
|
-
def add_rate_limit_rule(name, options)
|
|
252
|
-
ensure_not_frozen!
|
|
253
|
-
@security_config.rate_limiting_config[:custom_rules][name.to_s] = options
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# Add a trusted proxy server for accurate client IP detection.
|
|
257
|
-
# Only requests from trusted proxies will have their forwarded headers honored.
|
|
258
|
-
#
|
|
259
|
-
# @param proxy [String, Regexp] IP address, CIDR range, or regex pattern
|
|
260
|
-
# @example
|
|
261
|
-
# otto.add_trusted_proxy('10.0.0.0/8')
|
|
262
|
-
# otto.add_trusted_proxy(/^172\.16\./)
|
|
263
|
-
def add_trusted_proxy(proxy)
|
|
264
|
-
ensure_not_frozen!
|
|
265
|
-
@security_config.add_trusted_proxy(proxy)
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Set custom security headers that will be added to all responses.
|
|
269
|
-
# These merge with the default security headers.
|
|
270
|
-
#
|
|
271
|
-
# @param headers [Hash] Hash of header name => value pairs
|
|
272
|
-
# @example
|
|
273
|
-
# otto.set_security_headers({
|
|
274
|
-
# 'content-security-policy' => "default-src 'self'",
|
|
275
|
-
# 'strict-transport-security' => 'max-age=31536000'
|
|
276
|
-
# })
|
|
277
|
-
def set_security_headers(headers)
|
|
278
|
-
ensure_not_frozen!
|
|
279
|
-
@security_config.security_headers.merge!(headers)
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Enable HTTP Strict Transport Security (HSTS) header.
|
|
283
|
-
# WARNING: This can make your domain inaccessible if HTTPS is not properly
|
|
284
|
-
# configured. Only enable this when you're certain HTTPS is working correctly.
|
|
285
|
-
#
|
|
286
|
-
# @param max_age [Integer] Maximum age in seconds (default: 1 year)
|
|
287
|
-
# @param include_subdomains [Boolean] Apply to all subdomains (default: true)
|
|
288
|
-
# @example
|
|
289
|
-
# otto.enable_hsts!(max_age: 86400, include_subdomains: false)
|
|
290
|
-
def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
|
|
291
|
-
ensure_not_frozen!
|
|
292
|
-
@security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
# Enable Content Security Policy (CSP) header to prevent XSS attacks.
|
|
296
|
-
# The default policy only allows resources from the same origin.
|
|
297
|
-
#
|
|
298
|
-
# @param policy [String] CSP policy string (default: "default-src 'self'")
|
|
299
|
-
# @example
|
|
300
|
-
# otto.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")
|
|
301
|
-
def enable_csp!(policy = "default-src 'self'")
|
|
302
|
-
ensure_not_frozen!
|
|
303
|
-
@security_config.enable_csp!(policy)
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
# Enable X-Frame-Options header to prevent clickjacking attacks.
|
|
307
|
-
#
|
|
308
|
-
# @param option [String] Frame options: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'
|
|
309
|
-
# @example
|
|
310
|
-
# otto.enable_frame_protection!('DENY')
|
|
311
|
-
def enable_frame_protection!(option = 'SAMEORIGIN')
|
|
312
|
-
ensure_not_frozen!
|
|
313
|
-
@security_config.enable_frame_protection!(option)
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
# Enable Content Security Policy (CSP) with nonce support for dynamic header generation.
|
|
317
|
-
# This enables the res.send_csp_headers response helper method.
|
|
318
|
-
#
|
|
319
|
-
# @param debug [Boolean] Enable debug logging for CSP headers (default: false)
|
|
320
|
-
# @example
|
|
321
|
-
# otto.enable_csp_with_nonce!(debug: true)
|
|
322
|
-
def enable_csp_with_nonce!(debug: false)
|
|
323
|
-
ensure_not_frozen!
|
|
324
|
-
@security_config.enable_csp_with_nonce!(debug: debug)
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
# Add a single authentication strategy
|
|
328
|
-
#
|
|
329
|
-
# @param name [String] Strategy name
|
|
330
|
-
# @param strategy [Otto::Security::Authentication::AuthStrategy] Strategy instance
|
|
331
|
-
# @example
|
|
332
|
-
# otto.add_auth_strategy('custom', MyCustomStrategy.new)
|
|
333
|
-
# Add an authentication strategy with a registered name
|
|
334
|
-
#
|
|
335
|
-
# This is the primary public API for registering authentication strategies.
|
|
336
|
-
# The name you provide here will be available as `strategy_result.strategy_name`
|
|
337
|
-
# in your application code, making it easy to identify which strategy authenticated
|
|
338
|
-
# the current request.
|
|
339
|
-
#
|
|
340
|
-
# Also available via Otto::Security::Configurator for consolidated security config.
|
|
341
|
-
#
|
|
342
|
-
# @param name [String, Symbol] Strategy name (e.g., 'session', 'api_key', 'jwt')
|
|
343
|
-
# @param strategy [AuthStrategy] Strategy instance
|
|
344
|
-
# @example
|
|
345
|
-
# otto.add_auth_strategy('session', SessionStrategy.new(session_key: 'user_id'))
|
|
346
|
-
# otto.add_auth_strategy('api_key', APIKeyStrategy.new)
|
|
347
|
-
# @raise [ArgumentError] if strategy name already registered
|
|
348
|
-
def add_auth_strategy(name, strategy)
|
|
349
|
-
ensure_not_frozen!
|
|
350
|
-
# Ensure auth_config is initialized (handles edge case where it might be nil)
|
|
351
|
-
@auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' } if @auth_config.nil?
|
|
352
|
-
|
|
353
|
-
# Strict mode: Detect strategy name collisions
|
|
354
|
-
if @auth_config[:auth_strategies].key?(name)
|
|
355
|
-
raise ArgumentError, "Authentication strategy '#{name}' is already registered"
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
@auth_config[:auth_strategies][name] = strategy
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
# Register an error handler for expected business logic errors
|
|
362
|
-
#
|
|
363
|
-
# This allows you to handle known error conditions (like missing resources,
|
|
364
|
-
# expired data, rate limits) without logging them as unhandled 500 errors.
|
|
365
|
-
#
|
|
366
|
-
# @param error_class [Class, String] The exception class or class name to handle
|
|
367
|
-
# @param status [Integer] HTTP status code to return (default: 500)
|
|
368
|
-
# @param log_level [Symbol] Log level for expected errors (:info, :warn, :error)
|
|
369
|
-
# @param handler [Proc] Optional block to customize error response
|
|
370
|
-
#
|
|
371
|
-
# @example Basic usage with status code
|
|
372
|
-
# otto.register_error_handler(Onetime::MissingSecret, status: 404, log_level: :info)
|
|
373
|
-
# otto.register_error_handler(Onetime::SecretExpired, status: 410, log_level: :info)
|
|
374
|
-
#
|
|
375
|
-
# @example With custom response handler
|
|
376
|
-
# otto.register_error_handler(Onetime::RateLimited, status: 429, log_level: :warn) do |error, req|
|
|
377
|
-
# {
|
|
378
|
-
# error: 'Rate limit exceeded',
|
|
379
|
-
# retry_after: error.retry_after,
|
|
380
|
-
# message: error.message
|
|
381
|
-
# }
|
|
382
|
-
# end
|
|
383
|
-
#
|
|
384
|
-
# @example Using string class names (for lazy loading)
|
|
385
|
-
# otto.register_error_handler('Onetime::MissingSecret', status: 404, log_level: :info)
|
|
386
|
-
#
|
|
387
|
-
def register_error_handler(error_class, status: 500, log_level: :info, &handler)
|
|
388
|
-
ensure_not_frozen!
|
|
389
|
-
|
|
390
|
-
# Normalize error class to string for consistent lookup
|
|
391
|
-
error_class_name = error_class.is_a?(String) ? error_class : error_class.name
|
|
392
|
-
|
|
393
|
-
@error_handlers[error_class_name] = {
|
|
394
|
-
status: status,
|
|
395
|
-
log_level: log_level,
|
|
396
|
-
handler: handler
|
|
397
|
-
}
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
# Disable IP privacy to access original IP addresses
|
|
401
|
-
#
|
|
402
|
-
# IMPORTANT: By default, Otto masks public IP addresses for privacy.
|
|
403
|
-
# Private/localhost IPs (127.0.0.0/8, 10.0.0.0/8, etc.) are never masked.
|
|
404
|
-
# Only disable this if you need access to original public IPs.
|
|
405
|
-
#
|
|
406
|
-
# When disabled:
|
|
407
|
-
# - env['REMOTE_ADDR'] contains the real IP address
|
|
408
|
-
# - env['otto.original_ip'] also contains the real IP
|
|
409
|
-
# - No PrivateFingerprint is created
|
|
410
|
-
#
|
|
411
|
-
# @example
|
|
412
|
-
# otto.disable_ip_privacy!
|
|
413
|
-
def disable_ip_privacy!
|
|
414
|
-
ensure_not_frozen!
|
|
415
|
-
@security_config.ip_privacy_config.disable!
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
# Enable full IP privacy (mask ALL IPs including private/localhost)
|
|
419
|
-
#
|
|
420
|
-
# By default, Otto exempts private and localhost IPs from masking for
|
|
421
|
-
# better development experience. Call this method to mask ALL IPs
|
|
422
|
-
# regardless of type.
|
|
423
|
-
#
|
|
424
|
-
# @example Enable full privacy (mask all IPs)
|
|
425
|
-
# otto = Otto.new(routes_file)
|
|
426
|
-
# otto.enable_full_ip_privacy!
|
|
427
|
-
# # Now 127.0.0.1 → 127.0.0.0, 192.168.1.100 → 192.168.1.0
|
|
428
|
-
#
|
|
429
|
-
# @return [void]
|
|
430
|
-
# @raise [FrozenError] if called after configuration is frozen
|
|
431
|
-
def enable_full_ip_privacy!
|
|
432
|
-
ensure_not_frozen!
|
|
433
|
-
@security_config.ip_privacy_config.mask_private_ips = true
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
# Register a callback to be executed after each request completes
|
|
437
|
-
#
|
|
438
|
-
# Instance-level request completion callbacks allow each Otto instance
|
|
439
|
-
# to have its own isolated set of callbacks, preventing duplicate
|
|
440
|
-
# invocations in multi-app architectures (e.g., Rack::URLMap).
|
|
441
|
-
#
|
|
442
|
-
# The callback receives three arguments:
|
|
443
|
-
# - request: Rack::Request object
|
|
444
|
-
# - response: Rack::Response object (wrapping the response tuple)
|
|
445
|
-
# - duration: Request processing duration in microseconds
|
|
446
|
-
#
|
|
447
|
-
# @example Basic usage
|
|
448
|
-
# otto = Otto.new(routes_file)
|
|
449
|
-
# otto.on_request_complete do |req, res, duration|
|
|
450
|
-
# logger.info "Request completed", path: req.path, duration: duration
|
|
451
|
-
# end
|
|
452
|
-
#
|
|
453
|
-
# @example Multi-app architecture
|
|
454
|
-
# # App 1: Core Web Application
|
|
455
|
-
# core_router = Otto.new
|
|
456
|
-
# core_router.on_request_complete do |req, res, duration|
|
|
457
|
-
# logger.info "Core app request", path: req.path
|
|
458
|
-
# end
|
|
459
|
-
#
|
|
460
|
-
# # App 2: API Application
|
|
461
|
-
# api_router = Otto.new
|
|
462
|
-
# api_router.on_request_complete do |req, res, duration|
|
|
463
|
-
# logger.info "API request", path: req.path
|
|
464
|
-
# end
|
|
465
|
-
#
|
|
466
|
-
# # Each callback only fires for its respective Otto instance
|
|
467
|
-
#
|
|
468
|
-
# @yield [request, response, duration] Block to execute after each request
|
|
469
|
-
# @yieldparam request [Rack::Request] The request object
|
|
470
|
-
# @yieldparam response [Rack::Response] The response object
|
|
471
|
-
# @yieldparam duration [Integer] Duration in microseconds
|
|
472
|
-
# @return [self] Returns self for method chaining
|
|
473
|
-
# @raise [FrozenError] if called after configuration is frozen
|
|
474
|
-
def on_request_complete(&block)
|
|
475
|
-
ensure_not_frozen!
|
|
476
|
-
@request_complete_callbacks << block if block_given?
|
|
477
|
-
self
|
|
478
|
-
end
|
|
479
|
-
|
|
480
|
-
# Get registered request completion callbacks (for internal use)
|
|
481
|
-
#
|
|
482
|
-
# @api private
|
|
483
|
-
# @return [Array<Proc>] Array of registered callback blocks
|
|
484
|
-
attr_reader :request_complete_callbacks
|
|
485
|
-
|
|
486
|
-
# Configure IP privacy settings
|
|
487
|
-
#
|
|
488
|
-
# Privacy is enabled by default. Use this method to customize privacy
|
|
489
|
-
# behavior without disabling it entirely.
|
|
490
|
-
#
|
|
491
|
-
# @param octet_precision [Integer] Number of octets to mask (1 or 2, default: 1)
|
|
492
|
-
# @param hash_rotation [Integer] Seconds between key rotation (default: 86400)
|
|
493
|
-
# @param geo [Boolean] Enable geo-location resolution (default: true)
|
|
494
|
-
# @param redis [Redis] Redis connection for multi-server atomic key generation
|
|
495
|
-
#
|
|
496
|
-
# @example Mask 2 octets instead of 1
|
|
497
|
-
# otto.configure_ip_privacy(octet_precision: 2)
|
|
498
|
-
#
|
|
499
|
-
# @example Disable geo-location
|
|
500
|
-
# otto.configure_ip_privacy(geo: false)
|
|
501
|
-
#
|
|
502
|
-
# @example Custom hash rotation
|
|
503
|
-
# otto.configure_ip_privacy(hash_rotation: 24.hours)
|
|
504
|
-
#
|
|
505
|
-
# @example Multi-server with Redis
|
|
506
|
-
# redis = Redis.new(url: ENV['REDIS_URL'])
|
|
507
|
-
# otto.configure_ip_privacy(redis: redis)
|
|
508
|
-
def configure_ip_privacy(octet_precision: nil, hash_rotation: nil, geo: nil, redis: nil)
|
|
509
|
-
ensure_not_frozen!
|
|
510
|
-
config = @security_config.ip_privacy_config
|
|
511
|
-
|
|
512
|
-
config.octet_precision = octet_precision if octet_precision
|
|
513
|
-
config.hash_rotation_period = hash_rotation if hash_rotation
|
|
514
|
-
config.geo_enabled = geo unless geo.nil?
|
|
515
|
-
config.instance_variable_set(:@redis, redis) if redis
|
|
516
|
-
|
|
517
|
-
# Validate configuration
|
|
518
|
-
config.validate!
|
|
519
|
-
end
|
|
520
|
-
|
|
521
|
-
# Enable MCP (Model Context Protocol) server support
|
|
522
|
-
#
|
|
523
|
-
# @param options [Hash] MCP configuration options
|
|
524
|
-
# @option options [Boolean] :http Enable HTTP endpoint (default: true)
|
|
525
|
-
# @option options [Boolean] :stdio Enable STDIO communication (default: false)
|
|
526
|
-
# @option options [String] :endpoint HTTP endpoint path (default: '/_mcp')
|
|
527
|
-
# @example
|
|
528
|
-
# otto.enable_mcp!(http: true, endpoint: '/api/mcp')
|
|
529
|
-
def enable_mcp!(options = {})
|
|
530
|
-
ensure_not_frozen!
|
|
531
|
-
@mcp_server ||= Otto::MCP::Server.new(self)
|
|
532
|
-
|
|
533
|
-
@mcp_server.enable!(options)
|
|
534
|
-
Otto.logger.info '[MCP] Enabled MCP server' if Otto.debug
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
# Check if MCP is enabled
|
|
538
|
-
# @return [Boolean]
|
|
539
|
-
def mcp_enabled?
|
|
540
|
-
@mcp_server&.enabled?
|
|
541
|
-
end
|
|
156
|
+
# Security, Privacy, MCP, Middleware, HelperRegistry, and LifecycleHooks
|
|
157
|
+
# methods are provided by their respective Core modules (see includes above)
|
|
542
158
|
|
|
543
159
|
private
|
|
544
160
|
|
|
@@ -556,6 +172,15 @@ class Otto
|
|
|
556
172
|
@request_complete_callbacks = [] # Instance-level request completion callbacks
|
|
557
173
|
@error_handlers = {} # Registered error handlers for expected errors
|
|
558
174
|
|
|
175
|
+
# Initialize helper module registries
|
|
176
|
+
@request_helper_modules = []
|
|
177
|
+
@response_helper_modules = []
|
|
178
|
+
|
|
179
|
+
# Finalize request/response classes with built-in helpers
|
|
180
|
+
# Custom helpers can be registered via register_request_helpers/register_response_helpers
|
|
181
|
+
# before first request (before configuration freezing)
|
|
182
|
+
finalize_request_response_classes
|
|
183
|
+
|
|
559
184
|
# Add IP Privacy middleware first in stack (privacy by default for public IPs)
|
|
560
185
|
# Private/localhost IPs are automatically exempted from masking
|
|
561
186
|
@middleware.add_with_position(
|
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: 2.0.0.
|
|
4
|
+
version: 2.0.0.pre10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Delano Mandelbaum
|
|
@@ -175,6 +175,8 @@ files:
|
|
|
175
175
|
- docs/ipaddr-encoding-quirk.md
|
|
176
176
|
- docs/migrating/v2.0.0-pre1.md
|
|
177
177
|
- docs/migrating/v2.0.0-pre2.md
|
|
178
|
+
- docs/modern-authentication-authorization-landscape.md
|
|
179
|
+
- docs/multi-strategy-authentication-design.md
|
|
178
180
|
- examples/.gitignore
|
|
179
181
|
- examples/advanced_routes/README.md
|
|
180
182
|
- examples/advanced_routes/app.rb
|
|
@@ -238,15 +240,17 @@ files:
|
|
|
238
240
|
- lib/otto/core/error_handler.rb
|
|
239
241
|
- lib/otto/core/file_safety.rb
|
|
240
242
|
- lib/otto/core/freezable.rb
|
|
243
|
+
- lib/otto/core/helper_registry.rb
|
|
244
|
+
- lib/otto/core/lifecycle_hooks.rb
|
|
245
|
+
- lib/otto/core/middleware_management.rb
|
|
241
246
|
- lib/otto/core/middleware_stack.rb
|
|
242
247
|
- lib/otto/core/router.rb
|
|
243
248
|
- lib/otto/core/uri_generator.rb
|
|
244
249
|
- lib/otto/design_system.rb
|
|
245
250
|
- lib/otto/env_keys.rb
|
|
251
|
+
- lib/otto/errors.rb
|
|
246
252
|
- lib/otto/helpers.rb
|
|
247
253
|
- lib/otto/helpers/base.rb
|
|
248
|
-
- lib/otto/helpers/request.rb
|
|
249
|
-
- lib/otto/helpers/response.rb
|
|
250
254
|
- lib/otto/helpers/validation.rb
|
|
251
255
|
- lib/otto/locale.rb
|
|
252
256
|
- lib/otto/locale/config.rb
|
|
@@ -254,6 +258,7 @@ files:
|
|
|
254
258
|
- lib/otto/logging_helpers.rb
|
|
255
259
|
- lib/otto/mcp.rb
|
|
256
260
|
- lib/otto/mcp/auth/token.rb
|
|
261
|
+
- lib/otto/mcp/core.rb
|
|
257
262
|
- lib/otto/mcp/protocol.rb
|
|
258
263
|
- lib/otto/mcp/rate_limiting.rb
|
|
259
264
|
- lib/otto/mcp/registry.rb
|
|
@@ -262,9 +267,12 @@ files:
|
|
|
262
267
|
- lib/otto/mcp/server.rb
|
|
263
268
|
- lib/otto/privacy.rb
|
|
264
269
|
- lib/otto/privacy/config.rb
|
|
270
|
+
- lib/otto/privacy/core.rb
|
|
265
271
|
- lib/otto/privacy/geo_resolver.rb
|
|
266
272
|
- lib/otto/privacy/ip_privacy.rb
|
|
267
273
|
- lib/otto/privacy/redacted_fingerprint.rb
|
|
274
|
+
- lib/otto/request.rb
|
|
275
|
+
- lib/otto/response.rb
|
|
268
276
|
- lib/otto/response_handlers.rb
|
|
269
277
|
- lib/otto/response_handlers/auto.rb
|
|
270
278
|
- lib/otto/response_handlers/base.rb
|
|
@@ -299,6 +307,7 @@ files:
|
|
|
299
307
|
- lib/otto/security/authorization_error.rb
|
|
300
308
|
- lib/otto/security/config.rb
|
|
301
309
|
- lib/otto/security/configurator.rb
|
|
310
|
+
- lib/otto/security/core.rb
|
|
302
311
|
- lib/otto/security/csrf.rb
|
|
303
312
|
- lib/otto/security/middleware/csrf_middleware.rb
|
|
304
313
|
- lib/otto/security/middleware/ip_privacy_middleware.rb
|