Skip to content

Commit

Permalink
Add the user of express-processimage to veto individual operations.
Browse files Browse the repository at this point in the history
Works by passing { allowOperation: function (operationName, operationArgs) {...} }
and returning either true or false.

Along with the maxInputPixels/maxOutputPixels, this sort of fixes #4.
  • Loading branch information
papandreou committed Nov 23, 2015
1 parent 72bd087 commit 2f86e3c
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 127 deletions.
248 changes: 126 additions & 122 deletions lib/getFilterInfosAndTargetContentTypeFromQueryString.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,140 +200,144 @@ module.exports = function getFilterInfosAndTargetContentTypeFromQueryString(quer
}
}) : [];

var filterInfo;
if (filters[operationName]) {
flushOperations();
filterInfo = filters[operationName](operationArgs, {
numPreceedingFilters: filterInfos.length
});
if (filterInfo) {
filterInfo.usedQueryStringFragments = [keyValuePair];
filterInfo.operationName = operationName;
if (filterInfo.outputContentType) {
targetContentType = filterInfo.outputContentType;
if (typeof options.allowOperation === 'function' && !options.allowOperation(operationName, operationArgs)) {
leftOverQueryStringFragments.push(keyValuePair);
} else {
var filterInfo;
if (filters[operationName]) {
flushOperations();
filterInfo = filters[operationName](operationArgs, {
numPreceedingFilters: filterInfos.length
});
if (filterInfo) {
filterInfo.usedQueryStringFragments = [keyValuePair];
filterInfo.operationName = operationName;
if (filterInfo.outputContentType) {
targetContentType = filterInfo.outputContentType;
}
filterInfos.push(filterInfo);
operationNames.push(operationName);
usedQueryStringFragments.push(keyValuePair);
} else {
leftOverQueryStringFragments.push(keyValuePair);
}
filterInfos.push(filterInfo);
operationNames.push(operationName);
usedQueryStringFragments.push(keyValuePair);
} else {
leftOverQueryStringFragments.push(keyValuePair);
}
} else if (operationName === 'metadata' && sharp) {
flushOperations();
targetContentType = 'application/json; charset=utf-8';
filterInfos.push({
metadata: true,
outputContentType: targetContentType,
create: function () {
var sharpInstance = sharp();
var duplexStream = new Stream.Duplex();
duplexStream._write = function (chunk, encoding, cb) {
if (sharpInstance.write(chunk, encoding) === false) {
sharpInstance.once('drain', cb);
} else {
cb();
}
};
duplexStream._read = function (size) {
sharpInstance.metadata().then(function (metadata) {
if (metadata.format) {
// metadata.format is one of 'jpeg', 'png', 'webp' so this should be safe:
metadata.contentType = 'image/' + metadata.format;
} else if (operationName === 'metadata' && sharp) {
flushOperations();
targetContentType = 'application/json; charset=utf-8';
filterInfos.push({
metadata: true,
outputContentType: targetContentType,
create: function () {
var sharpInstance = sharp();
var duplexStream = new Stream.Duplex();
duplexStream._write = function (chunk, encoding, cb) {
if (sharpInstance.write(chunk, encoding) === false) {
sharpInstance.once('drain', cb);
} else {
cb();
}
_.defaults(metadata, sourceMetadata);
if (metadata.exif) {
var exifData;
try {
exifData = exifReader(metadata.exif);
} catch (e) {
// Error: Invalid EXIF
};
duplexStream._read = function (size) {
sharpInstance.metadata().then(function (metadata) {
if (metadata.format) {
// metadata.format is one of 'jpeg', 'png', 'webp' so this should be safe:
metadata.contentType = 'image/' + metadata.format;
}
metadata.exif = undefined;
if (exifData) {
_.defaults(metadata, exifData);
_.defaults(metadata, sourceMetadata);
if (metadata.exif) {
var exifData;
try {
exifData = exifReader(metadata.exif);
} catch (e) {
// Error: Invalid EXIF
}
metadata.exif = undefined;
if (exifData) {
_.defaults(metadata, exifData);
}
}
}
if (metadata.icc) {
try {
metadata.icc = icc.parse(metadata.icc);
} catch (e) {
// Error: Error: Invalid ICC profile, remove the Buffer
metadata.icc = undefined;
if (metadata.icc) {
try {
metadata.icc = icc.parse(metadata.icc);
} catch (e) {
// Error: Error: Invalid ICC profile, remove the Buffer
metadata.icc = undefined;
}
}
}
duplexStream.push(JSON.stringify(metadata));
duplexStream.push(null);
}, function (err) {
duplexStream.emit('error', err);
duplexStream.push(JSON.stringify(metadata));
duplexStream.push(null);
}, function (err) {
duplexStream.emit('error', err);
});
};
duplexStream.on('finish', function () {
sharpInstance.end();
});
};
duplexStream.on('finish', function () {
sharpInstance.end();
});
return duplexStream;
}
});
} else if (isOperationByEngineNameAndName[operationName]) {
usedQueryStringFragments.push(keyValuePair);
flushOperations();
defaultEngineName = operationName;
} else if (engineNamesByOperationName[operationName]) {
// Check if at least one of the engines supporting this operation is allowed
var candidateEngineNames = engineNamesByOperationName[operationName].filter(function (engineName) {
return filters[engineName] !== false;
});
if (candidateEngineNames.length > 0) {
if (currentEngineName && !isOperationByEngineNameAndName[currentEngineName]) {
flushOperations();
}

if (!currentEngineName || candidateEngineNames.indexOf(currentEngineName) === -1) {
if (candidateEngineNames.indexOf(defaultEngineName) !== -1) {
currentEngineName = defaultEngineName;
} else {
currentEngineName = candidateEngineNames[0];
}
}
var sourceContentType = targetContentType;
if (operationName === 'setFormat' && operationArgs.length > 0) {
var targetFormat = operationArgs[0].toLowerCase();
if (targetFormat === 'jpg') {
targetFormat = 'jpeg';
return duplexStream;
}
targetContentType = 'image/' + targetFormat;
} else if (operationName === 'jpeg' || operationName === 'png' || operationName === 'webp') {
targetContentType = 'image/' + operationName;
}
operations.push({sourceContentType: sourceContentType, name: operationName, args: operationArgs, usedQueryStringFragment: keyValuePair});
});
} else if (isOperationByEngineNameAndName[operationName]) {
usedQueryStringFragments.push(keyValuePair);
}
} else {
var operationNameLowerCase = operationName.toLowerCase(),
FilterConstructor = filterConstructorByOperationName[operationNameLowerCase];
if (FilterConstructor && filters[operationNameLowerCase] !== false) {
operationNames.push(operationNameLowerCase);
flushOperations();
if (operationNameLowerCase === 'svgfilter' && root && options.sourceFilePath) {
operationArgs.push('--root', 'file://' + root, '--url', 'file://' + options.sourceFilePath);
defaultEngineName = operationName;
} else if (engineNamesByOperationName[operationName]) {
// Check if at least one of the engines supporting this operation is allowed
var candidateEngineNames = engineNamesByOperationName[operationName].filter(function (engineName) {
return filters[engineName] !== false;
});
if (candidateEngineNames.length > 0) {
if (currentEngineName && !isOperationByEngineNameAndName[currentEngineName]) {
flushOperations();
}

if (!currentEngineName || candidateEngineNames.indexOf(currentEngineName) === -1) {
if (candidateEngineNames.indexOf(defaultEngineName) !== -1) {
currentEngineName = defaultEngineName;
} else {
currentEngineName = candidateEngineNames[0];
}
}
var sourceContentType = targetContentType;
if (operationName === 'setFormat' && operationArgs.length > 0) {
var targetFormat = operationArgs[0].toLowerCase();
if (targetFormat === 'jpg') {
targetFormat = 'jpeg';
}
targetContentType = 'image/' + targetFormat;
} else if (operationName === 'jpeg' || operationName === 'png' || operationName === 'webp') {
targetContentType = 'image/' + operationName;
}
operations.push({sourceContentType: sourceContentType, name: operationName, args: operationArgs, usedQueryStringFragment: keyValuePair});
usedQueryStringFragments.push(keyValuePair);
}
filterInfo = {
create: function () {
return new FilterConstructor(operationArgs);
},
operationName: operationNameLowerCase,
usedQueryStringFragments: [keyValuePair]
};
filterInfos.push(filterInfo);
usedQueryStringFragments.push(keyValuePair);
if (operationNameLowerCase === 'inkscape') {
var filter = filterInfo.create();
filterInfo.create = function () {
return filter;
} else {
var operationNameLowerCase = operationName.toLowerCase(),
FilterConstructor = filterConstructorByOperationName[operationNameLowerCase];
if (FilterConstructor && filters[operationNameLowerCase] !== false) {
operationNames.push(operationNameLowerCase);
flushOperations();
if (operationNameLowerCase === 'svgfilter' && root && options.sourceFilePath) {
operationArgs.push('--root', 'file://' + root, '--url', 'file://' + options.sourceFilePath);
}
filterInfo = {
create: function () {
return new FilterConstructor(operationArgs);
},
operationName: operationNameLowerCase,
usedQueryStringFragments: [keyValuePair]
};
targetContentType = 'image/' + filter.outputFormat;
filterInfos.push(filterInfo);
usedQueryStringFragments.push(keyValuePair);
if (operationNameLowerCase === 'inkscape') {
var filter = filterInfo.create();
filterInfo.create = function () {
return filter;
};
targetContentType = 'image/' + filter.outputFormat;
}
} else {
leftOverQueryStringFragments.push(keyValuePair);
}
} else {
leftOverQueryStringFragments.push(keyValuePair);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/processImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ module.exports = function (options) {
if (contentType && contentType.indexOf('image/') === 0) {
var contentLengthHeaderValue = res.getHeader('Content-Length');
var filterInfosAndTargetFormat = getFilterInfosAndTargetContentTypeFromQueryString(queryString, _.defaults({
allowOperation: options.allowOperation,
sourceFilePath: options.root && Path.resolve(options.root, req.url.substr(1)),
sourceMetadata: {
contentType: contentType,
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@
"magicpen": "5.5.0",
"magicpen-prism": "2.2.1",
"mocha": "2.3.4",
"sinon": "1.17.2",
"unexpected": "10.3.1",
"unexpected-express": "8.0.0",
"unexpected-image": "2.0.0",
"unexpected-resemble": "3.0.0"
"unexpected-resemble": "3.0.0",
"unexpected-sinon": "9.0.0"
},
"scripts": {
"lint": "jshint .",
Expand Down
46 changes: 42 additions & 4 deletions test/processImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var express = require('express'),
fs = require('fs'),
pathModule = require('path'),
unexpected = require('unexpected'),
sinon = require('sinon'),
processImage = require('../lib/processImage'),
root = pathModule.resolve(__dirname, '..', 'testdata') + '/',
sharp;
Expand All @@ -26,10 +27,11 @@ describe('express-processimage', function () {
});

var expect = unexpected.clone()
.installPlugin(require('unexpected-express'))
.installPlugin(require('unexpected-image'))
.installPlugin(require('unexpected-resemble'))
.installPlugin(require('magicpen-prism'))
.use(require('unexpected-express'))
.use(require('unexpected-image'))
.use(require('unexpected-resemble'))
.use(require('unexpected-sinon'))
.use(require('magicpen-prism'))
.addAssertion('to yield response', function (expect, subject, value) {
return expect(
express()
Expand Down Expand Up @@ -301,6 +303,42 @@ describe('express-processimage', function () {
});
});

describe('with an allowOperation option', function () {
beforeEach(function () {
config.allowOperation = sinon.spy(function (keyValue) {
return keyValue !== 'png';
}).named('allowOperation');
});

it('should allow an operation for which allowOperation returns true', function () {
return expect('GET /turtle.jpg?resize=87', 'to yield response', {
headers: {
'Content-Type': 'image/jpeg'
},
body: expect.it('to have metadata satisfying', { size: { width: 87 } })
}).then(function () {
expect(config.allowOperation, 'to have calls satisfying', function () {
config.allowOperation('resize', [87]);
});
});
});

it('should disallow an operation for which allowOperation returns false', function () {
return expect('GET /turtle.jpg?png', 'to yield response', {
headers: {
'Content-Type': 'image/jpeg'
},
body: expect.it('to have metadata satisfying', {
format: 'JPEG'
})
}).then(function () {
expect(config.allowOperation, 'to have calls satisfying', function () {
config.allowOperation('png', []);
});
});
});
});

describe.skipIf(!sharp, 'when sharp is available', function () {
it('should allow retrieving the image metadata as JSON', function () {
return expect('GET /turtle.jpg?metadata', 'to yield response', {
Expand Down

0 comments on commit 2f86e3c

Please sign in to comment.