otto 1.5.0 → 2.0.0.pre1

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -5
  3. data/.github/workflows/claude-code-review.yml +53 -0
  4. data/.github/workflows/claude.yml +49 -0
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +24 -345
  7. data/CHANGELOG.rst +83 -0
  8. data/CLAUDE.md +56 -0
  9. data/Gemfile +21 -5
  10. data/Gemfile.lock +69 -31
  11. data/README.md +2 -0
  12. data/bin/rspec +16 -0
  13. data/changelog.d/20250911_235619_delano_next.rst +28 -0
  14. data/changelog.d/20250912_123055_delano_remove_ostruct.rst +21 -0
  15. data/changelog.d/20250912_175625_claude_delano_remove_ostruct.rst +21 -0
  16. data/changelog.d/README.md +120 -0
  17. data/changelog.d/scriv.ini +5 -0
  18. data/docs/.gitignore +1 -0
  19. data/docs/migrating/v2.0.0-pre1.md +276 -0
  20. data/examples/.gitignore +1 -0
  21. data/examples/advanced_routes/README.md +33 -0
  22. data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
  23. data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
  24. data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
  25. data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
  26. data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
  27. data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
  28. data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
  29. data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
  30. data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
  31. data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
  32. data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
  33. data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
  34. data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
  35. data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
  36. data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
  37. data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
  38. data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
  39. data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
  40. data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
  41. data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
  42. data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
  43. data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
  44. data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
  45. data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
  46. data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
  47. data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
  48. data/examples/advanced_routes/app.rb +33 -0
  49. data/examples/advanced_routes/config.rb +23 -0
  50. data/examples/advanced_routes/config.ru +7 -0
  51. data/examples/advanced_routes/puma.rb +20 -0
  52. data/examples/advanced_routes/routes +167 -0
  53. data/examples/advanced_routes/run.rb +39 -0
  54. data/examples/advanced_routes/test.rb +58 -0
  55. data/examples/authentication_strategies/README.md +32 -0
  56. data/examples/authentication_strategies/app/auth.rb +68 -0
  57. data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
  58. data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
  59. data/examples/authentication_strategies/config.ru +24 -0
  60. data/examples/authentication_strategies/routes +37 -0
  61. data/examples/basic/README.md +29 -0
  62. data/examples/basic/app.rb +7 -35
  63. data/examples/basic/routes +0 -9
  64. data/examples/mcp_demo/README.md +87 -0
  65. data/examples/mcp_demo/app.rb +51 -0
  66. data/examples/mcp_demo/config.ru +17 -0
  67. data/examples/mcp_demo/routes +9 -0
  68. data/examples/security_features/README.md +46 -0
  69. data/examples/security_features/app.rb +23 -24
  70. data/examples/security_features/config.ru +8 -10
  71. data/lib/otto/core/configuration.rb +167 -0
  72. data/lib/otto/core/error_handler.rb +86 -0
  73. data/lib/otto/core/file_safety.rb +61 -0
  74. data/lib/otto/core/middleware_stack.rb +157 -0
  75. data/lib/otto/core/router.rb +183 -0
  76. data/lib/otto/core/uri_generator.rb +44 -0
  77. data/lib/otto/design_system.rb +7 -5
  78. data/lib/otto/helpers/base.rb +3 -0
  79. data/lib/otto/helpers/request.rb +10 -8
  80. data/lib/otto/helpers/response.rb +5 -4
  81. data/lib/otto/helpers/validation.rb +85 -0
  82. data/lib/otto/mcp/auth/token.rb +77 -0
  83. data/lib/otto/mcp/protocol.rb +164 -0
  84. data/lib/otto/mcp/rate_limiting.rb +155 -0
  85. data/lib/otto/mcp/registry.rb +100 -0
  86. data/lib/otto/mcp/route_parser.rb +77 -0
  87. data/lib/otto/mcp/server.rb +206 -0
  88. data/lib/otto/mcp/validation.rb +123 -0
  89. data/lib/otto/response_handlers/auto.rb +39 -0
  90. data/lib/otto/response_handlers/base.rb +16 -0
  91. data/lib/otto/response_handlers/default.rb +16 -0
  92. data/lib/otto/response_handlers/factory.rb +39 -0
  93. data/lib/otto/response_handlers/json.rb +28 -0
  94. data/lib/otto/response_handlers/redirect.rb +25 -0
  95. data/lib/otto/response_handlers/view.rb +24 -0
  96. data/lib/otto/response_handlers.rb +9 -135
  97. data/lib/otto/route.rb +9 -9
  98. data/lib/otto/route_definition.rb +30 -33
  99. data/lib/otto/route_handlers/base.rb +121 -0
  100. data/lib/otto/route_handlers/class_method.rb +89 -0
  101. data/lib/otto/route_handlers/factory.rb +29 -0
  102. data/lib/otto/route_handlers/instance_method.rb +69 -0
  103. data/lib/otto/route_handlers/lambda.rb +59 -0
  104. data/lib/otto/route_handlers/logic_class.rb +93 -0
  105. data/lib/otto/route_handlers.rb +10 -376
  106. data/lib/otto/security/authentication/auth_strategy.rb +44 -0
  107. data/lib/otto/security/authentication/authentication_middleware.rb +123 -0
  108. data/lib/otto/security/authentication/failure_result.rb +36 -0
  109. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
  110. data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
  111. data/lib/otto/security/authentication/strategies/public_strategy.rb +19 -0
  112. data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
  113. data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
  114. data/lib/otto/security/authentication/strategy_result.rb +223 -0
  115. data/lib/otto/security/authentication.rb +28 -282
  116. data/lib/otto/security/config.rb +15 -11
  117. data/lib/otto/security/configurator.rb +219 -0
  118. data/lib/otto/security/csrf.rb +8 -143
  119. data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
  120. data/lib/otto/security/middleware/rate_limit_middleware.rb +38 -0
  121. data/lib/otto/security/middleware/validation_middleware.rb +252 -0
  122. data/lib/otto/security/rate_limiter.rb +86 -0
  123. data/lib/otto/security/rate_limiting.rb +16 -0
  124. data/lib/otto/security/validator.rb +8 -292
  125. data/lib/otto/static.rb +3 -0
  126. data/lib/otto/utils.rb +14 -0
  127. data/lib/otto/version.rb +3 -1
  128. data/lib/otto.rb +184 -414
  129. data/otto.gemspec +11 -6
  130. metadata +134 -25
  131. data/examples/dynamic_pages/app.rb +0 -115
  132. data/examples/dynamic_pages/config.ru +0 -30
  133. data/examples/dynamic_pages/routes +0 -21
  134. data/examples/helpers_demo/app.rb +0 -244
  135. data/examples/helpers_demo/config.ru +0 -26
  136. data/examples/helpers_demo/routes +0 -7
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'webrick'
5
+
6
+ # The shared config file returns the configured Otto app
7
+ otto_app = require_relative 'config'
8
+
9
+ # Create a simple WEBrick server
10
+ server = WEBrick::HTTPServer.new(
11
+ Port: 9292,
12
+ Logger: WEBrick::Log.new($stdout, WEBrick::Log::INFO)
13
+ )
14
+
15
+ # Mount the Otto app
16
+ server.mount '/', WEBrick::HTTPServlet::ProcHandler.new(proc { |req, res|
17
+ env = req.meta_vars.merge({
18
+ 'REQUEST_METHOD' => req.request_method,
19
+ 'PATH_INFO' => req.path_info,
20
+ 'QUERY_STRING' => req.query_string || '',
21
+ 'rack.input' => StringIO.new(req.body || ''),
22
+ 'CONTENT_TYPE' => req.content_type,
23
+ 'CONTENT_LENGTH' => req.content_length&.to_s
24
+ })
25
+
26
+ status, headers, body = otto_app.call(env)
27
+
28
+ res.status = status
29
+ headers.each { |k, v| res[k] = v }
30
+ body.each { |chunk| res.body << chunk }
31
+ })
32
+
33
+ # Handle Ctrl+C gracefully
34
+ trap('INT') { server.shutdown }
35
+
36
+ puts "Otto Advanced Routes Example running on http://localhost:9292"
37
+ puts "Press Ctrl+C to stop"
38
+
39
+ server.start
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # The shared config file returns the configured Otto app
5
+ otto_app = require_relative 'config'
6
+
7
+ # Simple test runner to demonstrate routes without a server
8
+ def test_route(method, path, params = {})
9
+ query_string = params.map { |k, v| "#{k}=#{v}" }.join('&')
10
+
11
+ env = {
12
+ 'REQUEST_METHOD' => method.to_s.upcase,
13
+ 'PATH_INFO' => path,
14
+ 'QUERY_STRING' => query_string,
15
+ 'rack.input' => StringIO.new(''),
16
+ 'HTTP_ACCEPT' => 'application/json',
17
+ }
18
+
19
+ status, headers, body = otto_app.call(env)
20
+
21
+ puts "#{method.to_s.upcase} #{path}#{query_string.empty? ? '' : '?' + query_string}"
22
+ puts "Status: #{status}"
23
+ puts "Content-Type: #{headers['content-type'] || headers['Content-Type']}"
24
+ puts "Body: #{body.join}"
25
+ puts "---"
26
+ end
27
+
28
+ puts "Otto Advanced Routes Syntax Test"
29
+ puts "================================"
30
+
31
+ # Test basic routes
32
+ test_route(:get, '/')
33
+ test_route(:post, '/feedback')
34
+
35
+ # Test JSON routes
36
+ test_route(:get, '/api/users')
37
+ test_route(:get, '/api/health')
38
+
39
+ # Test Logic classes
40
+ test_route(:get, '/logic/simple')
41
+ test_route(:post, '/logic/process')
42
+
43
+ # Test namespaced Logic classes
44
+ test_route(:get, '/logic/admin')
45
+ test_route(:get, '/logic/v2/dashboard')
46
+
47
+ # Test CSRF exempt routes
48
+ test_route(:post, '/api/webhook')
49
+ test_route(:put, '/api/external')
50
+
51
+ # Test custom parameters
52
+ test_route(:get, '/config/env')
53
+ test_route(:get, '/api/v1')
54
+
55
+ # Test complex routes
56
+ test_route(:post, '/test/everything')
57
+
58
+ puts "All tests completed!"
@@ -0,0 +1,32 @@
1
+ # Otto - Authentication Strategies Example
2
+
3
+ This example demonstrates how to use Otto's powerful authentication features.
4
+
5
+ ## Structure
6
+
7
+ * `config.ru`: The Rackup file. It initializes Otto and loads the authentication strategies.
8
+ * `routes`: Defines the application's routes and the authentication required for each.
9
+ * `app/auth.rb`: Contains the definitions for all authentication strategies. This is where you would add your own.
10
+ * `app/controllers/`: Contains the controller classes that handle requests.
11
+
12
+ ## Running the Demo
13
+
14
+ 1. Make sure you have the necessary gems installed (`bundle install`).
15
+ 2. Run the application from the root of the `otto` project:
16
+
17
+ ```sh
18
+ rackup examples/authentication_strategies/config.ru
19
+ ```
20
+
21
+ 3. Open your browser and navigate to `http://localhost:9292`.
22
+
23
+ ## Trying the Authentication Strategies
24
+
25
+ You can test the different authentication strategies by providing a `token` or `api_key` parameter in the URL.
26
+
27
+ * **Authenticated User:** [http://localhost:9292/profile?token=demo_token](http://localhost:9292/profile?token=demo_token)
28
+ * **Admin User:** [http://localhost:9292/admin?token=admin_token](http://localhost:9292/admin?token=admin_token)
29
+ * **User with 'write' permission:** [http://localhost:9292/edit?token=demo_token](http://localhost:9292/edit?token=demo_token)
30
+ * **API Key:** [http://localhost:9292/api/data?api_key=demo_api_key_123](http://localhost:9292/api/data?api_key=demo_api_key_123)
31
+
32
+ If you try to access a protected route without the correct token, you'll get an authentication error.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authentication result data class to contain the session, user and anything
4
+ # else we want to make available to the route handlers/controllers/logic classes.
5
+ AuthResultData = Data.define(:session, :user) do
6
+ def initialize(session: {}, user: {})
7
+ super(session: session, user: user)
8
+ end
9
+
10
+ # Provide user_context method for compatibility with existing AuthResult
11
+ def user_context
12
+ { session: session, user: user }
13
+ end
14
+ end
15
+
16
+ module AuthenticationSetup
17
+ def self.configure(otto)
18
+ # Simple auth strategy that checks for a token parameter or header
19
+ otto.add_auth_strategy('authenticated', lambda do |req|
20
+ token = req.params['token'] || req.get_header('HTTP_AUTHORIZATION')
21
+ if token == 'demo_token'
22
+ AuthResultData.new(
23
+ session: { session_id: 'demo_session_123', user_id: 1 },
24
+ user: { name: 'Demo User', role: 'user', permissions: %w[read write] }
25
+ )
26
+ end
27
+ end)
28
+
29
+ # Role-based auth strategy
30
+ otto.add_auth_strategy('role:admin', lambda do |req|
31
+ token = req.params['token'] || req.get_header('HTTP_AUTHORIZATION')
32
+ if token == 'admin_token'
33
+ AuthResultData.new(
34
+ session: { session_id: 'admin_session_456', user_id: 2 },
35
+ user: { name: 'Admin User', role: 'admin', permissions: %w[read write admin delete] }
36
+ )
37
+ end
38
+ end)
39
+
40
+ # Permission-based auth strategy
41
+ otto.add_auth_strategy('permission:write', lambda do |req|
42
+ token = req.params['token'] || req.get_header('HTTP_AUTHORIZATION')
43
+ case token
44
+ when 'demo_token'
45
+ AuthResultData.new(
46
+ session: { session_id: 'demo_session_123', user_id: 1 },
47
+ user: { name: 'Demo User', role: 'user', permissions: %w[read write] }
48
+ )
49
+ when 'admin_token'
50
+ AuthResultData.new(
51
+ session: { session_id: 'admin_session_456', user_id: 2 },
52
+ user: { name: 'Admin User', role: 'admin', permissions: %w[read write admin delete] }
53
+ )
54
+ end
55
+ end)
56
+
57
+ # API key auth strategy
58
+ otto.add_auth_strategy('api_key', lambda do |req|
59
+ api_key = req.params['api_key'] || req.get_header('HTTP_X_API_KEY')
60
+ if api_key == 'demo_api_key_123'
61
+ AuthResultData.new(
62
+ session: { api_session: 'api_session_abc' },
63
+ user: { name: 'API Client', type: 'api', permissions: ['api_access'] }
64
+ )
65
+ end
66
+ end)
67
+ end
68
+ end
@@ -0,0 +1,29 @@
1
+ require 'json'
2
+
3
+ class AuthController
4
+ def self.login_form
5
+ [200, { 'content-type' => 'text/html' }, ['<h1>Login</h1><p>Use ?token=demo_token, ?token=admin_token, or ?api_key=demo_api_key_123</p>']]
6
+ end
7
+
8
+ def self.login
9
+ # In a real app, you'd handle login logic here.
10
+ # For this demo, we redirect to the profile.
11
+ [302, { 'location' => '/profile' }, ['']]
12
+ end
13
+
14
+ def self.show_profile
15
+ [200, { 'content-type' => 'text/html' }, ['<h1>User Profile</h1><p>You are authenticated.</p>']]
16
+ end
17
+
18
+ def self.admin_panel
19
+ [200, { 'content-type' => 'text/html' }, ['<h1>Admin Panel</h1><p>Role: admin required</p>']]
20
+ end
21
+
22
+ def self.edit_content
23
+ [200, { 'content-type' => 'text/html' }, ['<h1>Edit Content</h1><p>Permission: write required</p>']]
24
+ end
25
+
26
+ def self.api_data
27
+ [200, { 'content-type' => 'application/json' }, ['{"data": "This is some secret API data."}']]
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+ require 'time'
3
+
4
+ class MainController
5
+ def self.index
6
+ [200, { 'content-type' => 'text/html' }, ['<h1>Authentication Strategies Example</h1>']]
7
+ end
8
+
9
+ def self.receive_feedback
10
+ [200, { 'content-type' => 'text/plain' }, ['Feedback received']]
11
+ end
12
+
13
+ def self.dashboard
14
+ [200, { 'content-type' => 'text/html' }, ['<h1>Dashboard</h1><p>Welcome to your dashboard!</p>']]
15
+ end
16
+
17
+ def self.reports
18
+ [200, { 'content-type' => 'text/html' }, ['<h1>Reports</h1><p>Admin-only reports section</p>']]
19
+ end
20
+
21
+ def self.not_found
22
+ [404, { 'content-type' => 'text/html' }, ['<h1>404 - Page Not Found</h1><p>Advanced routes example</p>']]
23
+ end
24
+
25
+ def self.server_error
26
+ [500, { 'content-type' => 'text/html' }, ['<h1>500 - Server Error</h1><p>Something went wrong</p>']]
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require_relative '../../lib/otto'
5
+ require_relative 'app/auth'
6
+ require_relative 'app/controllers/main_controller'
7
+ require_relative 'app/controllers/auth_controller'
8
+
9
+ # Configure Otto with advanced features
10
+ otto = Otto.new('routes')
11
+
12
+ # Enable security features to demonstrate advanced route parameters
13
+ otto.enable_csrf_protection!
14
+ otto.enable_request_validation!
15
+ otto.enable_authentication!
16
+
17
+ # Load and configure authentication strategies
18
+ AuthenticationSetup.configure(otto)
19
+
20
+ # Set error handlers
21
+ otto.not_found = ->(_env) { MainController.not_found }
22
+ otto.server_error = ->(_env, _error) { MainController.server_error }
23
+
24
+ run otto
@@ -0,0 +1,37 @@
1
+ # Authentication Strategies Example
2
+
3
+ # ========================================
4
+ # PUBLIC ROUTES
5
+ # ========================================
6
+
7
+ GET / MainController#index
8
+ GET /login AuthController#login_form
9
+ POST /login AuthController#login
10
+
11
+ # ========================================
12
+ # AUTHENTICATED ROUTES
13
+ # ========================================
14
+
15
+ # Requires a user to be logged in.
16
+ # Try: /profile?token=demo_token
17
+ GET /profile AuthController#show_profile auth=authenticated
18
+
19
+ # Requires the 'admin' role.
20
+ # Try: /admin?token=admin_token
21
+ GET /admin AuthController#admin_panel auth=role:admin
22
+
23
+ # Requires the 'write' permission.
24
+ # A user with the 'user' role has this.
25
+ # Try: /edit?token=demo_token
26
+ GET /edit AuthController#edit_content auth=permission:write
27
+
28
+ # A route protected by an API key
29
+ # Try: /api/data?api_key=demo_api_key_123
30
+ GET /api/data AuthController#api_data auth=api_key response=json
31
+
32
+ # ========================================
33
+ # ERROR HANDLERS
34
+ # ========================================
35
+
36
+ GET /404 MainController#not_found response=view
37
+ GET /500 MainController#server_error response=view
@@ -0,0 +1,29 @@
1
+ # Otto - Basic Example
2
+
3
+ This example demonstrates a basic Otto application with a single route that accepts both GET and POST requests.
4
+
5
+ ## How to Run
6
+
7
+ 1. Make sure you have `bundler` and `thin` installed:
8
+ ```sh
9
+ gem install bundler thin
10
+ ```
11
+
12
+ 2. Install the dependencies from the root of the project:
13
+ ```sh
14
+ bundle install
15
+ ```
16
+
17
+ 3. Start the server from this directory (`examples/basic`):
18
+ ```sh
19
+ thin -e dev -R config.ru -p 10770 start
20
+ ```
21
+
22
+ 4. Open your browser and navigate to `http://localhost:10770`.
23
+
24
+ ## File Structure
25
+
26
+ * `README.md`: This file.
27
+ * `app.rb`: Contains the application logic. It has two methods: `index` to display the main page and `receive_feedback` to handle form submissions.
28
+ * `config.ru`: The Rack configuration file that loads the Otto framework and the application.
29
+ * `routes`: Defines the URL routes and maps them to methods in the `App` class.
@@ -1,7 +1,8 @@
1
- # examples/basic/app.rb (Streamlined with Design System)
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
- otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
41
- else
42
- otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
43
- otto_card('Your Message') { otto_code_block(message, 'text') }
44
- end
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
@@ -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.
@@ -0,0 +1,51 @@
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
16
+
17
+ def self.health(_req, res)
18
+ res.headers['content-type'] = 'text/plain'
19
+ res.body = 'OK'
20
+ end
21
+ end
22
+
23
+ # UserAPI provides handlers for the MCP tool and resource routes.
24
+ class UserAPI
25
+ # MCP Resource: mcp_list_users
26
+ # Accessible via JSON-RPC method "users/list"
27
+ def self.mcp_list_users
28
+ {
29
+ users: [
30
+ { id: 1, name: 'Alice', email: 'alice@example.com' },
31
+ { id: 2, name: 'Bob', email: 'bob@example.com' },
32
+ ],
33
+ }.to_json
34
+ end
35
+
36
+ # MCP Tool: mcp_create_user
37
+ # Accessible via JSON-RPC method "create_user"
38
+ def self.mcp_create_user(arguments, _env)
39
+ name = arguments['name'] || 'Anonymous'
40
+ email = arguments['email'] || "#{name.downcase}@example.com"
41
+
42
+ new_user = {
43
+ id: rand(1000..9999),
44
+ name: name,
45
+ email: email,
46
+ created_at: Time.now.iso8601,
47
+ }
48
+
49
+ "Created user: #{new_user.to_json}"
50
+ end
51
+ end
@@ -0,0 +1,17 @@
1
+ # examples/mcp_demo/config.ru
2
+
3
+ require_relative '../../lib/otto'
4
+ require_relative 'app'
5
+
6
+ # Initialize Otto with MCP support
7
+ app = Otto.new('routes', {
8
+ mcp_enabled: true,
9
+ auth_tokens: ['demo-token-123', 'another-token-456'],
10
+ requests_per_minute: 60, # Rate limiting for the MCP endpoint
11
+ tools_per_minute: 20,
12
+ })
13
+
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,9 @@
1
+ # Basic HTTP routes
2
+ GET / DemoApp.index
3
+ GET /health DemoApp.health
4
+
5
+ # MCP Resources - provide read-only data
6
+ MCP /users UserAPI.mcp_list_users
7
+
8
+ # MCP Tools - provide actions/operations
9
+ TOOL /create_user UserAPI.mcp_create_user
@@ -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.