Skip to content

Commit

Permalink
Restrict when range request logic is applied. Closes #60
Browse files Browse the repository at this point in the history
  • Loading branch information
kanongil committed May 3, 2016
1 parent 0b33f1e commit bd6a007
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 13 deletions.
34 changes: 21 additions & 13 deletions lib/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,30 @@ internals.addContentRange = function (response, callback) {
if (!request.headers['if-range'] ||
request.headers['if-range'] === response.headers.etag) { // Ignoring last-modified date (weak)

// Parse header
// Check that response is not encoded once transmitted

const ranges = Ammo.header(request.headers.range, length);
if (!ranges) {
const error = Boom.rangeNotSatisfiable();
error.output.headers['content-range'] = 'bytes */' + length;
return callback(error);
}
const mime = request.server.mime.type(response.headers['content-type'] || 'application/octet-stream');
const encoding = (request.connection.settings.compression && mime.compressible && !response.headers['content-encoding'] ? request.info.acceptEncoding : null);

if (encoding === 'identity' || !encoding) {

// Parse header

const ranges = Ammo.header(request.headers.range, length);
if (!ranges) {
const error = Boom.rangeNotSatisfiable();
error.output.headers['content-range'] = 'bytes */' + length;
return callback(error);
}

// Prepare transform
// Prepare transform

if (ranges.length === 1) { // Ignore requests for multiple ranges
range = ranges[0];
response.code(206);
response.bytes(range.to - range.from + 1);
response.header('content-range', 'bytes ' + range.from + '-' + range.to + '/' + length);
if (ranges.length === 1) { // Ignore requests for multiple ranges
range = ranges[0];
response.code(206);
response.bytes(range.to - range.from + 1);
response.header('content-range', 'bytes ' + range.from + '-' + range.to + '/' + length);
}
}
}
}
Expand Down
113 changes: 113 additions & 0 deletions test/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,83 @@ describe('file', () => {
});
});

it('reads partial file content for a non-compressible file', (done) => {

const server = provisionServer();
server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/image.png'), etagMethod: false } } });

// Catch createReadStream options

let createOptions;
const orig = Fs.createReadStream;
Fs.createReadStream = function (path, options) {

Fs.createReadStream = orig;
createOptions = options;

return Fs.createReadStream(path, options);
};

server.inject({ url: '/file', headers: { 'range': 'bytes=1-4', 'accept-encoding': 'gzip' } }, (res) => {

expect(res.statusCode).to.equal(206);
expect(res.headers['content-length']).to.equal(4);
expect(res.headers['content-range']).to.equal('bytes 1-4/42010');
expect(res.headers['accept-ranges']).to.equal('bytes');
expect(res.rawPayload).to.deep.equal(new Buffer('PNG\r', 'ascii'));
expect(createOptions).to.include({ start: 1, end: 4 });
done();
});
});

it('returns 200 when content-length is missing', (done) => {

const server = provisionServer();
server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/image.png') } } });

server.ext('onPreResponse', (request, reply) => {

delete request.response.headers['content-length'];
return reply.continue();
});

server.inject({ url: '/file', headers: { 'range': 'bytes=1-5' } }, (res) => {

expect(res.statusCode).to.equal(200);
expect(res.headers['content-length']).to.not.exist();
done();
});
});

it('returns 200 for dynamically compressed responses', (done) => {

const server = provisionServer();
server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/note.txt'), lookupCompressed: false } } });
server.inject({ url: '/file', headers: { 'range': 'bytes=1-3', 'accept-encoding': 'gzip' } }, (res) => {

expect(res.statusCode).to.equal(200);
expect(res.headers['content-encoding']).to.equal('gzip');
expect(res.headers['content-length']).to.not.exist();
expect(res.headers['content-range']).to.not.exist();
expect(res.headers['accept-ranges']).to.equal('bytes');
done();
});
});

it('returns a subset of a file when compression is disabled', (done) => {

const server = provisionServer({ compression: false });
server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/note.txt'), lookupCompressed: false } } });
server.inject({ url: '/file', headers: { 'range': 'bytes=1-3', 'accept-encoding': 'gzip' } }, (res) => {

expect(res.statusCode).to.equal(206);
expect(res.headers['content-encoding']).to.not.exist();
expect(res.headers['content-length']).to.equal(3);
expect(res.headers['content-range']).to.equal('bytes 1-3/4');
done();
});
});

it('returns a subset of a file using precompressed file', (done) => {

const server = provisionServer();
Expand All @@ -1416,6 +1493,42 @@ describe('file', () => {
done();
});
});

it('returns a subset for dynamically compressed responses with "identity" encoding', (done) => {

const server = provisionServer();
server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/note.txt'), lookupCompressed: false } } });
server.inject({ url: '/file', headers: { 'range': 'bytes=1-3', 'accept-encoding': 'identity' } }, (res) => {

expect(res.statusCode).to.equal(206);
expect(res.headers['content-encoding']).to.not.exist();
expect(res.headers['content-length']).to.equal(3);
expect(res.headers['content-range']).to.equal('bytes 1-3/4');
done();
});
});

it('returns a subset when content-type is missing', (done) => {

const server = provisionServer();
server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/note.txt') } } });

server.ext('onPreResponse', (request, reply) => {

delete request.response.headers['content-type'];
return reply.continue();
});

server.inject({ url: '/file', headers: { 'range': 'bytes=1-5' } }, (res) => {

expect(res.statusCode).to.equal(206);
expect(res.headers['content-encoding']).to.not.exist();
expect(res.headers['content-length']).to.equal(3);
expect(res.headers['content-range']).to.equal('bytes 1-3/4');
expect(res.headers['content-type']).to.not.exist();
done();
});
});
});

it('has not leaked file descriptors', { skip: process.platform === 'win32' }, (done) => {
Expand Down

0 comments on commit bd6a007

Please sign in to comment.