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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -3
  3. data/.github/workflows/claude-code-review.yml +29 -13
  4. data/.github/workflows/code-smells.yml +146 -0
  5. data/.gitignore +4 -0
  6. data/.pre-commit-config.yaml +2 -2
  7. data/.reek.yml +99 -0
  8. data/CHANGELOG.rst +90 -0
  9. data/CLAUDE.md +116 -45
  10. data/Gemfile +5 -2
  11. data/Gemfile.lock +70 -24
  12. data/README.md +49 -1
  13. data/changelog.d/20251103_235431_delano_86_improve_error_logging.rst +15 -0
  14. data/changelog.d/20251109_025012_claude_fix_backtrace_sanitization.rst +37 -0
  15. data/docs/.gitignore +1 -0
  16. data/docs/ipaddr-encoding-quirk.md +34 -0
  17. data/docs/migrating/v2.0.0-pre2.md +11 -18
  18. data/examples/advanced_routes/README.md +137 -20
  19. data/examples/authentication_strategies/README.md +212 -19
  20. data/examples/authentication_strategies/config.ru +0 -1
  21. data/examples/backtrace_sanitization_demo.rb +86 -0
  22. data/examples/basic/README.md +61 -10
  23. data/examples/error_handler_registration.rb +136 -0
  24. data/examples/logging_improvements.rb +76 -0
  25. data/examples/mcp_demo/README.md +187 -27
  26. data/examples/security_features/README.md +249 -30
  27. data/examples/simple_geo_resolver.rb +107 -0
  28. data/lib/otto/core/configuration.rb +90 -45
  29. data/lib/otto/core/error_handler.rb +138 -8
  30. data/lib/otto/core/file_safety.rb +2 -2
  31. data/lib/otto/core/freezable.rb +93 -0
  32. data/lib/otto/core/middleware_stack.rb +25 -18
  33. data/lib/otto/core/router.rb +62 -9
  34. data/lib/otto/core/uri_generator.rb +2 -2
  35. data/lib/otto/core.rb +10 -0
  36. data/lib/otto/design_system.rb +2 -2
  37. data/lib/otto/env_keys.rb +65 -12
  38. data/lib/otto/helpers/base.rb +2 -2
  39. data/lib/otto/helpers/request.rb +85 -2
  40. data/lib/otto/helpers/response.rb +5 -5
  41. data/lib/otto/helpers/validation.rb +2 -2
  42. data/lib/otto/helpers.rb +6 -0
  43. data/lib/otto/locale/config.rb +56 -0
  44. data/lib/otto/locale/middleware.rb +160 -0
  45. data/lib/otto/locale.rb +10 -0
  46. data/lib/otto/logging_helpers.rb +273 -0
  47. data/lib/otto/mcp/auth/token.rb +2 -2
  48. data/lib/otto/mcp/protocol.rb +2 -2
  49. data/lib/otto/mcp/rate_limiting.rb +2 -2
  50. data/lib/otto/mcp/registry.rb +2 -2
  51. data/lib/otto/mcp/route_parser.rb +2 -2
  52. data/lib/otto/mcp/schema_validation.rb +2 -2
  53. data/lib/otto/mcp/server.rb +2 -2
  54. data/lib/otto/mcp.rb +5 -0
  55. data/lib/otto/privacy/config.rb +201 -0
  56. data/lib/otto/privacy/geo_resolver.rb +285 -0
  57. data/lib/otto/privacy/ip_privacy.rb +177 -0
  58. data/lib/otto/privacy/redacted_fingerprint.rb +146 -0
  59. data/lib/otto/privacy.rb +31 -0
  60. data/lib/otto/response_handlers/auto.rb +2 -0
  61. data/lib/otto/response_handlers/base.rb +2 -0
  62. data/lib/otto/response_handlers/default.rb +2 -0
  63. data/lib/otto/response_handlers/factory.rb +2 -0
  64. data/lib/otto/response_handlers/json.rb +2 -0
  65. data/lib/otto/response_handlers/redirect.rb +2 -0
  66. data/lib/otto/response_handlers/view.rb +2 -0
  67. data/lib/otto/response_handlers.rb +2 -2
  68. data/lib/otto/route.rb +4 -4
  69. data/lib/otto/route_definition.rb +42 -15
  70. data/lib/otto/route_handlers/base.rb +2 -1
  71. data/lib/otto/route_handlers/class_method.rb +18 -25
  72. data/lib/otto/route_handlers/factory.rb +18 -16
  73. data/lib/otto/route_handlers/instance_method.rb +8 -5
  74. data/lib/otto/route_handlers/lambda.rb +8 -20
  75. data/lib/otto/route_handlers/logic_class.rb +25 -8
  76. data/lib/otto/route_handlers.rb +2 -2
  77. data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +5 -5
  78. data/lib/otto/security/authentication/auth_strategy.rb +13 -6
  79. data/lib/otto/security/authentication/route_auth_wrapper.rb +304 -41
  80. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
  81. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +7 -1
  82. data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
  83. data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
  84. data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
  85. data/lib/otto/security/authentication/strategy_result.rb +6 -5
  86. data/lib/otto/security/authentication.rb +5 -6
  87. data/lib/otto/security/authorization_error.rb +73 -0
  88. data/lib/otto/security/config.rb +53 -9
  89. data/lib/otto/security/configurator.rb +17 -15
  90. data/lib/otto/security/csrf.rb +2 -2
  91. data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
  92. data/lib/otto/security/middleware/ip_privacy_middleware.rb +231 -0
  93. data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
  94. data/lib/otto/security/middleware/validation_middleware.rb +15 -0
  95. data/lib/otto/security/rate_limiter.rb +2 -2
  96. data/lib/otto/security/rate_limiting.rb +2 -2
  97. data/lib/otto/security/validator.rb +2 -2
  98. data/lib/otto/security.rb +12 -0
  99. data/lib/otto/static.rb +2 -2
  100. data/lib/otto/utils.rb +27 -2
  101. data/lib/otto/version.rb +3 -3
  102. data/lib/otto.rb +344 -89
  103. data/otto.gemspec +9 -2
  104. metadata +72 -8
  105. data/lib/otto/security/authentication/authentication_middleware.rb +0 -140
