Skip to content

Commit ca03351

Browse files
idantoMurderlon
andauthored
@tus/server: add allowedCredentials and allowedOrigins options (#636)
Co-authored-by: Merlijn Vos <[email protected]>
1 parent 8b46c44 commit ca03351

File tree

5 files changed

+119
-3
lines changed

5 files changed

+119
-3
lines changed

.changeset/warm-bikes-carry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@tus/server': minor
3+
---
4+
5+
- Add `allowedCredentials` option for the Access-Control-Allow-Credentials header
6+
- Add `allowedOrigins` option for setting domains in Access-Control-Allow-Origin

packages/server/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ Max file size (in bytes) allowed when uploading (`number` |
6969
(`(req, id: string | null) => Promise<number> | number`)). When providing a function
7070
during the OPTIONS request the id will be `null`.
7171

72+
#### `options.allowedCredentials`
73+
74+
Sets `Access-Control-Allow-Credentials` (`boolean`, default: `false`).
75+
76+
#### `options.allowedOrigins`
77+
78+
Trusted origins (`string[]`).
79+
80+
Sends the client's origin back in `Access-Control-Allow-Origin` if it matches.
81+
7282
#### `options.postReceiveInterval`
7383

7484
Interval in milliseconds for sending progress of an upload over

packages/server/src/server.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,11 @@ export class Server extends EventEmitter {
219219
}
220220

221221
// Enable CORS
222+
res.setHeader('Access-Control-Allow-Origin', this.getCorsOrigin(req))
222223
res.setHeader('Access-Control-Expose-Headers', EXPOSED_HEADERS)
223-
if (req.headers.origin) {
224-
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
224+
225+
if (this.options.allowedCredentials === true) {
226+
res.setHeader('Access-Control-Allow-Credentials', 'true')
225227
}
226228

227229
// Invoke the handler for the method requested
@@ -233,6 +235,23 @@ export class Server extends EventEmitter {
233235
return this.write(context, req, res, 404, 'Not found\n')
234236
}
235237

238+
private getCorsOrigin(req: http.IncomingMessage): string {
239+
const origin = req.headers.origin
240+
const isOriginAllowed =
241+
this.options.allowedOrigins?.some((allowedOrigin) => allowedOrigin === origin) ??
242+
true
243+
244+
if (origin && isOriginAllowed) {
245+
return origin
246+
}
247+
248+
if (this.options.allowedOrigins && this.options.allowedOrigins.length > 0) {
249+
return this.options.allowedOrigins[0]
250+
}
251+
252+
return '*'
253+
}
254+
236255
write(
237256
context: CancellationContext,
238257
req: http.IncomingMessage,

packages/server/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ export type ServerOptions = {
3434
*/
3535
allowedHeaders?: string[]
3636

37+
/**
38+
* Set `Access-Control-Allow-Credentials` to true or false (the default)
39+
*/
40+
allowedCredentials?: boolean
41+
42+
/**
43+
* Add trusted origins to `Access-Control-Allow-Origin`.
44+
*/
45+
allowedOrigins?: string[]
46+
3747
/**
3848
* Interval in milliseconds for sending progress of an upload over `EVENTS.POST_RECEIVE_V2`
3949
*/

packages/server/test/Server.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,19 @@ describe('Server', () => {
165165
})
166166
})
167167

168+
it('OPTIONS should return returns custom headers in Access-Control-Allow-Credentials', (done) => {
169+
server.options.allowedCredentials = true
170+
171+
request(listener)
172+
.options('/')
173+
.expect(204, '', (err, res) => {
174+
res.headers.should.have.property('access-control-allow-credentials')
175+
res.headers['access-control-allow-credentials'].should.containEql('true')
176+
server.options.allowedCredentials = undefined
177+
done(err)
178+
})
179+
})
180+
168181
it('HEAD should 404 non files', (done) => {
169182
request(listener)
170183
.head('/')
@@ -252,8 +265,37 @@ describe('Server', () => {
252265
done()
253266
})
254267

255-
it('should allow overriding the HTTP method', async () => {
268+
it('should allow overriding the HTTP origin', async () => {
269+
const origin = 'vimeo.com'
270+
const req = httpMocks.createRequest({
271+
headers: {origin},
272+
method: 'OPTIONS',
273+
url: '/',
274+
})
275+
// @ts-expect-error todo
276+
const res = new http.ServerResponse({method: 'OPTIONS'})
277+
await server.handle(req, res)
278+
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
279+
})
280+
281+
it('should allow overriding the HTTP origin only if match allowedOrigins', async () => {
282+
const origin = 'vimeo.com'
283+
server.options.allowedOrigins = ['vimeo.com']
284+
const req = httpMocks.createRequest({
285+
headers: {origin},
286+
method: 'OPTIONS',
287+
url: '/',
288+
})
289+
// @ts-expect-error todo
290+
const res = new http.ServerResponse({method: 'OPTIONS'})
291+
await server.handle(req, res)
292+
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
293+
assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'vimeo.com')
294+
})
295+
296+
it('should allow overriding the HTTP origin only if match allowedOrigins with multiple allowed domains', async () => {
256297
const origin = 'vimeo.com'
298+
server.options.allowedOrigins = ['google.com', 'vimeo.com']
257299
const req = httpMocks.createRequest({
258300
headers: {origin},
259301
method: 'OPTIONS',
@@ -263,6 +305,35 @@ describe('Server', () => {
263305
const res = new http.ServerResponse({method: 'OPTIONS'})
264306
await server.handle(req, res)
265307
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
308+
assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'vimeo.com')
309+
})
310+
311+
it(`should now allow overriding the HTTP origin if doesn't match allowedOrigins`, async () => {
312+
const origin = 'vimeo.com'
313+
server.options.allowedOrigins = ['google.com']
314+
const req = httpMocks.createRequest({
315+
headers: {origin},
316+
method: 'OPTIONS',
317+
url: '/',
318+
})
319+
// @ts-expect-error todo
320+
const res = new http.ServerResponse({method: 'OPTIONS'})
321+
await server.handle(req, res)
322+
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
323+
assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'google.com')
324+
})
325+
326+
it('should return Access-Control-Allow-Origin if no origin header', async () => {
327+
server.options.allowedOrigins = ['google.com']
328+
const req = httpMocks.createRequest({
329+
method: 'OPTIONS',
330+
url: '/',
331+
})
332+
// @ts-expect-error todo
333+
const res = new http.ServerResponse({method: 'OPTIONS'})
334+
await server.handle(req, res)
335+
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
336+
assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'google.com')
266337
})
267338

268339
it('should not invoke handlers if onIncomingRequest throws', (done) => {

0 commit comments

Comments
 (0)