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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -2
  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 +26 -344
  7. data/CHANGELOG.rst +131 -0
  8. data/CLAUDE.md +56 -0
  9. data/Gemfile +11 -4
  10. data/Gemfile.lock +38 -42
  11. data/README.md +2 -0
  12. data/bin/rspec +4 -4
  13. data/changelog.d/README.md +120 -0
  14. data/changelog.d/scriv.ini +5 -0
  15. data/docs/.gitignore +2 -0
  16. data/docs/migrating/v2.0.0-pre1.md +276 -0
  17. data/docs/migrating/v2.0.0-pre2.md +345 -0
  18. data/examples/.gitignore +1 -0
  19. data/examples/advanced_routes/README.md +33 -0
  20. data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
  21. data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
  22. data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
  23. data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
  24. data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
  25. data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
  26. data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
  27. data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
  28. data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
  29. data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
  30. data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
  31. data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
  32. data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
  33. data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
  34. data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
  35. data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
  36. data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
  37. data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
  38. data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
  39. data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
  40. data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
  41. data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
  42. data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
  43. data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
  44. data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
  45. data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
  46. data/examples/advanced_routes/app.rb +33 -0
  47. data/examples/advanced_routes/config.rb +23 -0
  48. data/examples/advanced_routes/config.ru +7 -0
  49. data/examples/advanced_routes/puma.rb +20 -0
  50. data/examples/advanced_routes/routes +167 -0
  51. data/examples/advanced_routes/run.rb +39 -0
  52. data/examples/advanced_routes/test.rb +58 -0
  53. data/examples/authentication_strategies/README.md +32 -0
  54. data/examples/authentication_strategies/app/auth.rb +68 -0
  55. data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
  56. data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
  57. data/examples/authentication_strategies/config.ru +24 -0
  58. data/examples/authentication_strategies/routes +37 -0
  59. data/examples/basic/README.md +29 -0
  60. data/examples/basic/app.rb +7 -35
  61. data/examples/basic/routes +0 -9
  62. data/examples/mcp_demo/README.md +87 -0
  63. data/examples/mcp_demo/app.rb +29 -34
  64. data/examples/mcp_demo/config.ru +9 -60
  65. data/examples/security_features/README.md +46 -0
  66. data/examples/security_features/app.rb +23 -24
  67. data/examples/security_features/config.ru +8 -10
  68. data/lib/otto/core/configuration.rb +167 -0
  69. data/lib/otto/core/error_handler.rb +86 -0
  70. data/lib/otto/core/file_safety.rb +61 -0
  71. data/lib/otto/core/middleware_stack.rb +237 -0
  72. data/lib/otto/core/router.rb +184 -0
  73. data/lib/otto/core/uri_generator.rb +44 -0
  74. data/lib/otto/design_system.rb +7 -5
  75. data/lib/otto/env_keys.rb +114 -0
  76. data/lib/otto/helpers/base.rb +5 -21
  77. data/lib/otto/helpers/request.rb +10 -8
  78. data/lib/otto/helpers/response.rb +27 -4
  79. data/lib/otto/helpers/validation.rb +9 -7
  80. data/lib/otto/mcp/auth/token.rb +10 -9
  81. data/lib/otto/mcp/protocol.rb +24 -27
  82. data/lib/otto/mcp/rate_limiting.rb +8 -3
  83. data/lib/otto/mcp/registry.rb +7 -2
  84. data/lib/otto/mcp/route_parser.rb +10 -15
  85. data/lib/otto/mcp/{validation.rb → schema_validation.rb} +16 -11
  86. data/lib/otto/mcp/server.rb +45 -22
  87. data/lib/otto/response_handlers/auto.rb +39 -0
  88. data/lib/otto/response_handlers/base.rb +16 -0
  89. data/lib/otto/response_handlers/default.rb +16 -0
  90. data/lib/otto/response_handlers/factory.rb +39 -0
  91. data/lib/otto/response_handlers/json.rb +34 -0
  92. data/lib/otto/response_handlers/redirect.rb +25 -0
  93. data/lib/otto/response_handlers/view.rb +24 -0
  94. data/lib/otto/response_handlers.rb +9 -135
  95. data/lib/otto/route.rb +51 -55
  96. data/lib/otto/route_definition.rb +15 -18
  97. data/lib/otto/route_handlers/base.rb +121 -0
  98. data/lib/otto/route_handlers/class_method.rb +89 -0
  99. data/lib/otto/route_handlers/factory.rb +42 -0
  100. data/lib/otto/route_handlers/instance_method.rb +69 -0
  101. data/lib/otto/route_handlers/lambda.rb +59 -0
  102. data/lib/otto/route_handlers/logic_class.rb +93 -0
  103. data/lib/otto/route_handlers.rb +10 -405
  104. data/lib/otto/security/authentication/auth_strategy.rb +44 -0
  105. data/lib/otto/security/authentication/authentication_middleware.rb +140 -0
  106. data/lib/otto/security/authentication/failure_result.rb +44 -0
  107. data/lib/otto/security/authentication/route_auth_wrapper.rb +149 -0
  108. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
  109. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +19 -0
  110. data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -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 +337 -0
  114. data/lib/otto/security/authentication.rb +28 -282
  115. data/lib/otto/security/config.rb +14 -23
  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 +54 -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 +141 -498
  128. data/otto.gemspec +4 -2
  129. metadata +99 -18
  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,12 @@ 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