@@ -1,42 +1,82 @@
1
1
  # Otto MCP Demo
2
2
 
3
- This example demonstrates Otto's Model-Controller-Protocol (MCP) feature. MCP provides a standardized JSON-RPC 2.0 endpoint (`/_mcp`) that allows you to expose application resources and tools securely.
3
+ This example demonstrates Otto's Model-Controller-Protocol (MCP) feature. MCP provides a standardized JSON-RPC 2.0 endpoint (`/_mcp`) for programmatic access to your application resources and tools.
4
4
 
5
- This is useful for building CLIs, admin interfaces, or allowing other services to interact with your application programmatically.
5
+ MCP is useful for building CLIs, admin interfaces, integrating with AI systems, or allowing other services to interact with your application.
6
+
7
+ ## What You'll Learn
8
+
9
+ - How to set up an MCP endpoint for programmatic access
10
+ - Exposing application resources via JSON-RPC 2.0
11
+ - Securing MCP endpoints with bearer token authentication
12
+ - Distinguishing between read-only resources and executable tools
13
+ - Organizing methods for both web and MCP interfaces
14
+ - How MCP integrates with your existing Otto application
6
15
 
7
16
  ## Features Demonstrated
8
17
 
9
- * **MCP Endpoint:** A single `POST /_mcp` endpoint for all API interactions.
10
- * **Authentication:** Requests to the MCP endpoint are protected by bearer token authentication.
11
- * **Rate Limiting:** The endpoint has its own rate limiting to prevent abuse.
12
- * **Resources:** Read-only data exposed via `MCP` routes (e.g., listing users).
13
- * **Tools:** Actions or operations exposed via `TOOL` routes (e.g., creating a user).
18
+ - **MCP Endpoint**: Single `POST /_mcp` endpoint for all interactions
19
+ - **Authentication**: Bearer token authentication for secure access
20
+ - **Rate Limiting**: Built-in rate limiting to prevent abuse
21
+ - **Resources**: Read-only data exposed via `MCP` routes
22
+ - **Tools**: Executable actions exposed via `TOOL` routes
23
+ - **Web Interface**: Separate web routes coexist with MCP routes
24
+ - **JSON-RPC 2.0**: Standard protocol for all MCP interactions
14
25
 
