Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@tus/server: add allowedCredentials and allowedOrigins options #636

Merged
merged 12 commits into from
Sep 5, 2024
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 support in CORS Access-Control-Allow-Credentials header
- Add support in whitelist domains for Access-Control-Allow-Origin
38 changes: 36 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,38 @@ export class Server extends EventEmitter {
return this.write(context, req, res, 404, 'Not found\n')
}

private isOriginAllowed(origin?: string): boolean {
if (!origin) {
//If no origin header all allowed - backward compatibility
return true
}

if (this.options.allowedOrigins) {
//If there is a list the origin header should match the list
return this.options.allowedOrigins?.some((allowedOrigin) => {
return allowedOrigin === origin
})
} else {
//If there is no list of allowedOrigins all are allowed
return true
}
}

// Method to get the appropriate CORS origin
private getCorsOrigin(req: http.IncomingMessage): string {
const origin = req.headers.origin

if (origin && this.isOriginAllowed(origin)) {
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.
*/
allowedCredentials?: boolean

/**
* set `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
Loading