From c080766aa95ede3620ccf05421039efc992db2c6 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 20 Sep 2022 09:53:10 +1000 Subject: [PATCH 1/4] fix: invalid range crashes server --- spec/ParseFile.spec.js | 137 ++++++++++++++++++++-- src/Adapters/Files/GridFSBucketAdapter.js | 33 ++++-- src/Routers/FilesRouter.js | 7 +- 3 files changed, 156 insertions(+), 21 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 281ce45cc2..40ad112ae6 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -692,7 +692,126 @@ describe('Parse.File testing', () => { }); }); - xdescribe('Gridstore Range tests', () => { + describe('Gridstore Range tests', () => { + it('supports bytes range out of range', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1//files/file.txt ', + body: repeat('argle bargle', 100), + }); + const b = response.data; + const file = await request({ + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + Range: 'bytes=15000-18000', + }, + }); + expect(file.headers['content-range']).toBe('bytes 1212-1212/1212'); + }); + + it('supports bytes range if end greater than start', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1//files/file.txt ', + body: repeat('argle bargle', 100), + }); + const b = response.data; + const file = await request({ + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + Range: 'bytes=15000-100', + }, + }); + expect(file.headers['content-range']).toBe('bytes 100-1212/1212'); + }); + + it('supports bytes range if end is undefined', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1//files/file.txt ', + body: repeat('argle bargle', 100), + }); + const b = response.data; + const file = await request({ + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + Range: 'bytes=100-', + }, + }); + expect(file.headers['content-range']).toBe('bytes 100-1212/1212'); + }); + + it('supports bytes range if start and end undefined', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1//files/file.txt ', + body: repeat('argle bargle', 100), + }); + const b = response.data; + const file = await request({ + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + Range: 'bytes=abc-efs', + }, + }).catch(e => e); + expect(file.headers['content-range']).toBeUndefined(); + }); + + it('supports bytes range if start and end undefined', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1//files/file.txt ', + body: repeat('argle bargle', 100), + }); + const b = response.data; + const file = await request({ + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + }, + }).catch(e => e); + expect(file.headers['content-range']).toBeUndefined(); + }); + it('supports range requests', done => { const headers = { 'Content-Type': 'application/octet-stream', @@ -781,7 +900,7 @@ describe('Parse.File testing', () => { }); }); - xit('supports getting last n bytes', done => { + it('supports getting last n bytes', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -879,8 +998,8 @@ describe('Parse.File testing', () => { }); }); - it('fails to stream unknown file', done => { - request({ + it('fails to stream unknown file', async () => { + const response = await request({ url: 'http://localhost:8378/1/files/test/file.txt', headers: { 'Content-Type': 'application/octet-stream', @@ -888,12 +1007,10 @@ describe('Parse.File testing', () => { 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=13-240', }, - }).then(response => { - expect(response.status).toBe(404); - const body = response.text; - expect(body).toEqual('File not found.'); - done(); - }); + }).catch(e => e); + expect(response.status).toBe(404); + const body = response.text; + expect(body).toEqual('File not found.'); }); }); diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 06896e73b5..a03b90814e 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -228,22 +228,35 @@ export class GridFSBucketAdapter extends FilesAdapter { const partialstart = parts[0]; const partialend = parts[1]; - const start = parseInt(partialstart, 10); - const end = partialend ? parseInt(partialend, 10) : files[0].length - 1; + const fileLength = files[0].length; + const fileStart = parseInt(partialstart, 10); + const fileEnd = partialend ? parseInt(partialend, 10) : fileLength; - res.writeHead(206, { - 'Accept-Ranges': 'bytes', - 'Content-Length': end - start + 1, - 'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length, - 'Content-Type': contentType, - }); + let start = Math.min(fileStart || 0, fileEnd, fileLength); + let end = Math.max(fileStart || 0, fileEnd) + 1 || fileLength; + end = Math.min(end, fileLength); + if (isNaN(fileStart)) { + start = fileLength - end + 1; + end = fileLength; + } + start = Math.max(start, 0); + + res.status(206); + res.header('Accept-Ranges', 'bytes'); + res.header('Content-Length', end - start); + res.header('Content-Range', 'bytes ' + start + '-' + end + '/' + fileLength); + res.header('Content-Type', contentType); const stream = bucket.openDownloadStreamByName(filename); stream.start(start); + if (end) { + stream.end(end); + } stream.on('data', chunk => { res.write(chunk); }); - stream.on('error', () => { - res.sendStatus(404); + stream.on('error', (e) => { + res.status(404); + res.send(e.message); }); stream.on('end', () => { res.end(); diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 2ea33987ba..f8f25475a7 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -266,5 +266,10 @@ export class FilesRouter { } function isFileStreamable(req, filesController) { - return req.get('Range') && typeof filesController.adapter.handleFileStream === 'function'; + const range = (req.get('Range') || '/-/').split('-'); + const start = Number(range[0]); + const end = Number(range[1]); + return ( + (!isNaN(start) || !isNaN(end)) && typeof filesController.adapter.handleFileStream === 'function' + ); } From 5e539cb28f313b995865ea63cab5fa7b9de5a10b Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 23 Sep 2022 20:53:41 +1000 Subject: [PATCH 2/4] changes --- spec/ParseFile.spec.js | 74 +++++++++++++++++++++++ src/Adapters/Files/GridFSBucketAdapter.js | 2 +- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 40ad112ae6..ea8b46e118 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -812,6 +812,80 @@ describe('Parse.File testing', () => { expect(file.headers['content-range']).toBeUndefined(); }); + it('supports bytes range if end is greater than size', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1//files/file.txt ', + body: repeat('argle bargle', 100), + }); + const b = response.data; + const file = await request({ + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + Range: 'bytes=0-2000', + }, + }).catch(e => e); + expect(file.headers['content-range']).toBe('bytes 0-1212/1212'); + }); + + it('supports bytes range if end is greater than size', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1//files/file.txt ', + body: repeat('argle bargle', 100), + }); + const b = response.data; + const file = await request({ + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + Range: 'bytes=0-2000', + }, + }).catch(e => e); + expect(file.headers['content-range']).toBe('bytes 0-1212/1212'); + }); + + it('supports bytes range with 0 length', async () => { + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1//files/file.txt ', + body: 'a', + }).catch(e => e); + console.log(response.text); + const b = response.data; + const file = await request({ + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + Range: 'bytes=-2000', + }, + }).catch(e => e); + console.log(response); + expect(file.headers['content-range']).toBe('bytes 0-1/1'); + }); + it('supports range requests', done => { const headers = { 'Content-Type': 'application/octet-stream', diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index a03b90814e..8ff4d2b8eb 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -234,11 +234,11 @@ export class GridFSBucketAdapter extends FilesAdapter { let start = Math.min(fileStart || 0, fileEnd, fileLength); let end = Math.max(fileStart || 0, fileEnd) + 1 || fileLength; - end = Math.min(end, fileLength); if (isNaN(fileStart)) { start = fileLength - end + 1; end = fileLength; } + end = Math.min(end, fileLength); start = Math.max(start, 0); res.status(206); From 2788184d29b99c0161daa16e0c739588b3e24a45 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 23 Sep 2022 20:54:02 +1000 Subject: [PATCH 3/4] Update ParseFile.spec.js --- spec/ParseFile.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index ea8b46e118..3e15a5894c 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -872,7 +872,6 @@ describe('Parse.File testing', () => { url: 'http://localhost:8378/1//files/file.txt ', body: 'a', }).catch(e => e); - console.log(response.text); const b = response.data; const file = await request({ url: b.url, @@ -882,7 +881,6 @@ describe('Parse.File testing', () => { Range: 'bytes=-2000', }, }).catch(e => e); - console.log(response); expect(file.headers['content-range']).toBe('bytes 0-1/1'); }); From 8c97b55724bcbed000a90688342206a2464d9f14 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 15 Oct 2022 00:09:43 +0200 Subject: [PATCH 4/4] Update ParseFile.spec.js --- spec/ParseFile.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 3e15a5894c..ed21304d39 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -692,7 +692,7 @@ describe('Parse.File testing', () => { }); }); - describe('Gridstore Range tests', () => { + describe_only_db('mongo')('Gridstore Range', () => { it('supports bytes range out of range', async () => { const headers = { 'Content-Type': 'application/octet-stream',