aris 1.4.2 → 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/CHANGELOG.md +251 -0
- data/README.md +18 -0
- data/docs/ADAPTERS.md +478 -0
- data/docs/ARCHITECTURE.md +222 -0
- data/docs/CONTENT.md +967 -0
- data/docs/PERFORMANCE.md +492 -0
- data/docs/PLUGIN_DEVELOPMENT.md +688 -0
- data/docs/USAGE.md +4998 -0
- data/docs/plugins/API_KEY_AUTH.md +232 -0
- data/docs/plugins/BASIC_AUTH.md +582 -0
- data/docs/plugins/BEARER_AUTH.md +394 -0
- data/docs/plugins/CACHE.md +369 -0
- data/docs/plugins/COMPRESSION.md +216 -0
- data/docs/plugins/COOKIES.md +30 -0
- data/docs/plugins/CORS.md +283 -0
- data/docs/plugins/CSRF.md +751 -0
- data/docs/plugins/ETAG.md +308 -0
- data/docs/plugins/FORM_PARSER.md +193 -0
- data/docs/plugins/HEALTH_CHECK.md +469 -0
- data/docs/plugins/JSON.md +291 -0
- data/docs/plugins/MULTIPART.md +427 -0
- data/docs/plugins/RATE_LIMITER.md +368 -0
- data/docs/plugins/REQUEST_ID.md +369 -0
- data/docs/plugins/REQUEST_LOGGER.md +151 -0
- data/docs/plugins/SECURITY.md +193 -0
- data/docs/plugins/SESSION.md +98 -0
- data/lib/aris/adapters/rack/adapter.rb +17 -2
- data/lib/aris/adapters/rack/request.rb +29 -11
- data/lib/aris/plugins/basic_auth.rb +3 -1
- data/lib/aris/plugins/cookies.rb +4 -32
- data/lib/aris/plugins/cors.rb +8 -1
- data/lib/aris/plugins/csrf.rb +63 -22
- data/lib/aris/plugins/flash.rb +3 -1
- data/lib/aris/plugins/form_parser.rb +52 -31
- data/lib/aris/plugins/multipart.rb +22 -2
- data/lib/aris/plugins/request_logger.rb +8 -1
- data/lib/aris/plugins/security_headers.rb +8 -1
- data/lib/aris/plugins/session.rb +150 -99
- data/lib/aris/response_helpers.rb +41 -0
- data/lib/aris/version.rb +2 -2
- metadata +31 -3
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Request Logger Plugin
|
|
2
|
+
|
|
3
|
+
Log incoming HTTP requests in text or JSON format.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require_relative 'aris/plugins/request_logger'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
logger = Aris::Plugins::RequestLogger.build
|
|
15
|
+
|
|
16
|
+
Aris.routes({
|
|
17
|
+
"example.com": {
|
|
18
|
+
use: [logger],
|
|
19
|
+
"/users": { get: { to: UsersHandler } }
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Output (text):**
|
|
25
|
+
```
|
|
26
|
+
GET /users
|
|
27
|
+
POST /users/123
|
|
28
|
+
DELETE /users/456
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
logger = Aris::Plugins::RequestLogger.build(
|
|
37
|
+
format: :json, # :text or :json
|
|
38
|
+
exclude: ['/health', '/metrics'], # Skip these paths
|
|
39
|
+
logger: Rails.logger # Custom logger (default: STDOUT)
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## JSON Format
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
logger = Aris::Plugins::RequestLogger.build(format: :json)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Output:**
|
|
52
|
+
```json
|
|
53
|
+
{"method":"GET","path":"/users","host":"api.example.com","timestamp":"2025-01-10T12:34:56Z"}
|
|
54
|
+
{"method":"POST","path":"/users","host":"api.example.com","timestamp":"2025-01-10T12:35:02Z"}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Common Patterns
|
|
60
|
+
|
|
61
|
+
### Exclude Health Checks
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
logger = Aris::Plugins::RequestLogger.build(
|
|
65
|
+
exclude: ['/health', '/ping', '/metrics']
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Custom Logger
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# File logger
|
|
73
|
+
file_logger = Logger.new('log/requests.log')
|
|
74
|
+
logger = Aris::Plugins::RequestLogger.build(logger: file_logger)
|
|
75
|
+
|
|
76
|
+
# Rails logger
|
|
77
|
+
logger = Aris::Plugins::RequestLogger.build(logger: Rails.logger)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Different Logs Per Domain
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
api_logger = Aris::Plugins::RequestLogger.build(
|
|
84
|
+
format: :json,
|
|
85
|
+
logger: Logger.new('log/api.log')
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
admin_logger = Aris::Plugins::RequestLogger.build(
|
|
89
|
+
format: :text,
|
|
90
|
+
logger: Logger.new('log/admin.log')
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
Aris.routes({
|
|
94
|
+
"api.example.com": {
|
|
95
|
+
use: [api_logger],
|
|
96
|
+
"/data": { get: { to: DataHandler } }
|
|
97
|
+
},
|
|
98
|
+
"admin.example.com": {
|
|
99
|
+
use: [admin_logger],
|
|
100
|
+
"/dashboard": { get: { to: DashboardHandler } }
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Production Tips
|
|
108
|
+
|
|
109
|
+
**1. JSON Format for Log Aggregation**
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# Works great with ELK, Splunk, CloudWatch
|
|
113
|
+
logger = Aris::Plugins::RequestLogger.build(
|
|
114
|
+
format: :json,
|
|
115
|
+
logger: Logger.new(STDOUT) # Docker captures STDOUT
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**2. Exclude Noisy Endpoints**
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
logger = Aris::Plugins::RequestLogger.build(
|
|
123
|
+
exclude: [
|
|
124
|
+
'/health',
|
|
125
|
+
'/metrics',
|
|
126
|
+
'/favicon.ico',
|
|
127
|
+
'/robots.txt'
|
|
128
|
+
]
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**3. Log Level**
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
custom_logger = Logger.new(STDOUT)
|
|
136
|
+
custom_logger.level = Logger::INFO # or WARN, ERROR
|
|
137
|
+
|
|
138
|
+
logger = Aris::Plugins::RequestLogger.build(logger: custom_logger)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Limitations
|
|
144
|
+
|
|
145
|
+
- Logs incoming requests only (no response status/duration)
|
|
146
|
+
- For response logging, use Rack middleware or application logs
|
|
147
|
+
- No request body logging (use separate middleware for that)
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
Need help? Check out the [full plugin development guide](../docs/plugin-development.md).
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Security Headers Plugin
|
|
2
|
+
|
|
3
|
+
Add essential security headers to protect against common web vulnerabilities.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require_relative 'aris/plugins/security_headers'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
### Secure Defaults
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
security = Aris::Plugins::SecurityHeaders.build
|
|
17
|
+
|
|
18
|
+
Aris.routes({
|
|
19
|
+
"example.com": {
|
|
20
|
+
use: [security],
|
|
21
|
+
"/": { get: { to: HomeHandler } }
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Headers set:**
|
|
27
|
+
```
|
|
28
|
+
X-Frame-Options: SAMEORIGIN
|
|
29
|
+
X-content-type-Options: nosniff
|
|
30
|
+
X-XSS-Protection: 0
|
|
31
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
security = Aris::Plugins::SecurityHeaders.build(
|
|
40
|
+
x_frame_options: 'DENY',
|
|
41
|
+
x_content_type_options: 'nosniff',
|
|
42
|
+
hsts: { max_age: 63072000, include_subdomains: true, preload: true },
|
|
43
|
+
csp: "default-src 'self'; script-src 'self' 'unsafe-inline'",
|
|
44
|
+
referrer_policy: 'no-referrer',
|
|
45
|
+
permissions_policy: 'geolocation=(), microphone=()'
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Options
|
|
50
|
+
|
|
51
|
+
| Option | Default | Description |
|
|
52
|
+
|:---|:---|:---|
|
|
53
|
+
| `x_frame_options` | `'SAMEORIGIN'` | Prevent clickjacking (`DENY`, `SAMEORIGIN`, `nil`) |
|
|
54
|
+
| `x_content_type_options` | `'nosniff'` | Prevent MIME sniffing |
|
|
55
|
+
| `x_xss_protection` | `'0'` | Disable legacy XSS filter (modern browsers ignore) |
|
|
56
|
+
| `hsts` | Not set | HTTP Strict Transport Security |
|
|
57
|
+
| `csp` | Not set | Content Security Policy |
|
|
58
|
+
| `referrer_policy` | `'strict-origin-when-cross-origin'` | Referrer behavior |
|
|
59
|
+
| `permissions_policy` | Not set | Control browser features |
|
|
60
|
+
| `defaults` | `true` | Enable default headers |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Common Patterns
|
|
65
|
+
|
|
66
|
+
### Production API
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
api_security = Aris::Plugins::SecurityHeaders.build(
|
|
70
|
+
x_frame_options: 'DENY',
|
|
71
|
+
hsts: { max_age: 31536000, include_subdomains: true },
|
|
72
|
+
csp: "default-src 'none'",
|
|
73
|
+
referrer_policy: 'no-referrer'
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Web Application
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
web_security = Aris::Plugins::SecurityHeaders.build(
|
|
81
|
+
x_frame_options: 'SAMEORIGIN',
|
|
82
|
+
hsts: true,
|
|
83
|
+
csp: "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'",
|
|
84
|
+
referrer_policy: 'strict-origin-when-cross-origin'
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Different Security Per Route
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
strict = Aris::Plugins::SecurityHeaders.build(x_frame_options: 'DENY')
|
|
92
|
+
relaxed = Aris::Plugins::SecurityHeaders.build(x_frame_options: 'SAMEORIGIN')
|
|
93
|
+
|
|
94
|
+
Aris.routes({
|
|
95
|
+
"example.com": {
|
|
96
|
+
"/admin": {
|
|
97
|
+
use: [strict],
|
|
98
|
+
get: { to: AdminHandler }
|
|
99
|
+
},
|
|
100
|
+
"/public": {
|
|
101
|
+
use: [relaxed],
|
|
102
|
+
get: { to: PublicHandler }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Header Details
|
|
111
|
+
|
|
112
|
+
**X-Frame-Options**
|
|
113
|
+
- `DENY` - Cannot be framed at all
|
|
114
|
+
- `SAMEORIGIN` - Can be framed by same origin only
|
|
115
|
+
|
|
116
|
+
**HSTS (HTTP Strict Transport Security)**
|
|
117
|
+
```ruby
|
|
118
|
+
hsts: true # Simple: max-age=31536000; includeSubDomains
|
|
119
|
+
|
|
120
|
+
hsts: {
|
|
121
|
+
max_age: 63072000, # 2 years
|
|
122
|
+
include_subdomains: true,
|
|
123
|
+
preload: true # Submit to browser preload list
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Content-Security-Policy**
|
|
128
|
+
```ruby
|
|
129
|
+
csp: "default-src 'self'; script-src 'self' 'unsafe-inline'; img-src * data:"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Permissions-Policy**
|
|
133
|
+
```ruby
|
|
134
|
+
permissions_policy: 'camera=(), microphone=(), geolocation=(self)'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Production Tips
|
|
140
|
+
|
|
141
|
+
**1. Start Strict, Relax as Needed**
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# Start with strictest settings
|
|
145
|
+
security = Aris::Plugins::SecurityHeaders.build(
|
|
146
|
+
x_frame_options: 'DENY',
|
|
147
|
+
hsts: { max_age: 31536000, include_subdomains: true },
|
|
148
|
+
csp: "default-src 'self'"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Relax only where necessary
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**2. Test CSP in Report-Only Mode First**
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# Development/Staging
|
|
158
|
+
security = Aris::Plugins::SecurityHeaders.build(
|
|
159
|
+
csp: "default-src 'self'; report-uri /csp-report"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Monitor violations before enforcing
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**3. HSTS Considerations**
|
|
166
|
+
|
|
167
|
+
- Start with short `max_age` (300 seconds) to test
|
|
168
|
+
- Increase gradually (3600 → 86400 → 31536000)
|
|
169
|
+
- Only enable `preload` when confident (irreversible!)
|
|
170
|
+
|
|
171
|
+
**4. Environment-Specific Configs**
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
security = Aris::Plugins::SecurityHeaders.build(
|
|
175
|
+
hsts: Rails.env.production? ? { max_age: 31536000 } : nil,
|
|
176
|
+
csp: ENV['CSP_POLICY']
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Security Notes
|
|
183
|
+
|
|
184
|
+
- ✅ Essential first layer of defense
|
|
185
|
+
- ✅ Protect against clickjacking, XSS, MIME sniffing
|
|
186
|
+
- ✅ Combine with HTTPS (especially HSTS)
|
|
187
|
+
- ❌ Headers alone don't prevent all attacks
|
|
188
|
+
- ❌ CSP requires careful tuning for complex apps
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
Need help? Check out the [full plugin development guide](../docs/plugin-development.md).
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Session Plugin
|
|
2
|
+
|
|
3
|
+
Cookie-based sessions that are safe to use for authentication.
|
|
4
|
+
|
|
5
|
+
The session is stored in a single cookie as an **AES-256-GCM encrypted and authenticated** payload, keyed from `Aris::Config.secret_key_base`. The browser cannot read what is inside, and a modified, forged, expired, or wrong-key cookie loads as an empty session instead of raising.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
Aris.configure do |c|
|
|
11
|
+
c.secret_key_base = ENV.fetch('SECRET_KEY_BASE') # at least 32 bytes; generate with:
|
|
12
|
+
# ruby -rsecurerandom -e 'puts SecureRandom.hex(32)'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Aris.routes({
|
|
16
|
+
"example.com": {
|
|
17
|
+
use: [:session],
|
|
18
|
+
"/login" => { post: { to: LoginHandler } },
|
|
19
|
+
"/logout" => { post: { to: LogoutHandler } },
|
|
20
|
+
"/me" => { get: { to: MeHandler } }
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Without a secret, the first write to a session raises `ArgumentError` with a message that says so.
|
|
26
|
+
|
|
27
|
+
## Reading and writing
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
class LoginHandler
|
|
31
|
+
def self.call(request, response, params)
|
|
32
|
+
user = Users.authenticate(request.form_params['email'], request.form_params['password'])
|
|
33
|
+
return response.redirect('/login') unless user
|
|
34
|
+
request.session[:user_id] = user[:id]
|
|
35
|
+
response.redirect('/')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class MeHandler
|
|
40
|
+
def self.call(request, response, params)
|
|
41
|
+
user_id = request.session[:user_id] # nil when not logged in
|
|
42
|
+
request.session.key?(:user_id) # => true/false
|
|
43
|
+
request.session.fetch(:theme, 'light')
|
|
44
|
+
response.text("You are #{user_id}")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class LogoutHandler
|
|
49
|
+
def self.call(request, response, params)
|
|
50
|
+
request.session.destroy # clears the data and deletes the cookie
|
|
51
|
+
response.redirect('/login')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Other methods: `delete(key)`, `clear`, `to_h`, `changed?`, `destroyed?`. Keys are symbols (strings are converted).
|
|
57
|
+
|
|
58
|
+
The cookie is only written when the session changed during the request. An empty session removes the cookie.
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
Set defaults for every scope:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
Aris::Plugins::Session.default_config = Aris::Plugins::Session.default_config.merge(
|
|
66
|
+
key: '_myapp_session',
|
|
67
|
+
expire_after: 7 * 24 * 3600 # seconds; default 14 days
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or build a configured instance for one scope:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
use: [Aris::Plugins::Session.build(key: '_admin', expire_after: 3600, same_site: :strict)]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Option | Default | Meaning |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `key` | `'_aris_session'` | Cookie name |
|
|
80
|
+
| `expire_after` | 14 days | Lifetime in seconds — enforced server-side, not just via `Max-Age` |
|
|
81
|
+
| `secret` | `Aris::Config.secret_key_base` | Encryption key source (≥ 32 bytes) |
|
|
82
|
+
| `secure` | from `Config.cookie_options`, else `true` in production | Send the cookie only over HTTPS |
|
|
83
|
+
| `same_site` | `:lax` | `SameSite` attribute |
|
|
84
|
+
| `path` | `'/'` | Cookie path |
|
|
85
|
+
|
|
86
|
+
The cookie is always `HttpOnly`.
|
|
87
|
+
|
|
88
|
+
## Rotating the secret
|
|
89
|
+
|
|
90
|
+
Changing `secret_key_base` invalidates every existing session (everyone is logged out). That is the intended way to revoke all sessions at once.
|
|
91
|
+
|
|
92
|
+
## Pairing with CSRF
|
|
93
|
+
|
|
94
|
+
`use: [:session, :form_parser, :csrf]` — the CSRF token lives in the session. See [CSRF.md](CSRF.md).
|
|
95
|
+
|
|
96
|
+
## Before 1.5
|
|
97
|
+
|
|
98
|
+
Sessions were Base64-encoded JSON (readable and forgeable) and the Rack adapter never read cookies back, so they did not persist at all. Any 1.4 session cookie is ignored by 1.5.
|
|
@@ -141,17 +141,32 @@ module Aris
|
|
|
141
141
|
body = active_res.body
|
|
142
142
|
body = [body] if body.is_a?(String)
|
|
143
143
|
|
|
144
|
-
return [active_res.status, active_res.headers, body]
|
|
144
|
+
return [active_res.status, rack3_headers(active_res.headers), body]
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# 3. Final Fallback (If Joys rendered nothing)
|
|
148
148
|
case result
|
|
149
|
-
when Array then result
|
|
149
|
+
when Array then [result[0], rack3_headers(result[1] || {}), result[2]]
|
|
150
150
|
when Hash then [200, {'content-type' => 'application/json'}, [result.to_json]]
|
|
151
151
|
else [200, {'content-type' => 'text/plain'}, [result.to_s]]
|
|
152
152
|
end
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
# Rack 3 requires lowercase header names, and a header with several
|
|
156
|
+
# values (set-cookie) must be an Array, never a comma-joined String.
|
|
157
|
+
def rack3_headers(headers)
|
|
158
|
+
out = {}
|
|
159
|
+
headers.each do |key, value|
|
|
160
|
+
name = key.to_s.downcase
|
|
161
|
+
if out.key?(name)
|
|
162
|
+
out[name] = Array(out[name]) + Array(value)
|
|
163
|
+
else
|
|
164
|
+
out[name] = value
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
out
|
|
168
|
+
end
|
|
169
|
+
|
|
155
170
|
|
|
156
171
|
end
|
|
157
172
|
end
|
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
# lib/aris/adapters/rack/request.rb
|
|
2
|
+
require 'rack/utils'
|
|
3
|
+
|
|
2
4
|
module Aris
|
|
3
5
|
module Adapters
|
|
4
6
|
module Rack
|
|
5
7
|
class Request
|
|
6
8
|
attr_reader :env
|
|
7
9
|
attr_accessor :json_body
|
|
8
|
-
|
|
10
|
+
|
|
9
11
|
def initialize(env)
|
|
10
12
|
@env = env
|
|
11
13
|
end
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
+
|
|
15
|
+
# Cookies sent by the client, parsed from the Cookie header.
|
|
16
|
+
# (Before 1.5 this read env['rack.request.cookie_hash'], which nothing
|
|
17
|
+
# populated, so cookies were always empty under the Rack adapter.)
|
|
14
18
|
def cookies
|
|
15
|
-
@
|
|
19
|
+
@cookies ||= begin
|
|
20
|
+
header = @env['HTTP_COOKIE']
|
|
21
|
+
header && !header.empty? ? ::Rack::Utils.parse_cookies_header(header) : {}
|
|
22
|
+
end
|
|
16
23
|
end
|
|
17
|
-
|
|
24
|
+
|
|
18
25
|
def host
|
|
19
26
|
@env['HTTP_HOST'] || @env['SERVER_NAME']
|
|
20
27
|
end
|
|
21
|
-
|
|
28
|
+
|
|
22
29
|
alias_method :domain, :host
|
|
23
30
|
|
|
24
31
|
def request_method
|
|
25
32
|
@env['REQUEST_METHOD']
|
|
26
33
|
end
|
|
27
|
-
|
|
34
|
+
|
|
28
35
|
def method
|
|
29
36
|
@env['REQUEST_METHOD']
|
|
30
37
|
end
|
|
@@ -32,7 +39,7 @@ module Aris
|
|
|
32
39
|
def path_info
|
|
33
40
|
@env['PATH_INFO']
|
|
34
41
|
end
|
|
35
|
-
|
|
42
|
+
|
|
36
43
|
alias_method :path, :path_info
|
|
37
44
|
|
|
38
45
|
def query
|
|
@@ -43,14 +50,25 @@ module Aris
|
|
|
43
50
|
@env.select { |k, v| k.start_with?('HTTP_') }
|
|
44
51
|
end
|
|
45
52
|
|
|
53
|
+
# The raw request body. Read once and memoized, and the input is
|
|
54
|
+
# rewound afterwards, so plugins and handlers can all call it.
|
|
46
55
|
def body
|
|
47
|
-
@
|
|
56
|
+
return @body if defined?(@body)
|
|
57
|
+
input = @env['rack.input']
|
|
58
|
+
@body = if input
|
|
59
|
+
input.rewind if input.respond_to?(:rewind)
|
|
60
|
+
data = input.read
|
|
61
|
+
input.rewind if input.respond_to?(:rewind)
|
|
62
|
+
data
|
|
63
|
+
end
|
|
48
64
|
end
|
|
49
65
|
|
|
66
|
+
# Query-string parameters. The FormParser plugin merges parsed form
|
|
67
|
+
# fields into this hash as well, so handlers can read both from here.
|
|
50
68
|
def params
|
|
51
69
|
@params ||= ::Rack::Utils.parse_nested_query(@env['QUERY_STRING'] || '')
|
|
52
70
|
end
|
|
53
|
-
|
|
71
|
+
|
|
54
72
|
def [](key)
|
|
55
73
|
case key
|
|
56
74
|
when :method then method
|
|
@@ -63,4 +81,4 @@ module Aris
|
|
|
63
81
|
end
|
|
64
82
|
end
|
|
65
83
|
end
|
|
66
|
-
end
|
|
84
|
+
end
|
data/lib/aris/plugins/cookies.rb
CHANGED
|
@@ -1,46 +1,18 @@
|
|
|
1
1
|
# lib/aris/plugins/cookies.rb
|
|
2
2
|
module Aris
|
|
3
3
|
module Plugins
|
|
4
|
+
# Since 1.5, `set_cookie` and `delete_cookie` live on every response
|
|
5
|
+
# (Aris::ResponseHelpers), so this plugin no longer needs to inject them.
|
|
6
|
+
# It stays registered so `use: [:cookies]` keeps working in existing apps.
|
|
4
7
|
class Cookies
|
|
5
8
|
def self.call(request, response)
|
|
6
|
-
# Only add cookie helpers when plugin is used
|
|
7
|
-
add_cookie_helpers(request, response)
|
|
8
9
|
nil # Continue pipeline
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def self.build(**config)
|
|
12
13
|
self
|
|
13
14
|
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def self.add_cookie_helpers(request, response)
|
|
18
|
-
# Add cookie writing methods to response
|
|
19
|
-
response.define_singleton_method(:set_cookie) do |name, value, options = {}|
|
|
20
|
-
default_options = Aris::Config.cookie_options || {}
|
|
21
|
-
merged_options = default_options.merge(options)
|
|
22
|
-
|
|
23
|
-
cookie_parts = ["#{name}=#{value}"]
|
|
24
|
-
cookie_parts << "Path=#{merged_options[:path]}" if merged_options[:path]
|
|
25
|
-
cookie_parts << "HttpOnly" if merged_options[:httponly]
|
|
26
|
-
cookie_parts << "Secure" if merged_options[:secure]
|
|
27
|
-
cookie_parts << "Max-Age=#{merged_options[:max_age]}" if merged_options[:max_age]
|
|
28
|
-
cookie_parts << "SameSite=#{merged_options[:same_site]}" if merged_options[:same_site]
|
|
29
|
-
|
|
30
|
-
cookie_string = cookie_parts.join("; ")
|
|
31
|
-
|
|
32
|
-
if headers['Set-Cookie']
|
|
33
|
-
headers['Set-Cookie'] = [headers['Set-Cookie'], cookie_string].join(", ")
|
|
34
|
-
else
|
|
35
|
-
headers['Set-Cookie'] = cookie_string
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
response.define_singleton_method(:delete_cookie) do |name, options = {}|
|
|
40
|
-
set_cookie(name, "", options.merge(max_age: 0))
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
15
|
end
|
|
44
16
|
end
|
|
45
17
|
end
|
|
46
|
-
Aris.register_plugin(:cookies, plugin_class: Aris::Plugins::Cookies)
|
|
18
|
+
Aris.register_plugin(:cookies, plugin_class: Aris::Plugins::Cookies)
|
data/lib/aris/plugins/cors.rb
CHANGED
|
@@ -53,6 +53,11 @@ module Aris
|
|
|
53
53
|
def self.build(**config)
|
|
54
54
|
new(**config)
|
|
55
55
|
end
|
|
56
|
+
|
|
57
|
+
# `use: [:cors]` (the bare symbol) runs an instance with default options.
|
|
58
|
+
def self.call(request, response)
|
|
59
|
+
(@default_instance ||= new).call(request, response)
|
|
60
|
+
end
|
|
56
61
|
|
|
57
62
|
private
|
|
58
63
|
|
|
@@ -78,4 +83,6 @@ module Aris
|
|
|
78
83
|
end
|
|
79
84
|
end
|
|
80
85
|
end
|
|
81
|
-
end
|
|
86
|
+
end
|
|
87
|
+
# Self-register so `use: [:cors]` works without an explicit register_plugin call.
|
|
88
|
+
Aris.register_plugin(:cors, plugin_class: Aris::Plugins::Cors)
|