otto 2.0.0.pre2 → 2.0.0.pre7
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 -3
- data/.github/workflows/claude-code-review.yml +29 -13
- data/.github/workflows/code-smells.yml +146 -0
- data/.gitignore +4 -0
- data/.pre-commit-config.yaml +2 -2
- data/.reek.yml +99 -0
- data/CHANGELOG.rst +90 -0
- data/CLAUDE.md +116 -45
- data/Gemfile +5 -2
- data/Gemfile.lock +70 -24
- data/README.md +49 -1
- data/changelog.d/20251103_235431_delano_86_improve_error_logging.rst +15 -0
- data/changelog.d/20251109_025012_claude_fix_backtrace_sanitization.rst +37 -0
- data/docs/.gitignore +1 -0
- data/docs/ipaddr-encoding-quirk.md +34 -0
- data/docs/migrating/v2.0.0-pre2.md +11 -18
- data/examples/advanced_routes/README.md +137 -20
- data/examples/authentication_strategies/README.md +212 -19
- data/examples/authentication_strategies/config.ru +0 -1
- 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 +90 -45
- data/lib/otto/core/error_handler.rb +138 -8
- data/lib/otto/core/file_safety.rb +2 -2
- data/lib/otto/core/freezable.rb +93 -0
- data/lib/otto/core/middleware_stack.rb +25 -18
- data/lib/otto/core/router.rb +62 -9
- data/lib/otto/core/uri_generator.rb +2 -2
- data/lib/otto/core.rb +10 -0
- data/lib/otto/design_system.rb +2 -2
- data/lib/otto/env_keys.rb +65 -12
- data/lib/otto/helpers/base.rb +2 -2
- data/lib/otto/helpers/request.rb +85 -2
- data/lib/otto/helpers/response.rb +5 -5
- data/lib/otto/helpers/validation.rb +2 -2
- data/lib/otto/helpers.rb +6 -0
- data/lib/otto/locale/config.rb +56 -0
- 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 +5 -0
- data/lib/otto/privacy/config.rb +201 -0
- data/lib/otto/privacy/geo_resolver.rb +285 -0
- data/lib/otto/privacy/ip_privacy.rb +177 -0
- data/lib/otto/privacy/redacted_fingerprint.rb +146 -0
- data/lib/otto/privacy.rb +31 -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 -1
- data/lib/otto/route_handlers/class_method.rb +18 -25
- data/lib/otto/route_handlers/factory.rb +18 -16
- data/lib/otto/route_handlers/instance_method.rb +8 -5
- data/lib/otto/route_handlers/lambda.rb +8 -20
- data/lib/otto/route_handlers/logic_class.rb +25 -8
- data/lib/otto/route_handlers.rb +2 -2
- data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +5 -5
- data/lib/otto/security/authentication/auth_strategy.rb +13 -6
- data/lib/otto/security/authentication/route_auth_wrapper.rb +304 -41
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/noauth_strategy.rb +7 -1
- 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 +5 -6
- data/lib/otto/security/authorization_error.rb +73 -0
- data/lib/otto/security/config.rb +53 -9
- data/lib/otto/security/configurator.rb +17 -15
- 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 +231 -0
- 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 +12 -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 +344 -89
- data/otto.gemspec +9 -2
- metadata +72 -8
- data/lib/otto/security/authentication/authentication_middleware.rb +0 -140
data/Gemfile.lock
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
otto (2.0.0.
|
|
4
|
+
otto (2.0.0.pre7)
|
|
5
|
+
concurrent-ruby (~> 1.3, < 2.0)
|
|
5
6
|
facets (~> 3.1)
|
|
7
|
+
ipaddr (~> 1, < 2.0)
|
|
6
8
|
logger (~> 1, < 2.0)
|
|
7
9
|
loofah (~> 2.20)
|
|
8
10
|
rack (~> 3.1, < 4.0)
|
|
9
11
|
rack-parser (~> 0.7)
|
|
10
|
-
rexml (
|
|
12
|
+
rexml (~> 3.4)
|
|
11
13
|
|
|
12
14
|
GEM
|
|
13
15
|
remote: https://rubygems.org/
|
|
14
16
|
specs:
|
|
15
17
|
ast (2.4.3)
|
|
18
|
+
benchmark (0.5.0)
|
|
16
19
|
bigdecimal (3.2.3)
|
|
17
20
|
concurrent-ruby (1.3.5)
|
|
18
21
|
crass (1.0.6)
|
|
@@ -21,15 +24,45 @@ GEM
|
|
|
21
24
|
irb (~> 1.10)
|
|
22
25
|
reline (>= 0.3.8)
|
|
23
26
|
diff-lcs (1.6.2)
|
|
24
|
-
|
|
27
|
+
dry-configurable (1.3.0)
|
|
28
|
+
dry-core (~> 1.1)
|
|
29
|
+
zeitwerk (~> 2.6)
|
|
30
|
+
dry-core (1.1.0)
|
|
31
|
+
concurrent-ruby (~> 1.0)
|
|
32
|
+
logger
|
|
33
|
+
zeitwerk (~> 2.6)
|
|
34
|
+
dry-inflector (1.2.0)
|
|
35
|
+
dry-initializer (3.2.0)
|
|
36
|
+
dry-logic (1.6.0)
|
|
37
|
+
bigdecimal
|
|
38
|
+
concurrent-ruby (~> 1.0)
|
|
39
|
+
dry-core (~> 1.1)
|
|
40
|
+
zeitwerk (~> 2.6)
|
|
41
|
+
dry-schema (1.14.1)
|
|
42
|
+
concurrent-ruby (~> 1.0)
|
|
43
|
+
dry-configurable (~> 1.0, >= 1.0.1)
|
|
44
|
+
dry-core (~> 1.1)
|
|
45
|
+
dry-initializer (~> 3.2)
|
|
46
|
+
dry-logic (~> 1.5)
|
|
47
|
+
dry-types (~> 1.8)
|
|
48
|
+
zeitwerk (~> 2.6)
|
|
49
|
+
dry-types (1.8.3)
|
|
50
|
+
bigdecimal (~> 3.0)
|
|
51
|
+
concurrent-ruby (~> 1.0)
|
|
52
|
+
dry-core (~> 1.0)
|
|
53
|
+
dry-inflector (~> 1.0)
|
|
54
|
+
dry-logic (~> 1.4)
|
|
55
|
+
zeitwerk (~> 2.6)
|
|
56
|
+
erb (5.1.1)
|
|
25
57
|
facets (3.1.0)
|
|
26
58
|
hana (1.3.7)
|
|
27
59
|
io-console (0.8.1)
|
|
60
|
+
ipaddr (1.2.7)
|
|
28
61
|
irb (1.15.2)
|
|
29
62
|
pp (>= 0.6.0)
|
|
30
63
|
rdoc (>= 4.0.0)
|
|
31
64
|
reline (>= 0.4.2)
|
|
32
|
-
json (2.15.
|
|
65
|
+
json (2.15.2)
|
|
33
66
|
json_schemer (2.4.0)
|
|
34
67
|
bigdecimal
|
|
35
68
|
hana (~> 1.3)
|
|
@@ -41,7 +74,7 @@ GEM
|
|
|
41
74
|
loofah (2.24.1)
|
|
42
75
|
crass (~> 1.0.2)
|
|
43
76
|
nokogiri (>= 1.12.0)
|
|
44
|
-
minitest (5.
|
|
77
|
+
minitest (5.26.0)
|
|
45
78
|
nokogiri (1.18.10-aarch64-linux-gnu)
|
|
46
79
|
racc (~> 1.4)
|
|
47
80
|
nokogiri (1.18.10-aarch64-linux-musl)
|
|
@@ -59,22 +92,22 @@ GEM
|
|
|
59
92
|
nokogiri (1.18.10-x86_64-linux-musl)
|
|
60
93
|
racc (~> 1.4)
|
|
61
94
|
parallel (1.27.0)
|
|
62
|
-
parser (3.3.
|
|
95
|
+
parser (3.3.10.0)
|
|
63
96
|
ast (~> 2.4.1)
|
|
64
97
|
racc
|
|
65
98
|
pastel (0.8.0)
|
|
66
99
|
tty-color (~> 0.5)
|
|
67
|
-
pp (0.6.
|
|
100
|
+
pp (0.6.3)
|
|
68
101
|
prettyprint
|
|
69
102
|
prettier_print (1.2.1)
|
|
70
103
|
prettyprint (0.2.0)
|
|
71
|
-
prism (1.
|
|
104
|
+
prism (1.6.0)
|
|
72
105
|
psych (5.2.6)
|
|
73
106
|
date
|
|
74
107
|
stringio
|
|
75
108
|
racc (1.8.1)
|
|
76
|
-
rack (3.2.
|
|
77
|
-
rack-attack (6.
|
|
109
|
+
rack (3.2.4)
|
|
110
|
+
rack-attack (6.8.0)
|
|
78
111
|
rack (>= 1.0, < 4)
|
|
79
112
|
rack-parser (0.7.0)
|
|
80
113
|
rack
|
|
@@ -85,27 +118,34 @@ GEM
|
|
|
85
118
|
rainbow (3.1.1)
|
|
86
119
|
rbs (3.9.5)
|
|
87
120
|
logger
|
|
88
|
-
rdoc (6.
|
|
121
|
+
rdoc (6.15.0)
|
|
89
122
|
erb
|
|
90
123
|
psych (>= 4.0.0)
|
|
124
|
+
tsort
|
|
125
|
+
reek (6.5.0)
|
|
126
|
+
dry-schema (~> 1.13)
|
|
127
|
+
logger (~> 1.6)
|
|
128
|
+
parser (~> 3.3.0)
|
|
129
|
+
rainbow (>= 2.0, < 4.0)
|
|
130
|
+
rexml (~> 3.1)
|
|
91
131
|
regexp_parser (2.11.3)
|
|
92
132
|
reline (0.6.2)
|
|
93
133
|
io-console (~> 0.5)
|
|
94
134
|
rexml (3.4.4)
|
|
95
|
-
rspec (3.13.
|
|
135
|
+
rspec (3.13.2)
|
|
96
136
|
rspec-core (~> 3.13.0)
|
|
97
137
|
rspec-expectations (~> 3.13.0)
|
|
98
138
|
rspec-mocks (~> 3.13.0)
|
|
99
|
-
rspec-core (3.13.
|
|
139
|
+
rspec-core (3.13.6)
|
|
100
140
|
rspec-support (~> 3.13.0)
|
|
101
141
|
rspec-expectations (3.13.5)
|
|
102
142
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
103
143
|
rspec-support (~> 3.13.0)
|
|
104
|
-
rspec-mocks (3.13.
|
|
144
|
+
rspec-mocks (3.13.6)
|
|
105
145
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
106
146
|
rspec-support (~> 3.13.0)
|
|
107
|
-
rspec-support (3.13.
|
|
108
|
-
rubocop (1.81.
|
|
147
|
+
rspec-support (3.13.6)
|
|
148
|
+
rubocop (1.81.7)
|
|
109
149
|
json (~> 2.3)
|
|
110
150
|
language_server-protocol (~> 3.17.0.2)
|
|
111
151
|
lint_roller (~> 1.1.0)
|
|
@@ -119,10 +159,10 @@ GEM
|
|
|
119
159
|
rubocop-ast (1.47.1)
|
|
120
160
|
parser (>= 3.3.7.2)
|
|
121
161
|
prism (~> 1.4)
|
|
122
|
-
rubocop-performance (1.26.
|
|
162
|
+
rubocop-performance (1.26.1)
|
|
123
163
|
lint_roller (~> 1.1)
|
|
124
164
|
rubocop (>= 1.75.0, < 2.0)
|
|
125
|
-
rubocop-ast (>= 1.
|
|
165
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
126
166
|
rubocop-rspec (3.7.0)
|
|
127
167
|
lint_roller (~> 1.1)
|
|
128
168
|
rubocop (~> 1.72, >= 1.72.1)
|
|
@@ -130,7 +170,7 @@ GEM
|
|
|
130
170
|
lint_roller (~> 1.1)
|
|
131
171
|
rubocop (~> 1.72, >= 1.72.1)
|
|
132
172
|
rubocop-ast (>= 1.44.0, < 2.0)
|
|
133
|
-
ruby-lsp (0.26.
|
|
173
|
+
ruby-lsp (0.26.2)
|
|
134
174
|
language_server-protocol (~> 3.17.0)
|
|
135
175
|
prism (>= 1.2, < 2.0)
|
|
136
176
|
rbs (>= 3, < 5)
|
|
@@ -140,21 +180,24 @@ GEM
|
|
|
140
180
|
stringio (3.1.7)
|
|
141
181
|
syntax_tree (6.3.0)
|
|
142
182
|
prettier_print (>= 1.2.0)
|
|
143
|
-
tryouts (3.
|
|
144
|
-
concurrent-ruby (~> 1.0)
|
|
183
|
+
tryouts (3.7.1)
|
|
184
|
+
concurrent-ruby (~> 1.0, < 2)
|
|
145
185
|
irb
|
|
146
186
|
minitest (~> 5.0)
|
|
147
187
|
pastel (~> 0.8)
|
|
148
188
|
prism (~> 1.0)
|
|
149
|
-
rspec (
|
|
189
|
+
rspec (>= 3.0, < 5.0)
|
|
150
190
|
tty-cursor (~> 0.7)
|
|
151
191
|
tty-screen (~> 0.8)
|
|
192
|
+
tsort (0.2.0)
|
|
152
193
|
tty-color (0.6.0)
|
|
153
194
|
tty-cursor (0.7.1)
|
|
154
195
|
tty-screen (0.8.2)
|
|
155
196
|
unicode-display_width (3.2.0)
|
|
156
197
|
unicode-emoji (~> 4.1)
|
|
157
198
|
unicode-emoji (4.1.0)
|
|
199
|
+
user_agent_parser (2.20.0)
|
|
200
|
+
zeitwerk (2.7.3)
|
|
158
201
|
|
|
159
202
|
PLATFORMS
|
|
160
203
|
aarch64-linux-gnu
|
|
@@ -167,21 +210,24 @@ PLATFORMS
|
|
|
167
210
|
x86_64-linux-musl
|
|
168
211
|
|
|
169
212
|
DEPENDENCIES
|
|
213
|
+
benchmark
|
|
170
214
|
debug
|
|
171
215
|
json_schemer
|
|
172
216
|
otto!
|
|
173
217
|
rack-attack
|
|
174
218
|
rack-test
|
|
175
219
|
rackup
|
|
220
|
+
reek (~> 6.5)
|
|
176
221
|
rspec (~> 3.13)
|
|
177
|
-
rubocop (~> 1.81.
|
|
222
|
+
rubocop (~> 1.81.7)
|
|
178
223
|
rubocop-performance
|
|
179
224
|
rubocop-rspec
|
|
180
225
|
rubocop-thread_safety
|
|
181
226
|
ruby-lsp
|
|
182
227
|
stackprof
|
|
183
228
|
syntax_tree
|
|
184
|
-
tryouts (~> 3.
|
|
229
|
+
tryouts (~> 3.7.1)
|
|
230
|
+
user_agent_parser (~> 2.18)
|
|
185
231
|
|
|
186
232
|
BUNDLED WITH
|
|
187
233
|
2.7.1
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Define your rack-apps in plain-text with built-in security.**
|
|
4
4
|
|
|
5
|
-
> **v2.0.0-
|
|
5
|
+
> **v2.0.0-pre6 Available**: This pre-release includes major improvements to middleware management, logging, and request callback handling. See [changelog](CHANGELOG.rst) for details and upgrade notes.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -13,6 +13,14 @@ $ cd myapp && ls
|
|
|
13
13
|
config.ru app.rb routes
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## Why Otto?
|
|
17
|
+
|
|
18
|
+
- **Security by Default**: Automatic IP masking for public addresses, user agent anonymization, CSRF protection, and input validation
|
|
19
|
+
- **Privacy First**: Masks public IPs, strips user agent versions, provides country-level geo-location only—no external APIs needed
|
|
20
|
+
- **Simple Routing**: Define routes in plain-text files with zero configuration overhead
|
|
21
|
+
- **Built-in Authentication**: Multiple strategies including API keys, tokens, role-based access, and custom implementations
|
|
22
|
+
- **Developer Friendly**: Works with any Rack server, minimal dependencies, easy testing and debugging
|
|
23
|
+
|
|
16
24
|
## Routes File
|
|
17
25
|
```
|
|
18
26
|
# routes
|
|
@@ -76,6 +84,22 @@ app = Otto.new("./routes", {
|
|
|
76
84
|
|
|
77
85
|
Security features include CSRF protection, input validation, security headers, and trusted proxy configuration.
|
|
78
86
|
|
|
87
|
+
## Privacy by Default
|
|
88
|
+
|
|
89
|
+
Otto automatically masks public IP addresses and anonymizes user agents to comply with GDPR, CCPA, and other privacy regulations:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Public IPs are automatically masked (203.0.113.9 → 203.0.113.0)
|
|
93
|
+
# Private IPs are NOT masked by default (127.0.0.1, 192.168.x.x, 10.x.x.x)
|
|
94
|
+
app = Otto.new("./routes")
|
|
95
|
+
|
|
96
|
+
# User agents: versions stripped for privacy
|
|
97
|
+
# Geo-location: country-level only, no external APIs or databases
|
|
98
|
+
# IP hashing: daily-rotating hashes enable analytics without tracking
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Private and localhost IPs are exempted by default for development convenience, but this behavior can be customized via `configure_ip_privacy()` method. Geolocation uses CDN headers (Cloudflare, AWS, etc.) with fallback to IP ranges—no external services required. See [CLAUDE.md](CLAUDE.md) for detailed configuration options.
|
|
102
|
+
|
|
79
103
|
## Internationalization Support
|
|
80
104
|
|
|
81
105
|
Otto provides built-in locale detection and management:
|
|
@@ -132,6 +156,24 @@ end
|
|
|
132
156
|
|
|
133
157
|
The locale helper checks multiple sources in order of precedence and validates against your configured locales.
|
|
134
158
|
|
|
159
|
+
## Examples
|
|
160
|
+
|
|
161
|
+
Otto includes comprehensive examples demonstrating different features:
|
|
162
|
+
|
|
163
|
+
- **[Basic Example](examples/basic/)** - Get your first Otto app running in minutes
|
|
164
|
+
- **[Advanced Routes](examples/advanced_routes/)** - Response types, CSRF exemption, logic classes, and namespaced routing
|
|
165
|
+
- **[Authentication Strategies](examples/authentication_strategies/)** - Token, API key, and role-based authentication
|
|
166
|
+
- **[Security Features](examples/security_features/)** - CSRF protection, input validation, file uploads, and security headers
|
|
167
|
+
- **[MCP Demo](examples/mcp_demo/)** - JSON-RPC 2.0 endpoints for CLI automation and integrations
|
|
168
|
+
|
|
169
|
+
### Standalone Tutorials
|
|
170
|
+
|
|
171
|
+
- **[Error Handler Registration](examples/error_handler_registration.rb)** - Prevent 500 errors for expected business exceptions
|
|
172
|
+
- **[Logging Improvements](examples/logging_improvements.rb)** - Structured logging with automatic timing
|
|
173
|
+
- **[Geo-location Extension](examples/simple_geo_resolver.rb)** - Extending geo-location with custom resolvers
|
|
174
|
+
|
|
175
|
+
See the [examples/](examples/) directory for more.
|
|
176
|
+
|
|
135
177
|
## Requirements
|
|
136
178
|
|
|
137
179
|
- Ruby 3.2+
|
|
@@ -143,6 +185,12 @@ The locale helper checks multiple sources in order of precedence and validates a
|
|
|
143
185
|
gem install otto
|
|
144
186
|
```
|
|
145
187
|
|
|
188
|
+
## Documentation
|
|
189
|
+
|
|
190
|
+
- **[CLAUDE.md](CLAUDE.md)** - Comprehensive developer guidance covering authentication architecture, configuration freezing, IP privacy, structured logging, and multi-app patterns
|
|
191
|
+
- **[docs/](docs/)** - Technical guides and migration guides
|
|
192
|
+
- **[CHANGELOG.rst](CHANGELOG.rst)** - Version history, breaking changes, and upgrade notes
|
|
193
|
+
|
|
146
194
|
## AI Development Assistance
|
|
147
195
|
|
|
148
196
|
Version 1.2.0's security features were developed with AI assistance:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Added
|
|
2
|
+
-----
|
|
3
|
+
|
|
4
|
+
- Error handler registration system for expected business logic errors. Register handlers with ``otto.register_error_handler(ErrorClass, status: 404, log_level: :info)`` to return proper HTTP status codes and avoid logging expected errors as 500s with backtraces. Supports custom response handlers via blocks for complete control over error responses.
|
|
5
|
+
|
|
6
|
+
Changed
|
|
7
|
+
-------
|
|
8
|
+
|
|
9
|
+
- Backtrace logging now always logs at ERROR level (was DEBUG) with sanitized file paths for security. Backtraces for unhandled 500 errors are always logged regardless of ``OTTO_DEBUG`` setting, with paths sanitized to prevent exposing system information (project files show relative paths, gems show ``[GEM] name-version/path``, Ruby stdlib shows ``[RUBY] filename``).
|
|
10
|
+
- Increased backtrace limit from 10 to 20 lines for critical errors to provide better debugging context.
|
|
11
|
+
|
|
12
|
+
AI Assistance
|
|
13
|
+
-------------
|
|
14
|
+
|
|
15
|
+
- Implemented error handler registration architecture with comprehensive test coverage (17 test cases) using sequential thinking to work through security implications and design decisions. AI assisted with path sanitization strategy, error classification patterns, and ensuring backward compatibility with existing error handling.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.. Changed in: otto
|
|
2
|
+
.. Fixes issue:
|
|
3
|
+
|
|
4
|
+
Improved backtrace sanitization security and readability
|
|
5
|
+
---------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
**Security Enhancements:**
|
|
8
|
+
|
|
9
|
+
- Fixed bundler gem path detection to correctly sanitize git-based gems
|
|
10
|
+
- Now properly handles nested gem paths like ``/gems/3.4.0/bundler/gems/otto-abc123/``
|
|
11
|
+
- Strips git hash suffixes from bundler gems (``otto-abc123def456`` → ``otto``)
|
|
12
|
+
- Removes version numbers from regular gems (``rack-3.2.4`` → ``rack``)
|
|
13
|
+
- Prevents exposure of absolute paths, usernames, and project names in logs
|
|
14
|
+
|
|
15
|
+
**Improvements:**
|
|
16
|
+
|
|
17
|
+
- Bundler gems now show as ``[GEM] otto/lib/otto/route.rb:142`` instead of ``[GEM] 3.4.0/bundler/gems/...``
|
|
18
|
+
- Regular gems show cleaner output: ``[GEM] rack/lib/rack.rb:20`` instead of ``[GEM] rack-3.2.4/lib/rack.rb:20``
|
|
19
|
+
- Multi-hyphenated gem names handled correctly (``active-record-import-1.5.0`` → ``active-record-import``)
|
|
20
|
+
- Better handling of version-only directory names in gem paths
|
|
21
|
+
|
|
22
|
+
**Documentation:**
|
|
23
|
+
|
|
24
|
+
- Added comprehensive backtrace sanitization section to CLAUDE.md
|
|
25
|
+
- Documented security guarantees and sanitization rules
|
|
26
|
+
- Added examples showing before/after path transformations
|
|
27
|
+
- Created comprehensive test suite for backtrace sanitization
|
|
28
|
+
|
|
29
|
+
**Rationale:**
|
|
30
|
+
|
|
31
|
+
Raw backtraces expose sensitive information:
|
|
32
|
+
- Usernames (``/Users/alice/``, ``/home/admin/``)
|
|
33
|
+
- Project structure and internal organization
|
|
34
|
+
- Gem installation paths and Ruby versions
|
|
35
|
+
- System architecture details
|
|
36
|
+
|
|
37
|
+
This improvement ensures all backtraces are sanitized automatically, preventing accidental leakage of sensitive system information while maintaining readability for debugging.
|
data/docs/.gitignore
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# IPAddr#to_s Encoding Quirk in Ruby 3
|
|
2
|
+
|
|
3
|
+
Ruby's `IPAddr#to_s` returns inconsistent encodings: IPv4 addresses use US-ASCII, IPv6 addresses use UTF-8.
|
|
4
|
+
|
|
5
|
+
## Behavior
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
IPAddr.new('192.168.1.1').to_s.encoding # => #<Encoding:US-ASCII>
|
|
9
|
+
IPAddr.new('::1').to_s.encoding # => #<Encoding:UTF-8>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Cause
|
|
13
|
+
|
|
14
|
+
Different string construction in IPAddr's `_to_string` method:
|
|
15
|
+
|
|
16
|
+
- **IPv4**: `Array#join('.')` → US-ASCII optimization
|
|
17
|
+
- **IPv6**: `String#%` → UTF-8 default
|
|
18
|
+
|
|
19
|
+
## Impact
|
|
20
|
+
|
|
21
|
+
- Rack expects UTF-8 strings
|
|
22
|
+
- Mixed encodings cause `Encoding::CompatibilityError`
|
|
23
|
+
- String operations fail on encoding mismatches
|
|
24
|
+
|
|
25
|
+
## Solution
|
|
26
|
+
|
|
27
|
+
Use `force_encoding('UTF-8')` instead of `encode('UTF-8')`:
|
|
28
|
+
|
|
29
|
+
- IP addresses contain only ASCII characters
|
|
30
|
+
- ASCII bytes are identical in US-ASCII and UTF-8
|
|
31
|
+
- `force_encoding` changes label only (O(1))
|
|
32
|
+
- `encode` creates new string (O(n))
|
|
33
|
+
|
|
34
|
+
This ensures consistent UTF-8 encoding across all IP strings.
|
|
@@ -27,12 +27,6 @@ if strategy_result.is_a?(Otto::Security::Authentication::StrategyResult)
|
|
|
27
27
|
# handle success
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# Or for middleware - FailureResult indicates failure
|
|
31
|
-
if strategy_result.is_a?(Otto::Security::Authentication::FailureResult)
|
|
32
|
-
# handle failure
|
|
33
|
-
else
|
|
34
|
-
# handle success
|
|
35
|
-
end
|
|
36
30
|
```
|
|
37
31
|
|
|
38
32
|
### 2. New Semantic Distinction
|
|
@@ -116,7 +110,7 @@ session['authenticated_at'] # Timestamp
|
|
|
116
110
|
**Optional session keys:**
|
|
117
111
|
```ruby
|
|
118
112
|
session['email'] # User email
|
|
119
|
-
session['ip_address'] # Client IP
|
|
113
|
+
session['ip_address'] # Client IP (masked by default via IPPrivacyMiddleware)
|
|
120
114
|
session['user_agent'] # Client UA
|
|
121
115
|
session['locale'] # User locale
|
|
122
116
|
```
|
|
@@ -140,7 +134,7 @@ class Controller::Base
|
|
|
140
134
|
session: session,
|
|
141
135
|
user: cust,
|
|
142
136
|
auth_method: 'session', # Hardcoded - loses semantic meaning
|
|
143
|
-
metadata: { ip: req.
|
|
137
|
+
metadata: { ip: req.masked_ip } # Uses masked IP (privacy by default)
|
|
144
138
|
)
|
|
145
139
|
end
|
|
146
140
|
end
|
|
@@ -148,10 +142,10 @@ end
|
|
|
148
142
|
|
|
149
143
|
**Correct approach:**
|
|
150
144
|
```ruby
|
|
151
|
-
# GOOD - Use
|
|
145
|
+
# GOOD - Use RouteAuthWrapper-provided result
|
|
152
146
|
class Controller::Base
|
|
153
147
|
def strategy_result
|
|
154
|
-
req.env['otto.strategy_result'] # Created by
|
|
148
|
+
req.env['otto.strategy_result'] # Created by RouteAuthWrapper
|
|
155
149
|
end
|
|
156
150
|
end
|
|
157
151
|
|
|
@@ -188,7 +182,6 @@ expect(result).to be_failure
|
|
|
188
182
|
|
|
189
183
|
# After
|
|
190
184
|
expect(result).to be_a(Otto::Security::Authentication::StrategyResult)
|
|
191
|
-
expect(result).to be_a(Otto::Security::Authentication::FailureResult)
|
|
192
185
|
```
|
|
193
186
|
|
|
194
187
|
## Architecture Clarifications
|
|
@@ -196,13 +189,13 @@ expect(result).to be_a(Otto::Security::Authentication::FailureResult)
|
|
|
196
189
|
### When StrategyResult is Created
|
|
197
190
|
|
|
198
191
|
1. **Routes WITH `auth=...` requirement:**
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
-
|
|
192
|
+
- RouteAuthWrapper executes strategy
|
|
193
|
+
- Always returns `StrategyResult` (success or failure)
|
|
194
|
+
- RouteAuthWrapper returns 401/302 response on `AuthFailure`
|
|
202
195
|
|
|
203
196
|
2. **Routes WITHOUT `auth=...` requirement:**
|
|
204
|
-
-
|
|
205
|
-
-
|
|
197
|
+
- No RouteAuthWrapper wrapping
|
|
198
|
+
- No `StrategyResult` created (routes without auth don't need it)
|
|
206
199
|
|
|
207
200
|
3. **Auth app (Roda) routes:**
|
|
208
201
|
- Manually creates `StrategyResult` for Logic class compatibility
|
|
@@ -213,7 +206,7 @@ expect(result).to be_a(Otto::Security::Authentication::FailureResult)
|
|
|
213
206
|
**Multi-app setup (Auth + Core + API):**
|
|
214
207
|
- **Shared:** Session middleware, Redis session, Logic classes, Customer model
|
|
215
208
|
- **Auth app:** Creates StrategyResult manually, uses Roda routing
|
|
216
|
-
- **Core/API apps:** StrategyResult from
|
|
209
|
+
- **Core/API apps:** StrategyResult from RouteAuthWrapper
|
|
217
210
|
- **Integration:** Pure session-based, no direct code calls between apps
|
|
218
211
|
|
|
219
212
|
## Testing Your Migration
|
|
@@ -339,7 +332,7 @@ middleware.add_with_position(
|
|
|
339
332
|
|
|
340
333
|
Review the comprehensive inline documentation in:
|
|
341
334
|
- `lib/otto/security/authentication/strategy_result.rb` (lines 1-90) - Auth semantics
|
|
342
|
-
- `lib/otto/security/authentication/
|
|
335
|
+
- `lib/otto/security/authentication/route_auth_wrapper.rb` - Auth handler wrapper
|
|
343
336
|
- `lib/otto/env_keys.rb` - Complete env key registry
|
|
344
337
|
|
|
345
338
|
The documentation includes detailed usage patterns, session contracts, and examples for common scenarios.
|
|
@@ -1,33 +1,150 @@
|
|
|
1
1
|
# Otto - Advanced Routes Example
|
|
2
2
|
|
|
3
|
-
This example demonstrates
|
|
3
|
+
This example demonstrates advanced routing features in Otto, including response type negotiation, CSRF exemptions, logic classes, and namespaced routing.
|
|
4
|
+
|
|
5
|
+
## What You'll Learn
|
|
6
|
+
|
|
7
|
+
- How to define response types (JSON, view, redirect) in routes
|
|
8
|
+
- Using logic classes to encapsulate business logic
|
|
9
|
+
- CSRF exemption for APIs and webhooks
|
|
10
|
+
- Routing to namespaced classes with complex hierarchies
|
|
11
|
+
- Custom route parameters for flexible routing
|
|
12
|
+
- How Otto handles multiple controllers and modules
|
|
4
13
|
|
|
5
14
|
## Project Structure
|
|
6
15
|
|
|
7
|
-
The example is
|
|
16
|
+
The example is organized to separate concerns:
|
|
8
17
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
18
|
+
- `config.ru`: Rack configuration that loads and runs the Otto application
|
|
19
|
+
- `routes`: Comprehensive reference for advanced routing syntax
|
|
20
|
+
- `app.rb`: Loader that requires all controller and logic files
|
|
21
|
+
- `app/controllers/`: Handler classes (`RoutesApp`, namespaced controllers)
|
|
22
|
+
- `app/logic/`: Business logic classes (simple, nested, namespaced)
|
|
23
|
+
- `run.rb`, `puma.rb`, `test.rb`: Alternative server/test runners
|
|
15
24
|
|
|
16
25
|
## Key Features Demonstrated
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
### Response Types
|
|
28
|
+
Define how responses are formatted directly in routes:
|
|
29
|
+
```
|
|
30
|
+
GET /api/users UserController#list response=json
|
|
31
|
+
GET /page PageController#show response=view
|
|
32
|
+
GET /old-url PageController#new-url response=redirect
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Logic Classes
|
|
36
|
+
Route to specialized classes that encapsulate business logic:
|
|
37
|
+
```
|
|
38
|
+
GET /calculate DataProcessor # Otto auto-instantiates and calls #process
|
|
39
|
+
GET /report ReportGenerator # Same pattern
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### CSRF Exemption
|
|
43
|
+
Mark routes that don't need CSRF tokens (APIs, webhooks):
|
|
44
|
+
```
|
|
45
|
+
POST /api/webhook WebhookHandler#receive csrf=exempt
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Namespaced Routing
|
|
49
|
+
Handle complex class hierarchies naturally:
|
|
50
|
+
```
|
|
51
|
+
GET /v2/dashboard V2::Logic::Dashboard
|
|
52
|
+
GET /admin/panel Admin::Panel#dashboard
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Custom Parameters
|
|
56
|
+
Add arbitrary key-value pairs for flexible routing:
|
|
57
|
+
```
|
|
58
|
+
GET /admin AdminPanel#dashboard role=admin
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## How to Run
|
|
62
|
+
|
|
63
|
+
### Using rackup (recommended)
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
cd examples/advanced_routes
|
|
67
|
+
rackup config.ru
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Using alternative runners
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
ruby run.rb # Basic rackup
|
|
74
|
+
ruby puma.rb # Using puma server
|
|
75
|
+
ruby test.rb # For testing
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The application will be running at `http://localhost:9292`.
|
|
79
|
+
|
|
80
|
+
## Testing Routes
|
|
81
|
+
|
|
82
|
+
Use curl to test the different routes:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
# JSON response
|
|
86
|
+
curl http://localhost:9292/json/test
|
|
87
|
+
|
|
88
|
+
# View response
|
|
89
|
+
curl http://localhost:9292/view/test
|
|
90
|
+
|
|
91
|
+
# Logic class routing
|
|
92
|
+
curl http://localhost:9292/logic/simple
|
|
93
|
+
|
|
94
|
+
# Namespaced routing
|
|
95
|
+
curl http://localhost:9292/logic/v2/dashboard
|
|
96
|
+
|
|
97
|
+
# Custom parameters
|
|
98
|
+
curl "http://localhost:9292/custom?role=admin"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Expected Output
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
Listening on 127.0.0.1:9292, CTRL+C to stop
|
|
105
|
+
|
|
106
|
+
[JSON response]
|
|
107
|
+
GET /json/test 200 OK
|
|
108
|
+
Content-Type: application/json
|
|
109
|
+
{"status": "success", "data": {...}}
|
|
110
|
+
|
|
111
|
+
[Logic class routing]
|
|
112
|
+
GET /logic/simple 200 OK
|
|
113
|
+
{"processed": true, "input": "test"}
|
|
114
|
+
|
|
115
|
+
[Namespaced routing]
|
|
116
|
+
GET /logic/v2/dashboard 200 OK
|
|
117
|
+
{"version": "2.0", "dashboard": {...}}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## File Structure Details
|
|
121
|
+
|
|
122
|
+
### Routes File
|
|
123
|
+
The `routes` file is extensively commented to explain each feature:
|
|
124
|
+
- Response type specification
|
|
125
|
+
- CSRF exemption for APIs
|
|
126
|
+
- Logic class routing syntax
|
|
127
|
+
- Namespaced class resolution
|
|
128
|
+
- Custom parameter examples
|
|
129
|
+
|
|
130
|
+
### Controllers (`app/controllers/`)
|
|
131
|
+
- `RoutesApp`: Main controller with basic handlers
|
|
132
|
+
- Namespaced modules: Demonstrate complex class hierarchies
|
|
133
|
+
- Handlers return appropriate responses (JSON, HTML, redirects)
|
|
134
|
+
|
|
135
|
+
### Logic Classes (`app/logic/`)
|
|
136
|
+
- Simple classes: Basic business logic
|
|
137
|
+
- Nested classes: Show how Otto handles namespace resolution
|
|
138
|
+
- Parameterized logic: Demonstrate custom route parameters
|
|
23
139
|
|
|
24
|
-
##
|
|
140
|
+
## Next Steps
|
|
25
141
|
|
|
26
|
-
|
|
27
|
-
|
|
142
|
+
- Review the `routes` file for syntax reference
|
|
143
|
+
- Examine handler methods to see request/response patterns
|
|
144
|
+
- Check logic classes for business logic encapsulation patterns
|
|
145
|
+
- Explore [Authentication](../authentication_strategies/) for protecting routes
|
|
146
|
+
- See [Security Features](../security_features/) for CSRF, validation, file uploads
|
|
28
147
|
|
|
29
|
-
|
|
30
|
-
rackup examples/advanced_routes/config.ru
|
|
31
|
-
```
|
|
148
|
+
## Further Reading
|
|
32
149
|
|
|
33
|
-
|
|
150
|
+
- [CLAUDE.md](../../CLAUDE.md) - Comprehensive developer guidance
|