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,283 @@
|
|
|
1
|
+
# CORS Plugin
|
|
2
|
+
|
|
3
|
+
Enable Cross-Origin Resource Sharing (CORS) for APIs accessed from web browsers on different domains.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# lib/aris.rb already includes this
|
|
9
|
+
require_relative 'aris/plugins/cors'
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Basic Usage
|
|
13
|
+
|
|
14
|
+
### Allow All Origins
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
cors = Aris::Plugins::Cors.build(origins: '*')
|
|
18
|
+
|
|
19
|
+
Aris.routes({
|
|
20
|
+
"api.example.com": {
|
|
21
|
+
use: [cors],
|
|
22
|
+
"/data": { get: { to: DataHandler } }
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Headers set:**
|
|
28
|
+
```
|
|
29
|
+
Access-Control-Allow-Origin: *
|
|
30
|
+
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
|
31
|
+
Access-Control-Allow-Headers: content-type, Authorization
|
|
32
|
+
Access-Control-Max-Age: 86400
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Specific Origins
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
cors = Aris::Plugins::Cors.build(
|
|
39
|
+
origins: [
|
|
40
|
+
'https://app.example.com',
|
|
41
|
+
'https://admin.example.com'
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Only requests from these origins get CORS headers. Others are blocked by the browser.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
cors = Aris::Plugins::Cors.build(
|
|
54
|
+
origins: ['https://app.example.com'], # Array or '*'
|
|
55
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
56
|
+
headers: ['content-type', 'Authorization', 'X-Custom'],
|
|
57
|
+
credentials: true,
|
|
58
|
+
max_age: 3600, # Cache preflight for 1 hour
|
|
59
|
+
expose_headers: ['X-Total-Count', 'X-Page']
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Option | Default | Description |
|
|
64
|
+
|:---|:---|:---|
|
|
65
|
+
| `origins` | `'*'` | Allowed origins (array or wildcard) |
|
|
66
|
+
| `methods` | All common methods | HTTP methods to allow |
|
|
67
|
+
| `headers` | content-type, Authorization | Headers clients can send |
|
|
68
|
+
| `credentials` | `false` | Allow cookies/auth headers |
|
|
69
|
+
| `max_age` | `86400` | Preflight cache time (seconds) |
|
|
70
|
+
| `expose_headers` | `[]` | Headers clients can read |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Common Patterns
|
|
75
|
+
|
|
76
|
+
### Frontend + API on Different Domains
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# API on api.example.com
|
|
80
|
+
# Frontend on app.example.com
|
|
81
|
+
|
|
82
|
+
cors = Aris::Plugins::Cors.build(
|
|
83
|
+
origins: ['https://app.example.com'],
|
|
84
|
+
credentials: true # Allow cookies
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
Aris.routes({
|
|
88
|
+
"api.example.com": {
|
|
89
|
+
use: [cors],
|
|
90
|
+
"/users": { get: { to: UsersHandler } }
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Frontend (React/Vue):**
|
|
96
|
+
```javascript
|
|
97
|
+
fetch('https://api.example.com/users', {
|
|
98
|
+
credentials: 'include' // Send cookies
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Multiple Environments
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
origins = case ENV['RACK_ENV']
|
|
106
|
+
when 'production'
|
|
107
|
+
['https://app.example.com']
|
|
108
|
+
when 'staging'
|
|
109
|
+
['https://staging.example.com']
|
|
110
|
+
when 'development'
|
|
111
|
+
['http://localhost:3000', 'http://localhost:8080']
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
cors = Aris::Plugins::Cors.build(origins: origins)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Public API + Private Admin API
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
public_cors = Aris::Plugins::Cors.build(origins: '*')
|
|
121
|
+
|
|
122
|
+
admin_cors = Aris::Plugins::Cors.build(
|
|
123
|
+
origins: ['https://admin.example.com'],
|
|
124
|
+
credentials: true
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
Aris.routes({
|
|
128
|
+
"api.example.com": {
|
|
129
|
+
use: [public_cors],
|
|
130
|
+
"/public": { get: { to: PublicHandler } }
|
|
131
|
+
},
|
|
132
|
+
"admin-api.example.com": {
|
|
133
|
+
use: [admin_cors],
|
|
134
|
+
"/admin": { get: { to: AdminHandler } }
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## How CORS Works
|
|
142
|
+
|
|
143
|
+
**Simple Request (GET, POST with simple headers):**
|
|
144
|
+
1. Browser sends request with `Origin` header
|
|
145
|
+
2. Server responds with `Access-Control-Allow-Origin`
|
|
146
|
+
3. Browser allows response if origin matches
|
|
147
|
+
|
|
148
|
+
**Preflight Request (PUT, DELETE, custom headers):**
|
|
149
|
+
1. Browser sends OPTIONS request first
|
|
150
|
+
2. Server responds with allowed methods/headers
|
|
151
|
+
3. Browser sends actual request if allowed
|
|
152
|
+
4. CORS plugin automatically handles OPTIONS with 204 response
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Troubleshooting
|
|
157
|
+
|
|
158
|
+
**"CORS error" in browser console:**
|
|
159
|
+
|
|
160
|
+
Check origin is in allowed list:
|
|
161
|
+
```ruby
|
|
162
|
+
cors = Aris::Plugins::Cors.build(
|
|
163
|
+
origins: ['https://app.example.com'] # Must match exactly
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Credentials not working:**
|
|
168
|
+
|
|
169
|
+
Enable both in plugin and frontend:
|
|
170
|
+
```ruby
|
|
171
|
+
cors = Aris::Plugins::Cors.build(
|
|
172
|
+
origins: ['https://app.example.com'], # Can't use '*' with credentials
|
|
173
|
+
credentials: true
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
fetch(url, { credentials: 'include' })
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Custom headers blocked:**
|
|
182
|
+
|
|
183
|
+
Add to allowed headers:
|
|
184
|
+
```ruby
|
|
185
|
+
cors = Aris::Plugins::Cors.build(
|
|
186
|
+
headers: ['content-type', 'Authorization', 'X-My-Custom-Header']
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Production Tips
|
|
193
|
+
|
|
194
|
+
**1. Be Specific in Production**
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
# ❌ Too permissive
|
|
198
|
+
cors = Aris::Plugins::Cors.build(origins: '*')
|
|
199
|
+
|
|
200
|
+
# ✅ Explicit origins
|
|
201
|
+
cors = Aris::Plugins::Cors.build(
|
|
202
|
+
origins: [
|
|
203
|
+
'https://app.example.com',
|
|
204
|
+
'https://www.example.com'
|
|
205
|
+
]
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**2. Use Environment Variables**
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
cors = Aris::Plugins::Cors.build(
|
|
213
|
+
origins: ENV['CORS_ORIGINS'].split(',')
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**3. Different CORS for Different Routes**
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
Aris.routes({
|
|
221
|
+
"api.example.com": {
|
|
222
|
+
"/public": {
|
|
223
|
+
use: [public_cors],
|
|
224
|
+
"/posts": { get: { to: PostsHandler } }
|
|
225
|
+
},
|
|
226
|
+
"/admin": {
|
|
227
|
+
use: [admin_cors],
|
|
228
|
+
"/users": { delete: { to: DeleteUserHandler } }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Important: OPTIONS Routes
|
|
237
|
+
|
|
238
|
+
CORS preflight requires OPTIONS to be defined:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
Aris.routes({
|
|
242
|
+
"api.example.com": {
|
|
243
|
+
use: [cors],
|
|
244
|
+
"/users": {
|
|
245
|
+
get: { to: UsersHandler },
|
|
246
|
+
post: { to: CreateUserHandler },
|
|
247
|
+
options: { to: UsersHandler } # Required for preflight
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
Or use a catch-all handler:
|
|
252
|
+
rubyclass OptionsHandler
|
|
253
|
+
def self.call(request, params)
|
|
254
|
+
# CORS plugin handles the response
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
This is a common pattern in web frameworks - the route must exist for middleware to run.
|
|
261
|
+
|
|
262
|
+
----
|
|
263
|
+
|
|
264
|
+
## Security Notes
|
|
265
|
+
|
|
266
|
+
- ✅ CORS prevents malicious sites from making requests on behalf of users
|
|
267
|
+
- ✅ Always use specific origins in production (not `'*'`)
|
|
268
|
+
- ✅ Only enable `credentials: true` when necessary
|
|
269
|
+
- ✅ Combine with CSRF protection for state-changing requests
|
|
270
|
+
- ❌ CORS alone doesn't prevent XSS or authentication bypass
|
|
271
|
+
- ❌ CORS is browser-enforced (curl/Postman ignore it)
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
Need help? Check out the [full plugin development guide](../docs/plugin-development.md).
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Add to `lib/aris.rb`:**
|
|
279
|
+
```ruby
|
|
280
|
+
require_relative 'aris/plugins/cors'
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
All tests passing! 🎉
|