15
26
  ## How to Run
16
27
 
17
- 1. Make sure you have `bundler` and `thin` installed:
18
- ```sh
19
- gem install bundler thin
20
- ```
28
+ ### Using rackup (recommended)
29
+
30
+ ```sh
31
+ cd examples/mcp_demo
32
+ rackup config.ru
33
+ ```
34
+
35
+ ### Using thin
36
+
37
+ ```sh
38
+ cd examples/mcp_demo
39
+ thin -R config.ru -p 9292 start
40
+ ```
41
+
42
+ The server will start on `http://localhost:9292`.
43
+
44
+ - **Web interface**: Navigate to `http://localhost:9292` in your browser
45
+ - **MCP endpoint**: Send JSON-RPC 2.0 requests to `http://localhost:9292/_mcp`
21
46
 
22
- 2. Install the dependencies from the root of the project:
23
- ```sh
24
- bundle install
25
- ```
47
+ ## Authentication
26
48
 
27
- 3. Start the server from this directory (`examples/mcp_demo`):
28
- ```sh
29
- thin -R config.ru -p 9292 start
30
- ```
31
- *Note: This demo uses port 9292 as is conventional for Rack apps.*
49
+ All MCP requests require bearer token authentication via the `Authorization` header.
32
50
 
33
- 4. Open your browser and navigate to `http://localhost:9292` to see the welcome page.
51
+ Valid tokens:
52
+ - `demo-token-123` - Standard user
53
+ - `another-token-456` - Alternative user
34
54
 
35
55
  ## Interacting with the MCP Endpoint
36
56
 
37
- All interactions happen via `POST` requests to `http://localhost:9292/_mcp`. You must provide an `Authorization: Bearer <token>` header and a `Content-Type: application/json` header.
57
+ All MCP interactions use the `POST /_mcp` endpoint. Each request is a JSON-RPC 2.0 request with:
38
58
 
39
- Valid tokens are `demo-token-123` and `another-token-456`.
59
+ - `jsonrpc`: Always `"2.0"`
60
+ - `method`: The RPC method name (derived from route path)
61
+ - `id`: Request ID (for matching responses)
62
+ - `params`: Optional parameters as an object
63
+
64
+ Required headers:
65
+ - `Authorization: Bearer <token>`
66
+ - `Content-Type: application/json`
67
+
68
+ Example:
69
+ ```sh
70
+ curl -X POST http://localhost:9292/_mcp \
71
+ -H 'Authorization: Bearer demo-token-123' \
72
+ -H 'Content-Type: application/json' \
73
+ -d '{
74
+ "jsonrpc": "2.0",
75
+ "method": "initialize",
76
+ "id": 1,
77
+ "params": {}
78
+ }'
79
+ ```
40
80
 
41
81
  ### MCP: Initialize
42
82
 
@@ -79,9 +119,129 @@ curl -X POST http://localhost:9292/_mcp \
79
119
  }'
