otto 1.6.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 +1 -1
  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 +10 -3
  10. data/Gemfile.lock +23 -28
  11. data/README.md +2 -0
  12. data/bin/rspec +4 -4
  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 +29 -34
  66. data/examples/mcp_demo/config.ru +9 -60
  67. data/examples/security_features/README.md +46 -0
  68. data/examples/security_features/app.rb +23 -24
  69. data/examples/security_features/config.ru +8 -10
  70. data/lib/otto/core/configuration.rb +167 -0
  71. data/lib/otto/core/error_handler.rb +86 -0
  72. data/lib/otto/core/file_safety.rb +61 -0
  73. data/lib/otto/core/middleware_stack.rb +157 -0
  74. data/lib/otto/core/router.rb +183 -0
  75. data/lib/otto/core/uri_generator.rb +44 -0
  76. data/lib/otto/design_system.rb +7 -5
  77. data/lib/otto/helpers/base.rb +3 -0
  78. data/lib/otto/helpers/request.rb +10 -8
  79. data/lib/otto/helpers/response.rb +5 -4
  80. data/lib/otto/helpers/validation.rb +9 -7
  81. data/lib/otto/mcp/auth/token.rb +10 -9
  82. data/lib/otto/mcp/protocol.rb +24 -27
  83. data/lib/otto/mcp/rate_limiting.rb +8 -3
  84. data/lib/otto/mcp/registry.rb +7 -2
  85. data/lib/otto/mcp/route_parser.rb +10 -15
  86. data/lib/otto/mcp/server.rb +21 -11
  87. data/lib/otto/mcp/validation.rb +14 -10
  88. data/lib/otto/response_handlers/auto.rb +39 -0
  89. data/lib/otto/response_handlers/base.rb +16 -0
  90. data/lib/otto/response_handlers/default.rb +16 -0
  91. data/lib/otto/response_handlers/factory.rb +39 -0
  92. data/lib/otto/response_handlers/json.rb +28 -0
  93. data/lib/otto/response_handlers/redirect.rb +25 -0
  94. data/lib/otto/response_handlers/view.rb +24 -0
  95. data/lib/otto/response_handlers.rb +9 -135
  96. data/lib/otto/route.rb +9 -9
  97. data/lib/otto/route_definition.rb +15 -18
  98. data/lib/otto/route_handlers/base.rb +121 -0
  99. data/lib/otto/route_handlers/class_method.rb +89 -0
  100. data/lib/otto/route_handlers/factory.rb +29 -0
  101. data/lib/otto/route_handlers/instance_method.rb +69 -0
  102. data/lib/otto/route_handlers/lambda.rb +59 -0
  103. data/lib/otto/route_handlers/logic_class.rb +93 -0
  104. data/lib/otto/route_handlers.rb +10 -405
  105. data/lib/otto/security/authentication/auth_strategy.rb +44 -0
  106. data/lib/otto/security/authentication/authentication_middleware.rb +123 -0
  107. data/lib/otto/security/authentication/failure_result.rb +36 -0
  108. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
  109. data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
  110. data/lib/otto/security/authentication/strategies/public_strategy.rb +19 -0
  111. data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
  112. data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
  113. data/lib/otto/security/authentication/strategy_result.rb +223 -0
  114. data/lib/otto/security/authentication.rb +28 -282
  115. data/lib/otto/security/config.rb +14 -12
  116. data/lib/otto/security/configurator.rb +219 -0
  117. data/lib/otto/security/csrf.rb +8 -143
  118. data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
  119. data/lib/otto/security/middleware/rate_limit_middleware.rb +38 -0
  120. data/lib/otto/security/middleware/validation_middleware.rb +252 -0
  121. data/lib/otto/security/rate_limiter.rb +86 -0
  122. data/lib/otto/security/rate_limiting.rb +10 -105
  123. data/lib/otto/security/validator.rb +8 -253
  124. data/lib/otto/static.rb +3 -0
  125. data/lib/otto/utils.rb +14 -0
  126. data/lib/otto/version.rb +3 -1
  127. data/lib/otto.rb +142 -498
  128. data/otto.gemspec +2 -2
  129. metadata +89 -28
  130. data/examples/dynamic_pages/app.rb +0 -115
  131. data/examples/dynamic_pages/config.ru +0 -30
  132. data/examples/dynamic_pages/routes +0 -21
  133. data/examples/helpers_demo/app.rb +0 -244
  134. data/examples/helpers_demo/config.ru +0 -26
  135. data/examples/helpers_demo/routes +0 -7
  136. data/lib/concurrent_cache_store.rb +0 -68
