Skip to content

Commit 1a14679

Browse files
rprietoRomain Prieto
authored andcommitted
New generic mode that always returns '*'
1 parent 9a388d0 commit 1a14679

File tree

7 files changed

+256
-135
lines changed

7 files changed

+256
-135
lines changed

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
const corsMiddleware = require('restify-cors-middleware')
2020

2121
const cors = corsMiddleware({
22-
preflightMaxAge: 5, //Optional
22+
preflightMaxAge: 5,
2323
origins: ['http://api.myapp.com', 'http://web.myapp.com'],
2424
allowHeaders: ['API-Token'],
2525
exposeHeaders: ['API-Token-Expiry']
@@ -31,23 +31,25 @@ server.use(cors.actual)
3131

3232
## Allowed origins
3333

34-
You can specify the full list of domains and subdomains allowed in your application:
34+
You can specify the full list of domains and subdomains allowed in your application.
35+
As a convenience method, you can use the `*` character as a wildcard.
3536

3637
```js
3738
origins: [
3839
'http://myapp.com',
39-
'http://*.myapp.com'
40+
'http://*.myotherapp.com'
4041
]
4142
```
4243

43-
For added security, this middleware sets `Access-Control-Allow-Origin` to the origin that matched, not the configured wildcard.
44-
This means callers won't know about other domains that are supported.
44+
The `Access-Control-Allow-Origin` header will be set to the actual origin that matched, on a per-request basis. The person making the request will not know about the full configuration, like other allowed domains or any wildcards in use.
4545

46-
Setting `origins: ['*']` is also valid, although it comes with obvious security implications. Note that it will still return a customised response (matching Origin), so any caching layer (reverse proxy or CDN) will grow in size accordingly.
46+
The main side-effect is that every response will include `Vary: Origin`, since the response headers depend on the origin. This is the safest setup, but will decrease your cache hit-rate / increase your cache size with every origin.
4747

48-
## Troubleshooting
48+
## Open CORS setup
4949

50-
As per the spec, requests without an `Origin` will not receive any headers. Requests with a matching `Origin` will receive the appropriate response headers. Always be careful that any reverse proxies (e.g. Varnish) very their cache depending on the origin, so you don't serve CORS headers to the wrong request.
50+
Using `origins: ['*']` is also a valid setup, which comes with obvious security implications. This means **any** domain will be able to query your API. However it does have performance benefits. When using `['*']`, the middleware always responds with `Access-Control-Allow-Origin: *` which means responses can be cached regardless of origins.
51+
52+
Each API should weigh the security and performance angles before choosing this approach.
5153

5254
## Compliance to the spec
5355

src/actual.js

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@ exports.handler = function (options) {
66
return function (req, res, next) {
77
var originHeader = req.headers['origin']
88

9-
// If either no origin was set, or the origin isn't supported, continue
10-
// without setting any headers
11-
if (!originHeader || !matcher(originHeader)) {
9+
if (originMatcher.generic(options.origins)) {
10+
res.setHeader(constants['AC_ALLOW_ORIGIN'], '*')
11+
res.setHeader(constants['AC_ALLOW_CREDS'], false) // not compatible with *
12+
res.setHeader(constants['AC_EXPOSE_HEADERS'], options.exposeHeaders.join(', '))
13+
return next()
14+
} else {
15+
// If either no origin was set, or the origin isn't supported, continue
16+
// without setting any headers
17+
if (!originHeader || !matcher(originHeader)) {
18+
return next()
19+
}
20+
// if match was found, let's set some headers.
21+
res.setHeader(constants['AC_ALLOW_ORIGIN'], originHeader)
22+
res.setHeader(constants['STR_VARY'], constants['STR_ORIGIN'])
23+
if (options.credentials) {
24+
res.setHeader(constants['AC_ALLOW_CREDS'], 'true')
25+
}
26+
res.setHeader(constants['AC_EXPOSE_HEADERS'], options.exposeHeaders.join(', '))
1227
return next()
1328
}
14-
15-
// if match was found, let's set some headers.
16-
res.setHeader(constants['AC_ALLOW_ORIGIN'], originHeader)
17-
res.setHeader(constants['STR_VARY'], constants['STR_ORIGIN'])
18-
if (options.credentials) {
19-
res.setHeader(constants['AC_ALLOW_CREDS'], 'true')
20-
}
21-
res.setHeader(constants['AC_EXPOSE_HEADERS'],
22-
options.exposeHeaders.join(', '))
23-
24-
return next()
2529
}
2630
}

src/origin-matcher.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11

2+
exports.generic = function (list) {
3+
return list.length === 1 && list[0] === '*'
4+
}
5+
26
exports.create = function (allowedOrigins) {
37
// pre-compile list of matchers, so regexes are only built once
48
var matchers = allowedOrigins.map(createMatcher)

src/preflight.js

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,70 @@ var constants = require('./constants.js')
33

44
exports.handler = function (options) {
55
var matcher = originMatcher.create(options.origins)
6-
return function (req, res, next) {
7-
if (req.method !== 'OPTIONS') return next()
86

9-
// 6.2.1 and 6.2.2
10-
var originHeader = req.headers['origin']
11-
if (!matcher(originHeader)) return next()
7+
if (originMatcher.generic(options.origins)) {
8+
//
9+
// If origins = ['*'] then we always set generic CORS headers
10+
// This is the simplest case, similar to what restify.fullResponse() used to do
11+
// Must must keep the headers generic because they can be cached by reverse proxies
12+
//
1213

13-
// 6.2.3
14-
var requestedMethod = req.headers[constants['AC_REQ_METHOD']]
15-
if (!requestedMethod) return next()
14+
return function (req, res, next) {
15+
if (req.method !== 'OPTIONS') return next()
16+
res.once('header', function () {
17+
res.header(constants['AC_ALLOW_ORIGIN'], '*')
18+
res.header(constants['AC_ALLOW_CREDS'], false) // not compatible with *
19+
res.header(constants['AC_ALLOW_METHODS'], 'GET, PUT, POST, DELETE, OPTIONS')
20+
res.header(constants['AC_ALLOW_HEADERS'], options.allowHeaders.join(', '))
21+
})
22+
res.send(constants['HTTP_NO_CONTENT'])
23+
}
24+
} else {
25+
//
26+
// Full CORS mode
27+
// This is the "better" option where we have a list of origins
28+
// In this case, we return customised CORS headers for each request
29+
// And must set the "Vary: Origin" header
30+
//
1631

17-
// 6.2.4
18-
// var requestedHeaders = req.headers[constants['AC_REQ_HEADERS']]
19-
// requestedHeaders = requestedHeaders ? requestedHeaders.split(', ') : []
32+
return function (req, res, next) {
33+
if (req.method !== 'OPTIONS') return next()
2034

21-
var allowedMethods = [requestedMethod, 'OPTIONS']
22-
var allowedHeaders = constants['ALLOW_HEADERS']
23-
.concat(options.allowHeaders)
35+
// 6.2.1 and 6.2.2
36+
var originHeader = req.headers['origin']
37+
if (!matcher(originHeader)) return next()
2438

25-
res.once('header', function () {
26-
// 6.2.7
27-
res.header(constants['AC_ALLOW_ORIGIN'], originHeader)
28-
res.header(constants['AC_ALLOW_CREDS'], true)
39+
// 6.2.3
40+
var requestedMethod = req.headers[constants['AC_REQ_METHOD']]
41+
if (!requestedMethod) return next()
2942

30-
// 6.2.8
31-
if (options.preflightMaxAge) {
32-
res.header(constants['AC_MAX_AGE'], options.preflightMaxAge)
33-
}
43+
// 6.2.4
44+
// var requestedHeaders = req.headers[constants['AC_REQ_HEADERS']]
45+
// requestedHeaders = requestedHeaders ? requestedHeaders.split(', ') : []
46+
var allowedMethods = [requestedMethod, 'OPTIONS']
47+
var allowedHeaders = constants['ALLOW_HEADERS'].concat(options.allowHeaders)
3448

35-
// 6.2.9
36-
res.header(constants['AC_ALLOW_METHODS'], allowedMethods.join(', '))
49+
res.once('header', function () {
50+
// 6.2.7
51+
res.header(constants['AC_ALLOW_ORIGIN'], originHeader)
52+
res.header(constants['AC_ALLOW_CREDS'], true)
3753

38-
// 6.2.10
39-
res.header(constants['AC_ALLOW_HEADERS'], allowedHeaders.join(', '))
40-
})
54+
// 6.2.8
55+
if (options.preflightMaxAge) {
56+
res.header(constants['AC_MAX_AGE'], options.preflightMaxAge)
57+
}
4158

42-
res.send(constants['HTTP_NO_CONTENT'])
59+
// 6.2.9
60+
res.header(constants['AC_ALLOW_METHODS'], allowedMethods.join(', '))
61+
62+
// 6.2.10
63+
res.header(constants['AC_ALLOW_HEADERS'], allowedHeaders.join(', '))
64+
65+
// 6.4
66+
res.header('Vary', 'Origin')
67+
})
68+
69+
res.send(constants['HTTP_NO_CONTENT'])
70+
}
4371
}
4472
}

test/cors.actual.spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,30 @@ describe('CORS: simple / actual requests', function () {
9494
.expect(200)
9595
.end(done)
9696
})
97+
98+
xit('6.4 FIXME Does not need the Vary header if it accepts all Origins', function (done) {
99+
var server = test.corsServer({
100+
origins: ['*']
101+
})
102+
request(server)
103+
.get('/test')
104+
.set('Origin', 'http://api.myapp.com')
105+
.expect('access-control-allow-origin', '*')
106+
.expect(test.noHeader('vary'))
107+
.expect(200)
108+
.end(done)
109+
})
110+
111+
xit('6.4 FIXME Sets the Vary header if it returns the actual origin', function (done) {
112+
var server = test.corsServer({
113+
origins: ['http://api.myapp.com']
114+
})
115+
request(server)
116+
.get('/test')
117+
.set('Origin', 'http://api.myapp.com')
118+
.expect('access-control-allow-origin', 'http://api.myapp.com')
119+
.expect('vary', 'Origin')
120+
.expect(200)
121+
.end(done)
122+
})
97123
})

0 commit comments

Comments
 (0)