Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"workspaces": [
"packages/*",
"test"
Expand Down
5 changes: 3 additions & 2 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@
"@tus/utils": "^0.6.0",
"debug": "^4.3.4",
"lodash.throttle": "^4.1.1",
"set-cookie-parser": "^2.7.1"
"set-cookie-parser": "^2.7.1",
"srvx": "^0.2.7"
},
"devDependencies": {
"@types/debug": "^4.1.12",
"@types/lodash.throttle": "^4.1.9",
"@types/mocha": "^10.0.6",
"@types/set-cookie-parser": "^2.4.10",
"@types/node": "^22.13.7",
"@types/set-cookie-parser": "^2.4.10",
"@types/sinon": "^17.0.3",
"@types/supertest": "^2.0.16",
"mocha": "^11.0.1",
Expand Down
30 changes: 17 additions & 13 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import http from 'node:http'
import {EventEmitter} from 'node:events'

import {toNodeHandler} from 'srvx/node'
import debug from 'debug'
import {EVENTS, ERRORS, EXPOSED_HEADERS, REQUEST_METHODS, TUS_RESUMABLE} from '@tus/utils'
import type {DataStore, Upload, CancellationContext} from '@tus/utils'

import {BaseHandler} from './handlers/BaseHandler.js'
import {GetHandler} from './handlers/GetHandler.js'
import {HeadHandler} from './handlers/HeadHandler.js'
import {OptionsHandler} from './handlers/OptionsHandler.js'
Expand All @@ -15,7 +15,6 @@ import {DeleteHandler} from './handlers/DeleteHandler.js'
import {validateHeader} from './validators/HeaderValidator.js'
import type {ServerOptions, RouteHandler, WithOptional} from './types.js'
import {MemoryLocker} from './lockers/index.js'
import {getRequest, setResponse} from './web.js'

type Handlers = {
GET: InstanceType<typeof GetHandler>
Expand Down Expand Up @@ -119,19 +118,11 @@ export class Server extends EventEmitter {
}

get(path: string, handler: RouteHandler) {
this.handlers.GET.registerPath(this.options.path + path, handler)
this.handlers.GET.registerPath(path, handler)
}

async handle(req: http.IncomingMessage, res: http.ServerResponse) {
const {proto, host} = BaseHandler.extractHostAndProto(
// @ts-expect-error it's fine
new Headers(req.headers),
this.options.respectForwardedHeaders
)
const base = `${proto}://${host}${this.options.path}`
const webReq = await getRequest({request: req, base})
const webRes = await this.handler(webReq)
return setResponse(res, webRes)
return toNodeHandler(this.handler.bind(this))(req, res)
}

async handleWeb(req: Request) {
Expand All @@ -142,6 +133,19 @@ export class Server extends EventEmitter {
const context = this.createContext()
const headers = new Headers()

// @ts-expect-error temporary until https://github.com/unjs/srvx/issues/44 is fixed
req.headers.get = (key: string) => {
for (const [k, v] of req.headers.entries()) {
if (k === key) {
if (v === '') {
return null
}
return v
}
}
return null
}

const onError = async (error: {
status_code?: number
body?: string
Expand All @@ -164,7 +168,7 @@ export class Server extends EventEmitter {
if (req.method === 'GET') {
const handler = this.handlers.GET
const res = await handler.send(req, context, headers).catch(onError)
context.abort
context.abort()
return res
}

Expand Down
134 changes: 81 additions & 53 deletions packages/server/src/test/Server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,75 +264,71 @@ describe('Server', () => {
done()
})

it('should allow overriding the HTTP origin', async () => {
it('should allow overriding the HTTP origin', (done) => {
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)
request(listener)
.options('/')
.set('Origin', origin)
.expect(204)
.then((res) => {
assert.equal(res.headers['access-control-allow-origin'], origin)
done()
})
.catch(done)
})

it('should allow overriding the HTTP origin only if match allowedOrigins', async () => {
it('should allow overriding the HTTP origin only if match allowedOrigins', (done) => {
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')
request(listener)
.options('/')
.set('Origin', origin)
.expect(204)
.then((res) => {
assert.equal(res.headers['access-control-allow-origin'], origin)
done()
})
.catch(done)
})

it('should allow overriding the HTTP origin only if match allowedOrigins with multiple allowed domains', async () => {
it('should allow overriding the HTTP origin only if match allowedOrigins with multiple allowed domains', (done) => {
const origin = 'vimeo.com'
server.options.allowedOrigins = ['google.com', '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')
request(listener)
.options('/')
.set('Origin', origin)
.expect(204)
.then((res) => {
assert.equal(res.headers['access-control-allow-origin'], origin)
done()
})
.catch(done)
})

it(`should now allow overriding the HTTP origin if doesn't match allowedOrigins`, async () => {
it('should not allow overriding the HTTP origin if does not match allowedOrigins', (done) => {
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')
request(listener)
.options('/')
.set('Origin', origin)
.expect(204)
.then((res) => {
assert.equal(res.headers['access-control-allow-origin'], 'google.com')
done()
})
.catch(done)
})

it('should return Access-Control-Allow-Origin if no origin header', async () => {
it('should return Access-Control-Allow-Origin if no origin header', (done) => {
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')
request(listener)
.options('/')
.expect(204)
.then((res) => {
assert.equal(res.headers['access-control-allow-origin'], 'google.com')
done()
})
.catch(done)
})

it('should not invoke handlers if onIncomingRequest throws', (done) => {
Expand Down Expand Up @@ -427,6 +423,38 @@ describe('Server', () => {
})
})

it('should preserve custom request', (done) => {
const userData = {username: 'admin'}
const server = new Server({
path: '/test/output',
datastore: new FileStore({directory}),
async onIncomingRequest(req) {
// @ts-expect-error fine
if (req?.node?.req?.user?.username === 'admin') {
done()
} else {
done(new Error('user data should be preserved in onIncomingRequest'))
}
},
})

// Simulate Express middleware by adding user property to request
const req = httpMocks.createRequest({
method: 'POST',
url: server.options.path,
headers: {
'tus-resumable': TUS_RESUMABLE,
'upload-length': '12345678',
},
})

// Add custom property like Express middleware would
req.user = userData

const res = httpMocks.createResponse({req})
server.handle(req, res)
})

it('should fire when a file is deleted', (done) => {
server.on(EVENTS.POST_TERMINATE, (req, id) => {
assert.ok(req)
Expand Down
Loading