otto 1.2.0 → 1.4.0
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/dependabot.yml +15 -0
- data/.github/workflows/ci.yml +34 -0
- data/.gitignore +1 -0
- data/.pre-commit-config.yaml +107 -0
- data/.pre-push-config.yaml +88 -0
- data/.rubocop.yml +365 -21
- data/Gemfile +1 -3
- data/Gemfile.lock +78 -46
- data/README.md +58 -2
- data/examples/basic/app.rb +20 -20
- data/examples/basic/config.ru +1 -1
- data/examples/dynamic_pages/app.rb +30 -30
- data/examples/dynamic_pages/config.ru +1 -1
- data/examples/helpers_demo/app.rb +244 -0
- data/examples/helpers_demo/config.ru +26 -0
- data/examples/helpers_demo/routes +7 -0
- data/examples/security_features/app.rb +90 -87
- data/examples/security_features/config.ru +19 -17
- data/lib/otto/design_system.rb +22 -22
- data/lib/otto/helpers/base.rb +27 -0
- data/lib/otto/helpers/request.rb +226 -9
- data/lib/otto/helpers/response.rb +85 -38
- data/lib/otto/route.rb +17 -12
- data/lib/otto/security/config.rb +132 -38
- data/lib/otto/security/csrf.rb +23 -27
- data/lib/otto/security/validator.rb +33 -30
- data/lib/otto/static.rb +1 -1
- data/lib/otto/version.rb +1 -25
- data/lib/otto.rb +171 -61
- data/otto.gemspec +11 -12
- metadata +15 -15
- data/.rubocop_todo.yml +0 -152
- data/VERSION.yml +0 -5
@@ -0,0 +1,244 @@
|
|
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
|
@@ -0,0 +1,26 @@
|
|
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
|
@@ -0,0 +1,7 @@
|
|
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
|
@@ -9,8 +9,8 @@ class SecureApp
|
|
9
9
|
attr_reader :req, :res
|
10
10
|
|
11
11
|
def initialize(req, res)
|
12
|
-
@req
|
13
|
-
@res
|
12
|
+
@req = req
|
13
|
+
@res = res
|
14
14
|
res.headers['content-type'] = 'text/html; charset=utf-8'
|
15
15
|
end
|
16
16
|
|
@@ -28,49 +28,47 @@ class SecureApp
|
|
28
28
|
<p class="otto-mb-md">Security demonstration for the Otto framework</p>
|
29
29
|
</div>
|
30
30
|
|
31
|
-
#{otto_card(
|
32
|
-
|
31
|
+
#{otto_card('CSRF Protected Feedback') do
|
33
32
|
<<~FORM
|
34
33
|
<form method="post" action="/feedback" class="otto-form">
|
35
34
|
#{csrf_tag}
|
36
35
|
<label>Message:</label>
|
37
|
-
#{otto_textarea(
|
38
|
-
#{otto_button(
|
36
|
+
#{otto_textarea('message', placeholder: 'Enter your feedback...', required: true)}
|
37
|
+
#{otto_button('Submit Feedback', variant: 'primary')}
|
39
38
|
</form>
|
40
39
|
FORM
|
41
|
-
|
42
40
|
end}
|
43
41
|
|
44
|
-
#{otto_card(
|
42
|
+
#{otto_card('File Upload Validation') do
|
45
43
|
<<~UPLOAD
|
46
44
|
<form method="post" action="/upload" enctype="multipart/form-data" class="otto-form">
|
47
45
|
#{csrf_tag}
|
48
46
|
<label>Choose file:</label>
|
49
47
|
<input type="file" name="upload_file" class="otto-input">
|
50
|
-
#{otto_button(
|
48
|
+
#{otto_button('Upload File', variant: 'primary')}
|
51
49
|
</form>
|
52
50
|
UPLOAD
|
53
51
|
end}
|
54
52
|
|
55
|
-
#{otto_card(
|
53
|
+
#{otto_card('User Profile Input Validation') do
|
56
54
|
<<~PROFILE
|
57
55
|
<form method="post" action="/profile" class="otto-form">
|
58
56
|
#{csrf_tag}
|
59
57
|
<label>Name:</label>
|
60
|
-
#{otto_input(
|
58
|
+
#{otto_input('name', placeholder: 'Your name', required: true)}
|
61
59
|
|
62
60
|
<label>Email:</label>
|
63
|
-
#{otto_input(
|
61
|
+
#{otto_input('email', type: 'email', placeholder: 'your@email.com', required: true)}
|
64
62
|
|
65
63
|
<label>Bio:</label>
|
66
|
-
#{otto_textarea(
|
64
|
+
#{otto_textarea('bio', placeholder: 'Tell us about yourself...')}
|
67
65
|
|
68
|
-
#{otto_button(
|
66
|
+
#{otto_button('Update Profile', variant: 'primary')}
|
69
67
|
</form>
|
70
68
|
PROFILE
|
71
69
|
end}
|
72
70
|
|
73
|
-
#{otto_card(
|
71
|
+
#{otto_card('Security Information') do
|
74
72
|
<<~INFO
|
75
73
|
<h3>Security Features Active:</h3>
|
76
74
|
<ul>
|
@@ -87,13 +85,13 @@ class SecureApp
|
|
87
85
|
</p>
|
88
86
|
|
89
87
|
<p class="otto-mt-md">
|
90
|
-
#{otto_link(
|
88
|
+
#{otto_link('View Request Headers', '/headers')}
|
91
89
|
</p>
|
92
90
|
INFO
|
93
91
|
end}
|
94
92
|
HTML
|
95
93
|
|
96
|
-
res.body = otto_page(content,
|
94
|
+
res.body = otto_page(content, 'Otto Security Features')
|
97
95
|
end
|
98
96
|
|
99
97
|
def receive_feedback
|
@@ -104,29 +102,28 @@ class SecureApp
|
|
104
102
|
safe_message = validate_input(message, max_length: 1000, allow_html: false)
|
105
103
|
else
|
106
104
|
safe_message = message.to_s.strip
|
107
|
-
raise
|
105
|
+
raise 'Message too long' if safe_message.length > 1000
|
108
106
|
end
|
109
107
|
|
110
|
-
if safe_message.empty?
|
111
|
-
|
108
|
+
content = if safe_message.empty?
|
109
|
+
otto_alert('error', 'Validation Error', 'Message cannot be empty.')
|
112
110
|
else
|
113
|
-
|
114
|
-
#{otto_alert(
|
111
|
+
<<~HTML
|
112
|
+
#{otto_alert('success', 'Feedback Received', 'Thank you for your feedback!')}
|
115
113
|
|
116
|
-
#{otto_card(
|
114
|
+
#{otto_card('Your Message') do
|
117
115
|
otto_code_block(safe_message, 'text')
|
118
116
|
end}
|
119
117
|
HTML
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
content = otto_alert("error", "Processing Error", "An error occurred processing your request.")
|
118
|
+
end
|
119
|
+
rescue Otto::Security::ValidationError => ex
|
120
|
+
content = otto_alert('error', 'Security Validation Failed', ex.message)
|
121
|
+
rescue StandardError
|
122
|
+
content = otto_alert('error', 'Processing Error', 'An error occurred processing your request.')
|
126
123
|
end
|
127
124
|
|
128
|
-
content += "<p class=\"otto-mt-lg\">#{otto_link(
|
129
|
-
res.body = otto_page(content,
|
125
|
+
content += "<p class=\"otto-mt-lg\">#{otto_link('← Back to form', '/')}</p>"
|
126
|
+
res.body = otto_page(content, 'Feedback Response')
|
130
127
|
end
|
131
128
|
|
132
129
|
def upload_file
|
@@ -134,31 +131,39 @@ class SecureApp
|
|
134
131
|
uploaded_file = req.params['upload_file']
|
135
132
|
|
136
133
|
if uploaded_file.nil? || uploaded_file.empty?
|
137
|
-
content = otto_alert(
|
134
|
+
content = otto_alert('error', 'Upload Error', 'No file was selected.')
|
138
135
|
else
|
139
|
-
|
136
|
+
begin
|
137
|
+
filename = begin
|
138
|
+
uploaded_file[:filename]
|
139
|
+
rescue StandardError
|
140
|
+
uploaded_file.original_filename
|
141
|
+
end
|
142
|
+
rescue StandardError
|
143
|
+
'unknown'
|
144
|
+
end
|
140
145
|
|
141
|
-
if respond_to?(:sanitize_filename)
|
142
|
-
|
146
|
+
safe_filename = if respond_to?(:sanitize_filename)
|
147
|
+
sanitize_filename(filename)
|
143
148
|
else
|
144
|
-
|
145
|
-
|
149
|
+
File.basename(filename.to_s).gsub(/[^\w\-_\.]/, '_')
|
150
|
+
end
|
146
151
|
|
147
152
|
file_info = {
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
153
|
+
'Original filename' => filename,
|
154
|
+
'Sanitized filename' => safe_filename,
|
155
|
+
'Content type' => uploaded_file[:type] || 'unknown',
|
156
|
+
'Security status' => 'File validated and processed safely',
|
152
157
|
}
|
153
158
|
|
154
|
-
info_html = file_info.map
|
159
|
+
info_html = file_info.map do |key, value|
|
155
160
|
"<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
|
156
|
-
|
161
|
+
end.join
|
157
162
|
|
158
163
|
content = <<~HTML
|
159
|
-
#{otto_alert(
|
164
|
+
#{otto_alert('success', 'File Upload Successful', 'File processed and validated successfully!')}
|
160
165
|
|
161
|
-
#{otto_card(
|
166
|
+
#{otto_card('File Information') do
|
162
167
|
info_html
|
163
168
|
end}
|
164
169
|
|
@@ -168,64 +173,62 @@ class SecureApp
|
|
168
173
|
</div>
|
169
174
|
HTML
|
170
175
|
end
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
content = otto_alert("error", "Upload Error", "An error occurred during file upload.")
|
176
|
+
rescue Otto::Security::ValidationError => ex
|
177
|
+
content = otto_alert('error', 'File Validation Failed', ex.message)
|
178
|
+
rescue StandardError
|
179
|
+
content = otto_alert('error', 'Upload Error', 'An error occurred during file upload.')
|
176
180
|
end
|
177
181
|
|
178
|
-
content += "<p class=\"otto-mt-lg\">#{otto_link(
|
179
|
-
res.body = otto_page(content,
|
182
|
+
content += "<p class=\"otto-mt-lg\">#{otto_link('← Back to form', '/')}</p>"
|
183
|
+
res.body = otto_page(content, 'Upload Response')
|
180
184
|
end
|
181
185
|
|
182
186
|
def update_profile
|
183
187
|
begin
|
184
|
-
name
|
188
|
+
name = req.params['name']
|
185
189
|
email = req.params['email']
|
186
|
-
bio
|
190
|
+
bio = req.params['bio']
|
187
191
|
|
188
192
|
if respond_to?(:validate_input)
|
189
|
-
safe_name
|
193
|
+
safe_name = validate_input(name, max_length: 100)
|
190
194
|
safe_email = validate_input(email, max_length: 255)
|
191
|
-
safe_bio
|
195
|
+
safe_bio = validate_input(bio, max_length: 500, allow_html: false)
|
192
196
|
else
|
193
|
-
safe_name
|
197
|
+
safe_name = name.to_s.strip[0..99]
|
194
198
|
safe_email = email.to_s.strip[0..254]
|
195
|
-
safe_bio
|
199
|
+
safe_bio = bio.to_s.strip[0..499]
|
196
200
|
end
|
197
201
|
|
198
202
|
unless safe_email.match?(/\A[^@\s]+@[^@\s]+\z/)
|
199
|
-
raise Otto::Security::ValidationError,
|
203
|
+
raise Otto::Security::ValidationError, 'Invalid email format'
|
200
204
|
end
|
201
205
|
|
202
206
|
profile_data = {
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
+
'Name' => safe_name,
|
208
|
+
'Email' => safe_email,
|
209
|
+
'Bio' => safe_bio,
|
210
|
+
'Updated' => Time.now.strftime('%Y-%m-%d %H:%M:%S UTC'),
|
207
211
|
}
|
208
212
|
|
209
|
-
profile_html = profile_data.map
|
213
|
+
profile_html = profile_data.map do |key, value|
|
210
214
|
"<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
|
211
|
-
|
215
|
+
end.join
|
212
216
|
|
213
217
|
content = <<~HTML
|
214
|
-
#{otto_alert(
|
218
|
+
#{otto_alert('success', 'Profile Updated', 'Your profile has been updated successfully!')}
|
215
219
|
|
216
|
-
#{otto_card(
|
220
|
+
#{otto_card('Profile Data') do
|
217
221
|
profile_html
|
218
222
|
end}
|
219
223
|
HTML
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
content = otto_alert("error", "Update Error", "An error occurred updating your profile.")
|
224
|
+
rescue Otto::Security::ValidationError => ex
|
225
|
+
content = otto_alert('error', 'Profile Validation Failed', ex.message)
|
226
|
+
rescue StandardError
|
227
|
+
content = otto_alert('error', 'Update Error', 'An error occurred updating your profile.')
|
225
228
|
end
|
226
229
|
|
227
|
-
content += "<p class=\"otto-mt-lg\">#{otto_link(
|
228
|
-
res.body = otto_page(content,
|
230
|
+
content += "<p class=\"otto-mt-lg\">#{otto_link('← Back to form', '/')}</p>"
|
231
|
+
res.body = otto_page(content, 'Profile Update')
|
229
232
|
end
|
230
233
|
|
231
234
|
def show_headers
|
@@ -239,16 +242,16 @@ class SecureApp
|
|
239
242
|
end
|
240
243
|
|
241
244
|
response_data = {
|
242
|
-
message:
|
245
|
+
message: 'Request headers analysis (filtered for security)',
|
243
246
|
client_ip: req.respond_to?(:client_ipaddress) ? req.client_ipaddress : req.ip,
|
244
247
|
secure_connection: req.respond_to?(:secure?) ? req.secure? : false,
|
245
248
|
timestamp: Time.now.utc.iso8601,
|
246
249
|
headers: safe_headers,
|
247
250
|
security_analysis: {
|
248
|
-
csrf_protection: respond_to?(:csrf_token_valid?) ?
|
249
|
-
content_security:
|
250
|
-
xss_protection:
|
251
|
-
}
|
251
|
+
csrf_protection: respond_to?(:csrf_token_valid?) ? 'Active' : 'Basic',
|
252
|
+
content_security: 'Headers validated and filtered',
|
253
|
+
xss_protection: 'HTML escaping enabled',
|
254
|
+
},
|
252
255
|
}
|
253
256
|
|
254
257
|
require 'json'
|
@@ -257,17 +260,17 @@ class SecureApp
|
|
257
260
|
|
258
261
|
def not_found
|
259
262
|
res.status = 404
|
260
|
-
content
|
261
|
-
content
|
262
|
-
res.body
|
263
|
+
content = otto_alert('error', 'Page Not Found', 'The requested page could not be found.')
|
264
|
+
content += "<p>#{otto_link('← Back to home', '/')}</p>"
|
265
|
+
res.body = otto_page(content, '404 - Not Found')
|
263
266
|
end
|
264
267
|
|
265
268
|
def server_error
|
266
269
|
res.status = 500
|
267
|
-
error_id
|
268
|
-
content
|
269
|
-
content
|
270
|
-
content
|
271
|
-
res.body
|
270
|
+
error_id = req.env['otto.error_id'] || SecureRandom.hex(8)
|
271
|
+
content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
|
272
|
+
content += "<p><small>Error ID: #{escape_html(error_id)}</small></p>"
|
273
|
+
content += "<p>#{otto_link('← Back to home', '/')}</p>"
|
274
|
+
res.body = otto_page(content, '500 - Server Error')
|
272
275
|
end
|
273
276
|
end
|