otto 2.0.0.pre3 → 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 -1
- data/.github/workflows/claude-code-review.yml +1 -1
- 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 +74 -540
- data/Gemfile +4 -2
- data/Gemfile.lock +58 -19
- 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/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 +18 -25
- data/lib/otto/route_handlers/factory.rb +2 -2
- 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 +23 -6
- 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.rb +230 -78
- 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 +24 -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
data/examples/mcp_demo/README.md
CHANGED
|
@@ -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`)
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
```sh
|
|
24
|
-
bundle install
|
|
25
|
-
```
|
|
47
|
+
## Authentication
|
|
26
48
|
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
57
|
+
All MCP interactions use the `POST /_mcp` endpoint. Each request is a JSON-RPC 2.0 request with:
|
|
38
58
|
|
|
39
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
134
|
+
### Successful Form Submission
|
|
135
|
+
```
|
|
136
|
+
POST /feedback HTTP/1.1
|
|
137
|
+
Content-Type: application/x-www-form-urlencoded
|
|
26
138
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
thin -e dev -R config.ru -p 10770 start
|
|
30
|
-
```
|
|
139
|
+
_csrf_token=abc123...
|
|
140
|
+
message=Hello+world
|
|
31
141
|
|
|
32
|
-
|
|
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
|
-
|
|
148
|
+
### Failed CSRF Validation
|
|
149
|
+
```
|
|
150
|
+
HTTP/1.1 403 Forbidden
|
|
151
|
+
Content-Type: text/plain
|
|
35
152
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 ''
|