80
120
  ```
81
121
 
122
+ ## Expected Output
123
+
124
+ ### Successful Initialize Request
125
+ ```json
126
+ {
127
+ "jsonrpc": "2.0",
128
+ "result": {
129
+ "resources": [
130
+ {
131
+ "uri": "users",
132
+ "name": "User List",
133
+ "description": "List all users"
134
+ }
135
+ ],
136
+ "tools": [
137
+ {
138
+ "name": "create_user",
139
+ "description": "Create a new user",
140
+ "inputSchema": {
141
+ "type": "object",
142
+ "properties": {
143
+ "name": { "type": "string" },
144
+ "email": { "type": "string" }
145
+ }
146
+ }
147
+ }
148
+ ]
149
+ },
150
+ "id": 1
151
+ }
152
+ ```
153
+
154
+ ### Successful Resource Request
155
+ ```json
156
+ {
157
+ "jsonrpc": "2.0",
158
+ "result": {
159
+ "users": [
160
+ { "id": 1, "name": "Alice", "email": "alice@example.com" },
161
+ { "id": 2, "name": "Bob", "email": "bob@example.com" }
162
+ ]
163
+ },
164
+ "id": 2
165
+ }
166
+ ```
167
+
168
+ ### Successful Tool Execution
169
+ ```json
170
+ {
171
+ "jsonrpc": "2.0",
172
+ "result": {
173
+ "user": {
174
+ "id": 3,
175
+ "name": "Charlie",
176
+ "email": "charlie@example.com"
177
+ }
178
+ },
179
+ "id": 3
180
+ }
181
+ ```
182
+
183
+ ### Authentication Failure
184
+ ```json
185
+ {
186
+ "jsonrpc": "2.0",
187
+ "error": {
188
+ "code": -32003,
189
+ "message": "Unauthorized"
190
+ },
191
+ "id": 1
192
+ }
193
+ ```
194
+
82
195
  ## File Structure
83
196
 
84
- * `README.md`: This file.
85
- * `app.rb`: Contains the application logic, including the `DemoApp` for web pages and the `UserAPI` for MCP handlers.
86
- * `config.ru`: The Rack configuration file. It loads the Otto framework, enables MCP, and runs the application.
87
- * `routes`: Defines the standard web routes as well as the `MCP` and `TOOL` routes for the MCP endpoint.
197
+ - `README.md`: This file
198
+ - `app.rb`: Application logic
199
+ - `DemoApp`: Web interface with HTML pages
200
+ - `UserAPI`: MCP handlers for resources and tools
201
+ - `config.ru`: Rack configuration (loads Otto, enables MCP)
202
+ - `routes`: Route definitions for web and MCP routes
203
+
204
+ ## Route Types
205
+
206
+ ### Web Routes
207
+ Regular HTTP routes for the web interface:
208
+ ```
209
+ GET / DemoApp#welcome
210
+ GET /users DemoApp#list_users
211
+ ```
212
+
213
+ ### MCP Resource Routes
214
+ Read-only resources exposed via MCP:
215
+ ```
216
+ MCP /users UserAPI#mcp_list_users
217
+ ```
218
+ - Called via: `POST /_mcp` with method `users/list`
219
+
220
+ ### MCP Tool Routes
221
+ Executable operations exposed via MCP:
222
+ ```
223
+ TOOL /create_user UserAPI#mcp_create_user
224
+ ```
225
+ - Called via: `POST /_mcp` with method `create_user`
226
+
227
+ ## Understanding MCP Method Names
228
+
229
+ MCP route paths are converted to method names:
230
+
231
+ | Route Type | Path | Method Name | Handler |
232
+ |-----------|------|-------------|---------|
233
+ | MCP | `/users` | `users/list` | `mcp_list_users` |
234
+ | MCP | `/users/:id` | `users/get` | `mcp_get_user` |
235
+ | TOOL | `/create_user` | `create_user` | `mcp_create_user` |
236
+ | TOOL | `/users/:id/update` | `users/update` | `mcp_update_user` |
237
+
238
+ ## Next Steps
239
+
240
+ - Build a CLI that communicates with the MCP endpoint
241
+ - Integrate with AI systems that support MCP
242
+ - Combine with [Authentication](../authentication_strategies/) for role-based MCP access
243
+ - Explore [Advanced Routes](../advanced_routes/) for more routing patterns
244
+
245
+ ## Further Reading
246
+
247
+ - [CLAUDE.md](../../CLAUDE.md#mcp) - Detailed MCP documentation (if available)
@@ -1,46 +1,265 @@
1
1
  # Otto Security Features Example
2
2
 
3
- This example application demonstrates the built-in security features of the Otto framework. It is configured to be secure by default and provides a showcase of best practices.
3
+ This example demonstrates Otto's built-in security features, showing best practices for CSRF protection, input validation, file upload handling, and security headers.
4
+
5
+ ## What You'll Learn
6
+
7
+ - Enabling and using CSRF protection
8
+ - Input validation for preventing injection attacks
9
+ - XSS prevention through output escaping
10
+ - Secure file upload handling with filename sanitization
11
+ - Adding security headers (CSP, HSTS, etc.)
12
+ - Request limiting to prevent denial-of-service
13
+ - Trusted proxy configuration for reverse proxies
14
+ - Privacy features (IP masking, user agent anonymization)
4
15
 
5
16
  ## Security Features Demonstrated
6
17
 
7
- * **CSRF Protection:** All POST forms are protected with a CSRF token to prevent cross-site request forgery attacks.
8
- * **Input Validation:** All user-submitted data is validated on the server-side for length and content, preventing common injection attacks.
9
- * **XSS Prevention:** All output is properly escaped to prevent cross-site scripting (XSS). You can test this by submitting `<script>alert('XSS')</script>` in any form.
10
- * **Secure File Uploads:** File uploads are validated, and filenames are sanitized to prevent directory traversal and other file-based attacks.
11
- * **Security Headers:** The application sends important security headers like `Content-Security-Policy`, `Strict-Transport-Security`, and `X-Frame-Options`.
12
- * **Request Limiting:** The application is configured to limit the maximum request size, parameter depth, and number of parameter keys to prevent denial-of-service attacks.
13
- * **Trusted Proxies:** The configuration includes a list of trusted proxy servers, ensuring that `X-Forwarded-*` headers are handled correctly and securely.
18
+ ### CSRF Protection
19
+ All POST/PUT/DELETE requests include CSRF tokens in forms:
20
+ ```ruby
21
+ <form method="post">
22
+ <input type="hidden" name="_csrf_token" value="<%= @req.csrf_token %>">
23
+ <input type="text" name="message">
24
+ </form>
25
+ ```
26
+
27
+ ### Input Validation
28
+ Server-side validation of user-submitted data:
29
+ - Length limits (max 1000 chars for messages)
30
+ - Character restrictions (no HTML tags)
31
+ - Required field validation
32
+ - Type validation
33
+
34
+ ### XSS Prevention
35
+ All output is properly escaped:
36
+ ```ruby
37
+ @res.body = "<h1>#{ERB::Util.html_escape(user_input)}</h1>"
38
+ ```
39
+
40
+ ### Secure File Uploads
41
+ File uploads are validated and sanitized:
42
+ - File type checking (whitelist approach)
43
+ - Size limits (prevent large uploads)
44
+ - Filename sanitization (remove path traversal)
45
+ - Safe storage location
46
+
47
+ ### Security Headers
48
+ Automatic security headers are sent with responses:
49
+ - `Content-Security-Policy` - Prevents inline scripts
50
+ - `Strict-Transport-Security` - Enforces HTTPS
51
+ - `X-Frame-Options` - Prevents clickjacking
52
+ - `X-Content-Type-Options` - Prevents MIME sniffing
53
+
54
+ ### Request Limiting
55
+ Configure limits to prevent DOS attacks:
56
+ - Maximum request size
57
+ - Maximum parameter keys
58
+ - Maximum parameter depth
59
+
60
+ ### Trusted Proxies
61
+ Configure reverse proxy IPs for X-Forwarded-For headers:
62
+ ```ruby
63
+ app.add_trusted_proxy('10.0.0.0/8')
64
+ app.add_trusted_proxy(/^192\.168\./)
65
+ ```
66
+
67
+ ### Privacy by Default
68
+ Automatic privacy features:
69
+ - Public IP masking (203.0.113.50 → 203.0.113.0)
70
+ - User agent anonymization (versions stripped)
71
+ - Country-level geo-location only
72
+ - Private/localhost IPs NOT masked by default (configurable via `configure_ip_privacy()`)
14
73
 
15
74
  ## How to Run
16
75
 
17
- 1. Make sure you have `bundler` and `thin` installed:
18
- ```sh
19
- gem install bundler thin
20
- ```
76
+ ### Using rackup (recommended)
77
+
78
+ ```sh
79
+ cd examples/security_features
80
+ rackup config.ru -p 10770
81
+ ```
82
+
83
+ ### Using thin
84
+
85
+ ```sh
86
+ cd examples/security_features
87
+ thin -e dev -R config.ru -p 10770 start
88
+ ```
89
+
90
+ Open your browser and navigate to `http://localhost:10770`.
91
+
92
+ ## Testing Security Features
93
+
94
+ ### XSS Prevention
95
+
96
+ Try entering `<script>alert("XSS")</script>` in form fields:
97
+ - The script tag is rendered as text, not executed
98
+ - You'll see it displayed as literal HTML tags
99
+ - Browser's developer tools show escaped HTML
100
+
101
+ ### Input Validation
102
+
103
+ Test validation rules:
104
+ - Submit a message > 1000 characters (fails)
105
+ - Submit special characters like `<>` (fails)
106
+ - Submit valid text (succeeds)
107
+
108
+ ### CSRF Protection
109
+
110
+ Examine form submissions:
111
+ - All POST forms include a `_csrf_token` hidden field
112
+ - Each request has a unique token
113
+ - Removing the token causes 403 Forbidden
114
+ - Browser's developer tools show token in form data
115
+
116
+ ### File Uploads
117
+
118
+ Test file upload security:
119
+ - Try uploading an executable file (rejected)
120
+ - Try uploading a legitimate image (accepted)
121
+ - Check saved filename (sanitized, safe)
122
+ - Verify file permissions and location
123
+
124
+ ### Security Headers
125
+
126
+ Check response headers:
127
+ - Open browser's Network tab in developer tools
128
+ - Click any response to view headers
129
+ - Look for security headers in response
130
+ - Visit `/headers` endpoint to see all headers
131
+
132
+ ## Expected Output
21
133
 
