Skip to content

Commit

Permalink
@tus/server: add allowedCredentials and allowedOrigins options (#636)
Browse files Browse the repository at this point in the history
Co-authored-by: Merlijn Vos <merlijn@soverin.net>
  • Loading branch information
idanto and Murderlon authored Sep 5, 2024
1 parent 8b46c44 commit ca03351
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changeset/warm-bikes-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tus/server': minor
---

- Add `allowedCredentials` option for the Access-Control-Allow-Credentials header
- Add `allowedOrigins` option for setting domains in Access-Control-Allow-Origin
10 changes: 10 additions & 0 deletions packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ Max file size (in bytes) allowed when uploading (`number` |
(`(req, id: string | null) => Promise<number> | number`)). When providing a function
during the OPTIONS request the id will be `null`.

#### `options.allowedCredentials`

Sets `Access-Control-Allow-Credentials` (`boolean`, default: `false`).

#### `options.allowedOrigins`

Trusted origins (`string[]`).

Sends the client's origin back in `Access-Control-Allow-Origin` if it matches.

#### `options.postReceiveInterval`

Interval in milliseconds for sending progress of an upload over
Expand Down
23 changes: 21 additions & 2 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,11 @@ export class Server extends EventEmitter {
}

// Enable CORS
res.setHeader('Access-Control-Allow-Origin', this.getCorsOrigin(req))
res.setHeader('Access-Control-Expose-Headers', EXPOSED_HEADERS)
if (req.headers.origin) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)

if (this.options.allowedCredentials === true) {
res.setHeader('Access-Control-Allow-Credentials', 'true')
}

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

private getCorsOrigin(req: http.IncomingMessage): string {
const origin = req.headers.origin
const isOriginAllowed =
this.options.allowedOrigins?.some((allowedOrigin) => allowedOrigin === origin) ??
true

if (origin && isOriginAllowed) {
return origin
}

if (this.options.allowedOrigins && this.options.allowedOrigins.length > 0) {
return this.options.allowedOrigins[0]
}

return '*'
}

write(
context: CancellationContext,
req: http.IncomingMessage,
Expand Down
10 changes: 10 additions & 0 deletions packages/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ export type ServerOptions = {
*/
allowedHeaders?: string[]

/**
* Set `Access-Control-Allow-Credentials` to true or false (the default)
*/
allowedCredentials?: boolean

/**
* Add trusted origins to `Access-Control-Allow-Origin`.
*/
allowedOrigins?: string[]

/**
* Interval in milliseconds for sending progress of an upload over `EVENTS.POST_RECEIVE_V2`
*/
Expand Down
73 changes: 72 additions & 1 deletion packages/server/test/Server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ describe('Server', () => {
})
})

it('OPTIONS should return returns custom headers in Access-Control-Allow-Credentials', (done) => {
server.options.allowedCredentials = true

request(listener)
.options('/')
.expect(204, '', (err, res) => {
res.headers.should.have.property('access-control-allow-credentials')
res.headers['access-control-allow-credentials'].should.containEql('true')
server.options.allowedCredentials = undefined
done(err)
})
})

it('HEAD should 404 non files', (done) => {
request(listener)
.head('/')
Expand Down Expand Up @@ -252,8 +265,37 @@ describe('Server', () => {
done()
})

it('should allow overriding the HTTP method', async () => {
it('should allow overriding the HTTP origin', async () => {
const origin = 'vimeo.com'
const req = httpMocks.createRequest({
headers: {origin},
method: 'OPTIONS',
url: '/',
})
// @ts-expect-error todo
const res = new http.ServerResponse({method: 'OPTIONS'})
await server.handle(req, res)
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
})

it('should allow overriding the HTTP origin only if match allowedOrigins', async () => {
const origin = 'vimeo.com'
server.options.allowedOrigins = ['vimeo.com']
const req = httpMocks.createRequest({
headers: {origin},
method: 'OPTIONS',
url: '/',
})
// @ts-expect-error todo
const res = new http.ServerResponse({method: 'OPTIONS'})
await server.handle(req, res)
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'vimeo.com')
})

it('should allow overriding the HTTP origin only if match allowedOrigins with multiple allowed domains', async () => {
const origin = 'vimeo.com'
server.options.allowedOrigins = ['google.com', 'vimeo.com']
const req = httpMocks.createRequest({
headers: {origin},
method: 'OPTIONS',
Expand All @@ -263,6 +305,35 @@ describe('Server', () => {
const res = new http.ServerResponse({method: 'OPTIONS'})
await server.handle(req, res)
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'vimeo.com')
})

it(`should now allow overriding the HTTP origin if doesn't match allowedOrigins`, async () => {
const origin = 'vimeo.com'
server.options.allowedOrigins = ['google.com']
const req = httpMocks.createRequest({
headers: {origin},
method: 'OPTIONS',
url: '/',
})
// @ts-expect-error todo
const res = new http.ServerResponse({method: 'OPTIONS'})
await server.handle(req, res)
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'google.com')
})

it('should return Access-Control-Allow-Origin if no origin header', async () => {
server.options.allowedOrigins = ['google.com']
const req = httpMocks.createRequest({
method: 'OPTIONS',
url: '/',
})
// @ts-expect-error todo
const res = new http.ServerResponse({method: 'OPTIONS'})
await server.handle(req, res)
assert.equal(res.hasHeader('Access-Control-Allow-Origin'), true)
assert.equal(res.getHeader('Access-Control-Allow-Origin'), 'google.com')
})

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

0 comments on commit ca03351

Please sign in to comment.