data/otto.gemspec CHANGED
@@ -16,10 +16,10 @@ Gem::Specification.new do |spec|
16
16
 
17
17
  spec.required_ruby_version = ['>= 3.2', '< 4.0']
18
18
 
19
- spec.add_dependency 'ostruct', '~> 0.6.3'
19
+
20
20
  spec.add_dependency 'rack', '~> 3.1', '< 4.0'
21
21
  spec.add_dependency 'rack-parser', '~> 0.7'
22
- spec.add_dependency 'rexml', '~> 3.3', '>= 3.3.6'
22
+ spec.add_dependency 'rexml', '>= 3.3.6'
23
23
 
24
24
  # Security dependencies
25
25
  spec.add_dependency 'facets', '~> 3.1'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: otto
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 2.0.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -9,20 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: ostruct
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: 0.6.3
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: 0.6.3
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: rack
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -61,9 +47,6 @@ dependencies:
61
47
  name: rexml
62
48
  requirement: !ruby/object:Gem::Requirement
63
49
  requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '3.3'
67
50
  - - ">="
68
51
  - !ruby/object:Gem::Version
69
52
  version: 3.3.6
@@ -71,9 +54,6 @@ dependencies:
71
54
  prerelease: false
72
55
  version_requirements: !ruby/object:Gem::Requirement
73
56
  requirements:
74
- - - "~>"
75
- - !ruby/object:Gem::Version
76
- version: '3.3'
77
57
  - - ">="
78
58
  - !ruby/object:Gem::Version
79
59
  version: 3.3.6
@@ -113,34 +93,87 @@ extra_rdoc_files: []
113
93
  files:
114
94
  - ".github/dependabot.yml"
115
95
  - ".github/workflows/ci.yml"
96
+ - ".github/workflows/claude-code-review.yml"
97
+ - ".github/workflows/claude.yml"
116
98
  - ".gitignore"
117
99
  - ".pre-commit-config.yaml"
118
100
  - ".pre-push-config.yaml"
119
101
  - ".rspec"
120
102
  - ".rubocop.yml"
103
+ - CHANGELOG.rst
104
+ - CLAUDE.md
121
105
  - Gemfile
122
106
  - Gemfile.lock
123
107
  - LICENSE.txt
124
108
  - README.md
125
109
  - bin/rspec
110
+ - changelog.d/20250911_235619_delano_next.rst
111
+ - changelog.d/20250912_123055_delano_remove_ostruct.rst
112
+ - changelog.d/20250912_175625_claude_delano_remove_ostruct.rst
113
+ - changelog.d/README.md
114
+ - changelog.d/scriv.ini
126
115
  - docs/.gitignore