+ # Logger is not part of the default gems as of Ruby 3.5.0
20
+ spec.add_dependency 'logger', '~> 1', '< 2.0'
21
+
20
22
  spec.add_dependency 'rack', '~> 3.1', '< 4.0'
21
23
  spec.add_dependency 'rack-parser', '~> 0.7'
22
- spec.add_dependency 'rexml', '~> 3.3', '>= 3.3.6'
24
+ spec.add_dependency 'rexml', '>= 3.3.6'
23
25
 
24
26
  # Security dependencies
25
27
  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.pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -10,19 +10,25 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: ostruct
13
+ name: logger
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.6.3
18
+ version: '1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
26
  - - "~>"
24
27
  - !ruby/object:Gem::Version
25
- version: 0.6.3
28
+ version: '1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: rack
28
34
  requirement: !ruby/object:Gem::Requirement
@@ -61,9 +67,6 @@ dependencies:
61
67
  name: rexml
62
68
  requirement: !ruby/object:Gem::Requirement
63
69
  requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '3.3'
67
70
  - - ">="
68
71
  - !ruby/object:Gem::Version
69
72
  version: 3.3.6
@@ -71,9 +74,6 @@ dependencies:
71
74
  prerelease: false
72
75
  version_requirements: !ruby/object:Gem::Requirement
73
76
  requirements:
74
- - - "~>"
75
- - !ruby/object:Gem::Version
76
- version: '3.3'
77
77
  - - ">="
78
78
  - !ruby/object:Gem::Version
79
79
  version: 3.3.6
@@ -113,35 +113,87 @@ extra_rdoc_files: []
113
113
  files:
114
114
  - ".github/dependabot.yml"
115
115
  - ".github/workflows/ci.yml"
116
+ - ".github/workflows/claude-code-review.yml"
117
+ - ".github/workflows/claude.yml"
116
118
  - ".gitignore"
117
119
  - ".pre-commit-config.yaml"
118
120
  - ".pre-push-config.yaml"
119
121
  - ".rspec"
120
122
  - ".rubocop.yml"
123
+ - CHANGELOG.rst
124
+ - CLAUDE.md
121
125
  - Gemfile
122
126
  - Gemfile.lock
123
127
  - LICENSE.txt
124
128
  - README.md
125
129
  - bin/rspec
130
+ - changelog.d/README.md
131
+ - changelog.d/scriv.ini
126
132
  - docs/.gitignore
133
+ - docs/migrating/v2.0.0-pre1.md
134
+ - docs/migrating/v2.0.0-pre2.md
135
+ - examples/.gitignore
136
+ - examples/advanced_routes/README.md
137
+ - examples/advanced_routes/app.rb
138
+ - examples/advanced_routes/app/controllers/handlers/async.rb
139
+ - examples/advanced_routes/app/controllers/handlers/dynamic.rb
140
+ - examples/advanced_routes/app/controllers/handlers/static.rb
141
+ - examples/advanced_routes/app/controllers/modules/auth.rb
142
+ - examples/advanced_routes/app/controllers/modules/transformer.rb
143
+ - examples/advanced_routes/app/controllers/modules/validator.rb
144
+ - examples/advanced_routes/app/controllers/routes_app.rb
145
+ - examples/advanced_routes/app/controllers/v2/admin.rb
146
+ - examples/advanced_routes/app/controllers/v2/config.rb
147
+ - examples/advanced_routes/app/controllers/v2/settings.rb
148
+ - examples/advanced_routes/app/logic/admin/logic/manager.rb
149
+ - examples/advanced_routes/app/logic/admin/panel.rb
150
+ - examples/advanced_routes/app/logic/analytics_processor.rb
151
+ - examples/advanced_routes/app/logic/complex/business/handler.rb
152
+ - examples/advanced_routes/app/logic/data_logic.rb
153
+ - examples/advanced_routes/app/logic/data_processor.rb
154
+ - examples/advanced_routes/app/logic/input_validator.rb
155
+ - examples/advanced_routes/app/logic/nested/feature/logic.rb
156
+ - examples/advanced_routes/app/logic/reports_generator.rb
157
+ - examples/advanced_routes/app/logic/simple_logic.rb
158
+ - examples/advanced_routes/app/logic/system/config/manager.rb
159
+ - examples/advanced_routes/app/logic/test_logic.rb
160
+ - examples/advanced_routes/app/logic/transform_logic.rb
161
+ - examples/advanced_routes/app/logic/upload_logic.rb
162
+ - examples/advanced_routes/app/logic/v2/logic/dashboard.rb
163
+ - examples/advanced_routes/app/logic/v2/logic/processor.rb
164
+ - examples/advanced_routes/config.rb
165
+ - examples/advanced_routes/config.ru
166
+ - examples/advanced_routes/puma.rb
167
+ - examples/advanced_routes/routes
168
+ - examples/advanced_routes/run.rb
169
+ - examples/advanced_routes/test.rb
170
+ - examples/authentication_strategies/README.md
171
+ - examples/authentication_strategies/app/auth.rb
172
+ - examples/authentication_strategies/app/controllers/auth_controller.rb
173
+ - examples/authentication_strategies/app/controllers/main_controller.rb
174
+ - examples/authentication_strategies/config.ru
175
+ - examples/authentication_strategies/routes
176
+ - examples/basic/README.md
127
177
  - examples/basic/app.rb