22
- 2. Install the dependencies from the root of the project:
23
- ```sh
24
- bundle install
25
- ```
134
+ ### Successful Form Submission
135
+ ```
136
+ POST /feedback HTTP/1.1
137
+ Content-Type: application/x-www-form-urlencoded
26
138
 
27
- 3. Start the server from this directory (`examples/security_features`):
28
- ```sh
29
- thin -e dev -R config.ru -p 10770 start
30
- ```
139
+ _csrf_token=abc123...
140
+ message=Hello+world
31
141
 
32
- 4. Open your browser and navigate to `http://localhost:10770`.
142
+ HTTP/1.1 302 Found
143
+ Location: http://localhost:10770/
144
+ Content-Security-Policy: default-src 'self'
145
+ Strict-Transport-Security: max-age=31536000
146
+ ```
33
147
 
34
- ## What to Test
148
+ ### Failed CSRF Validation
149
+ ```
150
+ HTTP/1.1 403 Forbidden
151
+ Content-Type: text/plain
35
152
 
36
- * **XSS Protection:** Try entering `<script>alert("XSS")</script>` into any of the form fields. You will see that the input is safely displayed as text instead of being executed as a script.
37
- * **Input Validation:** Try submitting a very long message in the feedback form to see the length validation in action.
38
- * **File Uploads:** Try uploading different types of files. The application will show you how it sanitizes the filename.
39
- * **Security Headers:** Open your browser's developer tools and inspect the network requests. You will see the security headers in the response. You can also visit the `/headers` path to see a JSON representation of the request headers your browser is sending.
153
+ CSRF token validation failed
154
+ ```
155
+
156
+ ### Failed Input Validation
157
+ ```
158
+ HTTP/1.1 400 Bad Request
159
+ Content-Type: text/plain
160
+
161
+ Message is too long (max 1000 characters)
162
+ ```
163
+
164
+ ### Security Headers Response
165
+ ```
166
+ Content-Security-Policy: default-src 'self'
167
+ Strict-Transport-Security: max-age=31536000
168
+ X-Frame-Options: SAMEORIGIN
169
+ X-Content-Type-Options: nosniff
170
+ Referrer-Policy: strict-origin-when-cross-origin
171
+ ```
40
172
 
41
173
  ## File Structure
42
174
 
43
- * `README.md`: This file.
44
- * `app.rb`: The main application logic, demonstrating how to handle forms, file uploads, and user input in a secure way.
45
- * `config.ru`: The Rack configuration file. This is where the security features are enabled and configured for the Otto application.
46
- * `routes`: Defines the URL routes and maps them to methods in the `SecureApp` class.
175
+ - `README.md`: This file
176
+ - `app.rb`: Main application logic with security implementations
177
+ - Form validation and escaping
178
+ - File upload handling
179
+ - Header configuration
180
+ - `config.ru`: Rack configuration with security features enabled
181
+ - `routes`: URL routes mapped to SecureApp class methods
182
+
183
+ ## Key Configuration
184
+
185
+ In `config.ru`:
186
+ ```ruby
187
+ app = Otto.new("./routes")
188
+
189
+ # Enable security features
190
+ app.enable_csrf_protection!
191
+
192
+ # Configure request limits
193
+ app.security_config.request_size_limit = 1.megabyte
194
+ app.security_config.max_parameter_keys = 100
195
+ app.security_config.max_parameter_depth = 5
196
+
197
+ # Add trusted proxies if behind reverse proxy
198
+ app.add_trusted_proxy('10.0.0.0/8')
199
+
200
+ # Security headers
201
+ app.add_security_header('X-Custom-Header', 'value')
202
+ ```
203
+
204
+ ## Common Attack Scenarios
205
+
206
+ ### XSS Attack
207
+ ```javascript
208
+ <img src=x onerror="alert('XSS')">
209
+ ```
210
+ **Result**: Safely displayed as text, not executed
211
+
212
+ ### SQL Injection
213
+ ```sql
214
+ '; DROP TABLE users; --
215
+ ```
216
+ **Result**: Stored as literal text, invalid SQL
217
+
218
+ ### Path Traversal
219
+ ```
220
+ ../../../../../../etc/passwd
221
+ ```
222
+ **Result**: Filename sanitized to just `etc-passwd`
223
+
224
+ ### Large Request
225
+ ```
226
+ POST with 10MB body
227
+ ```
228
+ **Result**: Rejected with 413 Payload Too Large
229
+
230
+ ## Best Practices Demonstrated
231
+
232
+ 1. **Defense in Depth**: Multiple layers of security
233
+ 2. **Input Validation**: Whitelist approach (allow only safe input)
234
+ 3. **Output Escaping**: Escape all user-controlled output
235
+ 4. **CSRF Tokens**: Unique tokens for each request
236
+ 5. **Security Headers**: Prevent common attack vectors
237
+ 6. **File Upload Safety**: Validate type and sanitize names
238
+ 7. **Request Limiting**: Prevent denial-of-service
239
+
240
+ ## Testing with curl
241
+
242
+ ```sh
243
+ # Test CSRF protection (will fail without token)
244
+ curl -X POST http://localhost:10770/feedback \
245
+ -d "message=test"
246
+
247
+ # Test with valid CSRF token (get token from form first)
248
+ curl -X POST http://localhost:10770/feedback \
249
+ -d "_csrf_token=<token>" \
250
+ -d "message=test"
251
+
252
+ # Test input validation
253
+ curl -X POST http://localhost:10770/feedback \
254
+ -d "message=$(python -c 'print(\"x\" * 2000)')"
255
+ ```
256
+
257
+ ## Next Steps
258
+
259
+ - Review the application code to see implementation details
260
+ - Explore other examples for different features
261
+
262
+ ## Further Reading
263
+
264
+ - [CLAUDE.md](../../CLAUDE.md#security-features) - Security configuration reference
265
+ - [IP Privacy](../../CLAUDE.md#ip-privacy-privacy-by-default) - Privacy configuration
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Otto GeoResolver Extension Guide
5
+ #
6
+ # This guide shows two approaches to extend Otto's IP geolocation:
7
+ # 1. Configuration-based (simple, inline)
8
+ # 2. Subclass-based (full control)
9
+
10
+ require 'bundler/setup'
11
+ require 'otto'
12
+
13
+ # =============================================================================
14
+ # Quick Start: Configuration-based Extension
15
+ # =============================================================================
16
+
17
+ puts 'Simple Custom Geo Resolution'
18
+ puts '-' * 40
19
+
20
+ # Step 1: Define your resolver function
21
+ custom_resolver = lambda do |ip, _env|
22
+ case ip
23
+ when '1.2.3.4' then 'US'
24
+ when '5.6.7.8' then 'GB'
25
+ else nil # nil = use Otto's built-in resolver
26
+ end
27
+ end
28
+
29
+ # Step 2: Set it globally
30
+ Otto::Privacy::GeoResolver.custom_resolver = custom_resolver
31
+
32
+ # Step 3: Test it
33
+ puts "1.2.3.4 -> #{Otto::Privacy::GeoResolver.resolve('1.2.3.4', {})}"
34
+ puts "8.8.8.8 -> #{Otto::Privacy::GeoResolver.resolve('8.8.8.8', {})} (fallback)"
35
+
36
+ # Reset for next example
37
+ Otto::Privacy::GeoResolver.custom_resolver = nil
38
+
39
+ # =============================================================================
40
+ # Real-World Example: API Integration with Caching
41
+ # =============================================================================
42
+
43
+ puts "\nAPI Integration with Caching"
44
+ puts '-' * 40
45
+
46
+ class CachedGeoAPI
47
+ def initialize(api_key)
48
+ @api_key = api_key
49
+ @cache = {}
50
+ end
51
+
52
+ def call(ip, _env)
53
+ # Return cached result if available
54
+ return @cache[ip] if @cache.key?(ip)
55
+
56
+ # Simulate API call (replace with real HTTP request)
57
+ country = mock_api_call(ip)
58
+
59
+ # Cache the result
60
+ @cache[ip] = country
61
+ country
62
+ rescue StandardError => e
63
+ puts "API failed: #{e.message}"
64
+ nil # Fallback to Otto's resolver
65
+ end
66
+
67
+ private
68
+
69
+ def mock_api_call(ip)
70
+ # Replace this with: HTTP.get("https://api.example.com/geo?ip=#{ip}")
71
+ case ip
72
+ when /^1\./ then 'US'
73
+ when /^2\./ then 'GB'
74
+ end
75
+ end
76
+ end
77
+
78
+ # Use the cached API resolver
79
+ api_resolver = CachedGeoAPI.new('your_api_key')
80
+ Otto::Privacy::GeoResolver.custom_resolver = api_resolver
81
+
82
+ puts "1.2.3.4 -> #{Otto::Privacy::GeoResolver.resolve('1.2.3.4', {})}"
83
+ puts "1.2.3.4 -> #{Otto::Privacy::GeoResolver.resolve('1.2.3.4', {})} (cached)"
84
+
85
+ Otto::Privacy::GeoResolver.custom_resolver = nil
86
+
87
+ # =============================================================================
88
+ # Performance Tips
89
+ # =============================================================================
90
+
91
+ puts "\nPerformance Tips"
92
+ puts '-' * 40
93
+
94
+ puts '• Cache API results to avoid repeated calls'
95
+ puts "• Return nil from custom resolver to use Otto's fast fallback"
96
+ puts '• Use CloudFlare headers when available (fastest)'
97
+ puts '• Consider async/background geo updates for heavy traffic'
98
+
99
+ puts "\nProduction Pattern: Valkey/Redis Bloom Filters"
100
+ puts '-' * 40
101
+ puts 'For high-traffic applications, consider Bloom/Cuckoo filters:'
102
+ puts '• Store RIR IP prefixes in Valkey/Redis Bloom filter per country'
103
+ puts '• Memory: ~1MB for entire IPv4 table at 1% false positive rate'
104
+ puts '• Lookup: O(1) microsecond-level performance via BF.EXISTS'
105
+ puts '• Zero external dependencies, rebuild nightly from public RIR files'
106
+ puts '• Perfect for CDN header fallback when requests bypass edge'
107
+ puts ''