otto 1.1.0.pre.alpha3 → 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.
@@ -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