otto 1.3.0 → 1.5.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 +1 -1
- data/Gemfile +1 -3
- data/Gemfile.lock +5 -3
- data/README.md +58 -2
- data/docs/.gitignore +2 -0
- data/examples/helpers_demo/app.rb +244 -0
- data/examples/helpers_demo/config.ru +26 -0
- data/examples/helpers_demo/routes +7 -0
- data/lib/otto/helpers/base.rb +27 -0
- data/lib/otto/helpers/request.rb +223 -4
- data/lib/otto/helpers/response.rb +75 -0
- data/lib/otto/response_handlers.rb +141 -0
- data/lib/otto/route.rb +125 -54
- data/lib/otto/route_definition.rb +187 -0
- data/lib/otto/route_handlers.rb +383 -0
- data/lib/otto/security/authentication.rb +289 -0
- data/lib/otto/security/config.rb +99 -1
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +143 -3
- data/otto.gemspec +2 -2
- metadata +29 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a50c097fe32fba3c0a6d84be84f95f68fffe385675f3fc353681b24f11aefa63
|
4
|
+
data.tar.gz: 9ad3e5c757531c10b1fbd60d304a07aa5ce84915491acb6c36b61656b8b6f6b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1be28acdcec40db91e12c39926a5197326b0c46b5d269187f8cc3114cfbcae6ae025f1fa69c6c8820faa076c2067c3200838a15818291c729df40271cca3b00
|
7
|
+
data.tar.gz: bfa3af6f70fd650464b935b8b30c687b2393b9a687b68837ab21ffcb2ffb710acc1dea85942e043a8307a4b2d6360b4037846e710f2fc43e47a66100dddea807
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
3
|
+
# Please see the documentation for all configuration options:
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
5
|
+
|
6
|
+
version: 2
|
7
|
+
updates:
|
8
|
+
- package-ecosystem: "github-actions"
|
9
|
+
directory: "/" # Location of package manifests
|
10
|
+
schedule:
|
11
|
+
interval: "weekly"
|
12
|
+
- package-ecosystem: "bundler"
|
13
|
+
directory: "/"
|
14
|
+
schedule:
|
15
|
+
interval: "weekly"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- main
|
7
|
+
|
8
|
+
pull_request:
|
9
|
+
|
10
|
+
workflow_dispatch:
|
11
|
+
|
12
|
+
permissions:
|
13
|
+
contents: read
|
14
|
+
|
15
|
+
jobs:
|
16
|
+
test:
|
17
|
+
timeout-minutes: 10
|
18
|
+
runs-on: ubuntu-24.04
|
19
|
+
name: "RSpec Tests (Ruby ${{ matrix.ruby }})"
|
20
|
+
strategy:
|
21
|
+
fail-fast: true
|
22
|
+
matrix:
|
23
|
+
ruby: ["3.2", "3.3", "3.4", "3.5"]
|
24
|
+
|
25
|
+
steps:
|
26
|
+
- uses: actions/checkout@v4
|
27
|
+
- name: Set up Ruby
|
28
|
+
uses: ruby/setup-ruby@v1
|
29
|
+
with:
|
30
|
+
ruby-version: ${{ matrix.ruby }}
|
31
|
+
bundler-cache: true
|
32
|
+
|
33
|
+
- name: Run RSpec tests
|
34
|
+
run: bundle exec rspec
|
data/.gitignore
CHANGED
@@ -0,0 +1,107 @@
|
|
1
|
+
##
|
2
|
+
# Pre-Commit Hooks Configuration
|
3
|
+
#
|
4
|
+
# Fast, lightweight code quality checks that run before each commit
|
5
|
+
#
|
6
|
+
# Setup:
|
7
|
+
# 1. Install pre-commit:
|
8
|
+
# $ pip install pre-commit
|
9
|
+
#
|
10
|
+
# 2. Install git hooks:
|
11
|
+
# $ pre-commit install
|
12
|
+
#
|
13
|
+
# Usage:
|
14
|
+
# Hooks run automatically on 'git commit'
|
15
|
+
#
|
16
|
+
# Manual commands:
|
17
|
+
# - Check all files:
|
18
|
+
# $ pre-commit run --all-files
|
19
|
+
#
|
20
|
+
# - Update hooks:
|
21
|
+
# $ pre-commit autoupdate
|
22
|
+
#
|
23
|
+
# - Reinstall after config changes:
|
24
|
+
# $ pre-commit install
|
25
|
+
#
|
26
|
+
# Best Practices:
|
27
|
+
# - Reinstall hooks after modifying this config
|
28
|
+
# - Commit config changes in isolation
|
29
|
+
# - Keep checks fast to maintain workflow
|
30
|
+
#
|
31
|
+
# Resources:
|
32
|
+
# - Docs: https://pre-commit.com
|
33
|
+
# - Available hooks: https://pre-commit.com/hooks.html
|
34
|
+
#
|
35
|
+
# Note: These lightweight checks maintain code quality without
|
36
|
+
# slowing down the local development process.
|
37
|
+
|
38
|
+
# Hook installation configuration
|
39
|
+
default_install_hook_types:
|
40
|
+
- pre-commit # Primary code quality checks
|
41
|
+
- prepare-commit-msg # Commit message preprocessing
|
42
|
+
- post-commit # Actions after successful commit
|
43
|
+
- post-checkout # Triggered after git checkout
|
44
|
+
- post-merge # Triggered after git merge
|
45
|
+
|
46
|
+
# Default execution stage
|
47
|
+
default_stages: [pre-commit]
|
48
|
+
|
49
|
+
# Avoid multiple sequential commit failures
|
50
|
+
fail_fast: false
|
51
|
+
|
52
|
+
# Ignore generated and dependency directories
|
53
|
+
exclude: "^$"
|
54
|
+
|
55
|
+
repos:
|
56
|
+
# Meta hooks: basic checks for pre-commit config itself
|
57
|
+
- repo: meta
|
58
|
+
hooks:
|
59
|
+
- id: check-hooks-apply
|
60
|
+
- id: check-useless-excludes
|
61
|
+
|
62
|
+
# Standard pre-commit hooks: lightweight, universal checks
|
63
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
64
|
+
rev: v5.0.0
|
65
|
+
hooks:
|
66
|
+
# Formatting and basic sanitization
|
67
|
+
- id: trailing-whitespace # Remove trailing whitespaces
|
68
|
+
- id: end-of-file-fixer # Ensure files end with newline
|
69
|
+
- id: check-merge-conflict # Detect unresolved merge conflicts
|
70
|
+
- id: detect-private-key # Warn about committing private keys
|
71
|
+
- id: check-added-large-files # Prevent committing oversized files
|
72
|
+
args: ["--maxkb=2500"] # 2.5MB file size threshold
|
73
|
+
- id: no-commit-to-branch # Prevent direct commits to critical branches
|
74
|
+
args: ["--branch", "main", "--branch", "rel/.*"]
|
75
|
+
|
76
|
+
# # Ruby code quality and style checks (using local bundler environment)
|
77
|
+
# - repo: local
|
78
|
+
# hooks:
|
79
|
+
# - id: rubocop
|
80
|
+
# name: RuboCop
|
81
|
+
# description: Ruby static code analyzer and formatter
|
82
|
+
# entry: eval "$(rbenv init -)" && bundle exec rubocop
|
83
|
+
# language: system
|
84
|
+
# files: \.rb$
|
85
|
+
# pass_filenames: false
|
86
|
+
# - repo: https://github.com/rubocop-hq/rubocop
|
87
|
+
# rev: v1.79.1 # Or your desired version
|
88
|
+
# hooks:
|
89
|
+
# - id: rubocop
|
90
|
+
# files: \.rb$
|
91
|
+
# pass_filenames: false
|
92
|
+
# additional_dependencies:
|
93
|
+
# - rubocop-rails # Example of an additional gem
|
94
|
+
# - rubocop-performance
|
95
|
+
# - rubocop-rspec
|
96
|
+
|
97
|
+
# Commit message issue tracking integration
|
98
|
+
- repo: https://github.com/avilaton/add-msg-issue-prefix-hook
|
99
|
+
rev: v0.0.12
|
100
|
+
hooks:
|
101
|
+
- id: add-msg-issue-prefix
|
102
|
+
stages: [prepare-commit-msg]
|
103
|
+
description: Automatically prefix commits with issue numbers
|
104
|
+
args:
|
105
|
+
- "--default="
|
106
|
+
- '--pattern=(?:i18n(?=\/)|[a-zA-Z0-9]{0,10}-?[0-9]{1,5})'
|
107
|
+
- "--template=[#{}]"
|
@@ -0,0 +1,88 @@
|
|
1
|
+
##
|
2
|
+
# Pre-Push Hooks Configuration
|
3
|
+
#
|
4
|
+
# Quality control checks that run before code is pushed to remote repositories
|
5
|
+
#
|
6
|
+
# Setup:
|
7
|
+
# 1. Install the pre-push hook:
|
8
|
+
# $ pre-commit install --hook-type pre-push
|
9
|
+
#
|
10
|
+
# 2. Install required dependencies:
|
11
|
+
# - Ruby + Rubocop
|
12
|
+
# - Node.js + ESLint
|
13
|
+
# - TypeScript compiler
|
14
|
+
#
|
15
|
+
# Usage:
|
16
|
+
# Hooks run automatically on 'git push'
|
17
|
+
#
|
18
|
+
# Manual execution:
|
19
|
+
# - Run all checks:
|
20
|
+
# $ pre-commit run --config .pre-push-config.yaml --all-files
|
21
|
+
#
|
22
|
+
# - Run single check:
|
23
|
+
# $ pre-commit run <hook-id> --config .pre-push-config.yaml
|
24
|
+
# Example: pre-commit run rubocop --config .pre-push-config.yaml
|
25
|
+
#
|
26
|
+
# Included Checks:
|
27
|
+
# - Full codebase linting (Rubocop, ESLint)
|
28
|
+
# - YAML/JSON validation
|
29
|
+
# - TypeScript type checking
|
30
|
+
# - Code style enforcement
|
31
|
+
# - Security vulnerability scanning
|
32
|
+
#
|
33
|
+
# Related Files:
|
34
|
+
# - .pre-commit-config.yaml: Lightweight pre-commit checks
|
35
|
+
# - Documentation: https://pre-commit.com
|
36
|
+
#
|
37
|
+
# Note: These intensive checks run before pushing to catch issues early
|
38
|
+
# but allow faster local development with lighter pre-commit hooks.
|
39
|
+
|
40
|
+
# Allow all failures to happen so they can be corrected in one go
|
41
|
+
fail_fast: false
|
42
|
+
|
43
|
+
# Skip generated/dependency directories
|
44
|
+
exclude: "^(vendor|node_modules|dist|build)/"
|
45
|
+
|
46
|
+
default_install_hook_types:
|
47
|
+
- pre-push
|
48
|
+
- push
|
49
|
+
|
50
|
+
default_stages: [push]
|
51
|
+
|
52
|
+
repos:
|
53
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
54
|
+
rev: v4.6.0
|
55
|
+
hooks:
|
56
|
+
- id: check-yaml
|
57
|
+
name: Validate YAML files
|
58
|
+
args: ["--allow-multiple-documents"]
|
59
|
+
files: \.(yaml|yml)$
|
60
|
+
|
61
|
+
- id: check-toml
|
62
|
+
name: Validate TOML files
|
63
|
+
files: \.toml$
|
64
|
+
|
65
|
+
- id: check-json
|
66
|
+
name: Validate JSON files
|
67
|
+
files: \.json$
|
68
|
+
|
69
|
+
- id: pretty-format-json
|
70
|
+
name: Format JSON files
|
71
|
+
args: ["--autofix", "--no-sort-keys"]
|
72
|
+
files: \.json$
|
73
|
+
|
74
|
+
- id: mixed-line-ending
|
75
|
+
name: Check line endings
|
76
|
+
args: [--fix=lf]
|
77
|
+
|
78
|
+
- id: check-case-conflict
|
79
|
+
name: Check for case conflicts
|
80
|
+
|
81
|
+
- id: check-executables-have-shebangs
|
82
|
+
name: Check executable shebangs
|
83
|
+
|
84
|
+
- id: check-shebang-scripts-are-executable
|
85
|
+
name: Check shebang scripts are executable
|
86
|
+
|
87
|
+
- id: forbid-submodules
|
88
|
+
name: Check for submodules
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
source 'https://rubygems.org'
|
4
2
|
|
5
3
|
gemspec
|
@@ -18,5 +16,5 @@ group 'development' do
|
|
18
16
|
gem 'ruby-lsp', require: false
|
19
17
|
gem 'stackprof', require: false
|
20
18
|
gem 'syntax_tree', require: false
|
21
|
-
gem 'tryouts', require: false
|
19
|
+
gem 'tryouts', '~> 3.3.1', require: false
|
22
20
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
otto (1.
|
4
|
+
otto (1.5.0)
|
5
|
+
ostruct
|
5
6
|
rack (~> 3.1, < 4.0)
|
6
7
|
rack-parser (~> 0.7)
|
7
8
|
rexml (>= 3.3.6)
|
@@ -26,6 +27,7 @@ GEM
|
|
26
27
|
logger (1.7.0)
|
27
28
|
method_source (1.1.0)
|
28
29
|
minitest (5.25.5)
|
30
|
+
ostruct (0.6.3)
|
29
31
|
parallel (1.27.0)
|
30
32
|
parser (3.3.9.0)
|
31
33
|
ast (~> 2.4.1)
|
@@ -109,7 +111,7 @@ GEM
|
|
109
111
|
stringio (3.1.7)
|
110
112
|
syntax_tree (6.3.0)
|
111
113
|
prettier_print (>= 1.2.0)
|
112
|
-
tryouts (3.
|
114
|
+
tryouts (3.3.1)
|
113
115
|
irb
|
114
116
|
minitest (~> 5.0)
|
115
117
|
pastel (~> 0.8)
|
@@ -140,7 +142,7 @@ DEPENDENCIES
|
|
140
142
|
ruby-lsp
|
141
143
|
stackprof
|
142
144
|
syntax_tree
|
143
|
-
tryouts
|
145
|
+
tryouts (~> 3.3.1)
|
144
146
|
|
145
147
|
BUNDLED WITH
|
146
148
|
2.6.9
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Otto -
|
1
|
+
# Otto - A Ruby Gem
|
2
2
|
|
3
3
|
**Define your rack-apps in plain-text with built-in security.**
|
4
4
|
|
@@ -74,9 +74,65 @@ app = Otto.new("./routes", {
|
|
74
74
|
|
75
75
|
Security features include CSRF protection, input validation, security headers, and trusted proxy configuration.
|
76
76
|
|
77
|
+
## Internationalization Support
|
78
|
+
|
79
|
+
Otto provides built-in locale detection and management:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# Global configuration (affects all Otto instances)
|
83
|
+
Otto.configure do |opts|
|
84
|
+
opts.available_locales = { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' }
|
85
|
+
opts.default_locale = 'en'
|
86
|
+
end
|
87
|
+
|
88
|
+
# Or configure during initialization
|
89
|
+
app = Otto.new("./routes", {
|
90
|
+
available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
|
91
|
+
default_locale: 'en'
|
92
|
+
})
|
93
|
+
|
94
|
+
# Or configure at runtime
|
95
|
+
app.configure(
|
96
|
+
available_locales: { 'en' => 'English', 'es' => 'Spanish' },
|
97
|
+
default_locale: 'en'
|
98
|
+
)
|
99
|
+
|
100
|
+
# Legacy support (still works)
|
101
|
+
app = Otto.new("./routes", {
|
102
|
+
locale_config: {
|
103
|
+
available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
|
104
|
+
default_locale: 'en'
|
105
|
+
}
|
106
|
+
})
|
107
|
+
```
|
108
|
+
|
109
|
+
In your application, use the locale helper:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class App
|
113
|
+
def initialize(req, res)
|
114
|
+
@req, @res = req, res
|
115
|
+
end
|
116
|
+
|
117
|
+
def show_product
|
118
|
+
# Automatically detects locale from:
|
119
|
+
# 1. URL parameter: ?locale=es
|
120
|
+
# 2. User preference (if provided)
|
121
|
+
# 3. Accept-Language header
|
122
|
+
# 4. Default locale
|
123
|
+
locale = req.check_locale!
|
124
|
+
|
125
|
+
# Use locale for localized content
|
126
|
+
res.body = localized_content(locale)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
The locale helper checks multiple sources in order of precedence and validates against your configured locales.
|
132
|
+
|
77
133
|
## Requirements
|
78
134
|
|
79
|
-
- Ruby 3.
|
135
|
+
- Ruby 3.2+
|
80
136
|
- Rack 3.1+
|
81
137
|
|
82
138
|
## Installation
|
data/docs/.gitignore
ADDED
@@ -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
|