116
+ - docs/migrating/v2.0.0-pre1.md
117
+ - examples/.gitignore
118
+ - examples/advanced_routes/README.md
119
+ - examples/advanced_routes/app.rb
120
+ - examples/advanced_routes/app/controllers/handlers/async.rb
121
+ - examples/advanced_routes/app/controllers/handlers/dynamic.rb
122
+ - examples/advanced_routes/app/controllers/handlers/static.rb
123
+ - examples/advanced_routes/app/controllers/modules/auth.rb
124
+ - examples/advanced_routes/app/controllers/modules/transformer.rb
125
+ - examples/advanced_routes/app/controllers/modules/validator.rb
126
+ - examples/advanced_routes/app/controllers/routes_app.rb
127
+ - examples/advanced_routes/app/controllers/v2/admin.rb
128
+ - examples/advanced_routes/app/controllers/v2/config.rb
129
+ - examples/advanced_routes/app/controllers/v2/settings.rb
130
+ - examples/advanced_routes/app/logic/admin/logic/manager.rb
131
+ - examples/advanced_routes/app/logic/admin/panel.rb
132
+ - examples/advanced_routes/app/logic/analytics_processor.rb
133
+ - examples/advanced_routes/app/logic/complex/business/handler.rb
134
+ - examples/advanced_routes/app/logic/data_logic.rb
135
+ - examples/advanced_routes/app/logic/data_processor.rb
136
+ - examples/advanced_routes/app/logic/input_validator.rb
137
+ - examples/advanced_routes/app/logic/nested/feature/logic.rb
138
+ - examples/advanced_routes/app/logic/reports_generator.rb
139
+ - examples/advanced_routes/app/logic/simple_logic.rb
140
+ - examples/advanced_routes/app/logic/system/config/manager.rb
141
+ - examples/advanced_routes/app/logic/test_logic.rb
142
+ - examples/advanced_routes/app/logic/transform_logic.rb
143
+ - examples/advanced_routes/app/logic/upload_logic.rb
144
+ - examples/advanced_routes/app/logic/v2/logic/dashboard.rb
145
+ - examples/advanced_routes/app/logic/v2/logic/processor.rb
146
+ - examples/advanced_routes/config.rb
147
+ - examples/advanced_routes/config.ru
148
+ - examples/advanced_routes/puma.rb
149
+ - examples/advanced_routes/routes
150
+ - examples/advanced_routes/run.rb
151
+ - examples/advanced_routes/test.rb
152
+ - examples/authentication_strategies/README.md
153
+ - examples/authentication_strategies/app/auth.rb
154
+ - examples/authentication_strategies/app/controllers/auth_controller.rb
155
+ - examples/authentication_strategies/app/controllers/main_controller.rb
156
+ - examples/authentication_strategies/config.ru
157
+ - examples/authentication_strategies/routes
158
+ - examples/basic/README.md
127
159
  - examples/basic/app.rb
128
160
  - examples/basic/config.ru
129
161
  - examples/basic/routes
130
- - examples/dynamic_pages/app.rb
131
- - examples/dynamic_pages/config.ru
132
- - examples/dynamic_pages/routes
133
- - examples/helpers_demo/app.rb
134
- - examples/helpers_demo/config.ru
135
- - examples/helpers_demo/routes
162
+ - examples/mcp_demo/README.md
136
163
  - examples/mcp_demo/app.rb
137
164
  - examples/mcp_demo/config.ru
138
165
  - examples/mcp_demo/routes
166
+ - examples/security_features/README.md
139
167
  - examples/security_features/app.rb
140
168
  - examples/security_features/config.ru
141
169
  - examples/security_features/routes
142
- - lib/concurrent_cache_store.rb
143
170
  - lib/otto.rb
171
+ - lib/otto/core/configuration.rb
172
+ - lib/otto/core/error_handler.rb
173
+ - lib/otto/core/file_safety.rb
174
+ - lib/otto/core/middleware_stack.rb
175
+ - lib/otto/core/router.rb
176
+ - lib/otto/core/uri_generator.rb
144
177
  - lib/otto/design_system.rb
145
178
  - lib/otto/helpers/base.rb
146
179
  - lib/otto/helpers/request.rb
@@ -154,15 +187,43 @@ files:
154
187
  - lib/otto/mcp/server.rb
155
188
  - lib/otto/mcp/validation.rb
156
189
  - lib/otto/response_handlers.rb
