otto 1.1.0.pre.alpha4 → 1.2.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/.gitignore +2 -0
- data/.rspec +4 -0
- data/.rubocop.yml +11 -9
- data/.rubocop_todo.yml +152 -0
- data/Gemfile +16 -5
- data/Gemfile.lock +49 -3
- data/LICENSE.txt +1 -1
- data/README.md +61 -112
- data/VERSION.yml +3 -2
- data/examples/basic/app.rb +78 -0
- data/examples/basic/config.ru +30 -0
- data/examples/basic/routes +20 -0
- data/examples/dynamic_pages/app.rb +115 -0
- data/examples/dynamic_pages/config.ru +30 -0
- data/{example → examples/dynamic_pages}/routes +5 -3
- data/examples/security_features/app.rb +273 -0
- data/examples/security_features/config.ru +81 -0
- data/examples/security_features/routes +11 -0
- data/lib/otto/design_system.rb +463 -0
- data/lib/otto/helpers/request.rb +126 -15
- data/lib/otto/helpers/response.rb +99 -13
- data/lib/otto/route.rb +105 -13
- data/lib/otto/security/config.rb +316 -0
- data/lib/otto/security/csrf.rb +181 -0
- data/lib/otto/security/validator.rb +296 -0
- data/lib/otto/static.rb +18 -5
- data/lib/otto/version.rb +6 -4
- data/lib/otto.rb +330 -116
- data/otto.gemspec +13 -12
- metadata +41 -18
- data/CHANGES.txt +0 -35
- data/example/app.rb +0 -58
- data/example/config.ru +0 -35
- /data/{example/public → public}/favicon.ico +0 -0
- /data/{example/public → public}/img/otto.jpg +0 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
# examples/basic/config.ru
|
2
|
+
|
3
|
+
# OTTO EXAMPLE APP CONFIG - 2011-12-17
|
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
|
@@ -0,0 +1,20 @@
|
|
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
|
+
|
15
|
+
GET /bogus App#no_such_method
|
16
|
+
|
17
|
+
# You can also define these handlers when no
|
18
|
+
# route can be found or there's a server error. (optional)
|
19
|
+
GET /404 App#not_found
|
20
|
+
GET /500 App#server_error
|
@@ -0,0 +1,115 @@
|
|
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_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 { |key, value|
|
86
|
+
"<p><strong>#{key}:</strong> #{escape_html(value.to_s)}</p>"
|
87
|
+
}.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
|
@@ -0,0 +1,30 @@
|
|
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,3 +1,5 @@
|
|
1
|
+
# examples/basic/routes
|
2
|
+
|
1
3
|
# OTTO - ROUTES EXAMPLE
|
2
4
|
|
3
5
|
# Each route has three parts:
|
@@ -13,7 +15,7 @@ GET /product/:prodid App#display_product
|
|
13
15
|
|
14
16
|
GET /bogus App#no_such_method
|
15
17
|
|
16
|
-
# You can also define these handlers when no
|
17
|
-
# route can be found or there's a server error. (optional)
|
18
|
+
# You can also define these handlers when no
|
19
|
+
# route can be found or there's a server error. (optional)
|
18
20
|
GET /404 App#not_found
|
19
|
-
GET /500 App#server_error
|
21
|
+
GET /500 App#server_error
|
@@ -0,0 +1,273 @@
|
|
1
|
+
# examples/security_features/app.rb (Updated with Design System)
|
2
|
+
|
3
|
+
require_relative '../../lib/otto/design_system'
|
4
|
+
|
5
|
+
class SecureApp
|
6
|
+
include Otto::DesignSystem
|
7
|
+
include Otto::Security::CSRFHelpers
|
8
|
+
|
9
|
+
attr_reader :req, :res
|
10
|
+
|
11
|
+
def initialize(req, res)
|
12
|
+
@req = req
|
13
|
+
@res = res
|
14
|
+
res.headers['content-type'] = 'text/html; charset=utf-8'
|
15
|
+
end
|
16
|
+
|
17
|
+
def otto
|
18
|
+
self.class.otto
|
19
|
+
end
|
20
|
+
|
21
|
+
def index
|
22
|
+
csrf_tag = respond_to?(:csrf_form_tag) ? csrf_form_tag : ''
|
23
|
+
|
24
|
+
content = <<~HTML
|
25
|
+
<div class="otto-card otto-text-center">
|
26
|
+
<img src="/img/otto.jpg" alt="Otto Framework" class="otto-logo" />
|
27
|
+
<h1>Otto Security Features</h1>
|
28
|
+
<p class="otto-mb-md">Security demonstration for the Otto framework</p>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
#{otto_card("CSRF Protected Feedback") do
|
32
|
+
|
33
|
+
<<~FORM
|
34
|
+
<form method="post" action="/feedback" class="otto-form">
|
35
|
+
#{csrf_tag}
|
36
|
+
<label>Message:</label>
|
37
|
+
#{otto_textarea("message", placeholder: "Enter your feedback...", required: true)}
|
38
|
+
#{otto_button("Submit Feedback", variant: "primary")}
|
39
|
+
</form>
|
40
|
+
FORM
|
41
|
+
|
42
|
+
end}
|
43
|
+
|
44
|
+
#{otto_card("File Upload Validation") do
|
45
|
+
<<~UPLOAD
|
46
|
+
<form method="post" action="/upload" enctype="multipart/form-data" class="otto-form">
|
47
|
+
#{csrf_tag}
|
48
|
+
<label>Choose file:</label>
|
49
|
+
<input type="file" name="upload_file" class="otto-input">
|
50
|
+
#{otto_button("Upload File", variant: "primary")}
|
51
|
+
</form>
|
52
|
+
UPLOAD
|
53
|
+
end}
|
54
|
+
|
55
|
+
#{otto_card("User Profile Input Validation") do
|
56
|
+
<<~PROFILE
|
57
|
+
<form method="post" action="/profile" class="otto-form">
|
58
|
+
#{csrf_tag}
|
59
|
+
<label>Name:</label>
|
60
|
+
#{otto_input("name", placeholder: "Your name", required: true)}
|
61
|
+
|
62
|
+
<label>Email:</label>
|
63
|
+
#{otto_input("email", type: "email", placeholder: "your@email.com", required: true)}
|
64
|
+
|
65
|
+
<label>Bio:</label>
|
66
|
+
#{otto_textarea("bio", placeholder: "Tell us about yourself...")}
|
67
|
+
|
68
|
+
#{otto_button("Update Profile", variant: "primary")}
|
69
|
+
</form>
|
70
|
+
PROFILE
|
71
|
+
end}
|
72
|
+
|
73
|
+
#{otto_card("Security Information") do
|
74
|
+
<<~INFO
|
75
|
+
<h3>Security Features Active:</h3>
|
76
|
+
<ul>
|
77
|
+
<li><strong>CSRF Protection:</strong> All forms include CSRF tokens</li>
|
78
|
+
<li><strong>Input Validation:</strong> Server-side validation with length limits</li>
|
79
|
+
<li><strong>XSS Prevention:</strong> HTML escaping for all user inputs</li>
|
80
|
+
<li><strong>Security Headers:</strong> Comprehensive header security policy</li>
|
81
|
+
</ul>
|
82
|
+
|
83
|
+
<p class="otto-mt-md">
|
84
|
+
<strong>Test XSS Protection:</strong> Try entering
|
85
|
+
#{otto_code_block('<script>alert("XSS")</script>', 'html')}
|
86
|
+
in any form field to see input sanitization in action.
|
87
|
+
</p>
|
88
|
+
|
89
|
+
<p class="otto-mt-md">
|
90
|
+
#{otto_link("View Request Headers", "/headers")}
|
91
|
+
</p>
|
92
|
+
INFO
|
93
|
+
end}
|
94
|
+
HTML
|
95
|
+
|
96
|
+
res.body = otto_page(content, "Otto Security Features")
|
97
|
+
end
|
98
|
+
|
99
|
+
def receive_feedback
|
100
|
+
begin
|
101
|
+
message = req.params['message']
|
102
|
+
|
103
|
+
if respond_to?(:validate_input)
|
104
|
+
safe_message = validate_input(message, max_length: 1000, allow_html: false)
|
105
|
+
else
|
106
|
+
safe_message = message.to_s.strip
|
107
|
+
raise "Message too long" if safe_message.length > 1000
|
108
|
+
end
|
109
|
+
|
110
|
+
if safe_message.empty?
|
111
|
+
content = otto_alert("error", "Validation Error", "Message cannot be empty.")
|
112
|
+
else
|
113
|
+
content = <<~HTML
|
114
|
+
#{otto_alert("success", "Feedback Received", "Thank you for your feedback!")}
|
115
|
+
|
116
|
+
#{otto_card("Your Message") do
|
117
|
+
otto_code_block(safe_message, 'text')
|
118
|
+
end}
|
119
|
+
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.")
|
126
|
+
end
|
127
|
+
|
128
|
+
content += "<p class=\"otto-mt-lg\">#{otto_link("← Back to form", "/")}</p>"
|
129
|
+
res.body = otto_page(content, "Feedback Response")
|
130
|
+
end
|
131
|
+
|
132
|
+
def upload_file
|
133
|
+
begin
|
134
|
+
uploaded_file = req.params['upload_file']
|
135
|
+
|
136
|
+
if uploaded_file.nil? || uploaded_file.empty?
|
137
|
+
content = otto_alert("error", "Upload Error", "No file was selected.")
|
138
|
+
else
|
139
|
+
filename = uploaded_file[:filename] rescue uploaded_file.original_filename rescue 'unknown'
|
140
|
+
|
141
|
+
if respond_to?(:sanitize_filename)
|
142
|
+
safe_filename = sanitize_filename(filename)
|
143
|
+
else
|
144
|
+
safe_filename = File.basename(filename.to_s).gsub(/[^\w\-_\.]/, '_')
|
145
|
+
end
|
146
|
+
|
147
|
+
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"
|
152
|
+
}
|
153
|
+
|
154
|
+
info_html = file_info.map { |key, value|
|
155
|
+
"<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
|
156
|
+
}.join
|
157
|
+
|
158
|
+
content = <<~HTML
|
159
|
+
#{otto_alert("success", "File Upload Successful", "File processed and validated successfully!")}
|
160
|
+
|
161
|
+
#{otto_card("File Information") do
|
162
|
+
info_html
|
163
|
+
end}
|
164
|
+
|
165
|
+
<div class="otto-alert otto-alert-info">
|
166
|
+
<h3 class="otto-alert-title">Demo Notice</h3>
|
167
|
+
<p class="otto-alert-message">This is a demonstration - files are validated but not permanently stored.</p>
|
168
|
+
</div>
|
169
|
+
HTML
|
170
|
+
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
|
+
end
|
177
|
+
|
178
|
+
content += "<p class=\"otto-mt-lg\">#{otto_link("← Back to form", "/")}</p>"
|
179
|
+
res.body = otto_page(content, "Upload Response")
|
180
|
+
end
|
181
|
+
|
182
|
+
def update_profile
|
183
|
+
begin
|
184
|
+
name = req.params['name']
|
185
|
+
email = req.params['email']
|
186
|
+
bio = req.params['bio']
|
187
|
+
|
188
|
+
if respond_to?(:validate_input)
|
189
|
+
safe_name = validate_input(name, max_length: 100)
|
190
|
+
safe_email = validate_input(email, max_length: 255)
|
191
|
+
safe_bio = validate_input(bio, max_length: 500, allow_html: false)
|
192
|
+
else
|
193
|
+
safe_name = name.to_s.strip[0..99]
|
194
|
+
safe_email = email.to_s.strip[0..254]
|
195
|
+
safe_bio = bio.to_s.strip[0..499]
|
196
|
+
end
|
197
|
+
|
198
|
+
unless safe_email.match?(/\A[^@\s]+@[^@\s]+\z/)
|
199
|
+
raise Otto::Security::ValidationError, "Invalid email format"
|
200
|
+
end
|
201
|
+
|
202
|
+
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
|
+
}
|
208
|
+
|
209
|
+
profile_html = profile_data.map { |key, value|
|
210
|
+
"<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
|
211
|
+
}.join
|
212
|
+
|
213
|
+
content = <<~HTML
|
214
|
+
#{otto_alert("success", "Profile Updated", "Your profile has been updated successfully!")}
|
215
|
+
|
216
|
+
#{otto_card("Profile Data") do
|
217
|
+
profile_html
|
218
|
+
end}
|
219
|
+
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.")
|
225
|
+
end
|
226
|
+
|
227
|
+
content += "<p class=\"otto-mt-lg\">#{otto_link("← Back to form", "/")}</p>"
|
228
|
+
res.body = otto_page(content, "Profile Update")
|
229
|
+
end
|
230
|
+
|
231
|
+
def show_headers
|
232
|
+
res.headers['content-type'] = 'application/json; charset=utf-8'
|
233
|
+
|
234
|
+
safe_headers = {}
|
235
|
+
req.env.each do |key, value|
|
236
|
+
if key.start_with?('HTTP_') || %w[REQUEST_METHOD PATH_INFO QUERY_STRING].include?(key)
|
237
|
+
safe_headers[key] = value.to_s[0..200]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
response_data = {
|
242
|
+
message: "Request headers analysis (filtered for security)",
|
243
|
+
client_ip: req.respond_to?(:client_ipaddress) ? req.client_ipaddress : req.ip,
|
244
|
+
secure_connection: req.respond_to?(:secure?) ? req.secure? : false,
|
245
|
+
timestamp: Time.now.utc.iso8601,
|
246
|
+
headers: safe_headers,
|
247
|
+
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
|
+
}
|
252
|
+
}
|
253
|
+
|
254
|
+
require 'json'
|
255
|
+
res.body = JSON.pretty_generate(response_data)
|
256
|
+
end
|
257
|
+
|
258
|
+
def not_found
|
259
|
+
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
|
+
end
|
264
|
+
|
265
|
+
def server_error
|
266
|
+
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")
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# examples/security_features/config.ru
|
2
|
+
|
3
|
+
# OTTO SECURE EXAMPLE APP CONFIG - 2025-07-18
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
#
|
7
|
+
# $ thin -e dev -R config.ru -p 10770 start
|
8
|
+
#
|
9
|
+
|
10
|
+
public_path = File.expand_path('../../public', __dir__)
|
11
|
+
|
12
|
+
require_relative '../../lib/otto'
|
13
|
+
require_relative 'app'
|
14
|
+
|
15
|
+
# Create Otto app with security features enabled
|
16
|
+
app = Otto.new("./routes", {
|
17
|
+
# Enable CSRF protection for POST, PUT, DELETE requests
|
18
|
+
csrf_protection: true,
|
19
|
+
|
20
|
+
# Enable input validation and sanitization
|
21
|
+
request_validation: true,
|
22
|
+
|
23
|
+
# Configure trusted proxy servers (adjust for your infrastructure)
|
24
|
+
trusted_proxies: [
|
25
|
+
# The primary RFC 1918 private ranges
|
26
|
+
'127.0.0.1', # Local development
|
27
|
+
'10.0.0.0/8', # Private Class A
|
28
|
+
'172.16.0.0/12', # Private Class B
|
29
|
+
'192.168.0.0/16', # Private Class C
|
30
|
+
|
31
|
+
# Other reserved ranges that I often forget about
|
32
|
+
# '127.0.0.0/8', # Loopback
|
33
|
+
# '100.64.0.0/10', # Carrier-grade NAT
|
34
|
+
# '169.254.0.0/16', # RFC 3927 - Automatic Private IP Addressing (APIPA)
|
35
|
+
# '198.18.0.0/15', # RFC 2544 - Benchmarking methodology
|
36
|
+
# '203.0.113.0/24', # RFC 5737 - Documentation examples
|
37
|
+
# '224.0.0.0/4', # Multicast
|
38
|
+
# '240.0.0.0/4', # RFC 1112 - Class E (experimental)
|
39
|
+
],
|
40
|
+
|
41
|
+
# Custom security headers
|
42
|
+
security_headers: {
|
43
|
+
'content-security-policy' => "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'",
|
44
|
+
'strict-transport-security' => 'max-age=31536000; includeSubDomains',
|
45
|
+
'x-frame-options' => 'DENY'
|
46
|
+
}
|
47
|
+
})
|
48
|
+
|
49
|
+
# Optional: Configure additional security settings
|
50
|
+
app.security_config.max_request_size = 5 * 1024 * 1024 # 5MB limit
|
51
|
+
app.security_config.max_param_depth = 10 # Limit parameter nesting
|
52
|
+
app.security_config.max_param_keys = 50 # Limit parameters per request
|
53
|
+
|
54
|
+
# Optional: Add static file serving with security
|
55
|
+
app.option[:public] = public_path
|
56
|
+
|
57
|
+
# Development vs Production configuration
|
58
|
+
if ENV['RACK_ENV'] == 'production'
|
59
|
+
# Production-specific settings
|
60
|
+
app.security_config.require_secure_cookies = true
|
61
|
+
|
62
|
+
# More restrictive CSP for production
|
63
|
+
app.set_security_headers({
|
64
|
+
'content-security-policy' => "default-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'",
|
65
|
+
'strict-transport-security' => 'max-age=63072000; includeSubDomains; preload'
|
66
|
+
})
|
67
|
+
else
|
68
|
+
# Development-specific settings
|
69
|
+
puts "🔒 Security features enabled:"
|
70
|
+
puts " ✓ CSRF Protection"
|
71
|
+
puts " ✓ Input Validation"
|
72
|
+
puts " ✓ Request Size Limits"
|
73
|
+
puts " ✓ Security Headers"
|
74
|
+
puts " ✓ Trusted Proxy Support"
|
75
|
+
puts ""
|
76
|
+
end
|
77
|
+
|
78
|
+
# Mount the application
|
79
|
+
map('/') {
|
80
|
+
run app
|
81
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# examples/security_features/routes
|
2
|
+
|
3
|
+
GET / SecureApp#index
|
4
|
+
POST /feedback SecureApp#receive_feedback
|
5
|
+
POST /upload SecureApp#upload_file
|
6
|
+
POST /profile SecureApp#update_profile
|
7
|
+
GET /headers SecureApp#show_headers
|
8
|
+
|
9
|
+
# Error handlers (optional)
|
10
|
+
GET /404 SecureApp#not_found
|
11
|
+
GET /500 SecureApp#server_error
|