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.
@@ -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 = req
13
- @res = 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("CSRF Protected Feedback") do
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("message", placeholder: "Enter your feedback...", required: true)}
38
- #{otto_button("Submit Feedback", variant: "primary")}
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("File Upload Validation") do
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("Upload File", variant: "primary")}
48
+ #{otto_button('Upload File', variant: 'primary')}
51
49
  </form>
52
50
  UPLOAD
53
51
  end}
54
52
 
55
- #{otto_card("User Profile Input Validation") do
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("name", placeholder: "Your name", required: true)}
58
+ #{otto_input('name', placeholder: 'Your name', required: true)}
61
59
 
62
60
  <label>Email:</label>
63
- #{otto_input("email", type: "email", placeholder: "your@email.com", required: true)}
61
+ #{otto_input('email', type: 'email', placeholder: 'your@email.com', required: true)}
64
62
 
65
63
  <label>Bio:</label>
66
- #{otto_textarea("bio", placeholder: "Tell us about yourself...")}
64
+ #{otto_textarea('bio', placeholder: 'Tell us about yourself...')}
67
65
 
68
- #{otto_button("Update Profile", variant: "primary")}
66
+ #{otto_button('Update Profile', variant: 'primary')}
69
67
  </form>
70
68
  PROFILE
71
69
  end}
72
70
 
73
- #{otto_card("Security Information") do
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("View Request Headers", "/headers")}
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, "Otto Security Features")
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 "Message too long" if safe_message.length > 1000
105
+ raise 'Message too long' if safe_message.length > 1000
108
106
  end
109
107
 
110
- if safe_message.empty?
111
- content = otto_alert("error", "Validation Error", "Message cannot be empty.")
108
+ content = if safe_message.empty?
109
+ otto_alert('error', 'Validation Error', 'Message cannot be empty.')
112
110
  else
113
- content = <<~HTML
114
- #{otto_alert("success", "Feedback Received", "Thank you for your feedback!")}
111
+ <<~HTML
112
+ #{otto_alert('success', 'Feedback Received', 'Thank you for your feedback!')}
115
113
 
116
- #{otto_card("Your Message") do
114
+ #{otto_card('Your Message') do
117
115
  otto_code_block(safe_message, 'text')
118
116
  end}
119
117
  HTML
120
- end
121
-
122
- rescue Otto::Security::ValidationError => e
123
- content = otto_alert("error", "Security Validation Failed", e.message)
124
- rescue => e
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("← Back to form", "/")}</p>"
129
- res.body = otto_page(content, "Feedback Response")
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("error", "Upload Error", "No file was selected.")
134
+ content = otto_alert('error', 'Upload Error', 'No file was selected.')
138
135
  else
139
- filename = uploaded_file[:filename] rescue uploaded_file.original_filename rescue 'unknown'
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
- safe_filename = sanitize_filename(filename)
146
+ safe_filename = if respond_to?(:sanitize_filename)
147
+ sanitize_filename(filename)
143
148
  else
144
- safe_filename = File.basename(filename.to_s).gsub(/[^\w\-_\.]/, '_')
145
- end
149
+ File.basename(filename.to_s).gsub(/[^\w\-_\.]/, '_')
150
+ end
146
151
 
147
152
  file_info = {
148
- "Original filename" => filename,
149
- "Sanitized filename" => safe_filename,
150
- "Content type" => uploaded_file[:type] || 'unknown',
151
- "Security status" => "File validated and processed safely"
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 { |key, value|
159
+ info_html = file_info.map do |key, value|
155
160
  "<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
156
- }.join
161
+ end.join
157
162
 
158
163
  content = <<~HTML
159
- #{otto_alert("success", "File Upload Successful", "File processed and validated successfully!")}
164
+ #{otto_alert('success', 'File Upload Successful', 'File processed and validated successfully!')}
160
165
 
161
- #{otto_card("File Information") do
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
- rescue Otto::Security::ValidationError => e
173
- content = otto_alert("error", "File Validation Failed", e.message)
174
- rescue => e
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("← Back to form", "/")}</p>"
179
- res.body = otto_page(content, "Upload Response")
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 = req.params['name']
188
+ name = req.params['name']
185
189
  email = req.params['email']
186
- bio = req.params['bio']
190
+ bio = req.params['bio']
187
191
 
188
192
  if respond_to?(:validate_input)
189
- safe_name = validate_input(name, max_length: 100)
193
+ safe_name = validate_input(name, max_length: 100)
190
194
  safe_email = validate_input(email, max_length: 255)
191
- safe_bio = validate_input(bio, max_length: 500, allow_html: false)
195
+ safe_bio = validate_input(bio, max_length: 500, allow_html: false)
192
196
  else
193
- safe_name = name.to_s.strip[0..99]
197
+ safe_name = name.to_s.strip[0..99]
194
198
  safe_email = email.to_s.strip[0..254]
195
- safe_bio = bio.to_s.strip[0..499]
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, "Invalid email format"
203
+ raise Otto::Security::ValidationError, 'Invalid email format'
200
204
  end
201
205
 
202
206
  profile_data = {
203
- "Name" => safe_name,
204
- "Email" => safe_email,
205
- "Bio" => safe_bio,
206
- "Updated" => Time.now.strftime("%Y-%m-%d %H:%M:%S UTC")
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 { |key, value|
213
+ profile_html = profile_data.map do |key, value|
210
214
  "<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
211
- }.join
215
+ end.join
212
216
 
213
217
  content = <<~HTML
214
- #{otto_alert("success", "Profile Updated", "Your profile has been updated successfully!")}
218
+ #{otto_alert('success', 'Profile Updated', 'Your profile has been updated successfully!')}
215
219
 
216
- #{otto_card("Profile Data") do
220
+ #{otto_card('Profile Data') do
217
221
  profile_html
218
222
  end}
219
223
  HTML
220
-
221
- rescue Otto::Security::ValidationError => e
222
- content = otto_alert("error", "Profile Validation Failed", e.message)
223
- rescue => e
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("← Back to form", "/")}</p>"
228
- res.body = otto_page(content, "Profile Update")
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: "Request headers analysis (filtered for security)",
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?) ? "Active" : "Basic",
249
- content_security: "Headers validated and filtered",
250
- xss_protection: "HTML escaping enabled"
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 = otto_alert("error", "Page Not Found", "The requested page could not be found.")
261
- content += "<p>#{otto_link("← Back to home", "/")}</p>"
262
- res.body = otto_page(content, "404 - Not Found")
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 = req.env['otto.error_id'] || SecureRandom.hex(8)
268
- content = otto_alert("error", "Server Error", "An internal server error occurred.")
269
- content += "<p><small>Error ID: #{escape_html(error_id)}</small></p>"
270
- content += "<p>#{otto_link("← Back to home", "/")}</p>"
271
- res.body = otto_page(content, "500 - Server Error")
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