otto 2.0.0.pre3 → 2.0.0.pre8
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/code-smells.yml +143 -0
- data/.gitignore +4 -0
- data/.pre-commit-config.yaml +2 -2
- data/.reek.yml +99 -0
- data/CHANGELOG.rst +156 -0
- data/CLAUDE.md +74 -540
- data/Gemfile +4 -2
- data/Gemfile.lock +58 -19
- data/README.md +49 -1
- data/examples/advanced_routes/README.md +137 -20
- data/examples/authentication_strategies/README.md +212 -19
- data/examples/backtrace_sanitization_demo.rb +86 -0
- data/examples/basic/README.md +61 -10
- data/examples/error_handler_registration.rb +136 -0
- data/examples/logging_improvements.rb +76 -0
- data/examples/mcp_demo/README.md +187 -27
- data/examples/security_features/README.md +249 -30
- data/examples/simple_geo_resolver.rb +107 -0
- data/lib/otto/core/configuration.rb +15 -20
- data/lib/otto/core/error_handler.rb +138 -8
- data/lib/otto/core/file_safety.rb +2 -2
- data/lib/otto/core/freezable.rb +2 -2
- data/lib/otto/core/middleware_stack.rb +2 -2
- data/lib/otto/core/router.rb +61 -8
- data/lib/otto/core/uri_generator.rb +2 -2
- data/lib/otto/core.rb +2 -0
- data/lib/otto/design_system.rb +2 -2
- data/lib/otto/env_keys.rb +61 -12
- data/lib/otto/helpers/base.rb +2 -2
- data/lib/otto/helpers/request.rb +8 -3
- data/lib/otto/helpers/response.rb +2 -2
- data/lib/otto/helpers/validation.rb +2 -2
- data/lib/otto/helpers.rb +2 -0
- data/lib/otto/locale/config.rb +2 -2
- data/lib/otto/locale/middleware.rb +160 -0
- data/lib/otto/locale.rb +10 -0
- data/lib/otto/logging_helpers.rb +273 -0
- data/lib/otto/mcp/auth/token.rb +2 -2
- data/lib/otto/mcp/protocol.rb +2 -2
- data/lib/otto/mcp/rate_limiting.rb +2 -2
- data/lib/otto/mcp/registry.rb +2 -2
- data/lib/otto/mcp/route_parser.rb +2 -2
- data/lib/otto/mcp/schema_validation.rb +2 -2
- data/lib/otto/mcp/server.rb +2 -2
- data/lib/otto/mcp.rb +2 -0
- data/lib/otto/privacy/config.rb +2 -0
- data/lib/otto/privacy/geo_resolver.rb +199 -29
- data/lib/otto/privacy/ip_privacy.rb +2 -0
- data/lib/otto/privacy/redacted_fingerprint.rb +18 -8
- data/lib/otto/privacy.rb +2 -0
- data/lib/otto/response_handlers/auto.rb +2 -0
- data/lib/otto/response_handlers/base.rb +2 -0
- data/lib/otto/response_handlers/default.rb +2 -0
- data/lib/otto/response_handlers/factory.rb +2 -0
- data/lib/otto/response_handlers/json.rb +2 -0
- data/lib/otto/response_handlers/redirect.rb +2 -0
- data/lib/otto/response_handlers/view.rb +2 -0
- data/lib/otto/response_handlers.rb +2 -2
- data/lib/otto/route.rb +4 -4
- data/lib/otto/route_definition.rb +42 -15
- data/lib/otto/route_handlers/base.rb +2 -0
- data/lib/otto/route_handlers/class_method.rb +26 -26
- data/lib/otto/route_handlers/factory.rb +2 -2
- data/lib/otto/route_handlers/instance_method.rb +16 -6
- data/lib/otto/route_handlers/lambda.rb +8 -20
- data/lib/otto/route_handlers/logic_class.rb +33 -8
- data/lib/otto/route_handlers.rb +2 -2
- data/lib/otto/security/authentication/auth_failure.rb +2 -2
- data/lib/otto/security/authentication/auth_strategy.rb +11 -4
- data/lib/otto/security/authentication/route_auth_wrapper/response_builder.rb +123 -0
- data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +120 -0
- data/lib/otto/security/authentication/route_auth_wrapper/strategy_resolver.rb +69 -0
- data/lib/otto/security/authentication/route_auth_wrapper.rb +185 -195
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/noauth_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategy_result.rb +6 -5
- data/lib/otto/security/authentication.rb +2 -2
- data/lib/otto/security/authorization_error.rb +73 -0
- data/lib/otto/security/config.rb +2 -2
- data/lib/otto/security/configurator.rb +17 -2
- data/lib/otto/security/csrf.rb +2 -2
- data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
- data/lib/otto/security/middleware/ip_privacy_middleware.rb +31 -11
- data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
- data/lib/otto/security/middleware/validation_middleware.rb +15 -0
- data/lib/otto/security/rate_limiter.rb +2 -2
- data/lib/otto/security/rate_limiting.rb +2 -2
- data/lib/otto/security/validator.rb +2 -2
- data/lib/otto/security.rb +3 -0
- data/lib/otto/static.rb +2 -2
- data/lib/otto/utils.rb +27 -2
- data/lib/otto/version.rb +3 -3
- data/lib/otto.rb +174 -14
- data/otto.gemspec +7 -3
- metadata +25 -15
- data/benchmark_middleware_wrap.rb +0 -163
- data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +0 -36
- data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +0 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
# lib/otto/core/configuration.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
require_relative '../security/csrf'
|
|
6
6
|
require_relative '../security/validator'
|
|
@@ -14,6 +14,7 @@ class Otto
|
|
|
14
14
|
# Configuration module providing locale and application configuration methods
|
|
15
15
|
module Configuration
|
|
16
16
|
include Otto::Core::Freezable
|
|
17
|
+
|
|
17
18
|
def configure_locale(opts)
|
|
18
19
|
# Check if we have any locale configuration
|
|
19
20
|
has_direct_options = opts[:available_locales] || opts[:default_locale]
|
|
@@ -68,9 +69,7 @@ class Otto
|
|
|
68
69
|
@auth_config[:auth_strategies] = opts[:auth_strategies] if opts[:auth_strategies]
|
|
69
70
|
@auth_config[:default_auth_strategy] = opts[:default_auth_strategy] if opts[:default_auth_strategy]
|
|
70
71
|
|
|
71
|
-
#
|
|
72
|
-
return unless opts[:auth_strategies] && !opts[:auth_strategies].empty?
|
|
73
|
-
|
|
72
|
+
# No-op: authentication strategies are configured via @auth_config above
|
|
74
73
|
end
|
|
75
74
|
|
|
76
75
|
def configure_mcp(opts)
|
|
@@ -143,7 +142,6 @@ class Otto
|
|
|
143
142
|
# Update existing @auth_config rather than creating a new one
|
|
144
143
|
@auth_config[:auth_strategies] = strategies
|
|
145
144
|
@auth_config[:default_auth_strategy] = default_strategy
|
|
146
|
-
|
|
147
145
|
end
|
|
148
146
|
|
|
149
147
|
# Freeze the application configuration to prevent runtime modifications.
|
|
@@ -157,38 +155,36 @@ class Otto
|
|
|
157
155
|
# @return [self]
|
|
158
156
|
def freeze_configuration!
|
|
159
157
|
if frozen_configuration?
|
|
160
|
-
Otto.
|
|
158
|
+
Otto.structured_log(:debug, 'Configuration already frozen', { status: 'skipped' }) if Otto.debug
|
|
161
159
|
return self
|
|
162
160
|
end
|
|
163
161
|
|
|
164
|
-
|
|
162
|
+
start_time = Otto::Utils.now_in_μs
|
|
165
163
|
|
|
166
164
|
# Deep freeze configuration objects with memoization support
|
|
167
|
-
Otto.logger.debug '[Otto::Configuration] Freezing security_config' if Otto.debug
|
|
168
165
|
@security_config.deep_freeze! if @security_config.respond_to?(:deep_freeze!)
|
|
169
|
-
|
|
170
|
-
Otto.logger.debug '[Otto::Configuration] Freezing locale_config' if Otto.debug
|
|
171
166
|
@locale_config.deep_freeze! if @locale_config.respond_to?(:deep_freeze!)
|
|
172
|
-
|
|
173
|
-
Otto.logger.debug '[Otto::Configuration] Freezing middleware stack' if Otto.debug
|
|
174
167
|
@middleware.deep_freeze! if @middleware.respond_to?(:deep_freeze!)
|
|
175
168
|
|
|
176
169
|
# Deep freeze configuration hashes (recursively freezes nested structures)
|
|
177
|
-
Otto.logger.debug '[Otto::Configuration] Freezing auth_config hash' if Otto.debug
|
|
178
170
|
deep_freeze_value(@auth_config) if @auth_config
|
|
179
|
-
|
|
180
|
-
Otto.logger.debug '[Otto::Configuration] Freezing option hash' if Otto.debug
|
|
181
171
|
deep_freeze_value(@option) if @option
|
|
182
172
|
|
|
183
173
|
# Deep freeze route structures (prevent modification of nested hashes/arrays)
|
|
184
|
-
Otto.logger.debug '[Otto::Configuration] Freezing route structures' if Otto.debug
|
|
185
174
|
deep_freeze_value(@routes) if @routes
|
|
186
175
|
deep_freeze_value(@routes_literal) if @routes_literal
|
|
187
176
|
deep_freeze_value(@routes_static) if @routes_static
|
|
188
177
|
deep_freeze_value(@route_definitions) if @route_definitions
|
|
189
178
|
|
|
190
179
|
@configuration_frozen = true
|
|
191
|
-
|
|
180
|
+
|
|
181
|
+
duration = Otto::Utils.now_in_μs - start_time
|
|
182
|
+
frozen_objects = %w[security_config locale_config middleware auth_config option routes]
|
|
183
|
+
Otto.structured_log(:info, 'Freezing completed',
|
|
184
|
+
{
|
|
185
|
+
duration: duration,
|
|
186
|
+
frozen_objects: frozen_objects.join(','),
|
|
187
|
+
})
|
|
192
188
|
|
|
193
189
|
self
|
|
194
190
|
end
|
|
@@ -207,10 +203,9 @@ class Otto
|
|
|
207
203
|
raise FrozenError, 'Cannot modify frozen configuration' if frozen_configuration?
|
|
208
204
|
end
|
|
209
205
|
|
|
210
|
-
|
|
211
206
|
def middleware_enabled?(middleware_class)
|
|
212
207
|
# Only check the new middleware stack as the single source of truth
|
|
213
|
-
@middleware
|
|
208
|
+
@middleware&.includes?(middleware_class)
|
|
214
209
|
end
|
|
215
210
|
end
|
|
216
211
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
# lib/otto/core/error_handler.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
require 'securerandom'
|
|
6
6
|
require 'json'
|
|
@@ -11,10 +11,30 @@ class Otto
|
|
|
11
11
|
# Error handling module providing secure error reporting and logging functionality
|
|
12
12
|
module ErrorHandler
|
|
13
13
|
def handle_error(error, env)
|
|
14
|
+
# Check if this is a registered expected error
|
|
15
|
+
if handler_config = @error_handlers[error.class.name]
|
|
16
|
+
return handle_expected_error(error, env, handler_config)
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
# Log error details internally but don't expose them
|
|
15
20
|
error_id = SecureRandom.hex(8)
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
|
|
22
|
+
# Base context pattern: create once, reuse for correlation
|
|
23
|
+
base_context = Otto::LoggingHelpers.request_context(env)
|
|
24
|
+
|
|
25
|
+
# Include handler context if available (set by route handlers)
|
|
26
|
+
log_context = base_context.merge(
|
|
27
|
+
error: error.message,
|
|
28
|
+
error_class: error.class.name,
|
|
29
|
+
error_id: error_id
|
|
30
|
+
)
|
|
31
|
+
log_context[:handler] = env['otto.handler'] if env['otto.handler']
|
|
32
|
+
log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
|
|
33
|
+
|
|
34
|
+
Otto.structured_log(:error, 'Unhandled error in request', log_context)
|
|
35
|
+
|
|
36
|
+
Otto::LoggingHelpers.log_backtrace(error,
|
|
37
|
+
base_context.merge(error_id: error_id))
|
|
18
38
|
|
|
19
39
|
# Parse request for content negotiation
|
|
20
40
|
begin
|
|
@@ -30,7 +50,21 @@ class Otto
|
|
|
30
50
|
env['otto.error_id'] = error_id
|
|
31
51
|
return found_route.call(env)
|
|
32
52
|
rescue StandardError => e
|
|
33
|
-
|
|
53
|
+
# When the custom error handler itself fails, generate a new error ID
|
|
54
|
+
# to distinguish it from the original error, but link them.
|
|
55
|
+
custom_handler_error_id = SecureRandom.hex(8)
|
|
56
|
+
base_context = Otto::LoggingHelpers.request_context(env)
|
|
57
|
+
|
|
58
|
+
Otto.structured_log(:error, 'Error in custom error handler',
|
|
59
|
+
base_context.merge(
|
|
60
|
+
error: e.message,
|
|
61
|
+
error_class: e.class.name,
|
|
62
|
+
error_id: custom_handler_error_id,
|
|
63
|
+
original_error_id: error_id # Link to original error
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
Otto::LoggingHelpers.log_backtrace(e,
|
|
67
|
+
base_context.merge(error_id: custom_handler_error_id, original_error_id: error_id))
|
|
34
68
|
end
|
|
35
69
|
end
|
|
36
70
|
|
|
@@ -44,6 +78,102 @@ class Otto
|
|
|
44
78
|
|
|
45
79
|
private
|
|
46
80
|
|
|
81
|
+
# Handle expected business logic errors with custom status codes and logging
|
|
82
|
+
#
|
|
83
|
+
# @param error [Exception] The expected error to handle
|
|
84
|
+
# @param env [Hash] Rack environment hash
|
|
85
|
+
# @param handler_config [Hash] Configuration from error_handlers registry
|
|
86
|
+
# @return [Array] Rack response tuple [status, headers, body]
|
|
87
|
+
def handle_expected_error(error, env, handler_config)
|
|
88
|
+
# Generate error ID for correlation (even for expected errors)
|
|
89
|
+
error_id = SecureRandom.hex(8)
|
|
90
|
+
|
|
91
|
+
# Base context pattern: create once, reuse for correlation
|
|
92
|
+
base_context = Otto::LoggingHelpers.request_context(env)
|
|
93
|
+
|
|
94
|
+
# Include handler context if available
|
|
95
|
+
log_context = base_context.merge(
|
|
96
|
+
error: error.message,
|
|
97
|
+
error_class: error.class.name,
|
|
98
|
+
error_id: error_id,
|
|
99
|
+
expected: true # Mark as expected error
|
|
100
|
+
)
|
|
101
|
+
log_context[:handler] = env['otto.handler'] if env['otto.handler']
|
|
102
|
+
log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
|
|
103
|
+
|
|
104
|
+
# Log at configured level (info/warn instead of error)
|
|
105
|
+
log_level = handler_config[:log_level] || :info
|
|
106
|
+
Otto.structured_log(log_level, 'Expected error in request', log_context)
|
|
107
|
+
|
|
108
|
+
# Build response body
|
|
109
|
+
response_body = if handler_config[:handler]
|
|
110
|
+
# Use custom handler block if provided
|
|
111
|
+
begin
|
|
112
|
+
req = Rack::Request.new(env)
|
|
113
|
+
result = handler_config[:handler].call(error, req)
|
|
114
|
+
|
|
115
|
+
# Validate that custom handler returned a Hash
|
|
116
|
+
unless result.is_a?(Hash)
|
|
117
|
+
base_context = Otto::LoggingHelpers.request_context(env)
|
|
118
|
+
Otto.structured_log(:warn, 'Custom error handler returned non-hash value',
|
|
119
|
+
base_context.merge(
|
|
120
|
+
error_class: error.class.name,
|
|
121
|
+
handler_result_class: result.class.name,
|
|
122
|
+
error_id: error_id
|
|
123
|
+
))
|
|
124
|
+
result = { error: error.class.name.split('::').last, message: error.message }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
result
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
# If custom handler fails, fall back to default
|
|
130
|
+
base_context = Otto::LoggingHelpers.request_context(env)
|
|
131
|
+
Otto.structured_log(:warn, 'Error in custom error handler',
|
|
132
|
+
base_context.merge(
|
|
133
|
+
error: e.message,
|
|
134
|
+
error_class: e.class.name,
|
|
135
|
+
original_error_class: error.class.name,
|
|
136
|
+
error_id: error_id
|
|
137
|
+
))
|
|
138
|
+
{ error: error.class.name.split('::').last, message: error.message }
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
# Default response body
|
|
142
|
+
{ error: error.class.name.split('::').last, message: error.message }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Add error_id in development mode
|
|
146
|
+
response_body[:error_id] = error_id if Otto.env?(:dev, :development)
|
|
147
|
+
|
|
148
|
+
# Content negotiation
|
|
149
|
+
accept_header = env['HTTP_ACCEPT'].to_s
|
|
150
|
+
status = handler_config[:status] || 500
|
|
151
|
+
|
|
152
|
+
if accept_header.include?('application/json')
|
|
153
|
+
body = JSON.generate(response_body)
|
|
154
|
+
headers = {
|
|
155
|
+
'content-type' => 'application/json',
|
|
156
|
+
'content-length' => body.bytesize.to_s
|
|
157
|
+
}.merge(@security_config.security_headers)
|
|
158
|
+
|
|
159
|
+
[status, headers, [body]]
|
|
160
|
+
else
|
|
161
|
+
# Plain text response
|
|
162
|
+
body = if Otto.env?(:dev, :development)
|
|
163
|
+
"#{response_body[:error]}: #{response_body[:message]} (ID: #{error_id})"
|
|
164
|
+
else
|
|
165
|
+
"#{response_body[:error]}: #{response_body[:message]}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
headers = {
|
|
169
|
+
'content-type' => 'text/plain',
|
|
170
|
+
'content-length' => body.bytesize.to_s
|
|
171
|
+
}.merge(@security_config.security_headers)
|
|
172
|
+
|
|
173
|
+
[status, headers, [body]]
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
47
177
|
def secure_error_response(error_id)
|
|
48
178
|
body = if Otto.env?(:dev, :development)
|
|
49
179
|
"Server error (ID: #{error_id}). Check logs for details."
|
|
@@ -62,13 +192,13 @@ class Otto
|
|
|
62
192
|
def json_error_response(error_id)
|
|
63
193
|
error_data = if Otto.env?(:dev, :development)
|
|
64
194
|
{
|
|
65
|
-
|
|
66
|
-
|
|
195
|
+
error: 'Internal Server Error',
|
|
196
|
+
message: 'Server error occurred. Check logs for details.',
|
|
67
197
|
error_id: error_id,
|
|
68
198
|
}
|
|
69
199
|
else
|
|
70
200
|
{
|
|
71
|
-
|
|
201
|
+
error: 'Internal Server Error',
|
|
72
202
|
message: 'An error occurred. Please try again later.',
|
|
73
203
|
}
|
|
74
204
|
end
|
data/lib/otto/core/freezable.rb
CHANGED
data/lib/otto/core/router.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
# lib/otto/core/router.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
require_relative '../mcp/route_parser'
|
|
6
6
|
|
|
@@ -36,13 +36,28 @@ class Otto
|
|
|
36
36
|
route.otto = self
|
|
37
37
|
path_clean = path.gsub(%r{/$}, '')
|
|
38
38
|
@route_definitions[route.definition] = route
|
|
39
|
-
Otto.
|
|
39
|
+
Otto.structured_log(:debug, "Route loaded",
|
|
40
|
+
{
|
|
41
|
+
pattern: route.pattern.source,
|
|
42
|
+
verb: route.verb,
|
|
43
|
+
definition: route.definition,
|
|
44
|
+
type: 'pattern'
|
|
45
|
+
}
|
|
46
|
+
) if Otto.debug
|
|
40
47
|
@routes[route.verb] ||= []
|
|
41
48
|
@routes[route.verb] << route
|
|
42
49
|
@routes_literal[route.verb] ||= {}
|
|
43
50
|
@routes_literal[route.verb][path_clean] = route
|
|
44
51
|
rescue StandardError => e
|
|
45
|
-
Otto.
|
|
52
|
+
Otto.structured_log(:error, "Route load failed",
|
|
53
|
+
{
|
|
54
|
+
path: path,
|
|
55
|
+
verb: verb,
|
|
56
|
+
definition: definition,
|
|
57
|
+
error: e.message,
|
|
58
|
+
error_class: e.class.name
|
|
59
|
+
}
|
|
60
|
+
)
|
|
46
61
|
Otto.logger.debug e.backtrace.join("\n") if Otto.debug
|
|
47
62
|
end
|
|
48
63
|
self
|
|
@@ -81,14 +96,30 @@ class Otto
|
|
|
81
96
|
literal_routes.merge! routes_literal[:GET] if http_verb == :HEAD
|
|
82
97
|
|
|
83
98
|
if static_route && http_verb == :GET && routes_static[:GET].member?(base_path)
|
|
84
|
-
|
|
99
|
+
Otto.structured_log(:debug, "Route matched",
|
|
100
|
+
Otto::LoggingHelpers.request_context(env).merge(
|
|
101
|
+
type: 'static_cached',
|
|
102
|
+
base_path: base_path
|
|
103
|
+
)
|
|
104
|
+
)
|
|
85
105
|
static_route.call(env)
|
|
86
106
|
elsif literal_routes.has_key?(path_info_clean)
|
|
87
107
|
route = literal_routes[path_info_clean]
|
|
88
|
-
|
|
108
|
+
Otto.structured_log(:debug, "Route matched",
|
|
109
|
+
Otto::LoggingHelpers.request_context(env).merge(
|
|
110
|
+
type: 'literal',
|
|
111
|
+
handler: route.route_definition.definition,
|
|
112
|
+
auth_strategy: route.route_definition.auth_requirement || 'none'
|
|
113
|
+
)
|
|
114
|
+
)
|
|
89
115
|
route.call(env)
|
|
90
116
|
elsif static_route && http_verb == :GET && safe_file?(path_info)
|
|
91
|
-
Otto.
|
|
117
|
+
Otto.structured_log(:debug, "Route matched",
|
|
118
|
+
Otto::LoggingHelpers.request_context(env).merge(
|
|
119
|
+
type: 'static_new',
|
|
120
|
+
base_path: base_path
|
|
121
|
+
)
|
|
122
|
+
)
|
|
92
123
|
routes_static[:GET][base_path] = base_path
|
|
93
124
|
static_route.call(env)
|
|
94
125
|
else
|
|
@@ -123,7 +154,6 @@ class Otto
|
|
|
123
154
|
valid_routes.push(*routes[:GET]) if http_verb == :HEAD
|
|
124
155
|
|
|
125
156
|
valid_routes.each do |route|
|
|
126
|
-
# Otto.logger.debug " request: #{http_verb} #{path_info} (trying route: #{route.verb} #{route.pattern})"
|
|
127
157
|
next unless (match = route.pattern.match(path_info))
|
|
128
158
|
|
|
129
159
|
values = match.captures.to_a
|
|
@@ -133,13 +163,36 @@ class Otto
|
|
|
133
163
|
values.shift
|
|
134
164
|
extra_params = build_route_params(route, values)
|
|
135
165
|
found_route = route
|
|
166
|
+
|
|
167
|
+
# Log successful route match
|
|
168
|
+
Otto.structured_log(:debug, "Route matched",
|
|
169
|
+
Otto::LoggingHelpers.request_context(env).merge(
|
|
170
|
+
pattern: route.pattern.source,
|
|
171
|
+
handler: route.route_definition.definition,
|
|
172
|
+
auth_strategy: route.route_definition.auth_requirement || 'none',
|
|
173
|
+
route_params: extra_params
|
|
174
|
+
)
|
|
175
|
+
)
|
|
136
176
|
break
|
|
137
177
|
end
|
|
138
178
|
|
|
139
179
|
found_route ||= literal_routes['/404']
|
|
140
180
|
if found_route
|
|
181
|
+
# Log 404 route usage if we fell back to it
|
|
182
|
+
if found_route == literal_routes['/404']
|
|
183
|
+
Otto.structured_log(:info, "Route not found",
|
|
184
|
+
Otto::LoggingHelpers.request_context(env).merge(
|
|
185
|
+
fallback_to: '404_route'
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
end
|
|
141
189
|
found_route.call env, extra_params
|
|
142
190
|
else
|
|
191
|
+
Otto.structured_log(:info, "Route not found",
|
|
192
|
+
Otto::LoggingHelpers.request_context(env).merge(
|
|
193
|
+
fallback_to: 'default_not_found'
|
|
194
|
+
)
|
|
195
|
+
)
|
|
143
196
|
@not_found || Otto::Static.not_found
|
|
144
197
|
end
|
|
145
198
|
end
|
data/lib/otto/core.rb
CHANGED
data/lib/otto/design_system.rb
CHANGED
data/lib/otto/env_keys.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# lib/otto/env_keys.rb
|
|
2
2
|
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
#
|
|
3
5
|
# Central registry of all env['otto.*'] keys used throughout Otto framework.
|
|
4
6
|
# This documentation helps prevent key conflicts and aids multi-app integration.
|
|
5
7
|
#
|
|
@@ -43,19 +45,11 @@ class Otto
|
|
|
43
45
|
# - Routes WITHOUT auth requirement: Anonymous StrategyResult
|
|
44
46
|
STRATEGY_RESULT = 'otto.strategy_result'
|
|
45
47
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# Set by: RouteAuthWrapper (from strategy_result.user)
|
|
49
|
-
# Used by: Controllers, RouteHandlers
|
|
50
|
-
# Note: nil for anonymous/unauthenticated requests
|
|
51
|
-
USER = 'otto.user'
|
|
48
|
+
# REMOVED: Use strategy_result.user instead
|
|
49
|
+
# USER = 'otto.user'
|
|
52
50
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# Set by: RouteAuthWrapper (from strategy_result.user_context)
|
|
56
|
-
# Used by: Controllers, Analytics
|
|
57
|
-
# Note: Empty hash {} for anonymous requests
|
|
58
|
-
USER_CONTEXT = 'otto.user_context'
|
|
51
|
+
# REMOVED: Use strategy_result.metadata instead
|
|
52
|
+
# USER_CONTEXT = 'otto.user_context'
|
|
59
53
|
|
|
60
54
|
# =========================================================================
|
|
61
55
|
# SECURITY & CONFIGURATION
|
|
@@ -105,6 +99,61 @@ class Otto
|
|
|
105
99
|
# Used by: Error responses, logging, support
|
|
106
100
|
ERROR_ID = 'otto.error_id'
|
|
107
101
|
|
|
102
|
+
# =========================================================================
|
|
103
|
+
# PRIVACY (IP MASKING)
|
|
104
|
+
# =========================================================================
|
|
105
|
+
|
|
106
|
+
# Privacy-safe masked IP address
|
|
107
|
+
# Type: String (e.g., '192.168.1.0')
|
|
108
|
+
# Set by: IPPrivacyMiddleware
|
|
109
|
+
# Used by: Rate limiting, analytics, logging
|
|
110
|
+
module Privacy
|
|
111
|
+
MASKED_IP = 'otto.privacy.masked_ip'
|
|
112
|
+
|
|
113
|
+
# Geo-location country code
|
|
114
|
+
# Type: String (ISO 3166-1 alpha-2)
|
|
115
|
+
# Set by: IPPrivacyMiddleware
|
|
116
|
+
# Used by: Analytics, localization
|
|
117
|
+
GEO_COUNTRY = 'otto.privacy.geo_country'
|
|
118
|
+
|
|
119
|
+
# Daily-rotating IP hash for session correlation
|
|
120
|
+
# Type: String (hexadecimal)
|
|
121
|
+
# Set by: IPPrivacyMiddleware
|
|
122
|
+
# Used by: Session correlation without storing IPs
|
|
123
|
+
HASHED_IP = 'otto.privacy.hashed_ip'
|
|
124
|
+
|
|
125
|
+
# Privacy fingerprint object
|
|
126
|
+
# Type: Otto::Privacy::RedactedFingerprint
|
|
127
|
+
# Set by: IPPrivacyMiddleware
|
|
128
|
+
# Used by: Full privacy context access
|
|
129
|
+
FINGERPRINT = 'otto.privacy.fingerprint'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# =========================================================================
|
|
133
|
+
# ORIGINAL VALUES (Privacy Disabled)
|
|
134
|
+
# =========================================================================
|
|
135
|
+
|
|
136
|
+
# Original client IP address (only when privacy disabled)
|
|
137
|
+
# Type: String
|
|
138
|
+
# Set by: IPPrivacyMiddleware (when privacy disabled)
|
|
139
|
+
# Used by: Debugging, legitimate use cases requiring real IP
|
|
140
|
+
# NOTE: Not available when privacy is enabled (intentional)
|
|
141
|
+
ORIGINAL_IP = 'otto.original_ip'
|
|
142
|
+
|
|
143
|
+
# Original User-Agent string (only when privacy disabled)
|
|
144
|
+
# Type: String
|
|
145
|
+
# Set by: IPPrivacyMiddleware (when privacy disabled)
|
|
146
|
+
# Used by: Bot detection, browser feature detection
|
|
147
|
+
# NOTE: Not available when privacy is enabled (intentional)
|
|
148
|
+
ORIGINAL_USER_AGENT = 'otto.original_user_agent'
|
|
149
|
+
|
|
150
|
+
# Original Referer URL (only when privacy disabled)
|
|
151
|
+
# Type: String
|
|
152
|
+
# Set by: IPPrivacyMiddleware (when privacy disabled)
|
|
153
|
+
# Used by: Analytics, debugging
|
|
154
|
+
# NOTE: Not available when privacy is enabled (intentional)
|
|
155
|
+
ORIGINAL_REFERER = 'otto.original_referer'
|
|
156
|
+
|
|
108
157
|
# =========================================================================
|
|
109
158
|
# MCP (MODEL CONTEXT PROTOCOL)
|
|
110
159
|
# =========================================================================
|
data/lib/otto/helpers/base.rb
CHANGED
data/lib/otto/helpers/request.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/otto/helpers/request.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require_relative 'base'
|
|
4
6
|
|
|
@@ -56,14 +58,17 @@ class Otto
|
|
|
56
58
|
# Get anonymized user agent string
|
|
57
59
|
#
|
|
58
60
|
# Returns user agent with version numbers stripped for privacy.
|
|
59
|
-
#
|
|
61
|
+
# When privacy is enabled (default), env['HTTP_USER_AGENT'] is already
|
|
62
|
+
# anonymized by IPPrivacyMiddleware, so this just returns that value.
|
|
63
|
+
# When privacy is disabled, returns the raw user agent.
|
|
60
64
|
#
|
|
61
|
-
# @return [String, nil] Anonymized user agent
|
|
65
|
+
# @return [String, nil] Anonymized (or raw if privacy disabled) user agent
|
|
62
66
|
# @example
|
|
63
67
|
# req.anonymized_user_agent
|
|
64
68
|
# # => 'Mozilla/X.X (Windows NT X.X; Win64; x64) AppleWebKit/X.X'
|
|
69
|
+
# @deprecated Use env['HTTP_USER_AGENT'] directly (already anonymized when privacy enabled)
|
|
65
70
|
def anonymized_user_agent
|
|
66
|
-
|
|
71
|
+
user_agent
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
# Get masked IP address
|
data/lib/otto/helpers.rb
CHANGED