128
178
  - examples/basic/config.ru
129
179
  - 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
180
+ - examples/mcp_demo/README.md
136
181
  - examples/mcp_demo/app.rb
137
182
  - examples/mcp_demo/config.ru
138
183
  - examples/mcp_demo/routes
184
+ - examples/security_features/README.md
139
185
  - examples/security_features/app.rb
140
186
  - examples/security_features/config.ru
141
187
  - examples/security_features/routes
142
- - lib/concurrent_cache_store.rb
143
188
  - lib/otto.rb
189
+ - lib/otto/core/configuration.rb
190
+ - lib/otto/core/error_handler.rb
191
+ - lib/otto/core/file_safety.rb
192
+ - lib/otto/core/middleware_stack.rb
193
+ - lib/otto/core/router.rb
194
+ - lib/otto/core/uri_generator.rb
144
195
  - lib/otto/design_system.rb
196
+ - lib/otto/env_keys.rb
145
197
  - lib/otto/helpers/base.rb
146
198
  - lib/otto/helpers/request.rb
147
199
  - lib/otto/helpers/response.rb
@@ -151,18 +203,47 @@ files:
151
203
  - lib/otto/mcp/rate_limiting.rb
152
204
  - lib/otto/mcp/registry.rb
153
205
  - lib/otto/mcp/route_parser.rb
206
+ - lib/otto/mcp/schema_validation.rb
154
207
  - lib/otto/mcp/server.rb
155
- - lib/otto/mcp/validation.rb
156
208
  - lib/otto/response_handlers.rb
209
+ - lib/otto/response_handlers/auto.rb
210
+ - lib/otto/response_handlers/base.rb
211
+ - lib/otto/response_handlers/default.rb
212
+ - lib/otto/response_handlers/factory.rb
213
+ - lib/otto/response_handlers/json.rb
214
+ - lib/otto/response_handlers/redirect.rb
215
+ - lib/otto/response_handlers/view.rb
157
216
  - lib/otto/route.rb
158
217
  - lib/otto/route_definition.rb
159
218
  - lib/otto/route_handlers.rb
219
+ - lib/otto/route_handlers/base.rb
220
+ - lib/otto/route_handlers/class_method.rb
221
+ - lib/otto/route_handlers/factory.rb
222
+ - lib/otto/route_handlers/instance_method.rb
223
+ - lib/otto/route_handlers/lambda.rb
224
+ - lib/otto/route_handlers/logic_class.rb
160
225
  - lib/otto/security/authentication.rb
226
+ - lib/otto/security/authentication/auth_strategy.rb
227
+ - lib/otto/security/authentication/authentication_middleware.rb
228
+ - lib/otto/security/authentication/failure_result.rb
229
+ - lib/otto/security/authentication/route_auth_wrapper.rb
230
+ - lib/otto/security/authentication/strategies/api_key_strategy.rb
231
+ - lib/otto/security/authentication/strategies/noauth_strategy.rb
232
+ - lib/otto/security/authentication/strategies/permission_strategy.rb
233
+ - lib/otto/security/authentication/strategies/role_strategy.rb
234
+ - lib/otto/security/authentication/strategies/session_strategy.rb
235
+ - lib/otto/security/authentication/strategy_result.rb
161
236
  - lib/otto/security/config.rb
237
+ - lib/otto/security/configurator.rb
162
238
  - lib/otto/security/csrf.rb
239
+ - lib/otto/security/middleware/csrf_middleware.rb
240
+ - lib/otto/security/middleware/rate_limit_middleware.rb
241
+ - lib/otto/security/middleware/validation_middleware.rb
242
+ - lib/otto/security/rate_limiter.rb
163
243
  - lib/otto/security/rate_limiting.rb
164
244
  - lib/otto/security/validator.rb
165
245
  - lib/otto/static.rb
246
+ - lib/otto/utils.rb
166
247
  - lib/otto/version.rb
167
248
  - otto.gemspec
168
249
  - 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