diff --git a/.npmignore b/.npmignore index 7dfde815..4b1092db 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,6 @@ +*.yml +.editorconfig +/docs/ /artifacts/ /examples/ /tests/ diff --git a/README.md b/README.md index e6006290..20c4886a 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,24 @@ app.use('/myCustomAPIEndpoint', Fetcher.middleware({ Now when an XHR request is performed, your response will contain the `debug` property added above. +## Resource Parameter Validation + +You can provide your custom resource parameter value validator, by passing `paramValueValidator` function into the `Fetcher.middleware` method. It is passed two arguments, the parameter value (can be any object) and the parameter name. The `paramValueValidator` function is expected to return the validated parameter value. + +Take a look at the example below: + +```js +/** + Using the app.js from above, you can modify the Fetcher.middleware + method to pass in the responseFormatter function. + */ +app.use('/myCustomAPIEndpoint', Fetcher.middleware({ + paramValueValidator: function (value, name) { + // return validatedValue; + } +})); +``` + ## CORS Support Fetchr provides [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) support by allowing you to pass the full origin host into `corsPath` option. @@ -327,7 +345,6 @@ fetcher .end(callbackFn); ``` - ## CSRF Protection You can protect your XHR paths from CSRF attacks by adding a middleware in front of the fetchr middleware: diff --git a/libs/fetcher.js b/libs/fetcher.js index e52d53e2..c9bc4479 100644 --- a/libs/fetcher.js +++ b/libs/fetcher.js @@ -31,13 +31,21 @@ function parseValue(value) { } } -function parseParamValues (params) { +function parseParamValues (params, paramValueValidator) { + var shouldValidate = (typeof paramValueValidator === 'function'); return Object.keys(params).reduce(function (parsed, curr) { parsed[curr] = parseValue(params[curr]); + if (shouldValidate) { + parsed[curr] = paramValueValidator(parsed[curr], curr); + } return parsed; }, {}); } +function escapeResource(resource) { + return resource.replace(/[^\w\.]+/g, ''); +} + /** * Takes an error and resolves output and statusCode to respond to client with * @@ -288,6 +296,10 @@ Fetcher.isRegistered = function (name) { * @method middleware * @memberof Fetcher * @param {Object} [options] Optional configurations + * @param {Function} [options.paramValueValidator] Optional function to validate the resource + parameter values. First argument is the value object, which can be string, array, object. + This function is expected to return the validated value. Second argument is the + name of the resource parameter. * @param {Function} [options.responseFormatter=no op function] Function to modify the response before sending to client. First argument is the HTTP request object, second argument is the HTTP response object and the third argument is the service data object. @@ -311,7 +323,7 @@ Fetcher.middleware = function (options) { if (!Fetcher.isRegistered(resource)) { error = fumble.http.badRequest('Invalid Fetchr Access', { - debug: 'Bad resource ' + resource + debug: 'Bad resource ' + escapeResource(resource) }); error.source = 'fetchr'; return next(error); @@ -322,7 +334,7 @@ Fetcher.middleware = function (options) { serviceMeta: serviceMeta }); request - .params(parseParamValues(qs.parse(path.join('&')))) + .params(parseParamValues(qs.parse(path.join('&')), options.paramValueValidator)) .end(function (err, data) { var meta = serviceMeta[0] || {}; if (meta.headers) { @@ -359,7 +371,7 @@ Fetcher.middleware = function (options) { if (!Fetcher.isRegistered(singleRequest.resource)) { error = fumble.http.badRequest('Invalid Fetchr Access', { - debug: 'Bad resource ' + singleRequest.resource + debug: 'Bad resource ' + escapeResource(singleRequest.resource) }); error.source = 'fetchr'; return next(error); diff --git a/tests/unit/libs/fetcher.js b/tests/unit/libs/fetcher.js index 6e679011..19240753 100644 --- a/tests/unit/libs/fetcher.js +++ b/tests/unit/libs/fetcher.js @@ -464,6 +464,54 @@ describe('Server Fetcher', function () { middleware(req, res, next); }); + it('should allow resource param validation', function (done) { + var operation = 'read', + statusCodeSet = false, + params = { + foo: 'bar' + }, + req = { + method: 'GET', + path: '/' + mockService.name + ';' + qs.stringify(params, ';'), + query: { + returnMeta: true + } + }, + res = { + json: function(response) { + expect(response).to.exist; + expect(response).to.not.be.empty; + expect(response).to.contain.keys('data', 'meta'); + expect(response.data).to.contain.keys('operation', 'args'); + expect(response.data.operation.name).to.equal(operation); + expect(response.data.operation.success).to.be.true; + expect(response.data.args).to.contain.keys('params'); + expect(response.data.args.params.foo).to.equal('validated-foo-bar'); + expect(statusCodeSet).to.be.true; + done(); + }, + status: function(code) { + expect(code).to.equal(200); + statusCodeSet = true; + return this; + }, + send: function (code) { + console.log('Not Expected: middleware responded with', code); + } + }, + next = function () { + console.log('Not Expected: middleware skipped request'); + }, + middleware = Fetcher.middleware({ + pathPrefix: '/api', + paramValueValidator: function (value, name) { + return 'validated-' + name + '-' + value; + } + }); + + middleware(req, res, next); + }); + var paramsToQuerystring = function(params) { var str = ''; for (var key in params) { @@ -552,6 +600,9 @@ describe('Server Fetcher', function () { it('should skip invalid GET resource', function (done) { makeInvalidReqTest({method: 'GET', path: '/invalidService'}, 'Bad resource invalidService', done); }); + it('should escape resource name for invalid GET resource', function (done) { + makeInvalidReqTest({method: 'GET', path: '/invalid&Service'}, 'Bad resource invalidService', done); + }); it('should skip invalid POST request', function (done) { makeInvalidReqTest({method: 'POST', body: { requests: {