otto 1.6.0 → 2.0.0.pre2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +3 -2
- data/.github/workflows/claude-code-review.yml +53 -0
- data/.github/workflows/claude.yml +49 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +26 -344
- data/CHANGELOG.rst +131 -0
- data/CLAUDE.md +56 -0
- data/Gemfile +11 -4
- data/Gemfile.lock +38 -42
- data/README.md +2 -0
- data/bin/rspec +4 -4
- data/changelog.d/README.md +120 -0
- data/changelog.d/scriv.ini +5 -0
- data/docs/.gitignore +2 -0
- data/docs/migrating/v2.0.0-pre1.md +276 -0
- data/docs/migrating/v2.0.0-pre2.md +345 -0
- data/examples/.gitignore +1 -0
- data/examples/advanced_routes/README.md +33 -0
- data/examples/advanced_routes/app/controllers/handlers/async.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/dynamic.rb +9 -0
- data/examples/advanced_routes/app/controllers/handlers/static.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/auth.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/transformer.rb +9 -0
- data/examples/advanced_routes/app/controllers/modules/validator.rb +9 -0
- data/examples/advanced_routes/app/controllers/routes_app.rb +232 -0
- data/examples/advanced_routes/app/controllers/v2/admin.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/config.rb +9 -0
- data/examples/advanced_routes/app/controllers/v2/settings.rb +9 -0
- data/examples/advanced_routes/app/logic/admin/logic/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/admin/panel.rb +27 -0
- data/examples/advanced_routes/app/logic/analytics_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/complex/business/handler.rb +27 -0
- data/examples/advanced_routes/app/logic/data_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/data_processor.rb +25 -0
- data/examples/advanced_routes/app/logic/input_validator.rb +24 -0
- data/examples/advanced_routes/app/logic/nested/feature/logic.rb +27 -0
- data/examples/advanced_routes/app/logic/reports_generator.rb +27 -0
- data/examples/advanced_routes/app/logic/simple_logic.rb +25 -0
- data/examples/advanced_routes/app/logic/system/config/manager.rb +27 -0
- data/examples/advanced_routes/app/logic/test_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/transform_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/upload_logic.rb +23 -0
- data/examples/advanced_routes/app/logic/v2/logic/dashboard.rb +27 -0
- data/examples/advanced_routes/app/logic/v2/logic/processor.rb +27 -0
- data/examples/advanced_routes/app.rb +33 -0
- data/examples/advanced_routes/config.rb +23 -0
- data/examples/advanced_routes/config.ru +7 -0
- data/examples/advanced_routes/puma.rb +20 -0
- data/examples/advanced_routes/routes +167 -0
- data/examples/advanced_routes/run.rb +39 -0
- data/examples/advanced_routes/test.rb +58 -0
- data/examples/authentication_strategies/README.md +32 -0
- data/examples/authentication_strategies/app/auth.rb +68 -0
- data/examples/authentication_strategies/app/controllers/auth_controller.rb +29 -0
- data/examples/authentication_strategies/app/controllers/main_controller.rb +28 -0
- data/examples/authentication_strategies/config.ru +24 -0
- data/examples/authentication_strategies/routes +37 -0
- data/examples/basic/README.md +29 -0
- data/examples/basic/app.rb +7 -35
- data/examples/basic/routes +0 -9
- data/examples/mcp_demo/README.md +87 -0
- data/examples/mcp_demo/app.rb +29 -34
- data/examples/mcp_demo/config.ru +9 -60
- data/examples/security_features/README.md +46 -0
- data/examples/security_features/app.rb +23 -24
- data/examples/security_features/config.ru +8 -10
- data/lib/otto/core/configuration.rb +167 -0
- data/lib/otto/core/error_handler.rb +86 -0
- data/lib/otto/core/file_safety.rb +61 -0
- data/lib/otto/core/middleware_stack.rb +237 -0
- data/lib/otto/core/router.rb +184 -0
- data/lib/otto/core/uri_generator.rb +44 -0
- data/lib/otto/design_system.rb +7 -5
- data/lib/otto/env_keys.rb +114 -0
- data/lib/otto/helpers/base.rb +5 -21
- data/lib/otto/helpers/request.rb +10 -8
- data/lib/otto/helpers/response.rb +27 -4
- data/lib/otto/helpers/validation.rb +9 -7
- data/lib/otto/mcp/auth/token.rb +10 -9
- data/lib/otto/mcp/protocol.rb +24 -27
- data/lib/otto/mcp/rate_limiting.rb +8 -3
- data/lib/otto/mcp/registry.rb +7 -2
- data/lib/otto/mcp/route_parser.rb +10 -15
- data/lib/otto/mcp/{validation.rb → schema_validation.rb} +16 -11
- data/lib/otto/mcp/server.rb +45 -22
- data/lib/otto/response_handlers/auto.rb +39 -0
- data/lib/otto/response_handlers/base.rb +16 -0
- data/lib/otto/response_handlers/default.rb +16 -0
- data/lib/otto/response_handlers/factory.rb +39 -0
- data/lib/otto/response_handlers/json.rb +34 -0
- data/lib/otto/response_handlers/redirect.rb +25 -0
- data/lib/otto/response_handlers/view.rb +24 -0
- data/lib/otto/response_handlers.rb +9 -135
- data/lib/otto/route.rb +51 -55
- data/lib/otto/route_definition.rb +15 -18
- data/lib/otto/route_handlers/base.rb +121 -0
- data/lib/otto/route_handlers/class_method.rb +89 -0
- data/lib/otto/route_handlers/factory.rb +42 -0
- data/lib/otto/route_handlers/instance_method.rb +69 -0
- data/lib/otto/route_handlers/lambda.rb +59 -0
- data/lib/otto/route_handlers/logic_class.rb +93 -0
- data/lib/otto/route_handlers.rb +10 -405
- data/lib/otto/security/authentication/auth_strategy.rb +44 -0
- data/lib/otto/security/authentication/authentication_middleware.rb +140 -0
- data/lib/otto/security/authentication/failure_result.rb +44 -0
- data/lib/otto/security/authentication/route_auth_wrapper.rb +149 -0
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +40 -0
- data/lib/otto/security/authentication/strategies/noauth_strategy.rb +19 -0
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +47 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +57 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +41 -0
- data/lib/otto/security/authentication/strategy_result.rb +337 -0
- data/lib/otto/security/authentication.rb +28 -282
- data/lib/otto/security/config.rb +14 -23
- data/lib/otto/security/configurator.rb +219 -0
- data/lib/otto/security/csrf.rb +8 -143
- data/lib/otto/security/middleware/csrf_middleware.rb +151 -0
- data/lib/otto/security/middleware/rate_limit_middleware.rb +54 -0
- data/lib/otto/security/middleware/validation_middleware.rb +252 -0
- data/lib/otto/security/rate_limiter.rb +86 -0
- data/lib/otto/security/rate_limiting.rb +10 -105
- data/lib/otto/security/validator.rb +8 -253
- data/lib/otto/static.rb +3 -0
- data/lib/otto/utils.rb +14 -0
- data/lib/otto/version.rb +3 -1
- data/lib/otto.rb +141 -498
- data/otto.gemspec +4 -2
- metadata +99 -18
- data/examples/dynamic_pages/app.rb +0 -115
- data/examples/dynamic_pages/config.ru +0 -30
- data/examples/dynamic_pages/routes +0 -21
- data/examples/helpers_demo/app.rb +0 -244
- data/examples/helpers_demo/config.ru +0 -26
- data/examples/helpers_demo/routes +0 -7
- data/lib/concurrent_cache_store.rb +0 -68
data/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
|
-
|
|
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', '
|
|
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:
|
|
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:
|
|
13
|
+
name: logger
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
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:
|
|
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/
|
|
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
|