otto 2.0.0.pre9 → 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/CHANGELOG.rst +19 -0
- data/CLAUDE.md +44 -0
- data/Gemfile.lock +2 -2
- data/lib/otto/core/error_handler.rb +85 -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.rb +3 -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/schema_validation.rb +1 -1
- 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/route.rb +2 -4
- data/lib/otto/route_handlers/base.rb +5 -7
- data/lib/otto/route_handlers/lambda.rb +2 -2
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +1 -1
- 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.rb +1 -0
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +24 -443
- metadata +9 -3
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
# lib/otto/
|
|
1
|
+
# lib/otto/request.rb
|
|
2
2
|
#
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
require 'rack/request'
|
|
6
6
|
|
|
7
7
|
class Otto
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
# Otto's enhanced Rack::Request class with built-in helpers
|
|
9
|
+
#
|
|
10
|
+
# This class extends Rack::Request with Otto's framework helpers for
|
|
11
|
+
# HTTP request handling, privacy, security, and locale management.
|
|
12
|
+
# Projects can register additional helpers via Otto#register_request_helpers.
|
|
13
|
+
#
|
|
14
|
+
# @example Using Otto's request in route handlers
|
|
15
|
+
# def show(req, res)
|
|
16
|
+
# req.masked_ip # Privacy-safe masked IP
|
|
17
|
+
# req.geo_country # ISO country code
|
|
18
|
+
# req.check_locale! # Set locale for request
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @see Otto#register_request_helpers
|
|
22
|
+
class Request < Rack::Request
|
|
12
23
|
def user_agent
|
|
13
24
|
env['HTTP_USER_AGENT']
|
|
14
25
|
end
|
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
# lib/otto/
|
|
1
|
+
# lib/otto/response.rb
|
|
2
2
|
#
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
require 'rack/response'
|
|
6
6
|
|
|
7
7
|
class Otto
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
# Otto's enhanced Rack::Response class with built-in helpers
|
|
9
|
+
#
|
|
10
|
+
# This class extends Rack::Response with Otto's framework helpers for
|
|
11
|
+
# HTTP response handling, cookie management, CSP headers, and security.
|
|
12
|
+
# Projects can register additional helpers via Otto#register_response_helpers.
|
|
13
|
+
#
|
|
14
|
+
# @example Using Otto's response in route handlers
|
|
15
|
+
# def show(req, res)
|
|
16
|
+
# res.send_secure_cookie('session_id', token, 3600)
|
|
17
|
+
# res.send_csp_headers('text/html', nonce)
|
|
18
|
+
# res.no_cache!
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @see Otto#register_response_helpers
|
|
22
|
+
class Response < Rack::Response
|
|
23
|
+
# Reference to the request object (needed by some response helpers)
|
|
24
|
+
# @return [Otto::Request]
|
|
12
25
|
attr_accessor :request
|
|
13
26
|
|
|
14
27
|
def send_secure_cookie(name, value, ttl, opts = {})
|
|
@@ -171,7 +184,7 @@ class Otto
|
|
|
171
184
|
# # => "/myapp/admin/settings"
|
|
172
185
|
def app_path(*paths)
|
|
173
186
|
paths = paths.flatten.compact
|
|
174
|
-
paths.unshift(request.env['SCRIPT_NAME']) if request
|
|
187
|
+
paths.unshift(request.env['SCRIPT_NAME']) if request&.env&.[]('SCRIPT_NAME')
|
|
175
188
|
paths.join('/').gsub('//', '/')
|
|
176
189
|
end
|
|
177
190
|
end
|
data/lib/otto/route.rb
CHANGED
|
@@ -100,10 +100,8 @@ class Otto
|
|
|
100
100
|
# @return [Array] Rack response array [status, headers, body]
|
|
101
101
|
def call(env, extra_params = {})
|
|
102
102
|
extra_params ||= {}
|
|
103
|
-
req =
|
|
104
|
-
res =
|
|
105
|
-
req.extend Otto::RequestHelpers
|
|
106
|
-
res.extend Otto::ResponseHelpers
|
|
103
|
+
req = otto.request_class.new(env)
|
|
104
|
+
res = otto.response_class.new
|
|
107
105
|
res.request = req
|
|
108
106
|
|
|
109
107
|
# Make security config available to response helpers
|
|
@@ -23,8 +23,8 @@ class Otto
|
|
|
23
23
|
# @return [Array] Rack response array
|
|
24
24
|
def call(env, extra_params = {})
|
|
25
25
|
@start_time = Otto::Utils.now_in_μs
|
|
26
|
-
req =
|
|
27
|
-
res =
|
|
26
|
+
req = otto_instance ? otto_instance.request_class.new(env) : Otto::Request.new(env)
|
|
27
|
+
res = otto_instance ? otto_instance.response_class.new : Otto::Response.new
|
|
28
28
|
|
|
29
29
|
begin
|
|
30
30
|
setup_request_response(req, res, env, extra_params)
|
|
@@ -118,14 +118,12 @@ class Otto
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Setup request and response with the same extensions and processing as Route#call
|
|
121
|
-
# @param req [
|
|
122
|
-
# @param res [
|
|
121
|
+
# @param req [Otto::Request] Request object
|
|
122
|
+
# @param res [Otto::Response] Response object
|
|
123
123
|
# @param env [Hash] Rack environment
|
|
124
124
|
# @param extra_params [Hash] Additional parameters
|
|
125
125
|
def setup_request_response(req, res, env, extra_params)
|
|
126
|
-
#
|
|
127
|
-
req.extend Otto::RequestHelpers
|
|
128
|
-
res.extend Otto::ResponseHelpers
|
|
126
|
+
# Set request reference (helpers are already included in class)
|
|
129
127
|
res.request = req
|
|
130
128
|
|
|
131
129
|
# Make security config available to response helpers
|
|
@@ -11,8 +11,8 @@ class Otto
|
|
|
11
11
|
class LambdaHandler < BaseHandler
|
|
12
12
|
def call(env, extra_params = {})
|
|
13
13
|
start_time = Otto::Utils.now_in_μs
|
|
14
|
-
req =
|
|
15
|
-
res =
|
|
14
|
+
req = otto_instance ? otto_instance.request_class.new(env) : Otto::Request.new(env)
|
|
15
|
+
res = otto_instance ? otto_instance.response_class.new : Otto::Response.new
|
|
16
16
|
|
|
17
17
|
begin
|
|
18
18
|
# Security: Lambda handlers require pre-configured procs from Otto instance
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# lib/otto/security/core.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
class Otto
|
|
6
|
+
module Security
|
|
7
|
+
# Core security configuration methods included in the Otto class.
|
|
8
|
+
# Provides the public API for enabling and configuring security features.
|
|
9
|
+
module Core
|
|
10
|
+
# Enable CSRF protection for POST, PUT, DELETE, and PATCH requests.
|
|
11
|
+
# This will automatically add CSRF tokens to HTML forms and validate
|
|
12
|
+
# them on unsafe HTTP methods.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# otto.enable_csrf_protection!
|
|
16
|
+
def enable_csrf_protection!
|
|
17
|
+
ensure_not_frozen!
|
|
18
|
+
return if @middleware.includes?(Otto::Security::Middleware::CSRFMiddleware)
|
|
19
|
+
|
|
20
|
+
@security_config.enable_csrf_protection!
|
|
21
|
+
use Otto::Security::Middleware::CSRFMiddleware
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Enable request validation including input sanitization, size limits,
|
|
25
|
+
# and protection against XSS and SQL injection attacks.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# otto.enable_request_validation!
|
|
29
|
+
def enable_request_validation!
|
|
30
|
+
ensure_not_frozen!
|
|
31
|
+
return if @middleware.includes?(Otto::Security::Middleware::ValidationMiddleware)
|
|
32
|
+
|
|
33
|
+
@security_config.input_validation = true
|
|
34
|
+
use Otto::Security::Middleware::ValidationMiddleware
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Enable rate limiting to protect against abuse and DDoS attacks.
|
|
38
|
+
# This will automatically add rate limiting rules based on client IP.
|
|
39
|
+
#
|
|
40
|
+
# @param options [Hash] Rate limiting configuration options
|
|
41
|
+
# @option options [Integer] :requests_per_minute Maximum requests per minute per IP (default: 100)
|
|
42
|
+
# @option options [Hash] :custom_rules Custom rate limiting rules
|
|
43
|
+
# @example
|
|
44
|
+
# otto.enable_rate_limiting!(requests_per_minute: 50)
|
|
45
|
+
def enable_rate_limiting!(options = {})
|
|
46
|
+
ensure_not_frozen!
|
|
47
|
+
return if @middleware.includes?(Otto::Security::Middleware::RateLimitMiddleware)
|
|
48
|
+
|
|
49
|
+
@security.configure_rate_limiting(options)
|
|
50
|
+
use Otto::Security::Middleware::RateLimitMiddleware
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Add a custom rate limiting rule.
|
|
54
|
+
#
|
|
55
|
+
# @param name [String, Symbol] Rule name
|
|
56
|
+
# @param options [Hash] Rule configuration
|
|
57
|
+
# @option options [Integer] :limit Maximum requests
|
|
58
|
+
# @option options [Integer] :period Time period in seconds (default: 60)
|
|
59
|
+
# @option options [Proc] :condition Optional condition proc that receives request
|
|
60
|
+
# @example
|
|
61
|
+
# otto.add_rate_limit_rule('uploads', limit: 5, period: 300, condition: ->(req) { req.post? && req.path.include?('upload') })
|
|
62
|
+
def add_rate_limit_rule(name, options)
|
|
63
|
+
ensure_not_frozen!
|
|
64
|
+
@security_config.rate_limiting_config[:custom_rules][name.to_s] = options
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Add a trusted proxy server for accurate client IP detection.
|
|
68
|
+
# Only requests from trusted proxies will have their forwarded headers honored.
|
|
69
|
+
#
|
|
70
|
+
# @param proxy [String, Regexp] IP address, CIDR range, or regex pattern
|
|
71
|
+
# @example
|
|
72
|
+
# otto.add_trusted_proxy('10.0.0.0/8')
|
|
73
|
+
# otto.add_trusted_proxy(/^172\.16\./)
|
|
74
|
+
def add_trusted_proxy(proxy)
|
|
75
|
+
ensure_not_frozen!
|
|
76
|
+
@security_config.add_trusted_proxy(proxy)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Set custom security headers that will be added to all responses.
|
|
80
|
+
# These merge with the default security headers.
|
|
81
|
+
#
|
|
82
|
+
# @param headers [Hash] Hash of header name => value pairs
|
|
83
|
+
# @example
|
|
84
|
+
# otto.set_security_headers({
|
|
85
|
+
# 'content-security-policy' => "default-src 'self'",
|
|
86
|
+
# 'strict-transport-security' => 'max-age=31536000'
|
|
87
|
+
# })
|
|
88
|
+
def set_security_headers(headers)
|
|
89
|
+
ensure_not_frozen!
|
|
90
|
+
@security_config.security_headers.merge!(headers)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Enable HTTP Strict Transport Security (HSTS) header.
|
|
94
|
+
# WARNING: This can make your domain inaccessible if HTTPS is not properly
|
|
95
|
+
# configured. Only enable this when you're certain HTTPS is working correctly.
|
|
96
|
+
#
|
|
97
|
+
# @param max_age [Integer] Maximum age in seconds (default: 1 year)
|
|
98
|
+
# @param include_subdomains [Boolean] Apply to all subdomains (default: true)
|
|
99
|
+
# @example
|
|
100
|
+
# otto.enable_hsts!(max_age: 86400, include_subdomains: false)
|
|
101
|
+
def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
|
|
102
|
+
ensure_not_frozen!
|
|
103
|
+
@security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Enable Content Security Policy (CSP) header to prevent XSS attacks.
|
|
107
|
+
# The default policy only allows resources from the same origin.
|
|
108
|
+
#
|
|
109
|
+
# @param policy [String] CSP policy string (default: "default-src 'self'")
|
|
110
|
+
# @example
|
|
111
|
+
# otto.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")
|
|
112
|
+
def enable_csp!(policy = "default-src 'self'")
|
|
113
|
+
ensure_not_frozen!
|
|
114
|
+
@security_config.enable_csp!(policy)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Enable X-Frame-Options header to prevent clickjacking attacks.
|
|
118
|
+
#
|
|
119
|
+
# @param option [String] Frame options: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'
|
|
120
|
+
# @example
|
|
121
|
+
# otto.enable_frame_protection!('DENY')
|
|
122
|
+
def enable_frame_protection!(option = 'SAMEORIGIN')
|
|
123
|
+
ensure_not_frozen!
|
|
124
|
+
@security_config.enable_frame_protection!(option)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Enable Content Security Policy (CSP) with nonce support for dynamic header generation.
|
|
128
|
+
# This enables the res.send_csp_headers response helper method.
|
|
129
|
+
#
|
|
130
|
+
# @param debug [Boolean] Enable debug logging for CSP headers (default: false)
|
|
131
|
+
# @example
|
|
132
|
+
# otto.enable_csp_with_nonce!(debug: true)
|
|
133
|
+
def enable_csp_with_nonce!(debug: false)
|
|
134
|
+
ensure_not_frozen!
|
|
135
|
+
@security_config.enable_csp_with_nonce!(debug: debug)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Add an authentication strategy with a registered name
|
|
139
|
+
#
|
|
140
|
+
# This is the primary public API for registering authentication strategies.
|
|
141
|
+
# The name you provide here will be available as `strategy_result.strategy_name`
|
|
142
|
+
# in your application code, making it easy to identify which strategy authenticated
|
|
143
|
+
# the current request.
|
|
144
|
+
#
|
|
145
|
+
# Also available via Otto::Security::Configurator for consolidated security config.
|
|
146
|
+
#
|
|
147
|
+
# @param name [String, Symbol] Strategy name (e.g., 'session', 'api_key', 'jwt')
|
|
148
|
+
# @param strategy [AuthStrategy] Strategy instance
|
|
149
|
+
# @example
|
|
150
|
+
# otto.add_auth_strategy('session', SessionStrategy.new(session_key: 'user_id'))
|
|
151
|
+
# otto.add_auth_strategy('api_key', APIKeyStrategy.new)
|
|
152
|
+
# @raise [ArgumentError] if strategy name already registered
|
|
153
|
+
def add_auth_strategy(name, strategy)
|
|
154
|
+
ensure_not_frozen!
|
|
155
|
+
# Ensure auth_config is initialized (handles edge case where it might be nil)
|
|
156
|
+
@auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' } if @auth_config.nil?
|
|
157
|
+
|
|
158
|
+
# Strict mode: Detect strategy name collisions
|
|
159
|
+
if @auth_config[:auth_strategies].key?(name)
|
|
160
|
+
raise ArgumentError, "Authentication strategy '#{name}' is already registered"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
@auth_config[:auth_strategies][name] = strategy
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/otto/security.rb
CHANGED
data/lib/otto/version.rb
CHANGED