otto 1.6.0 → 2.0.0.pre2
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 +3 -2
- data/.github/workflows/claude-code-review.yml +53 -0
- data/.github/workflows/claude.yml +49 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +26 -344
- data/CHANGELOG.rst +131 -0
- data/CLAUDE.md +56 -0
- data/Gemfile +11 -4
- data/Gemfile.lock +38 -42
- data/README.md +2 -0
- data/bin/rspec +4 -4
- data/changelog.d/README.md +120 -0
- data/changelog.d/scriv.ini +5 -0
- data/docs/.gitignore +2 -0
- data/docs/migrating/v2.0.0-pre1.md +276 -0
- data/docs/migrating/v2.0.0-pre2.md +345 -0
- data/examples/.gitignore +1 -0
- data/examples/advanced_routes/README.md +33 -0
- data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
- data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
- data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
- data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
- data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
- data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
- data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
- data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
- data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
- data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
- data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
- data/examples/advanced_routes/app.rb +33 -0
- data/examples/advanced_routes/config.rb +23 -0
- data/examples/advanced_routes/config.ru +7 -0
- data/examples/advanced_routes/puma.rb +20 -0
- data/examples/advanced_routes/routes +167 -0
- data/examples/advanced_routes/run.rb +39 -0
- data/examples/advanced_routes/test.rb +58 -0
- data/examples/authentication_strategies/README.md +32 -0
- data/examples/authentication_strategies/app/auth.rb +68 -0
- data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
- data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
- data/examples/authentication_strategies/config.ru +24 -0
- data/examples/authentication_strategies/routes +37 -0
- data/examples/basic/README.md +29 -0
- data/examples/basic/app.rb +7 -35
- data/examples/basic/routes +0 -9
- data/examples/mcp_demo/README.md +87 -0
- data/examples/mcp_demo/app.rb +29 -34
- data/examples/mcp_demo/config.ru +9 -60
- data/examples/security_features/README.md +46 -0
- data/examples/security_features/app.rb +23 -24
- data/examples/security_features/config.ru +8 -10
- data/lib/otto/core/configuration.rb +167 -0
- data/lib/otto/core/error_handler.rb +86 -0
- data/lib/otto/core/file_safety.rb +61 -0
- data/lib/otto/core/middleware_stack.rb +237 -0
- data/lib/otto/core/router.rb +184 -0
- data/lib/otto/core/uri_generator.rb +44 -0
- data/lib/otto/design_system.rb +7 -5
- data/lib/otto/env_keys.rb +114 -0
- data/lib/otto/helpers/base.rb +5 -21
- data/lib/otto/helpers/request.rb +10 -8
- data/lib/otto/helpers/response.rb +27 -4
- data/lib/otto/helpers/validation.rb +9 -7
- data/lib/otto/mcp/auth/token.rb +10 -9
- data/lib/otto/mcp/protocol.rb +24 -27
- data/lib/otto/mcp/rate_limiting.rb +8 -3
- data/lib/otto/mcp/registry.rb +7 -2
- data/lib/otto/mcp/route_parser.rb +10 -15
- data/lib/otto/mcp/{validation.rb → schema_validation.rb} +16 -11
- data/lib/otto/mcp/server.rb +45 -22
- data/lib/otto/response_handlers/auto.rb +39 -0
- data/lib/otto/response_handlers/base.rb +16 -0
- data/lib/otto/response_handlers/default.rb +16 -0
- data/lib/otto/response_handlers/factory.rb +39 -0
- data/lib/otto/response_handlers/json.rb +34 -0
- data/lib/otto/response_handlers/redirect.rb +25 -0
- data/lib/otto/response_handlers/view.rb +24 -0
- data/lib/otto/response_handlers.rb +9 -135
- data/lib/otto/route.rb +51 -55
- data/lib/otto/route_definition.rb +15 -18
- data/lib/otto/route_handlers/base.rb +121 -0
- data/lib/otto/route_handlers/class_method.rb +89 -0
- data/lib/otto/route_handlers/factory.rb +42 -0
- data/lib/otto/route_handlers/instance_method.rb +69 -0
- data/lib/otto/route_handlers/lambda.rb +59 -0
- data/lib/otto/route_handlers/logic_class.rb +93 -0
- data/lib/otto/route_handlers.rb +10 -405
- data/lib/otto/security/authentication/auth_strategy.rb +44 -0
- data/lib/otto/security/authentication/authentication_middleware.rb +140 -0
- data/lib/otto/security/authentication/failure_result.rb +44 -0
- data/lib/otto/security/authentication/route_auth_wrapper.rb +149 -0
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
- data/lib/otto/security/authentication/strategies/noauth_strategy.rb +19 -0
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
- data/lib/otto/security/authentication/strategy_result.rb +337 -0
- data/lib/otto/security/authentication.rb +28 -282
- data/lib/otto/security/config.rb +14 -23
- data/lib/otto/security/configurator.rb +219 -0
- data/lib/otto/security/csrf.rb +8 -143
- data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
- data/lib/otto/security/middleware/rate_limit_middleware.rb +54 -0
- data/lib/otto/security/middleware/validation_middleware.rb +252 -0
- data/lib/otto/security/rate_limiter.rb +86 -0
- data/lib/otto/security/rate_limiting.rb +10 -105
- data/lib/otto/security/validator.rb +8 -253
- data/lib/otto/static.rb +3 -0
- data/lib/otto/utils.rb +14 -0
- data/lib/otto/version.rb +3 -1
- data/lib/otto.rb +141 -498
- data/otto.gemspec +4 -2
- metadata +99 -18
- data/examples/dynamic_pages/app.rb +0 -115
- data/examples/dynamic_pages/config.ru +0 -30
- data/examples/dynamic_pages/routes +0 -21
- data/examples/helpers_demo/app.rb +0 -244
- data/examples/helpers_demo/config.ru +0 -26
- data/examples/helpers_demo/routes +0 -7
- data/lib/concurrent_cache_store.rb +0 -68
data/examples/basic/app.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# examples/basic/app.rb
|
|
1
|
+
# examples/basic/app.rb
|
|
2
2
|
|
|
3
3
|
require_relative '../../lib/otto/design_system'
|
|
4
4
|
|
|
5
|
+
# Basic example application demonstrating Otto framework features.
|
|
5
6
|
class App
|
|
6
7
|
include Otto::DesignSystem
|
|
7
8
|
|
|
@@ -37,42 +38,13 @@ class App
|
|
|
37
38
|
message = req.params['msg']&.strip
|
|
38
39
|
|
|
39
40
|
content = if message.nil? || message.empty?
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
|
|
42
|
+
else
|
|
43
|
+
otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
|
|
44
|
+
otto_card('Your Message') { otto_code_block(message, 'text') }
|
|
45
|
+
end
|
|
45
46
|
|
|
46
47
|
content += "<p>#{otto_link('← Back', '/')}</p>"
|
|
47
48
|
res.body = otto_page(content, 'Feedback')
|
|
48
49
|
end
|
|
49
|
-
|
|
50
|
-
def redirect
|
|
51
|
-
res.redirect '/robots.txt'
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def robots_text
|
|
55
|
-
res.headers['content-type'] = 'text/plain'
|
|
56
|
-
res.body = ['User-agent: *', 'Disallow: /private'].join($/)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def display_product
|
|
60
|
-
res.headers['content-type'] = 'application/json; charset=utf-8'
|
|
61
|
-
prodid = req.params[:prodid]
|
|
62
|
-
res.body = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def not_found
|
|
66
|
-
res.status = 404
|
|
67
|
-
content = otto_alert('error', 'Not Found', 'The requested page could not be found.')
|
|
68
|
-
content += "<p>#{otto_link('← Home', '/')}</p>"
|
|
69
|
-
res.body = otto_page(content, '404')
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def server_error
|
|
73
|
-
res.status = 500
|
|
74
|
-
content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
|
|
75
|
-
content += "<p>#{otto_link('← Home', '/')}</p>"
|
|
76
|
-
res.body = otto_page(content, '500')
|
|
77
|
-
end
|
|
78
50
|
end
|
data/examples/basic/routes
CHANGED
|
@@ -9,12 +9,3 @@
|
|
|
9
9
|
|
|
10
10
|
GET / App#index
|
|
11
11
|
POST / App#receive_feedback
|
|
12
|
-
GET /redirect App#redirect
|
|
13
|
-
GET /robots.txt App#robots_text
|
|
14
|
-
|
|
15
|
-
GET /bogus App#no_such_method
|
|
16
|
-
|
|
17
|
-
# You can also define these handlers when no
|
|
18
|
-
# route can be found or there's a server error. (optional)
|
|
19
|
-
GET /404 App#not_found
|
|
20
|
-
GET /500 App#server_error
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Otto MCP Demo
|
|
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.
|
|
4
|
+
|
|
5
|
+
This is useful for building CLIs, admin interfaces, or allowing other services to interact with your application programmatically.
|
|
6
|
+
|
|
7
|
+
## Features Demonstrated
|
|
8
|
+
|
|
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).
|
|
14
|
+
|
|
15
|
+
## How to Run
|
|
16
|
+
|
|
17
|
+
1. Make sure you have `bundler` and `thin` installed:
|
|
18
|
+
```sh
|
|
19
|
+
gem install bundler thin
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
2. Install the dependencies from the root of the project:
|
|
23
|
+
```sh
|
|
24
|
+
bundle install
|
|
25
|
+
```
|
|
26
|
+
|
|
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.*
|
|
32
|
+
|
|
33
|
+
4. Open your browser and navigate to `http://localhost:9292` to see the welcome page.
|
|
34
|
+
|
|
35
|
+
## Interacting with the MCP Endpoint
|
|
36
|
+
|
|
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.
|
|
38
|
+
|
|
39
|
+
Valid tokens are `demo-token-123` and `another-token-456`.
|
|
40
|
+
|
|
41
|
+
### MCP: Initialize
|
|
42
|
+
|
|
43
|
+
The `initialize` method is a built-in MCP method that returns information about the available resources and tools.
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
curl -X POST http://localhost:9292/_mcp \
|
|
47
|
+
-H 'Authorization: Bearer demo-token-123' \
|
|
48
|
+
-H 'Content-Type: application/json' \
|
|
49
|
+
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Resource: List Users
|
|
53
|
+
|
|
54
|
+
This calls the `UserAPI.mcp_list_users` method defined as an `MCP` route. The method name for the JSON-RPC call is derived from the route path (`/users` -> `users/list`).
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
curl -X POST http://localhost:9292/_mcp \
|
|
58
|
+
-H 'Authorization: Bearer demo-token-123' \
|
|
59
|
+
-H 'Content-Type: application/json' \
|
|
60
|
+
-d '{"jsonrpc":"2.0","method":"users/list","id":2}'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Tool: Create User
|
|
64
|
+
|
|
65
|
+
This calls the `UserAPI.mcp_create_user` method defined as a `TOOL` route. The method name is derived from the route path (`/create_user` -> `create_user`).
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
curl -X POST http://localhost:9292/_mcp \
|
|
69
|
+
-H 'Authorization: Bearer demo-token-123' \
|
|
70
|
+
-H 'Content-Type: application/json' \
|
|
71
|
+
-d '{
|
|
72
|
+
"jsonrpc": "2.0",
|
|
73
|
+
"method": "create_user",
|
|
74
|
+
"id": 3,
|
|
75
|
+
"params": {
|
|
76
|
+
"name": "Charlie",
|
|
77
|
+
"email": "charlie@example.com"
|
|
78
|
+
}
|
|
79
|
+
}'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## File Structure
|
|
83
|
+
|
|
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.
|
data/examples/mcp_demo/app.rb
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
# examples/mcp_demo/app.rb
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
# DemoApp provides basic HTML pages for the demo.
|
|
6
|
+
class DemoApp
|
|
7
|
+
def self.index(_req, res)
|
|
8
|
+
res.headers['content-type'] = 'text/html; charset=utf-8'
|
|
9
|
+
res.body = <<~HTML
|
|
10
|
+
<h1>Otto MCP Demo</h1>
|
|
11
|
+
<p>This example demonstrates Otto's Model-Controller-Protocol (MCP) feature, which provides a JSON-RPC 2.0 endpoint for interacting with your application.</p>
|
|
12
|
+
<p>The MCP endpoint is available at: <code>POST /_mcp</code></p>
|
|
13
|
+
<p>See the <code>README.md</code> file for detailed `curl` commands to test the API.</p>
|
|
14
|
+
HTML
|
|
15
|
+
end
|
|
5
16
|
|
|
6
|
-
|
|
17
|
+
def self.health(_req, res)
|
|
18
|
+
res.headers['content-type'] = 'text/plain'
|
|
19
|
+
res.body = 'OK'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
7
22
|
|
|
23
|
+
# UserAPI provides handlers for the MCP tool and resource routes.
|
|
8
24
|
class UserAPI
|
|
25
|
+
# MCP Resource: mcp_list_users
|
|
26
|
+
# Accessible via JSON-RPC method "users/list"
|
|
9
27
|
def self.mcp_list_users
|
|
10
28
|
{
|
|
11
29
|
users: [
|
|
12
30
|
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
13
|
-
{ id: 2, name: 'Bob', email: 'bob@example.com' }
|
|
14
|
-
]
|
|
31
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
32
|
+
],
|
|
15
33
|
}.to_json
|
|
16
34
|
end
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
|
|
36
|
+
# MCP Tool: mcp_create_user
|
|
37
|
+
# Accessible via JSON-RPC method "create_user"
|
|
38
|
+
def self.mcp_create_user(arguments, _env)
|
|
20
39
|
name = arguments['name'] || 'Anonymous'
|
|
21
40
|
email = arguments['email'] || "#{name.downcase}@example.com"
|
|
22
41
|
|
|
@@ -24,33 +43,9 @@ class UserAPI
|
|
|
24
43
|
id: rand(1000..9999),
|
|
25
44
|
name: name,
|
|
26
45
|
email: email,
|
|
27
|
-
created_at: Time.now.iso8601
|
|
46
|
+
created_at: Time.now.iso8601,
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
"Created user: #{new_user.to_json}"
|
|
31
50
|
end
|
|
32
51
|
end
|
|
33
|
-
|
|
34
|
-
# Initialize Otto with MCP support
|
|
35
|
-
otto = Otto.new('routes', {
|
|
36
|
-
mcp_enabled: true,
|
|
37
|
-
auth_tokens: ['demo-token-123'], # Simple token auth
|
|
38
|
-
requests_per_minute: 10, # Lower for demo
|
|
39
|
-
tools_per_minute: 5
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
# Enable MCP with authentication tokens
|
|
43
|
-
otto.enable_mcp!({
|
|
44
|
-
auth_tokens: ['demo-token-123', 'another-token-456'],
|
|
45
|
-
enable_validation: true,
|
|
46
|
-
enable_rate_limiting: true
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
puts "Otto MCP Demo Server starting..."
|
|
50
|
-
puts "MCP endpoint: POST /_mcp"
|
|
51
|
-
puts "Auth tokens: demo-token-123, another-token-456"
|
|
52
|
-
puts "Usage: curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\"
|
|
53
|
-
puts " -d '{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"id\":1,\"params\":{}}' \\"
|
|
54
|
-
puts " http://localhost:9292/_mcp"
|
|
55
|
-
|
|
56
|
-
otto
|
data/examples/mcp_demo/config.ru
CHANGED
|
@@ -1,68 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
# examples/mcp_demo/config.ru
|
|
2
2
|
|
|
3
3
|
require_relative '../../lib/otto'
|
|
4
|
-
|
|
5
|
-
class DemoApp
|
|
6
|
-
def self.index(req, res)
|
|
7
|
-
res.body = <<-HTML
|
|
8
|
-
<h1>Otto MCP Demo</h1>
|
|
9
|
-
<p>MCP endpoint available at: <code>POST /_mcp</code></p>
|
|
10
|
-
<p>Auth tokens: <code>demo-token-123</code>, <code>another-token-456</code></p>
|
|
11
|
-
|
|
12
|
-
<h2>Test MCP Initialize</h2>
|
|
13
|
-
<pre>curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\
|
|
14
|
-
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}' \\
|
|
15
|
-
http://localhost:9292/_mcp</pre>
|
|
16
|
-
|
|
17
|
-
<h2>List Resources</h2>
|
|
18
|
-
<pre>curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\
|
|
19
|
-
-d '{"jsonrpc":"2.0","method":"resources/list","id":2}' \\
|
|
20
|
-
http://localhost:9292/_mcp</pre>
|
|
21
|
-
|
|
22
|
-
<h2>List Tools</h2>
|
|
23
|
-
<pre>curl -H 'Authorization: Bearer demo-token-123' -H 'Content-Type: application/json' \\
|
|
24
|
-
-d '{"jsonrpc":"2.0","method":"tools/list","id":3}' \\
|
|
25
|
-
http://localhost:9292/_mcp</pre>
|
|
26
|
-
HTML
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def self.health(req, res)
|
|
30
|
-
res.body = 'OK'
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
class UserAPI
|
|
35
|
-
def self.mcp_list_users
|
|
36
|
-
{
|
|
37
|
-
users: [
|
|
38
|
-
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
39
|
-
{ id: 2, name: 'Bob', email: 'bob@example.com' }
|
|
40
|
-
]
|
|
41
|
-
}.to_json
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def self.mcp_create_user(arguments, env)
|
|
45
|
-
# Tool handler that creates a user
|
|
46
|
-
name = arguments['name'] || 'Anonymous'
|
|
47
|
-
email = arguments['email'] || "#{name.downcase}@example.com"
|
|
48
|
-
|
|
49
|
-
new_user = {
|
|
50
|
-
id: rand(1000..9999),
|
|
51
|
-
name: name,
|
|
52
|
-
email: email,
|
|
53
|
-
created_at: Time.now.iso8601
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
"Created user: #{new_user.to_json}"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
4
|
+
require_relative 'app'
|
|
59
5
|
|
|
60
6
|
# Initialize Otto with MCP support
|
|
61
|
-
|
|
7
|
+
app = Otto.new('routes', {
|
|
62
8
|
mcp_enabled: true,
|
|
63
9
|
auth_tokens: ['demo-token-123', 'another-token-456'],
|
|
64
|
-
requests_per_minute: 60,
|
|
65
|
-
tools_per_minute: 20
|
|
10
|
+
requests_per_minute: 60, # Rate limiting for the MCP endpoint
|
|
11
|
+
tools_per_minute: 20,
|
|
66
12
|
})
|
|
67
13
|
|
|
68
|
-
|
|
14
|
+
# The `mcp_enabled: true` flag automatically sets up the /_mcp endpoint.
|
|
15
|
+
# The routes file maps MCP and TOOL methods to classes.
|
|
16
|
+
|
|
17
|
+
run app
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Otto Security Features Example
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Security Features Demonstrated
|
|
6
|
+
|
|
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.
|
|
14
|
+
|
|
15
|
+
## How to Run
|
|
16
|
+
|
|
17
|
+
1. Make sure you have `bundler` and `thin` installed:
|
|
18
|
+
```sh
|
|
19
|
+
gem install bundler thin
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
2. Install the dependencies from the root of the project:
|
|
23
|
+
```sh
|
|
24
|
+
bundle install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
3. Start the server from this directory (`examples/security_features`):
|
|
28
|
+
```sh
|
|
29
|
+
thin -e dev -R config.ru -p 10770 start
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
4. Open your browser and navigate to `http://localhost:10770`.
|
|
33
|
+
|
|
34
|
+
## What to Test
|
|
35
|
+
|
|
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.
|
|
40
|
+
|
|
41
|
+
## File Structure
|
|
42
|
+
|
|
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.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# examples/security_features/app.rb
|
|
1
|
+
# examples/security_features/app.rb
|
|
2
2
|
|
|
3
3
|
require_relative '../../lib/otto/design_system'
|
|
4
4
|
|
|
5
|
+
# Example application demonstrating Otto's security features including CSRF protection
|
|
5
6
|
class SecureApp
|
|
6
7
|
include Otto::DesignSystem
|
|
7
8
|
include Otto::Security::CSRFHelpers
|
|
@@ -106,18 +107,18 @@ class SecureApp
|
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
content = if safe_message.empty?
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
otto_alert('error', 'Validation Error', 'Message cannot be empty.')
|
|
111
|
+
else
|
|
112
|
+
<<~HTML
|
|
113
|
+
#{otto_alert('success', 'Feedback Received', 'Thank you for your feedback!')}
|
|
114
|
+
|
|
115
|
+
#{otto_card('Your Message') do
|
|
116
|
+
otto_code_block(safe_message, 'text')
|
|
117
|
+
end}
|
|
118
|
+
HTML
|
|
118
119
|
end
|
|
119
|
-
rescue Otto::Security::ValidationError =>
|
|
120
|
-
content = otto_alert('error', 'Security Validation Failed',
|
|
120
|
+
rescue Otto::Security::ValidationError => e
|
|
121
|
+
content = otto_alert('error', 'Security Validation Failed', e.message)
|
|
121
122
|
rescue StandardError
|
|
122
123
|
content = otto_alert('error', 'Processing Error', 'An error occurred processing your request.')
|
|
123
124
|
end
|
|
@@ -135,18 +136,18 @@ class SecureApp
|
|
|
135
136
|
else
|
|
136
137
|
begin
|
|
137
138
|
filename = begin
|
|
138
|
-
|
|
139
|
+
uploaded_file[:filename]
|
|
139
140
|
rescue StandardError
|
|
140
|
-
|
|
141
|
+
uploaded_file.original_filename
|
|
141
142
|
end
|
|
142
143
|
rescue StandardError
|
|
143
144
|
'unknown'
|
|
144
145
|
end
|
|
145
146
|
|
|
146
147
|
safe_filename = if respond_to?(:sanitize_filename)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
sanitize_filename(filename)
|
|
149
|
+
else
|
|
150
|
+
File.basename(filename.to_s).gsub(/[^\w\-_.]/, '_')
|
|
150
151
|
end
|
|
151
152
|
|
|
152
153
|
file_info = {
|
|
@@ -173,8 +174,8 @@ class SecureApp
|
|
|
173
174
|
</div>
|
|
174
175
|
HTML
|
|
175
176
|
end
|
|
176
|
-
rescue Otto::Security::ValidationError =>
|
|
177
|
-
content = otto_alert('error', 'File Validation Failed',
|
|
177
|
+
rescue Otto::Security::ValidationError => e
|
|
178
|
+
content = otto_alert('error', 'File Validation Failed', e.message)
|
|
178
179
|
rescue StandardError
|
|
179
180
|
content = otto_alert('error', 'Upload Error', 'An error occurred during file upload.')
|
|
180
181
|
end
|
|
@@ -199,9 +200,7 @@ class SecureApp
|
|
|
199
200
|
safe_bio = bio.to_s.strip[0..499]
|
|
200
201
|
end
|
|
201
202
|
|
|
202
|
-
unless safe_email.match?(/\A[^@\s]+@[^@\s]+\z/)
|
|
203
|
-
raise Otto::Security::ValidationError, 'Invalid email format'
|
|
204
|
-
end
|
|
203
|
+
raise Otto::Security::ValidationError, 'Invalid email format' unless safe_email.match?(/\A[^@\s]+@[^@\s]+\z/)
|
|
205
204
|
|
|
206
205
|
profile_data = {
|
|
207
206
|
'Name' => safe_name,
|
|
@@ -221,8 +220,8 @@ class SecureApp
|
|
|
221
220
|
profile_html
|
|
222
221
|
end}
|
|
223
222
|
HTML
|
|
224
|
-
rescue Otto::Security::ValidationError =>
|
|
225
|
-
content = otto_alert('error', 'Profile Validation Failed',
|
|
223
|
+
rescue Otto::Security::ValidationError => e
|
|
224
|
+
content = otto_alert('error', 'Profile Validation Failed', e.message)
|
|
226
225
|
rescue StandardError
|
|
227
226
|
content = otto_alert('error', 'Update Error', 'An error occurred updating your profile.')
|
|
228
227
|
end
|
|
@@ -14,8 +14,8 @@ require_relative 'app'
|
|
|
14
14
|
|
|
15
15
|
# Create Otto app with security features enabled
|
|
16
16
|
app = Otto.new('./routes', {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# Enable CSRF protection for POST, PUT, DELETE requests
|
|
18
|
+
csrf_protection: true,
|
|
19
19
|
|
|
20
20
|
# Enable input validation and sanitization
|
|
21
21
|
request_validation: true,
|
|
@@ -44,13 +44,12 @@ app = Otto.new('./routes', {
|
|
|
44
44
|
'strict-transport-security' => 'max-age=31536000; includeSubDomains',
|
|
45
45
|
'x-frame-options' => 'DENY',
|
|
46
46
|
},
|
|
47
|
-
}
|
|
48
|
-
)
|
|
47
|
+
})
|
|
49
48
|
|
|
50
49
|
# Optional: Configure additional security settings
|
|
51
|
-
app.security_config.max_request_size = 5 * 1024 * 1024
|
|
52
|
-
app.security_config.max_param_depth = 10
|
|
53
|
-
app.security_config.max_param_keys = 50
|
|
50
|
+
app.security_config.max_request_size = 5 * 1024 * 1024 # 5MB limit
|
|
51
|
+
app.security_config.max_param_depth = 10 # Limit parameter nesting
|
|
52
|
+
app.security_config.max_param_keys = 50 # Limit parameters per request
|
|
54
53
|
|
|
55
54
|
# Optional: Add static file serving with security
|
|
56
55
|
app.option[:public] = public_path
|
|
@@ -62,10 +61,9 @@ if ENV['RACK_ENV'] == 'production'
|
|
|
62
61
|
|
|
63
62
|
# More restrictive CSP for production
|
|
64
63
|
app.set_security_headers({
|
|
65
|
-
|
|
64
|
+
'content-security-policy' => "default-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'",
|
|
66
65
|
'strict-transport-security' => 'max-age=63072000; includeSubDomains; preload',
|
|
67
|
-
|
|
68
|
-
)
|
|
66
|
+
})
|
|
69
67
|
else
|
|
70
68
|
# Development-specific settings
|
|
71
69
|
puts '🔒 Security features enabled:'
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/otto/core/configuration.rb
|
|
4
|
+
|
|
5
|
+
require_relative '../security/csrf'
|
|
6
|
+
require_relative '../security/validator'
|
|
7
|
+
require_relative '../security/authentication'
|
|
8
|
+
require_relative '../security/rate_limiting'
|
|
9
|
+
require_relative '../mcp/server'
|
|
10
|
+
|
|
11
|
+
class Otto
|
|
12
|
+
module Core
|
|
13
|
+
# Configuration module providing locale and application configuration methods
|
|
14
|
+
module Configuration
|
|
15
|
+
def configure_locale(opts)
|
|
16
|
+
# Start with global configuration
|
|
17
|
+
global_config = self.class.global_config
|
|
18
|
+
@locale_config = nil
|
|
19
|
+
|
|
20
|
+
# Check if we have any locale configuration from any source
|
|
21
|
+
has_global_locale = global_config && (global_config[:available_locales] || global_config[:default_locale])
|
|
22
|
+
has_direct_options = opts[:available_locales] || opts[:default_locale]
|
|
23
|
+
has_legacy_config = opts[:locale_config]
|
|
24
|
+
|
|
25
|
+
# Only create locale_config if we have configuration from somewhere
|
|
26
|
+
return unless has_global_locale || has_direct_options || has_legacy_config
|
|
27
|
+
|
|
28
|
+
@locale_config = {}
|
|
29
|
+
|
|
30
|
+
# Apply global configuration first
|
|
31
|
+
if global_config && global_config[:available_locales]
|
|
32
|
+
@locale_config[:available_locales] =
|
|
33
|
+
global_config[:available_locales]
|
|
34
|
+
end
|
|
35
|
+
if global_config && global_config[:default_locale]
|
|
36
|
+
@locale_config[:default_locale] =
|
|
37
|
+
global_config[:default_locale]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Apply direct instance options (these override global config)
|
|
41
|
+
@locale_config[:available_locales] = opts[:available_locales] if opts[:available_locales]
|
|
42
|
+
@locale_config[:default_locale] = opts[:default_locale] if opts[:default_locale]
|
|
43
|
+
|
|
44
|
+
# Legacy support: Configure locale if provided in initialization options via locale_config hash
|
|
45
|
+
return unless opts[:locale_config]
|
|
46
|
+
|
|
47
|
+
locale_opts = opts[:locale_config]
|
|
48
|
+
if locale_opts[:available_locales] || locale_opts[:available]
|
|
49
|
+
@locale_config[:available_locales] =
|
|
50
|
+
locale_opts[:available_locales] || locale_opts[:available]
|
|
51
|
+
end
|
|
52
|
+
return unless locale_opts[:default_locale] || locale_opts[:default]
|
|
53
|
+
|
|
54
|
+
@locale_config[:default_locale] =
|
|
55
|
+
locale_opts[:default_locale] || locale_opts[:default]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def configure_security(opts)
|
|
59
|
+
# Enable CSRF protection if requested
|
|
60
|
+
enable_csrf_protection! if opts[:csrf_protection]
|
|
61
|
+
|
|
62
|
+
# Enable request validation if requested
|
|
63
|
+
enable_request_validation! if opts[:request_validation]
|
|
64
|
+
|
|
65
|
+
# Enable rate limiting if requested
|
|
66
|
+
if opts[:rate_limiting]
|
|
67
|
+
rate_limiting_opts = opts[:rate_limiting].is_a?(Hash) ? opts[:rate_limiting] : {}
|
|
68
|
+
enable_rate_limiting!(rate_limiting_opts)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Add trusted proxies if provided
|
|
72
|
+
Array(opts[:trusted_proxies]).each { |proxy| add_trusted_proxy(proxy) } if opts[:trusted_proxies]
|
|
73
|
+
|
|
74
|
+
# Set custom security headers
|
|
75
|
+
return unless opts[:security_headers]
|
|
76
|
+
|
|
77
|
+
set_security_headers(opts[:security_headers])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def configure_authentication(opts)
|
|
81
|
+
# Update existing @auth_config rather than creating a new one
|
|
82
|
+
# to maintain synchronization with the configurator
|
|
83
|
+
@auth_config[:auth_strategies] = opts[:auth_strategies] if opts[:auth_strategies]
|
|
84
|
+
@auth_config[:default_auth_strategy] = opts[:default_auth_strategy] if opts[:default_auth_strategy]
|
|
85
|
+
|
|
86
|
+
# Enable authentication middleware if strategies are configured
|
|
87
|
+
return unless opts[:auth_strategies] && !opts[:auth_strategies].empty?
|
|
88
|
+
|
|
89
|
+
enable_authentication!
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def configure_mcp(opts)
|
|
93
|
+
@mcp_server = nil
|
|
94
|
+
|
|
95
|
+
# Enable MCP if requested in options
|
|
96
|
+
return unless opts[:mcp_enabled] || opts[:mcp_http] || opts[:mcp_stdio]
|
|
97
|
+
|
|
98
|
+
@mcp_server = Otto::MCP::Server.new(self)
|
|
99
|
+
|
|
100
|
+
mcp_options = {}
|
|
101
|
+
mcp_options[:http_endpoint] = opts[:mcp_endpoint] if opts[:mcp_endpoint]
|
|
102
|
+
|
|
103
|
+
return unless opts[:mcp_http] != false # Default to true unless explicitly disabled
|
|
104
|
+
|
|
105
|
+
@mcp_server.enable!(mcp_options)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Configure locale settings for the application
|
|
109
|
+
#
|
|
110
|
+
# @param available_locales [Hash] Hash of available locales (e.g., { 'en' => 'English', 'es' => 'Spanish' })
|
|
111
|
+
# @param default_locale [String] Default locale to use as fallback
|
|
112
|
+
# @example
|
|
113
|
+
# otto.configure(
|
|
114
|
+
# available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
|
|
115
|
+
# default_locale: 'en'
|
|
116
|
+
# )
|
|
117
|
+
def configure(available_locales: nil, default_locale: nil)
|
|
118
|
+
@locale_config ||= {}
|
|
119
|
+
@locale_config[:available_locales] = available_locales if available_locales
|
|
120
|
+
@locale_config[:default_locale] = default_locale if default_locale
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Configure rate limiting settings.
|
|
124
|
+
#
|
|
125
|
+
# @param config [Hash] Rate limiting configuration
|
|
126
|
+
# @option config [Integer] :requests_per_minute Maximum requests per minute per IP
|
|
127
|
+
# @option config [Hash] :custom_rules Hash of custom rate limiting rules
|
|
128
|
+
# @option config [Object] :cache_store Custom cache store for rate limiting
|
|
129
|
+
# @example
|
|
130
|
+
# otto.configure_rate_limiting({
|
|
131
|
+
# requests_per_minute: 50,
|
|
132
|
+
# custom_rules: {
|
|
133
|
+
# 'api_calls' => { limit: 30, period: 60, condition: ->(req) { req.path.start_with?('/api') }}
|
|
134
|
+
# }
|
|
135
|
+
# })
|
|
136
|
+
def configure_rate_limiting(config)
|
|
137
|
+
@security_config.rate_limiting_config.merge!(config)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Configure authentication strategies for route-level access control.
|
|
141
|
+
#
|
|
142
|
+
# @param strategies [Hash] Hash mapping strategy names to strategy instances
|
|
143
|
+
# @param default_strategy [String] Default strategy to use when none specified
|
|
144
|
+
# @example
|
|
145
|
+
# otto.configure_auth_strategies({
|
|
146
|
+
# 'noauth' => Otto::Security::Authentication::Strategies::NoAuthStrategy.new,
|
|
147
|
+
# 'authenticated' => Otto::Security::Authentication::Strategies::SessionStrategy.new(session_key: 'user_id'),
|
|
148
|
+
# 'role:admin' => Otto::Security::Authentication::Strategies::RoleStrategy.new(['admin']),
|
|
149
|
+
# 'api_key' => Otto::Security::Authentication::Strategies::APIKeyStrategy.new(api_keys: ['secret123'])
|
|
150
|
+
# })
|
|
151
|
+
def configure_auth_strategies(strategies, default_strategy: 'noauth')
|
|
152
|
+
# Update existing @auth_config rather than creating a new one
|
|
153
|
+
@auth_config[:auth_strategies] = strategies
|
|
154
|
+
@auth_config[:default_auth_strategy] = default_strategy
|
|
155
|
+
|
|
156
|
+
enable_authentication! unless strategies.empty?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def middleware_enabled?(middleware_class)
|
|
162
|
+
# Only check the new middleware stack as the single source of truth
|
|
163
|
+
@middleware && @middleware.includes?(middleware_class)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|