190
+ - lib/otto/response_handlers/auto.rb
191
+ - lib/otto/response_handlers/base.rb
192
+ - lib/otto/response_handlers/default.rb
193
+ - lib/otto/response_handlers/factory.rb
194
+ - lib/otto/response_handlers/json.rb
195
+ - lib/otto/response_handlers/redirect.rb
196
+ - lib/otto/response_handlers/view.rb
157
197
  - lib/otto/route.rb
158
198
  - lib/otto/route_definition.rb
159
199
  - lib/otto/route_handlers.rb
200
+ - lib/otto/route_handlers/base.rb
201
+ - lib/otto/route_handlers/class_method.rb
202
+ - lib/otto/route_handlers/factory.rb
203
+ - lib/otto/route_handlers/instance_method.rb
204
+ - lib/otto/route_handlers/lambda.rb
205
+ - lib/otto/route_handlers/logic_class.rb
160
206
  - lib/otto/security/authentication.rb
207
+ - lib/otto/security/authentication/auth_strategy.rb
208
+ - lib/otto/security/authentication/authentication_middleware.rb
209
+ - lib/otto/security/authentication/failure_result.rb
210
+ - lib/otto/security/authentication/strategies/api_key_strategy.rb
211
+ - lib/otto/security/authentication/strategies/permission_strategy.rb
212
+ - lib/otto/security/authentication/strategies/public_strategy.rb
213
+ - lib/otto/security/authentication/strategies/role_strategy.rb
214
+ - lib/otto/security/authentication/strategies/session_strategy.rb
215
+ - lib/otto/security/authentication/strategy_result.rb
161
216
  - lib/otto/security/config.rb
217
+ - lib/otto/security/configurator.rb
162
218
  - lib/otto/security/csrf.rb
219
+ - lib/otto/security/middleware/csrf_middleware.rb
220
+ - lib/otto/security/middleware/rate_limit_middleware.rb
221
+ - lib/otto/security/middleware/validation_middleware.rb
222
+ - lib/otto/security/rate_limiter.rb
163
223
  - lib/otto/security/rate_limiting.rb
164
224
  - lib/otto/security/validator.rb
165
225
  - lib/otto/static.rb
226
+ - lib/otto/utils.rb
166
227
  - lib/otto/version.rb
167
228
  - otto.gemspec
168
229
  - public/favicon.ico
@@ -1,115 +0,0 @@
1
- # examples/basic/app.rb (Streamlined with Design System)
2
-
3
- require_relative '../../lib/otto/design_system'
4
-
5
- class App
6
- include Otto::DesignSystem
7
-
8
- attr_reader :req, :res
9
-
10
- def initialize(req, res)
11
- @req = req
12
- @res = res
13
- res.headers['content-type'] = 'text/html; charset=utf-8'
14
- end
15
-
16
- def index
17
- # This demonstrates nested heredocs in Ruby
18
- # The outer heredoc uses <<~HTML delimiter
19
- content = <<~HTML
20
- <div class="otto-card otto-text-center">
21
- <img src="/img/otto.jpg" alt="Otto Framework" class="otto-logo" />
22
- <h1>Otto Framework</h1>
23
- <p>Minimal Ruby web framework with style</p>
24
- </div>
25
-
26
- #{otto_card('Dynamic Pages') do
27
- # This is a nested heredoc within the outer HTML heredoc
28
- # It uses a different delimiter (EXAMPLES) to avoid conflicts
29
- # The #{} interpolation allows the inner heredoc to be embedded
30
- <<~EXAMPLES
31
- #{otto_link('Product #100', '/product/100')} - View product page<br>
32
- #{otto_link('Product #42', '/product/42')} - Different product<br>
33
- #{otto_link('API Data', '/product/100.json')} - JSON endpoint
34
- EXAMPLES
35
- end}
36
- HTML
37
-
38
- res.send_secure_cookie :sess, 1_234_567, 3600
39
- res.body = otto_page(content)
40
- end
41
-
42
- def receive_feedback
43
- message = req.params['msg']&.strip
44
-
45
- content = if message.nil? || message.empty?
46
- otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
47
- else
48
- otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
49
- otto_card('Your Message') { otto_code_block(message, 'text') }
50
- end
51
-
52
- content += "<p>#{otto_link('← Back', '/')}</p>"
53
- res.body = otto_page(content, 'Feedback')
54
- end
55
-
56
- def redirect
57
- res.redirect '/robots.txt'
58
- end
59
-
60
- def robots_text
61
- res.headers['content-type'] = 'text/plain'
62
- res.body = ['User-agent: *', 'Disallow: /private'].join($/)
63
- end
64
-
65
- def display_product
66
- prodid = req.params[:prodid]
67
-
68
- # Check if JSON is requested via .json extension or Accept header
69
- wants_json = req.path_info.end_with?('.json') ||
70
- req.env['HTTP_ACCEPT']&.include?('application/json')
71
-
72
- if wants_json
73
- res.headers['content-type'] = 'application/json; charset=utf-8'
74
- res.body = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
75
- else
76
- # Return HTML product page
77
- product_data = {
78
- 'Product ID' => prodid,
79
- 'Name' => "Sample Product ##{prodid}",
80
- 'Price' => "$#{rand(10..999)}.99",
81
- 'Description' => 'This is a demonstration product showing dynamic routing with parameter :prodid',
82
- 'Stock' => rand(0..50) > 5 ? 'In Stock' : 'Out of Stock',
83
- }
84
-
85
- product_html = product_data.map do |key, value|
86
- "<p><strong>#{key}:</strong> #{escape_html(value.to_s)}</p>"
87
- end.join
88
-
89
- content = <<~HTML
90
- #{otto_card('Product Details') { product_html }}
91
-
92
- <p>
93
- #{otto_link('← Back to Home', '/')} |
94
- #{otto_link('View as JSON', "/product/#{prodid}.json")}
95
- </p>
96
- HTML
97
-
98
- res.body = otto_page(content, "Product ##{prodid}")
99
- end
100
- end
101
-
102
- def not_found
103
- res.status = 404
104
- content = otto_alert('error', 'Not Found', 'The requested page could not be found.')
105
- content += "<p>#{otto_link('← Home', '/')}</p>"
106
- res.body = otto_page(content, '404')
107
- end
108
-
109
- def server_error
110
- res.status = 500
111
- content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
112
- content += "<p>#{otto_link('← Home', '/')}</p>"
113
- res.body = otto_page(content, '500')
114
- end
115
- end
@@ -1,30 +0,0 @@
1
- # examples/basic/config.ru
2
-
3
- # OTTO EXAMPLE APP CONFIG - 2025-08-18
4
- #
5
- # Usage:
6
- #
7
- # $ thin -e dev -R config.ru -p 10770 start
8
-
9
- public_path = File.expand_path('../../public', __dir__)
10
-
11
- require_relative '../../lib/otto'
12
- require_relative 'app'
13
-
14
- app = Otto.new('routes')
15
-
16
- # DEV: Run web apps with extra logging and reloading
17
- if Otto.env?(:dev)
18
-
19
- map('/') do
20
- use Rack::CommonLogger
21
- use Rack::Reloader, 0
22
- app.option[:public] = public_path
23
- app.add_static_path '/favicon.ico'
24
- run app
25
- end
26
-
27
- # PROD: run the webapp on the metal
28
- else
29
- map('/') { run app }
30
- end
@@ -1,21 +0,0 @@
1
- # examples/basic/routes
2
-
3
- # OTTO - ROUTES EXAMPLE
4
-
5
- # Each route has three parts:
6
- # * HTTP verb (GET, POST, PUT, DELETE or HEAD)
7
- # * URI path
8
- # * Ruby class and method to call
9
-
10
- GET / App#index
11
- POST / App#receive_feedback
12
- GET /redirect App#redirect
13
- GET /robots.txt App#robots_text
14
- GET /product/:prodid App#display_product
15
-
16
- GET /bogus App#no_such_method
17
-
18
- # You can also define these handlers when no
19
- # route can be found or there's a server error. (optional)
20
- GET /404 App#not_found
21
- GET /500 App#server_error
@@ -1,244 +0,0 @@
1
- require 'otto'
2
- require 'json'
3
-
4
- class HelpersDemo
5
- def initialize(req, res)
6
- @req, @res = req, res
7
- end
8
-
9
- attr_reader :req, :res
10
-
11
- def index
12
- res.headers['content-type'] = 'text/html'
13
- res.body = <<~HTML
14
- <h1>Otto Request & Response Helpers Demo</h1>
15
- <p>This demo shows Otto's built-in request and response helpers.</p>
16
-
17
- <h2>Available Demos:</h2>
18
- <ul>
19
- <li><a href="/request-info">Request Information</a> - Shows client IP, user agent, security info</li>
20
- <li><a href="/locale-demo?locale=es">Locale Detection</a> - Demonstrates locale detection and configuration</li>
21
- <li><a href="/secure-cookie">Secure Cookies</a> - Sets secure cookies with proper options</li>
22
- <li><a href="/headers">Response Headers</a> - Shows security headers and custom headers</li>
23
- <li>
24
- <form method="POST" action="/csp-demo">
25
- <button type="submit">CSP Headers Demo</button> - Content Security Policy with nonce
26
- </form>
27
- </li>
28
- </ul>
29
-
30
- <h2>Try These URLs:</h2>
31
- <ul>
32
- <li><a href="/locale-demo?locale=fr">French locale</a></li>
33
- <li><a href="/locale-demo?locale=invalid">Invalid locale (falls back to default)</a></li>
34
- </ul>
35
- HTML
36
- end
37
-
38
- def request_info
39
- # Demonstrate request helpers
40
- info = {
41
- 'Client IP' => req.client_ipaddress,
42
- 'User Agent' => req.user_agent,
43
- 'HTTP Host' => req.http_host,
44
- 'Server Name' => req.current_server_name,
45
- 'Request Path' => req.request_path,
46
- 'Request URI' => req.request_uri,
47
- 'Is Local?' => req.local?,
48
- 'Is Secure?' => req.secure?,
49
- 'Is AJAX?' => req.ajax?,
50
- 'Current Absolute URI' => req.current_absolute_uri,
51
- 'Request Method' => req.request_method
52
- }
53
-
54
- # Show collected proxy headers
55
- proxy_headers = req.collect_proxy_headers(
56
- header_prefix: 'X_DEMO_',
57
- additional_keys: ['HTTP_ACCEPT', 'HTTP_ACCEPT_LANGUAGE']
58
- )
59
-
60
- # Format request details for logging
61
- request_details = req.format_request_details(header_prefix: 'X_DEMO_')
62
-
63
- res.headers['content-type'] = 'text/html'
64
- res.body = <<~HTML
65
- <h1>Request Information</h1>
66
- <p><a href="/">← Back to index</a></p>
67
-
68
- <h2>Basic Request Info:</h2>
69
- <table border="1" style="border-collapse: collapse;">
70
- #{info.map { |k, v| "<tr><td><strong>#{k}</strong></td><td>#{v}</td></tr>" }.join("\n ")}
71
- </table>
72
-
73
- <h2>Proxy Headers:</h2>
74
- <pre>#{proxy_headers}</pre>
75
-
76
- <h2>Formatted Request Details (for logging):</h2>
77
- <pre>#{request_details}</pre>
78
-
79
- <h2>Application Path Helper:</h2>
80
- <p>App path for ['api', 'v1', 'users']: <code>#{req.app_path('api', 'v1', 'users')}</code></p>
81
- HTML
82
- end
83
-
84
- def locale_demo
85
- # Demonstrate locale detection with Otto configuration
86
- current_locale = req.check_locale!(req.params['locale'], {
87
- preferred_locale: 'es', # Simulate user preference
88
- locale_env_key: 'demo.locale',
89
- debug: true
90
- })
91
-
92
- # Show what was stored in environment
93
- stored_locale = req.env['demo.locale']
94
-
95
- res.headers['content-type'] = 'text/html'
96
- res.body = <<~HTML
97
- <h1>Locale Detection Demo</h1>
98
- <p><a href="/">← Back to index</a></p>
99
-
100
- <h2>Locale Detection Results:</h2>
101
- <table border="1" style="border-collapse: collapse;">
102
- <tr><td><strong>Detected Locale</strong></td><td>#{current_locale}</td></tr>
103
- <tr><td><strong>Stored in Environment</strong></td><td>#{stored_locale}</td></tr>
104
- <tr><td><strong>Query Parameter</strong></td><td>#{req.params['locale'] || 'none'}</td></tr>
105
- <tr><td><strong>Accept-Language Header</strong></td><td>#{req.env['HTTP_ACCEPT_LANGUAGE'] || 'none'}</td></tr>
106
- </table>
107
-
108
- <h2>Locale Sources (in precedence order):</h2>
109
- <ol>
110
- <li>URL Parameter: <code>?locale=#{req.params['locale'] || 'none'}</code></li>
111
- <li>User Preference: <code>es</code> (simulated)</li>
112
- <li>Rack Locale: <code>#{req.env['rack.locale']&.first || 'none'}</code></li>
113
- <li>Default: <code>en</code></li>
114
- </ol>
115
-
116
- <h2>Try Different Locales:</h2>
117
- <ul>
118
- <li><a href="/locale-demo?locale=en">English (en)</a></li>
119
- <li><a href="/locale-demo?locale=es">Spanish (es)</a></li>
120
- <li><a href="/locale-demo?locale=fr">French (fr)</a></li>
121
- <li><a href="/locale-demo?locale=invalid">Invalid locale</a></li>
122
- <li><a href="/locale-demo">No locale parameter</a></li>
123
- </ul>
124
- HTML
125
- end
126
-
127
- def secure_cookie
128
- # Demonstrate secure cookie helpers
129
- res.send_secure_cookie('demo_secure', 'secure_value_123', 3600, {
130
- path: '/helpers_demo',
131
- secure: !req.local?, # Only secure in production
132
- same_site: :strict
133
- })
134
-
135
- res.send_session_cookie('demo_session', 'session_value_456', {
136
- path: '/helpers_demo'
137
- })
138
-
139
- res.headers['content-type'] = 'text/html'
140
- res.body = <<~HTML
141
- <h1>Secure Cookies Demo</h1>
142
- <p><a href="/">← Back to index</a></p>
143
-
144
- <h2>Cookies Set:</h2>
145
- <ul>
146
- <li><strong>demo_secure</strong> - Secure cookie with 1 hour TTL</li>
147
- <li><strong>demo_session</strong> - Session cookie (no expiration)</li>
148
- </ul>
149
-
150
- <h2>Cookie Security Features:</h2>
151
- <ul>
152
- <li>Secure flag (HTTPS only in production)</li>
153
- <li>HttpOnly flag (prevents XSS access)</li>
154
- <li>SameSite=Strict (CSRF protection)</li>
155
- <li>Proper expiration handling</li>
156
- </ul>
157
-
158
- <p>Check your browser's developer tools to see the cookie headers!</p>
159
- HTML
160
- end
161
-
162
- def csp_demo
163
- # Demonstrate CSP headers with nonce
164
- nonce = SecureRandom.base64(16)
165
-
166
- res.send_csp_headers('text/html; charset=utf-8', nonce, {
167
- development_mode: req.local?,
168
- debug: true
169
- })
170
-
171
- res.body = <<~HTML
172
- <h1>Content Security Policy Demo</h1>
173
- <p><a href="/">← Back to index</a></p>
174
-
175
- <h2>CSP Header Generated</h2>
176
- <p>This page includes a CSP header with a nonce. Check the response headers!</p>
177
-
178
- <h2>Nonce Value:</h2>
179
- <p><code>#{nonce}</code></p>
180
-
181
- <h2>Inline Script with Nonce:</h2>
182
- <script nonce="#{nonce}">
183
- console.log('This script runs because it has the correct nonce!');
184
- document.addEventListener('DOMContentLoaded', function() {
185
- document.getElementById('nonce-demo').innerHTML = 'Nonce verification successful!';
186
- });
187
- </script>
188
-
189
- <div id="nonce-demo" style="padding: 10px; background: #d4edda; border: 1px solid #c3e6cb; color: #155724;">
190
- Loading...
191
- </div>
192
-
193
- <p><strong>Note:</strong> Without the nonce, inline scripts would be blocked by CSP.</p>
194
- HTML
195
- end
196
-
197
- def show_headers
198
- # Demonstrate response headers and security features
199
- res.set_cookie('demo_header', {
200
- value: 'header_demo_value',
201
- max_age: 1800,
202
- secure: !req.local?,
203
- httponly: true
204
- })
205
-
206
- # Add cache control
207
- res.no_cache!
208
-
209
- # Get security headers that would be added
210
- security_headers = res.cookie_security_headers
211
-
212
- res.headers['content-type'] = 'text/html'
213
- res.headers['X-Demo-Header'] = 'Custom header value'
214
-
215
- res.body = <<~HTML
216
- <h1>Response Headers Demo</h1>
217
- <p><a href="/">← Back to index</a></p>
218
-
219
- <h2>Custom Headers Set:</h2>
220
- <ul>
221
- <li><strong>X-Demo-Header:</strong> Custom header value</li>
222
- <li><strong>Cache-Control:</strong> no-store, no-cache, must-revalidate, max-age=0</li>
223
- <li><strong>Set-Cookie:</strong> demo_header (with security options)</li>
224
- </ul>
225
-
226
- <h2>Security Headers Available:</h2>
227
- <table border="1" style="border-collapse: collapse;">
228
- #{security_headers.map { |k, v| "<tr><td><strong>#{k}</strong></td><td>#{v}</td></tr>" }.join("\n ")}
229
- </table>
230
-
231
- <p>Use your browser's developer tools to inspect all response headers!</p>
232
- HTML
233
- end
234
-
235
- def not_found
236
- res.status = 404
237
- res.headers['content-type'] = 'text/html'
238
- res.body = <<~HTML
239
- <h1>404 - Page Not Found</h1>
240
- <p><a href="/">← Back to index</a></p>
241
- <p>This is a custom 404 page demonstrating error handling.</p>
242
- HTML
243
- end
244
- end
@@ -1,26 +0,0 @@
1
- require_relative '../../lib/otto'
2
- require_relative 'app'
3
-
4
- # Global configuration for all Otto instances
5
- Otto.configure do |opts|
6
- opts.available_locales = {
7
- 'en' => 'English',
8
- 'es' => 'Spanish',
9
- 'fr' => 'French'
10
- }
11
- opts.default_locale = 'en'
12
- end
13
-
14
- # Configure Otto with security features
15
- app = Otto.new("./routes", {
16
- # Security features
17
- csrf_protection: true,
18
- request_validation: true,
19
- trusted_proxies: ['127.0.0.1', '::1']
20
- })
21
-
22
- # Enable additional security headers
23
- app.enable_csp_with_nonce!(debug: true)
24
- app.enable_frame_protection!('SAMEORIGIN')
25
-
26
- run app
@@ -1,7 +0,0 @@
1
- GET / HelpersDemo#index
2
- GET /request-info HelpersDemo#request_info
3
- GET /locale-demo HelpersDemo#locale_demo
4
- GET /secure-cookie HelpersDemo#secure_cookie
5
- POST /csp-demo HelpersDemo#csp_demo
6
- GET /headers HelpersDemo#show_headers
7
- GET /404 HelpersDemo#not_found