diff --git a/Makefile b/Makefile index 1913b9e8..f523adda 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ config.status--test: ./configure --environment=test config/environments/test.js: config.status--test - ./config.status--test + ./config.status--test TEST_SUITE := $(shell find test/{acceptance,integration,unit} -name "*.js") TEST_SUITE_UNIT := $(shell find test/unit -name "*.js") diff --git a/NEWS.md b/NEWS.md index 9b18e6cb..16cb601e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,12 @@ ## 4.0.1 Released 2017-mm-dd + - Split and move `req2params` method to multiple middlewares. + - Use express error handler middleware to respond in case of something went wrong. + - Use `res.locals` object to share info between middlewares and leave `req.params` as an object containing properties mapped to the named route params. + - Move `LZMA` decompression to its own middleware. + + ## 4.0.0 Released 2017-10-04 diff --git a/lib/cartodb/api/auth_api.js b/lib/cartodb/api/auth_api.js index 484d66b1..562614c3 100644 --- a/lib/cartodb/api/auth_api.js +++ b/lib/cartodb/api/auth_api.js @@ -19,22 +19,22 @@ function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) { module.exports = AuthApi; -// Check if a request is authorized by a signer +// Check if the user is authorized by a signer // -// @param req express request object +// @param res express response object // @param callback function(err, signed_by) signed_by will be // null if the request is not signed by anyone // or will be a string cartodb username otherwise. // -AuthApi.prototype.authorizedBySigner = function(req, callback) { - if ( ! req.params.token || ! req.params.signer ) { +AuthApi.prototype.authorizedBySigner = function(res, callback) { + if ( ! res.locals.token || ! res.locals.signer ) { return callback(null, false); // no signer requested } var self = this; - var layergroup_id = req.params.token; - var auth_token = req.params.auth_token; + var layergroup_id = res.locals.token; + var auth_token = res.locals.auth_token; this.mapStore.load(layergroup_id, function(err, mapConfig) { if (err) { @@ -84,11 +84,12 @@ AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) { * Check access authorization * * @param req - standard req object. Importantly contains table and host information + * @param res - standard res object. Contains the auth parameters in locals * @param callback function(err, allowed) is access allowed not? */ -AuthApi.prototype.authorize = function(req, callback) { +AuthApi.prototype.authorize = function(req, res, callback) { var self = this; - var user = req.context.user; + var user = res.locals.user; step( function () { @@ -101,11 +102,11 @@ AuthApi.prototype.authorize = function(req, callback) { // if not authorized by api_key, continue if (!authorized) { // not authorized by api_key, check if authorized by signer - return self.authorizedBySigner(req, this); + return self.authorizedBySigner(res, this); } // authorized by api key, login as the given username and stop - self.pgConnection.setDBAuth(user, req.params, function(err) { + self.pgConnection.setDBAuth(user, res.locals, function(err) { callback(err, true); // authorized (or error) }); }, @@ -120,7 +121,7 @@ AuthApi.prototype.authorize = function(req, callback) { // if no signer name was given, let dbparams and // PostgreSQL do the rest. // - if ( ! req.params.signer ) { + if ( ! res.locals.signer ) { return callback(null, true); // authorized so far } @@ -128,7 +129,7 @@ AuthApi.prototype.authorize = function(req, callback) { return callback(null, false); } - self.pgConnection.setDBAuth(user, req.params, function(err) { + self.pgConnection.setDBAuth(user, res.locals, function(err) { req.profiler.done('setDBAuth'); callback(err, true); // authorized (or error) }); diff --git a/lib/cartodb/backends/dataview.js b/lib/cartodb/backends/dataview.js index 29dcd903..b6037ae6 100644 --- a/lib/cartodb/backends/dataview.js +++ b/lib/cartodb/backends/dataview.js @@ -24,7 +24,7 @@ module.exports = DataviewBackend; DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, params, callback) { - var dataviewName = params.dataviewName; + var dataviewName = params.dataviewName; step( function getMapConfig() { mapConfigProvider.getMapConfig(this); @@ -113,9 +113,7 @@ function getOverrideParams(params, ownFilter) { return overrideParams; } -DataviewBackend.prototype.search = function (mapConfigProvider, user, params, callback) { - var dataviewName = params.dataviewName; - +DataviewBackend.prototype.search = function (mapConfigProvider, user, dataviewName, params, callback) { step( function getMapConfig() { mapConfigProvider.getMapConfig(this); diff --git a/lib/cartodb/controllers/analyses.js b/lib/cartodb/controllers/analyses.js index 7760cc32..7aabc11b 100644 --- a/lib/cartodb/controllers/analyses.js +++ b/lib/cartodb/controllers/analyses.js @@ -10,8 +10,10 @@ var BaseController = require('./base'); var cors = require('../middleware/cors'); var userMiddleware = require('../middleware/user'); -function AnalysesController(authApi, pgConnection) { - BaseController.call(this, authApi, pgConnection); + +function AnalysesController(prepareContext) { + BaseController.call(this); + this.prepareContext = prepareContext; } util.inherits(AnalysesController, BaseController); @@ -19,7 +21,13 @@ util.inherits(AnalysesController, BaseController); module.exports = AnalysesController; AnalysesController.prototype.register = function(app) { - app.get(app.base_url_mapconfig + '/analyses/catalog', cors(), userMiddleware, this.catalog.bind(this)); + app.get( + app.base_url_mapconfig + '/analyses/catalog', + cors(), + userMiddleware, + this.prepareContext, + this.catalog.bind(this) + ); }; AnalysesController.prototype.sendResponse = function(req, res, resource) { @@ -27,17 +35,13 @@ AnalysesController.prototype.sendResponse = function(req, res, resource) { this.send(req, res, resource, 200); }; -AnalysesController.prototype.catalog = function(req, res) { +AnalysesController.prototype.catalog = function (req, res, next) { var self = this; - var username = req.context.user; + var username = res.locals.user; step( - function reqParams() { - self.req2params(req, this); - }, - function catalogQuery(err) { - assert.ifError(err); - var pg = new PSQL(dbParamsFromReqParams(req.params)); + function catalogQuery() { + var pg = new PSQL(dbParamsFromReqParams(res.locals)); getMetadata(username, pg, this); }, function prepareResponse(err, results) { @@ -80,7 +84,8 @@ AnalysesController.prototype.catalog = function(req, res) { err = new Error('Unauthorized'); err.http_status = 401; } - self.sendError(req, res, err); + + next(req, res, err); } else { self.sendResponse(req, res, { catalog: catalogWithTables }); } diff --git a/lib/cartodb/controllers/base.js b/lib/cartodb/controllers/base.js index d743a158..9a502bd4 100644 --- a/lib/cartodb/controllers/base.js +++ b/lib/cartodb/controllers/base.js @@ -1,130 +1,12 @@ -var assert = require('assert'); - -var _ = require('underscore'); -var step = require('step'); var debug = require('debug')('windshaft:cartodb'); -// Whitelist query parameters and attach format -var REQUEST_QUERY_PARAMS_WHITELIST = [ - 'config', - 'map_key', - 'api_key', - 'auth_token', - 'callback', - 'zoom', - 'lon', - 'lat', - // analysis - 'filters' // json -]; - -function BaseController(authApi, pgConnection) { - this.authApi = authApi; - this.pgConnection = pgConnection; +function BaseController() { } module.exports = BaseController; -// jshint maxcomplexity:8 -/** - * Whitelist input and get database name & default geometry type from - * subdomain/user metadata held in CartoDB Redis - * @param req - standard express request obj. Should have host & table - * @param callback - */ -BaseController.prototype.req2params = function(req, callback){ - var self = this; - - var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST; - if (Array.isArray(req.context.allowedQueryParams)) { - allowedQueryParams = allowedQueryParams.concat(req.context.allowedQueryParams); - } - req.query = _.pick(req.query, allowedQueryParams); - req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object - - var user = req.context.user; - - if ( req.params.token ) { - // Token might match the following patterns: - // - {user}@{tpl_id}@{token}:{cache_buster} - var tksplit = req.params.token.split(':'); - req.params.token = tksplit[0]; - if ( tksplit.length > 1 ) { - req.params.cache_buster= tksplit[1]; - } - tksplit = req.params.token.split('@'); - if ( tksplit.length > 1 ) { - req.params.signer = tksplit.shift(); - if ( ! req.params.signer ) { - req.params.signer = user; - } - else if ( req.params.signer !== user ) { - var err = new Error( - 'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"' - ); - err.http_status = 403; - req.profiler.done('req2params'); - callback(err); - return; - } - if ( tksplit.length > 1 ) { - /*var template_hash = */tksplit.shift(); // unused - } - req.params.token = tksplit.shift(); - } - } - - // bring all query values onto req.params object - _.extend(req.params, req.query); - - req.profiler.done('req2params.setup'); - - step( - function getPrivacy(){ - self.authApi.authorize(req, this); - }, - function validateAuthorization(err, authorized) { - req.profiler.done('authorize'); - assert.ifError(err); - if(!authorized) { - err = new Error("Sorry, you are unauthorized (permission denied)"); - err.http_status = 403; - throw err; - } - return null; - }, - function getDatabase(err){ - assert.ifError(err); - self.pgConnection.setDBConn(user, req.params, this); - }, - function finishSetup(err) { - if ( err ) { - req.profiler.done('req2params'); - return callback(err, req); - } - - // Add default database connection parameters - // if none given - _.defaults(req.params, { - dbuser: global.environment.postgres.user, - dbpassword: global.environment.postgres.password, - dbhost: global.environment.postgres.host, - dbport: global.environment.postgres.port - }); - - req.profiler.done('req2params'); - callback(null, req); - } - ); -}; -// jshint maxcomplexity:6 - // jshint maxcomplexity:9 BaseController.prototype.send = function(req, res, body, status, headers) { - if (req.params.dbhost) { - res.set('X-Served-By-DB-Host', req.params.dbhost); - } - res.set('X-Tiler-Profiler', req.profiler.toJSONString()); if (headers) { @@ -152,150 +34,3 @@ BaseController.prototype.send = function(req, res, body, status, headers) { } }; // jshint maxcomplexity:6 - -BaseController.prototype.sendError = function(req, res, err, label) { - var allErrors = Array.isArray(err) ? err : [err]; - - allErrors = populateTimeoutErrors(allErrors); - - label = label || 'UNKNOWN'; - err = allErrors[0] || new Error(label); - allErrors[0] = err; - - var statusCode = findStatusCode(err); - - if (err.message === 'Tile does not exist' && req.params.format === 'mvt') { - statusCode = 204; - } - - debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack); - - // If a callback was requested, force status to 200 - if (req.query && req.query.callback) { - statusCode = 200; - } - - var errorResponseBody = { - errors: allErrors.map(errorMessage), - errors_with_context: allErrors.map(errorMessageWithContext) - }; - - this.send(req, res, errorResponseBody, statusCode); -}; - -function stripConnectionInfo(message) { - // Strip connection info, if any - return message - // See https://github.com/CartoDB/Windshaft/issues/173 - .replace(/Connection string: '[^']*'\n\s/im, '') - // See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644 - .replace(/is the server.*encountered/im, 'encountered'); -} - -var ERROR_INFO_TO_EXPOSE = { - message: true, - layer: true, - type: true, - analysis: true, - subtype: true -}; - -function shouldBeExposed (prop) { - return !!ERROR_INFO_TO_EXPOSE[prop]; -} - -function errorMessage(err) { - // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 - var message = (_.isString(err) ? err : err.message) || 'Unknown error'; - - return stripConnectionInfo(message); -} - -function errorMessageWithContext(err) { - // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 - var message = (_.isString(err) ? err : err.message) || 'Unknown error'; - - var error = { - type: err.type || 'unknown', - message: stripConnectionInfo(message), - }; - - for (var prop in err) { - // type & message are properties from Error's prototype and will be skipped - if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) { - error[prop] = err[prop]; - } - } - - return error; -} -module.exports.errorMessage = errorMessage; - -function findStatusCode(err) { - var statusCode; - if ( err.http_status ) { - statusCode = err.http_status; - } else { - statusCode = statusFromErrorMessage('' + err); - } - return statusCode; -} -module.exports.findStatusCode = findStatusCode; - -function statusFromErrorMessage(errMsg) { - // Find an appropriate statusCode based on message - // jshint maxcomplexity:7 - var statusCode = 400; - if ( -1 !== errMsg.indexOf('permission denied') ) { - statusCode = 403; - } - else if ( -1 !== errMsg.indexOf('authentication failed') ) { - statusCode = 403; - } - else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) { - statusCode = 400; - } - else if ( -1 !== errMsg.indexOf('does not exist') ) { - if ( -1 !== errMsg.indexOf(' role ') ) { - statusCode = 403; // role 'xxx' does not exist - } else if ( errMsg.match(/function .* does not exist/) ) { - statusCode = 400; // invalid SQL (SQL function does not exist) - } else { - statusCode = 404; - } - } - - return statusCode; -} - -function isRenderTimeoutError (err) { - return err.message === 'Render timed out'; -} - -function isDatasourceTimeoutError (err) { - return err.message && err.message.match(/canceling statement due to statement timeout/i); -} - -function isTimeoutError (err) { - return isRenderTimeoutError(err) || isDatasourceTimeoutError(err); -} - -function populateTimeoutErrors (errors) { - return errors.map(function (error) { - if (isRenderTimeoutError(error)) { - error.subtype = 'render'; - } - - if (isDatasourceTimeoutError(error)) { - error.subtype = 'datasource'; - } - - if (isTimeoutError(error)) { - error.message = 'You are over platform\'s limits. Please contact us to know more details'; - error.type = 'limit'; - error.http_status = 429; - } - - return error; - }); -} diff --git a/lib/cartodb/controllers/layergroup.js b/lib/cartodb/controllers/layergroup.js index ed0f3eb5..c5e4c53a 100644 --- a/lib/cartodb/controllers/layergroup.js +++ b/lib/cartodb/controllers/layergroup.js @@ -28,9 +28,9 @@ var QueryTables = require('cartodb-query-tables'); * @param {AnalysisBackend} analysisBackend * @constructor */ -function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend, +function LayergroupController(prepareContext, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend, surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) { - BaseController.call(this, authApi, pgConnection); + BaseController.call(this); this.pgConnection = pgConnection; this.mapStore = mapStore; @@ -43,39 +43,65 @@ function LayergroupController(authApi, pgConnection, mapStore, tileBackend, prev this.dataviewBackend = new DataviewBackend(analysisBackend); this.analysisStatusBackend = new AnalysisStatusBackend(); + + this.prepareContext = prepareContext; } util.inherits(LayergroupController, BaseController); module.exports = LayergroupController; - LayergroupController.prototype.register = function(app) { - app.get(app.base_url_mapconfig + - '/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware, - this.tile.bind(this)); + app.get( + app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format', + cors(), + userMiddleware, + this.prepareContext, + this.tile.bind(this) + ); - app.get(app.base_url_mapconfig + - '/:token/:z/:x/:y.:format', cors(), userMiddleware, - this.tile.bind(this)); + app.get( + app.base_url_mapconfig + '/:token/:z/:x/:y.:format', + cors(), + userMiddleware, + this.prepareContext, + this.tile.bind(this) + ); - app.get(app.base_url_mapconfig + - '/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware, - this.layer.bind(this)); + app.get( + app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)', + cors(), + userMiddleware, + validateLayerRouteMiddleware, + this.prepareContext, + this.layer.bind(this) + ); - app.get(app.base_url_mapconfig + - '/:token/:layer/attributes/:fid', cors(), userMiddleware, - this.attributes.bind(this)); + app.get( + app.base_url_mapconfig + '/:token/:layer/attributes/:fid', + cors(), + userMiddleware, + this.prepareContext, + this.attributes.bind(this) + ); - app.get(app.base_url_mapconfig + - '/static/center/:token/:z/:lat/:lng/:width/:height.:format', - cors(), userMiddleware, allowQueryParams(['layer']), - this.center.bind(this)); + app.get( + app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format', + cors(), + userMiddleware, + allowQueryParams(['layer']), + this.prepareContext, + this.center.bind(this) + ); - app.get(app.base_url_mapconfig + - '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', - cors(), userMiddleware, allowQueryParams(['layer']), - this.bbox.bind(this)); + app.get( + app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', + cors(), + userMiddleware, + allowQueryParams(['layer']), + this.prepareContext, + this.bbox.bind(this) + ); // Undocumented/non-supported API endpoint methods. // Use at your own peril. @@ -98,6 +124,7 @@ LayergroupController.prototype.register = function(app) { cors(), userMiddleware, allowQueryParams(allowedDataviewQueryParams), + this.prepareContext, this.dataview.bind(this) ); @@ -106,6 +133,7 @@ LayergroupController.prototype.register = function(app) { cors(), userMiddleware, allowQueryParams(allowedDataviewQueryParams), + this.prepareContext, this.dataview.bind(this) ); @@ -114,6 +142,7 @@ LayergroupController.prototype.register = function(app) { cors(), userMiddleware, allowQueryParams(allowedDataviewQueryParams), + this.prepareContext, this.dataviewSearch.bind(this) ); @@ -122,30 +151,32 @@ LayergroupController.prototype.register = function(app) { cors(), userMiddleware, allowQueryParams(allowedDataviewQueryParams), + this.prepareContext, this.dataviewSearch.bind(this) ); - app.get(app.base_url_mapconfig + - '/:token/analysis/node/:nodeId', cors(), userMiddleware, - this.analysisNodeStatus.bind(this)); + app.get( + app.base_url_mapconfig + '/:token/analysis/node/:nodeId', + cors(), + userMiddleware, + this.prepareContext, + this.analysisNodeStatus.bind(this) + ); }; -LayergroupController.prototype.analysisNodeStatus = function(req, res) { +LayergroupController.prototype.analysisNodeStatus = function(req, res, next) { var self = this; step( - function setupParams() { - self.req2params(req, this); - }, - function retrieveNodeStatus(err) { - assert.ifError(err); - self.analysisStatusBackend.getNodeStatus(req.params, this); + function retrieveNodeStatus() { + self.analysisStatusBackend.getNodeStatus(res.locals, this); }, function finish(err, nodeStatus, stats) { req.profiler.add(stats || {}); if (err) { - self.sendError(req, res, err, 'GET NODE STATUS'); + err.label = 'GET NODE STATUS'; + next(err); } else { self.sendResponse(req, res, nodeStatus, 200, { 'Cache-Control': 'public,max-age=5', @@ -156,54 +187,50 @@ LayergroupController.prototype.analysisNodeStatus = function(req, res) { ); }; -LayergroupController.prototype.dataview = function(req, res) { +LayergroupController.prototype.dataview = function(req, res, next) { var self = this; step( - function setupParams() { - self.req2params(req, this); - }, - function retrieveDataview(err) { - assert.ifError(err); - + function retrieveDataview() { var mapConfigProvider = new MapStoreMapConfigProvider( - self.mapStore, req.context.user, self.userLimitsApi, req.params + self.mapStore, res.locals.user, self.userLimitsApi, res.locals + ); + self.dataviewBackend.getDataview( + mapConfigProvider, + res.locals.user, + res.locals, + this ); - self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this); }, function finish(err, dataview, stats) { req.profiler.add(stats || {}); if (err) { - self.sendError(req, res, err, 'GET DATAVIEW'); + err.label = 'GET DATAVIEW'; + next(err); } else { self.sendResponse(req, res, dataview, 200); } } ); - }; -LayergroupController.prototype.dataviewSearch = function(req, res) { +LayergroupController.prototype.dataviewSearch = function(req, res, next) { var self = this; step( - function setupParams() { - self.req2params(req, this); - }, - function searchDataview(err) { - assert.ifError(err); - + function searchDataview() { var mapConfigProvider = new MapStoreMapConfigProvider( - self.mapStore, req.context.user, self.userLimitsApi, req.params + self.mapStore, res.locals.user, self.userLimitsApi, res.locals ); - self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this); + self.dataviewBackend.search(mapConfigProvider, res.locals.user, req.params.dataviewName, res.locals, this); }, function finish(err, searchResult, stats) { req.profiler.add(stats || {}); if (err) { - self.sendError(req, res, err, 'GET DATAVIEW SEARCH'); + err.label = 'GET DATAVIEW SEARCH'; + next(err); } else { self.sendResponse(req, res, searchResult, 200); } @@ -212,28 +239,24 @@ LayergroupController.prototype.dataviewSearch = function(req, res) { }; -LayergroupController.prototype.attributes = function(req, res) { +LayergroupController.prototype.attributes = function(req, res, next) { var self = this; req.profiler.start('windshaft.maplayer_attribute'); step( - function setupParams() { - self.req2params(req, this); - }, - function retrieveFeatureAttributes(err) { - assert.ifError(err); - + function retrieveFeatureAttributes() { var mapConfigProvider = new MapStoreMapConfigProvider( - self.mapStore, req.context.user, self.userLimitsApi, req.params + self.mapStore, res.locals.user, self.userLimitsApi, res.locals ); - self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this); + self.attributesBackend.getFeatureAttributes(mapConfigProvider, res.locals, false, this); }, function finish(err, tile, stats) { req.profiler.add(stats || {}); if (err) { - self.sendError(req, res, err, 'GET ATTRIBUTES'); + err.label = 'GET ATTRIBUTES'; + next(err); } else { self.sendResponse(req, res, tile, 200); } @@ -243,37 +266,30 @@ LayergroupController.prototype.attributes = function(req, res) { }; // Gets a tile for a given token and set of tile ZXY coords. (OSM style) -LayergroupController.prototype.tile = function(req, res) { +LayergroupController.prototype.tile = function(req, res, next) { req.profiler.start('windshaft.map_tile'); - this.tileOrLayer(req, res); + this.tileOrLayer(req, res, next); }; // Gets a tile for a given token, layer set of tile ZXY coords. (OSM style) LayergroupController.prototype.layer = function(req, res, next) { - if (req.params.token === 'static') { - return next(); - } req.profiler.start('windshaft.maplayer_tile'); - this.tileOrLayer(req, res); + this.tileOrLayer(req, res, next); }; -LayergroupController.prototype.tileOrLayer = function (req, res) { +LayergroupController.prototype.tileOrLayer = function (req, res, next) { var self = this; step( - function mapController$prepareParams() { - self.req2params(req, this); - }, - function mapController$getTileOrGrid(err) { - assert.ifError(err); + function mapController$getTileOrGrid() { self.tileBackend.getTile( - new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params), + new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals), req.params, this ); }, function mapController$finalize(err, tile, headers, stats) { req.profiler.add(stats); - self.finalizeGetTileOrGrid(err, req, res, tile, headers); + self.finalizeGetTileOrGrid(err, req, res, tile, headers, next); } ); }; @@ -284,7 +300,7 @@ function getStatusCode(tile, format){ // This function is meant for being called as the very last // step by all endpoints serving tiles or grids -LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) { +LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers, next) { var supportedFormats = { grid_json: true, json_torque: true, @@ -313,7 +329,9 @@ LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, t } err.message = errMsg; - this.sendError(req, res, err, 'TILE RENDER'); + err.label = 'TILE RENDER'; + next(err); + global.statsClient.increment('windshaft.tiles.error'); global.statsClient.increment('windshaft.tiles.' + formatStat + '.error'); } else { @@ -323,42 +341,38 @@ LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, t } }; -LayergroupController.prototype.bbox = function(req, res) { +LayergroupController.prototype.bbox = function(req, res, next) { this.staticMap(req, res, +req.params.width, +req.params.height, { west: +req.params.west, north: +req.params.north, east: +req.params.east, south: +req.params.south - }); + }, null, next); }; -LayergroupController.prototype.center = function(req, res) { +LayergroupController.prototype.center = function(req, res, next) { this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, { lng: +req.params.lng, lat: +req.params.lat - }); + }, next); }; -LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) { +LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center, next) { var format = req.params.format === 'jpg' ? 'jpeg' : 'png'; - req.params.layer = 'all'; - req.params.format = 'png'; + req.params.format = req.params.format || 'png'; + res.locals.layer = res.locals.layer || 'all'; var self = this; step( - function reqParams() { - self.req2params(req, this); - }, - function getImage(err) { - assert.ifError(err); + function getImage() { if (center) { self.previewBackend.getImage( - new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params), + new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals), format, width, height, zoom, center, this); } else { self.previewBackend.getImage( - new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params), + new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals), format, width, height, zoom /* bounds */, this); } }, @@ -367,7 +381,8 @@ LayergroupController.prototype.staticMap = function(req, res, width, height, zoo req.profiler.add(stats || {}); if (err) { - self.sendError(req, res, err, 'STATIC_MAP'); + err.label = 'STATIC_MAP'; + next(err); } else { res.set('Content-Type', headers['Content-Type'] || 'image/' + format); self.sendResponse(req, res, image, 200); @@ -385,18 +400,18 @@ LayergroupController.prototype.sendResponse = function(req, res, body, status, h // Set Last-Modified header var lastUpdated; - if (req.params.cache_buster) { + if (res.locals.cache_buster) { // Assuming cache_buster is a timestamp - lastUpdated = new Date(parseInt(req.params.cache_buster)); + lastUpdated = new Date(parseInt(res.locals.cache_buster)); } else { lastUpdated = new Date(); } res.set('Last-Modified', lastUpdated.toUTCString()); - var dbName = req.params.dbname; + var dbName = res.locals.dbname; step( function getAffectedTables() { - self.getAffectedTables(req.context.user, dbName, req.params.token, this); + self.getAffectedTables(res.locals.user, dbName, res.locals.token, this); }, function sendResponse(err, affectedTables) { req.profiler.done('affectedTables'); @@ -472,3 +487,12 @@ LayergroupController.prototype.getAffectedTables = function(user, dbName, layerg callback ); }; + + +function validateLayerRouteMiddleware(req, res, next) { + if (req.params.token === 'static') { + return next('route'); + } + + next(); +} \ No newline at end of file diff --git a/lib/cartodb/controllers/map.js b/lib/cartodb/controllers/map.js index 282c3145..f2e86d13 100644 --- a/lib/cartodb/controllers/map.js +++ b/lib/cartodb/controllers/map.js @@ -20,7 +20,6 @@ var NamedMapsCacheEntry = require('../cache/model/named_maps_entry'); var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider'); - /** * @param {AuthApi} authApi * @param {PgConnection} pgConnection @@ -34,11 +33,11 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/cr * @param {StatsBackend} statsBackend * @constructor */ -function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend, +function MapController(prepareContext, pgConnection, templateMaps, mapBackend, metadataBackend, surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter, statsBackend) { - BaseController.call(this, authApi, pgConnection); + BaseController.call(this); this.pgConnection = pgConnection; this.templateMaps = templateMaps; @@ -52,6 +51,7 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata this.resourceLocator = new ResourceLocator(global.environment); this.statsBackend = statsBackend; + this.prepareContext = prepareContext; } util.inherits(MapController, BaseController); @@ -60,38 +60,60 @@ module.exports = MapController; MapController.prototype.register = function(app) { - app.get(app.base_url_mapconfig, cors(), userMiddleware, this.createGet.bind(this)); - app.post(app.base_url_mapconfig, cors(), userMiddleware, this.createPost.bind(this)); - app.get(app.base_url_templated + '/:template_id/jsonp', cors(), userMiddleware, this.jsonp.bind(this)); - app.post(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.instantiate.bind(this)); + app.get( + app.base_url_mapconfig, + cors(), + userMiddleware, + this.prepareContext, + this.createGet.bind(this) + ); + app.post( + app.base_url_mapconfig, + cors(), + userMiddleware, + this.prepareContext, + this.createPost.bind(this) + ); + app.get( + app.base_url_templated + '/:template_id/jsonp', + cors(), + userMiddleware, + this.prepareContext, + this.jsonp.bind(this) + ); + app.post( + app.base_url_templated + '/:template_id', + cors(), + userMiddleware, + this.prepareContext, + this.instantiate.bind(this) + ); app.options(app.base_url_mapconfig, cors('Content-Type')); }; -MapController.prototype.createGet = function(req, res){ +MapController.prototype.createGet = function(req, res, next){ req.profiler.start('windshaft.createmap_get'); - this.create(req, res, function createGet$prepareConfig(err, req) { - assert.ifError(err); - if ( ! req.params.config ) { + this.create(req, res, function createGet$prepareConfig(req, config) { + if ( ! config ) { throw new Error('layergroup GET needs a "config" parameter'); } - return JSON.parse(req.params.config); - }); + return JSON.parse(config); + }, next); }; -MapController.prototype.createPost = function(req, res) { +MapController.prototype.createPost = function(req, res, next) { req.profiler.start('windshaft.createmap_post'); - this.create(req, res, function createPost$prepareConfig(err, req) { - assert.ifError(err); + this.create(req, res, function createPost$prepareConfig(req) { if (!req.is('application/json')) { throw new Error('layergroup POST data must be of type application/json'); } return req.body; - }); + }, next); }; -MapController.prototype.instantiate = function(req, res) { +MapController.prototype.instantiate = function(req, res, next) { req.profiler.start('windshaft-cartodb.instance_template_post'); this.instantiateTemplate(req, res, function prepareTemplateParams(callback) { @@ -99,10 +121,10 @@ MapController.prototype.instantiate = function(req, res) { return callback(new Error('Template POST data must be of type application/json')); } return callback(null, req.body); - }); + }, next); }; -MapController.prototype.jsonp = function(req, res) { +MapController.prototype.jsonp = function(req, res, next) { req.profiler.start('windshaft-cartodb.instance_template_get'); this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) { @@ -121,10 +143,10 @@ MapController.prototype.jsonp = function(req, res) { } return callback(err, templateParams); - }); + }, next); }; -MapController.prototype.create = function(req, res, prepareConfigFn) { +MapController.prototype.create = function(req, res, prepareConfigFn, next) { var self = this; var mapConfig; @@ -132,35 +154,36 @@ MapController.prototype.create = function(req, res, prepareConfigFn) { var context = {}; step( - function setupParams(){ - self.req2params(req, this); + function prepareConfig () { + const requestMapConfig = prepareConfigFn(req, res.locals.config); + return requestMapConfig; }, - prepareConfigFn, function prepareAdapterMapConfig(err, requestMapConfig) { assert.ifError(err); context.analysisConfiguration = { - user: req.context.user, + user: res.locals.user, db: { - host: req.params.dbhost, - port: req.params.dbport, - dbname: req.params.dbname, - user: req.params.dbuser, - pass: req.params.dbpassword + host: res.locals.dbhost, + port: res.locals.dbport, + dbname: res.locals.dbname, + user: res.locals.dbuser, + pass: res.locals.dbpassword }, batch: { - username: req.context.user, - apiKey: req.params.api_key + username: res.locals.user, + apiKey: res.locals.api_key } }; - self.mapConfigAdapter.getMapConfig(req.context.user, requestMapConfig, req.params, context, this); + self.mapConfigAdapter.getMapConfig(res.locals.user, requestMapConfig, res.locals, context, this); }, function createLayergroup(err, requestMapConfig) { assert.ifError(err); var datasource = context.datasource || Datasource.EmptyDatasource(); mapConfig = new MapConfig(requestMapConfig, datasource); self.mapBackend.createLayergroup( - mapConfig, req.params, - new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params), + mapConfig, + res.locals, + new CreateLayergroupMapConfigProvider(mapConfig, res.locals.user, self.userLimitsApi, res.locals), this ); }, @@ -188,11 +211,12 @@ MapController.prototype.create = function(req, res, prepareConfigFn) { err = error; } - self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP'); + err.label = 'ANONYMOUS LAYERGROUP'; + next(err); } else { var analysesResults = context.analysesResults || []; - self.addDataviewsAndWidgetsUrls(req.context.user, layergroup, mapConfig.obj()); - self.addAnalysesMetadata(req.context.user, layergroup, analysesResults, true); + self.addDataviewsAndWidgetsUrls(res.locals.user, layergroup, mapConfig.obj()); + self.addAnalysesMetadata(res.locals.user, layergroup, analysesResults, true); addContextMetadata(layergroup, mapConfig.obj(), context); res.set('X-Layergroup-Id', layergroup.layergroupid); self.send(req, res, layergroup, 200); @@ -212,17 +236,14 @@ function addContextMetadata(layergroup, mapConfig, context) { } } -MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) { +MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn, next) { var self = this; - var cdbuser = req.context.user; + var cdbuser = res.locals.user; var mapConfigProvider; var mapConfig; step( - function setupParams(){ - self.req2params(req, this); - }, function getTemplateParams() { prepareParamsFn(this); }, @@ -237,8 +258,8 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn cdbuser, req.params.template_id, templateParams, - req.query.auth_token, - req.params + res.locals.auth_token, + res.locals ); mapConfigProvider.getMapConfig(this); }, @@ -259,7 +280,8 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn }, function finishTemplateInstantiation(err, layergroup) { if (err) { - self.sendError(req, res, err, 'NAMED MAP LAYERGROUP'); + err.label = 'NAMED MAP LAYERGROUP'; + next(err); } else { var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8); layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid; @@ -282,7 +304,7 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, analysesResults, callback) { var self = this; - var username = req.context.user; + var username = res.locals.user; var tasksleft = 2; // redis key and affectedTables var errors = []; @@ -323,7 +345,7 @@ function(req, res, mapconfig, layergroup, analysesResults, callback) { } }); - var dbName = req.params.dbname; + var dbName = res.locals.dbname; var layergroupId = layergroup.layergroupid; var dbConnection; diff --git a/lib/cartodb/controllers/named_maps.js b/lib/cartodb/controllers/named_maps.js index b6aa8bdf..759ae4b6 100644 --- a/lib/cartodb/controllers/named_maps.js +++ b/lib/cartodb/controllers/named_maps.js @@ -10,9 +10,9 @@ var cors = require('../middleware/cors'); var userMiddleware = require('../middleware/user'); var allowQueryParams = require('../middleware/allow-query-params'); -function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend, +function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend, surrogateKeysCache, tablesExtentApi, metadataBackend) { - BaseController.call(this, authApi, pgConnection); + BaseController.call(this); this.namedMapProviderCache = namedMapProviderCache; this.tileBackend = tileBackend; @@ -20,6 +20,7 @@ function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileB this.surrogateKeysCache = surrogateKeysCache; this.tablesExtentApi = tablesExtentApi; this.metadataBackend = metadataBackend; + this.prepareContext = prepareContext; } util.inherits(NamedMapsController, BaseController); @@ -27,18 +28,26 @@ util.inherits(NamedMapsController, BaseController); module.exports = NamedMapsController; NamedMapsController.prototype.register = function(app) { - app.get(app.base_url_templated + - '/:template_id/:layer/:z/:x/:y.(:format)', cors(), userMiddleware, - this.tile.bind(this)); + app.get( + app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)', + cors(), + userMiddleware, + this.prepareContext, + this.tile.bind(this) + ); - app.get(app.base_url_mapconfig + - '/static/named/:template_id/:width/:height.:format', cors(), userMiddleware, + app.get( + app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', + cors(), + userMiddleware, allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']), - this.staticMap.bind(this)); + this.prepareContext, + this.staticMap.bind(this) + ); }; NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) { - this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(req.context.user, namedMapProvider.getTemplateName())); + this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(res.locals.user, namedMapProvider.getTemplateName())); res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png'); res.set('Cache-Control', 'public,max-age=7200,must-revalidate'); @@ -75,24 +84,20 @@ NamedMapsController.prototype.sendResponse = function(req, res, resource, header ); }; -NamedMapsController.prototype.tile = function(req, res) { +NamedMapsController.prototype.tile = function(req, res, next) { var self = this; - var cdbUser = req.context.user; + var cdbUser = res.locals.user; var namedMapProvider; step( - function reqParams() { - self.req2params(req, this); - }, - function getNamedMapProvider(err) { - assert.ifError(err); + function getNamedMapProvider() { self.namedMapProviderCache.get( cdbUser, req.params.template_id, req.query.config, req.query.auth_token, - req.params, + res.locals, this ); }, @@ -104,7 +109,8 @@ NamedMapsController.prototype.tile = function(req, res) { function handleImage(err, tile, headers, stats) { req.profiler.add(stats); if (err) { - self.sendError(req, res, err, 'NAMED_MAP_TILE'); + err.label = 'NAMED_MAP_TILE'; + next(err); } else { self.sendResponse(req, res, tile, headers, namedMapProvider); } @@ -112,28 +118,24 @@ NamedMapsController.prototype.tile = function(req, res) { ); }; -NamedMapsController.prototype.staticMap = function(req, res) { +NamedMapsController.prototype.staticMap = function(req, res, next) { var self = this; - var cdbUser = req.context.user; + var cdbUser = res.locals.user; var format = req.params.format === 'jpg' ? 'jpeg' : 'png'; - req.params.format = 'png'; - req.params.layer = 'all'; + res.locals.format = req.params.format || 'png'; + res.locals.layer = res.locals.layer || 'all'; var namedMapProvider; step( - function reqParams() { - self.req2params(req, this); - }, - function getNamedMapProvider(err) { - assert.ifError(err); + function getNamedMapProvider() { self.namedMapProviderCache.get( cdbUser, req.params.template_id, req.query.config, req.query.auth_token, - req.params, + res.locals, this ); }, @@ -141,12 +143,12 @@ NamedMapsController.prototype.staticMap = function(req, res) { assert.ifError(err); namedMapProvider = _namedMapProvider; - - self.prepareLayerFilterFromPreviewLayers(cdbUser, req, namedMapProvider, this); + + self.prepareLayerFilterFromPreviewLayers(cdbUser, req, res.locals, namedMapProvider, this); }, function prepareImageOptions(err) { assert.ifError(err); - self.getStaticImageOptions(cdbUser, req.params, namedMapProvider, this); + self.getStaticImageOptions(cdbUser, res.locals, namedMapProvider, this); }, function getImage(err, imageOpts) { assert.ifError(err); @@ -180,7 +182,8 @@ NamedMapsController.prototype.staticMap = function(req, res) { req.profiler.add(stats || {}); if (err) { - self.sendError(req, res, err, 'STATIC_VIZ_MAP'); + err.label = 'STATIC_VIZ_MAP'; + next(err); } else { self.sendResponse(req, res, image, headers, namedMapProvider); } @@ -188,7 +191,13 @@ NamedMapsController.prototype.staticMap = function(req, res) { ); }; -NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (user, req, namedMapProvider, callback) { +NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function ( + user, + req, + params, + namedMapProvider, + callback +) { var self = this; namedMapProvider.getTemplate(function (err, template) { if (err) { @@ -213,7 +222,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us } // overwrites 'all' default filter - req.params.layer = layerVisibilityFilter.join(','); + params.layer = layerVisibilityFilter.join(','); // recreates the provider self.namedMapProviderCache.get( @@ -221,7 +230,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us req.params.template_id, req.query.config, req.query.auth_token, - req.params, + params, callback ); }); diff --git a/lib/cartodb/controllers/named_maps_admin.js b/lib/cartodb/controllers/named_maps_admin.js index 08ad5322..ae57ada3 100644 --- a/lib/cartodb/controllers/named_maps_admin.js +++ b/lib/cartodb/controllers/named_maps_admin.js @@ -15,8 +15,8 @@ var userMiddleware = require('../middleware/user'); * @param {TemplateMaps} templateMaps * @constructor */ -function NamedMapsAdminController(authApi, pgConnection, templateMaps) { - BaseController.call(this, authApi, pgConnection); +function NamedMapsAdminController(authApi, templateMaps) { + BaseController.call(this); this.authApi = authApi; this.templateMaps = templateMaps; @@ -26,19 +26,52 @@ util.inherits(NamedMapsAdminController, BaseController); module.exports = NamedMapsAdminController; -NamedMapsAdminController.prototype.register = function(app) { - app.post(app.base_url_templated, cors(), userMiddleware, this.create.bind(this)); - app.put(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.update.bind(this)); - app.get(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.retrieve.bind(this)); - app.delete(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.destroy.bind(this)); - app.get(app.base_url_templated, cors(), userMiddleware, this.list.bind(this)); - app.options(app.base_url_templated + '/:template_id', cors('Content-Type')); +NamedMapsAdminController.prototype.register = function (app) { + app.post( + app.base_url_templated + '/', + cors(), + userMiddleware, + this.create.bind(this) + ); + + app.put( + app.base_url_templated + '/:template_id', + cors(), + userMiddleware, + this.update.bind(this) + ); + + app.get( + app.base_url_templated + '/:template_id', + cors(), + userMiddleware, + this.retrieve.bind(this) + ); + + app.delete( + app.base_url_templated + '/:template_id', + cors(), + userMiddleware, + this.destroy.bind(this) + ); + + app.get( + app.base_url_templated + '/', + cors(), + userMiddleware, + this.list.bind(this) + ); + + app.options( + app.base_url_templated + '/:template_id', + cors('Content-Type') + ); }; -NamedMapsAdminController.prototype.create = function(req, res) { +NamedMapsAdminController.prototype.create = function(req, res, next) { var self = this; - var cdbuser = req.context.user; + var cdbuser = res.locals.user; step( function checkPerms(){ @@ -55,14 +88,14 @@ NamedMapsAdminController.prototype.create = function(req, res) { assert.ifError(err); return { template_id: tpl_id }; }, - finishFn(self, req, res, 'POST TEMPLATE') + finishFn(self, req, res, 'POST TEMPLATE', null, next) ); }; -NamedMapsAdminController.prototype.update = function(req, res) { +NamedMapsAdminController.prototype.update = function(req, res, next) { var self = this; - var cdbuser = req.context.user; + var cdbuser = res.locals.user; var template; var tpl_id; @@ -84,16 +117,16 @@ NamedMapsAdminController.prototype.update = function(req, res) { return { template_id: tpl_id }; }, - finishFn(self, req, res, 'PUT TEMPLATE') + finishFn(self, req, res, 'PUT TEMPLATE', null, next) ); }; -NamedMapsAdminController.prototype.retrieve = function(req, res) { +NamedMapsAdminController.prototype.retrieve = function(req, res, next) { var self = this; req.profiler.start('windshaft-cartodb.get_template'); - var cdbuser = req.context.user; + var cdbuser = res.locals.user; var tpl_id; step( function checkPerms(){ @@ -118,16 +151,16 @@ NamedMapsAdminController.prototype.retrieve = function(req, res) { delete tpl_val.auth_id; return { template: tpl_val }; }, - finishFn(self, req, res, 'GET TEMPLATE') + finishFn(self, req, res, 'GET TEMPLATE', null, next) ); }; -NamedMapsAdminController.prototype.destroy = function(req, res) { +NamedMapsAdminController.prototype.destroy = function(req, res, next) { var self = this; req.profiler.start('windshaft-cartodb.delete_template'); - var cdbuser = req.context.user; + var cdbuser = res.locals.user; var tpl_id; step( function checkPerms(){ @@ -144,15 +177,15 @@ NamedMapsAdminController.prototype.destroy = function(req, res) { assert.ifError(err); return ''; }, - finishFn(self, req, res, 'DELETE TEMPLATE', 204) + finishFn(self, req, res, 'DELETE TEMPLATE', 204, next) ); }; -NamedMapsAdminController.prototype.list = function(req, res) { +NamedMapsAdminController.prototype.list = function(req, res, next) { var self = this; req.profiler.start('windshaft-cartodb.get_template_list'); - var cdbuser = req.context.user; + var cdbuser = res.locals.user; step( function checkPerms(){ @@ -168,14 +201,15 @@ NamedMapsAdminController.prototype.list = function(req, res) { assert.ifError(err); return { template_ids: tpl_ids }; }, - finishFn(self, req, res, 'GET TEMPLATE LIST') + finishFn(self, req, res, 'GET TEMPLATE LIST', null, next) ); }; -function finishFn(controller, req, res, description, status) { +function finishFn(controller, req, res, description, status, next) { return function finish(err, response){ if (err) { - controller.sendError(req, res, err, description); + err.label = description; + next(err); } else { controller.send(req, res, response, status || 200); } diff --git a/lib/cartodb/middleware/allow-query-params.js b/lib/cartodb/middleware/allow-query-params.js index 04a27033..7ec31d74 100644 --- a/lib/cartodb/middleware/allow-query-params.js +++ b/lib/cartodb/middleware/allow-query-params.js @@ -3,7 +3,7 @@ module.exports = function allowQueryParams(params) { throw new Error('allowQueryParams must receive an Array of params'); } return function allowQueryParamsMiddleware(req, res, next) { - req.context.allowedQueryParams = params; + res.locals.allowedQueryParams = params; next(); }; }; diff --git a/lib/cartodb/middleware/context/authorize.js b/lib/cartodb/middleware/context/authorize.js new file mode 100644 index 00000000..a42b5407 --- /dev/null +++ b/lib/cartodb/middleware/context/authorize.js @@ -0,0 +1,20 @@ +module.exports = function authorizeMiddleware (authApi) { + return function (req, res, next) { + req.profiler.done('req2params.setup'); + + authApi.authorize(req, res, (err, authorized) => { + req.profiler.done('authorize'); + if (err) { + return next(err); + } + + if(!authorized) { + err = new Error("Sorry, you are unauthorized (permission denied)"); + err.http_status = 403; + return next(err); + } + + return next(); + }); + }; +}; diff --git a/lib/cartodb/middleware/context/clean-up-query-params.js b/lib/cartodb/middleware/context/clean-up-query-params.js new file mode 100644 index 00000000..280fe986 --- /dev/null +++ b/lib/cartodb/middleware/context/clean-up-query-params.js @@ -0,0 +1,32 @@ +const _ = require('underscore'); + +// Whitelist query parameters and attach format +const REQUEST_QUERY_PARAMS_WHITELIST = [ + 'config', + 'map_key', + 'api_key', + 'auth_token', + 'callback', + 'zoom', + 'lon', + 'lat', + // analysis + 'filters' // json +]; + +module.exports = function cleanUpQueryParamsMiddleware () { + return function cleanUpQueryParams (req, res, next) { + var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST; + + if (Array.isArray(res.locals.allowedQueryParams)) { + allowedQueryParams = allowedQueryParams.concat(res.locals.allowedQueryParams); + } + + req.query = _.pick(req.query, allowedQueryParams); + + // bring all query values onto res.locals object + _.extend(res.locals, req.query); + + next(); + }; +}; diff --git a/lib/cartodb/middleware/context/db-conn-setup.js b/lib/cartodb/middleware/context/db-conn-setup.js new file mode 100644 index 00000000..dec48f6d --- /dev/null +++ b/lib/cartodb/middleware/context/db-conn-setup.js @@ -0,0 +1,31 @@ +const _ = require('underscore'); + +module.exports = function dbConnSetupMiddleware(pgConnection) { + return function dbConnSetup(req, res, next) { + const user = res.locals.user; + pgConnection.setDBConn(user, res.locals, (err) => { + if (err) { + if (err.message && -1 !== err.message.indexOf('name not found')) { + err.http_status = 404; + } + req.profiler.done('req2params'); + return next(err); + } + + // Add default database connection parameters + // if none given + _.defaults(res.locals, { + dbuser: global.environment.postgres.user, + dbpassword: global.environment.postgres.password, + dbhost: global.environment.postgres.host, + dbport: global.environment.postgres.port + }); + + res.set('X-Served-By-DB-Host', res.locals.dbhost); + + req.profiler.done('req2params'); + + next(null); + }); + }; +}; diff --git a/lib/cartodb/middleware/context/index.js b/lib/cartodb/middleware/context/index.js new file mode 100644 index 00000000..640aafd6 --- /dev/null +++ b/lib/cartodb/middleware/context/index.js @@ -0,0 +1,15 @@ +const locals = require('./locals'); +const cleanUpQueryParams = require('./clean-up-query-params'); +const layergroupToken = require('./layergroup-token'); +const authorize = require('./authorize'); +const dbConnSetup = require('./db-conn-setup'); + +module.exports = function prepareContextMiddleware(authApi, pgConnection) { + return [ + locals, + cleanUpQueryParams(), + layergroupToken, + authorize(authApi), + dbConnSetup(pgConnection) + ]; +}; diff --git a/lib/cartodb/middleware/context/layergroup-token.js b/lib/cartodb/middleware/context/layergroup-token.js new file mode 100644 index 00000000..026d0806 --- /dev/null +++ b/lib/cartodb/middleware/context/layergroup-token.js @@ -0,0 +1,32 @@ +var LayergroupToken = require('../../models/layergroup-token'); + +module.exports = function layergroupTokenMiddleware(req, res, next) { + if (!res.locals.token) { + return next(); + } + + var user = res.locals.user; + + var layergroupToken = LayergroupToken.parse(res.locals.token); + res.locals.token = layergroupToken.token; + res.locals.cache_buster = layergroupToken.cacheBuster; + + if (layergroupToken.signer) { + res.locals.signer = layergroupToken.signer; + if (!res.locals.signer) { + res.locals.signer = user; + } else if (res.locals.signer !== user) { + var err = new Error(`Cannot use map signature of user "${res.locals.signer}" on db of user "${user}"`); + err.type = 'auth'; + err.http_status = 403; + if (req.query && req.query.callback) { + err.http_status = 200; + } + + req.profiler.done('req2params'); + return next(err); + } + } + + return next(); +}; diff --git a/lib/cartodb/middleware/context/locals.js b/lib/cartodb/middleware/context/locals.js new file mode 100644 index 00000000..bb7d5ee2 --- /dev/null +++ b/lib/cartodb/middleware/context/locals.js @@ -0,0 +1,8 @@ +const _ = require('underscore'); + +module.exports = function localsMiddleware(req, res, next) { + _.extend(res.locals, req.params); + + next(); +}; + diff --git a/lib/cartodb/middleware/error-middleware.js b/lib/cartodb/middleware/error-middleware.js new file mode 100644 index 00000000..fd19e24f --- /dev/null +++ b/lib/cartodb/middleware/error-middleware.js @@ -0,0 +1,172 @@ +const _ = require('underscore'); +const debug = require('debug')('windshaft:cartodb:error-middleware'); + +module.exports = function errorMiddleware (/* options */) { + return function error (err, req, res, next) { + // jshint unused:false + // jshint maxcomplexity:9 + var allErrors = Array.isArray(err) ? err : [err]; + + allErrors = populateTimeoutErrors(allErrors); + + const label = err.label || 'UNKNOWN'; + err = allErrors[0] || new Error(label); + allErrors[0] = err; + + var statusCode = findStatusCode(err); + + if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') { + statusCode = 204; + } + + debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack); + + // If a callback was requested, force status to 200 + if (req.query && req.query.callback) { + statusCode = 200; + } + + var errorResponseBody = { + errors: allErrors.map(errorMessage), + errors_with_context: allErrors.map(errorMessageWithContext) + }; + + res.set('X-Tiler-Profiler', req.profiler.toJSONString()); + + res.status(statusCode); + + if (req.query && req.query.callback) { + res.jsonp(errorResponseBody); + } else { + res.json(errorResponseBody); + } + + try { + // May throw due to dns, see + // See http://github.com/CartoDB/Windshaft/issues/166 + req.profiler.sendStats(); + } catch (err) { + debug("error sending profiling stats: " + err); + } + }; +}; + +function isRenderTimeoutError (err) { + return err.message === 'Render timed out'; +} + +function isDatasourceTimeoutError (err) { + return err.message && err.message.match(/canceling statement due to statement timeout/i); +} + +function isTimeoutError (err) { + return isRenderTimeoutError(err) || isDatasourceTimeoutError(err); +} + +function populateTimeoutErrors (errors) { + return errors.map(function (error) { + if (isRenderTimeoutError(error)) { + error.subtype = 'render'; + } + + if (isDatasourceTimeoutError(error)) { + error.subtype = 'datasource'; + } + + if (isTimeoutError(error)) { + error.message = 'You are over platform\'s limits. Please contact us to know more details'; + error.type = 'limit'; + error.http_status = 429; + } + + return error; + }); +} + +function findStatusCode(err) { + var statusCode; + if ( err.http_status ) { + statusCode = err.http_status; + } else { + statusCode = statusFromErrorMessage('' + err); + } + return statusCode; +} + +module.exports.findStatusCode = findStatusCode; + +function statusFromErrorMessage(errMsg) { + // Find an appropriate statusCode based on message + // jshint maxcomplexity:7 + var statusCode = 400; + if ( -1 !== errMsg.indexOf('permission denied') ) { + statusCode = 403; + } + else if ( -1 !== errMsg.indexOf('authentication failed') ) { + statusCode = 403; + } + else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) { + statusCode = 400; + } + else if ( -1 !== errMsg.indexOf('does not exist') ) { + if ( -1 !== errMsg.indexOf(' role ') ) { + statusCode = 403; // role 'xxx' does not exist + } else if ( errMsg.match(/function .* does not exist/) ) { + statusCode = 400; // invalid SQL (SQL function does not exist) + } else { + statusCode = 404; + } + } + + + return statusCode; +} + +function errorMessage(err) { + // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 + var message = (_.isString(err) ? err : err.message) || 'Unknown error'; + + return stripConnectionInfo(message); +} + +module.exports.errorMessage = errorMessage; + +function stripConnectionInfo(message) { + // Strip connection info, if any + return message + // See https://github.com/CartoDB/Windshaft/issues/173 + .replace(/Connection string: '[^']*'\n\s/im, '') + // See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644 + .replace(/is the server.*encountered/im, 'encountered'); +} + +var ERROR_INFO_TO_EXPOSE = { + message: true, + layer: true, + type: true, + analysis: true, + subtype: true +}; + +function shouldBeExposed (prop) { + return !!ERROR_INFO_TO_EXPOSE[prop]; +} + +function errorMessageWithContext(err) { + // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 + var message = (_.isString(err) ? err : err.message) || 'Unknown error'; + + var error = { + type: err.type || 'unknown', + message: stripConnectionInfo(message), + }; + + for (var prop in err) { + // type & message are properties from Error's prototype and will be skipped + if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) { + error[prop] = err[prop]; + } + } + + return error; +} diff --git a/lib/cartodb/middleware/lzma.js b/lib/cartodb/middleware/lzma.js index d58f16cc..6655cdeb 100644 --- a/lib/cartodb/middleware/lzma.js +++ b/lib/cartodb/middleware/lzma.js @@ -1,8 +1,8 @@ 'use strict'; -var LZMA = require('lzma').LZMA; +const LZMA = require('lzma').LZMA; -var lzmaWorker = new LZMA(); +const lzmaWorker = new LZMA(); module.exports = function lzmaMiddleware(req, res, next) { if (!req.query.hasOwnProperty('lzma')) { diff --git a/lib/cartodb/middleware/user.js b/lib/cartodb/middleware/user.js index 40934849..ce3fdd29 100644 --- a/lib/cartodb/middleware/user.js +++ b/lib/cartodb/middleware/user.js @@ -2,6 +2,13 @@ var CdbRequest = require('../models/cdb_request'); var cdbRequest = new CdbRequest(); module.exports = function userMiddleware(req, res, next) { - req.context.user = cdbRequest.userByReq(req); + res.locals.user = cdbRequest.userByReq(req); + + // avoid a req.params.user equals to undefined + // overwrites res.locals.user + if(!req.params.user) { + delete req.params.user; + } + next(); }; diff --git a/test/support/layergroup-token.js b/lib/cartodb/models/layergroup-token.js similarity index 100% rename from test/support/layergroup-token.js rename to lib/cartodb/models/layergroup-token.js diff --git a/lib/cartodb/server.js b/lib/cartodb/server.js index 723289ba..1ce6439f 100644 --- a/lib/cartodb/server.js +++ b/lib/cartodb/server.js @@ -4,8 +4,6 @@ var RedisPool = require('redis-mpool'); var cartodbRedis = require('cartodb-redis'); var _ = require('underscore'); -var lzmaMiddleware = require('./middleware/lzma'); - var controller = require('./controllers'); var SurrogateKeysCache = require('./cache/surrogate_keys_cache'); @@ -46,6 +44,11 @@ var MapConfigAdapter = require('./models/mapconfig/adapter'); var StatsBackend = require('./backends/stats'); +const lzmaMiddleware = require('./middleware/lzma'); +const errorMiddleware = require('./middleware/error-middleware'); + +const prepareContextMiddleware = require('./middleware/context'); + module.exports = function(serverOptions) { // Make stats client globally accessible global.statsClient = StatsClient.getInstance(serverOptions.statsd); @@ -209,12 +212,16 @@ module.exports = function(serverOptions) { var versions = getAndValidateVersions(serverOptions); + const prepareContext = typeof serverOptions.req2params === 'function' ? + serverOptions.req2params : + prepareContextMiddleware(authApi, pgConnection); + /******************************************************************************************************************* * Routing ******************************************************************************************************************/ new controller.Layergroup( - authApi, + prepareContext, pgConnection, mapStore, tileBackend, @@ -227,7 +234,7 @@ module.exports = function(serverOptions) { ).register(app); new controller.Map( - authApi, + prepareContext, pgConnection, templateMaps, mapBackend, @@ -240,8 +247,7 @@ module.exports = function(serverOptions) { ).register(app); new controller.NamedMaps( - authApi, - pgConnection, + prepareContext, namedMapProviderCache, tileBackend, previewBackend, @@ -250,9 +256,9 @@ module.exports = function(serverOptions) { metadataBackend ).register(app); - new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app); + new controller.NamedMapsAdmin(authApi, templateMaps).register(app); - new controller.Analyses(authApi, pgConnection).register(app); + new controller.Analyses(prepareContext).register(app); new controller.ServerInfo(versions).register(app); @@ -260,6 +266,8 @@ module.exports = function(serverOptions) { * END Routing ******************************************************************************************************************/ + app.use(errorMiddleware()); + return app; }; @@ -354,7 +362,6 @@ function bootstrap(opts) { app.use(bodyParser.json()); app.use(function bootstrap$prepareRequestResponse(req, res, next) { - req.context = req.context || {}; req.profiler = new Profiler({ statsd_client: global.statsClient, profile: opts.useProfiler diff --git a/test/acceptance/analysis/named-maps.js b/test/acceptance/analysis/named-maps.js index ca27ec37..b9c93e00 100644 --- a/test/acceptance/analysis/named-maps.js +++ b/test/acceptance/analysis/named-maps.js @@ -7,7 +7,7 @@ var serverOptions = require('../../../lib/cartodb/server_options'); var server = new CartodbWindshaft(serverOptions); var TestClient = require('../../support/test-client'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); describe('named-maps analysis', function() { diff --git a/test/acceptance/cache/cache_headers.js b/test/acceptance/cache/cache_headers.js index 2cd916af..e7d8caf3 100644 --- a/test/acceptance/cache/cache_headers.js +++ b/test/acceptance/cache/cache_headers.js @@ -8,7 +8,7 @@ var serverOptions = require('../../../lib/cartodb/server_options'); var server = new CartodbWindshaft(serverOptions); server.setMaxListeners(0); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); describe('get requests with cache headers', function() { diff --git a/test/acceptance/dynamic-styling-named-maps.js b/test/acceptance/dynamic-styling-named-maps.js index 5fe2db3c..87796c5e 100644 --- a/test/acceptance/dynamic-styling-named-maps.js +++ b/test/acceptance/dynamic-styling-named-maps.js @@ -1,6 +1,6 @@ var assert = require('../support/assert'); var step = require('step'); -var LayergroupToken = require('../support/layergroup-token'); +var LayergroupToken = require('../../lib/cartodb/models/layergroup-token'); var testHelper = require(__dirname + '/../support/test_helper'); var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server'); var serverOptions = require(__dirname + '/../../lib/cartodb/server_options'); diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index 210418ac..9d287a58 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -9,7 +9,7 @@ var mapnik = require('windshaft').mapnik; var semver = require('semver'); var helper = require(__dirname + '/../support/test_helper'); -var LayergroupToken = require('../support/layergroup-token'); +var LayergroupToken = require('../../lib/cartodb/models/layergroup-token'); var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures'; diff --git a/test/acceptance/multilayer_server.js b/test/acceptance/multilayer_server.js index ba44e2d1..b599cf9c 100644 --- a/test/acceptance/multilayer_server.js +++ b/test/acceptance/multilayer_server.js @@ -4,7 +4,7 @@ var assert = require('../support/assert'); var _ = require('underscore'); -var LayergroupToken = require('../support/layergroup-token'); +var LayergroupToken = require('../../lib/cartodb/models/layergroup-token'); var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner'); var QueryTables = require('cartodb-query-tables'); diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index 2b1cf4ef..9c0a9966 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server'); var serverOptions = require(__dirname + '/../../lib/cartodb/server_options'); var server = new CartodbWindshaft(serverOptions); -var LayergroupToken = require('../support/layergroup-token'); +var LayergroupToken = require('../../lib/cartodb/models/layergroup-token'); var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js'); diff --git a/test/acceptance/overviews_metadata.js b/test/acceptance/overviews_metadata.js index ad0f23e5..8af7b2a4 100644 --- a/test/acceptance/overviews_metadata.js +++ b/test/acceptance/overviews_metadata.js @@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server'); var serverOptions = require(__dirname + '/../../lib/cartodb/server_options'); var server = new CartodbWindshaft(serverOptions); -var LayergroupToken = require('../support/layergroup-token'); +var LayergroupToken = require('../../lib/cartodb/models/layergroup-token'); var RedisPool = require('redis-mpool'); diff --git a/test/acceptance/overviews_metadata_named_maps.js b/test/acceptance/overviews_metadata_named_maps.js index 8e9720ec..a6557910 100644 --- a/test/acceptance/overviews_metadata_named_maps.js +++ b/test/acceptance/overviews_metadata_named_maps.js @@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server'); var serverOptions = require(__dirname + '/../../lib/cartodb/server_options'); var server = new CartodbWindshaft(serverOptions); -var LayergroupToken = require('../support/layergroup-token'); +var LayergroupToken = require('../../lib/cartodb/models/layergroup-token'); var RedisPool = require('redis-mpool'); diff --git a/test/acceptance/ported/attributes.js b/test/acceptance/ported/attributes.js index 04b48826..39f3f461 100644 --- a/test/acceptance/ported/attributes.js +++ b/test/acceptance/ported/attributes.js @@ -6,7 +6,7 @@ var cartodbServer = require('../../../lib/cartodb/server'); var PortedServerOptions = require('./support/ported_server_options'); var BaseController = require('../../../lib/cartodb/controllers/base'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); describe('attributes', function() { diff --git a/test/acceptance/ported/multilayer.js b/test/acceptance/ported/multilayer.js index fa6648f4..0592c147 100644 --- a/test/acceptance/ported/multilayer.js +++ b/test/acceptance/ported/multilayer.js @@ -7,7 +7,7 @@ var step = require('step'); var mapnik = require('windshaft').mapnik; var cartodbServer = require('../../../lib/cartodb/server'); var ServerOptions = require('./support/ported_server_options'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); var BaseController = require('../../../lib/cartodb/controllers/base'); describe('multilayer', function() { diff --git a/test/acceptance/ported/multilayer_interactivity.js b/test/acceptance/ported/multilayer_interactivity.js index 3f3f12b1..7d512670 100644 --- a/test/acceptance/ported/multilayer_interactivity.js +++ b/test/acceptance/ported/multilayer_interactivity.js @@ -5,7 +5,7 @@ var _ = require('underscore'); var cartodbServer = require('../../../lib/cartodb/server'); var getLayerTypeFn = require('windshaft').model.MapConfig.prototype.getType; var PortedServerOptions = require('./support/ported_server_options'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); var BaseController = require('../../../lib/cartodb/controllers/base'); diff --git a/test/acceptance/ported/raster.js b/test/acceptance/ported/raster.js index fc26661b..b16dd56d 100644 --- a/test/acceptance/ported/raster.js +++ b/test/acceptance/ported/raster.js @@ -6,7 +6,7 @@ var cartodbServer = require('../../../lib/cartodb/server'); var ServerOptions = require('./support/ported_server_options'); var BaseController = require('../../../lib/cartodb/controllers/base'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); describe('raster', function() { diff --git a/test/acceptance/ported/retina.js b/test/acceptance/ported/retina.js index 0962619f..80c3fcc5 100644 --- a/test/acceptance/ported/retina.js +++ b/test/acceptance/ported/retina.js @@ -6,7 +6,7 @@ var cartodbServer = require('../../../lib/cartodb/server'); var ServerOptions = require('./support/ported_server_options'); var BaseController = require('../../../lib/cartodb/controllers/base'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); describe('retina support', function() { diff --git a/test/acceptance/ported/server_png8_format.js b/test/acceptance/ported/server_png8_format.js index a710cbd0..092e9ad7 100644 --- a/test/acceptance/ported/server_png8_format.js +++ b/test/acceptance/ported/server_png8_format.js @@ -7,7 +7,7 @@ var cartodbServer = require('../../../lib/cartodb/server'); var ServerOptions = require('./support/ported_server_options'); var BaseController = require('../../../lib/cartodb/controllers/base'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); var IMAGE_EQUALS_TOLERANCE_PER_MIL = 85; diff --git a/test/acceptance/ported/support/ported_server_options.js b/test/acceptance/ported/support/ported_server_options.js index 875d42dc..8604e68c 100644 --- a/test/acceptance/ported/support/ported_server_options.js +++ b/test/acceptance/ported/support/ported_server_options.js @@ -1,7 +1,7 @@ var _ = require('underscore'); var serverOptions = require('../../../../lib/cartodb/server_options'); -var LayergroupToken = require('../../../support/layergroup-token'); var mapnik = require('windshaft').mapnik; +var LayergroupToken = require('../../../../lib/cartodb/models/layergroup-token'); var OverviewsQueryRewriter = require('../../../../lib/cartodb/utils/overviews_query_rewriter'); var overviewsQueryRewriter = new OverviewsQueryRewriter({ zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)' @@ -48,7 +48,7 @@ module.exports = _.extend({}, serverOptions, { log_format: null, // do not log anything afterLayergroupCreateCalls: 0, useProfiler: true, - req2params: function(req, callback){ + req2params: function(req, res, callback){ if ( req.query.testUnexpectedError ) { return callback('test unexpected error'); @@ -56,13 +56,14 @@ module.exports = _.extend({}, serverOptions, { // this is in case you want to test sql parameters eg ...png?sql=select * from my_table limit 10 req.params = _.extend({}, req.params); + if (req.params.token) { req.params.token = LayergroupToken.parse(req.params.token).token; } _.extend(req.params, req.query); req.params.user = 'localhost'; - req.context = {user: 'localhost'}; + res.locals.user = 'localhost'; req.params.dbhost = global.environment.postgres.host; req.params.dbport = req.params.dbport || global.environment.postgres.port; @@ -73,6 +74,9 @@ module.exports = _.extend({}, serverOptions, { } req.params.dbname = 'test_windshaft_cartodb_user_1_db'; + // add all params to res.locals + res.locals = _.extend({}, req.params); + // increment number of calls counter global.req2params_calls = global.req2params_calls ? global.req2params_calls + 1 : 1; diff --git a/test/acceptance/ported/support/test_client.js b/test/acceptance/ported/support/test_client.js index dad3ff3e..640f1b64 100644 --- a/test/acceptance/ported/support/test_client.js +++ b/test/acceptance/ported/support/test_client.js @@ -1,5 +1,5 @@ var testHelper = require('../../../support/test_helper'); -var LayergroupToken = require('../../../support/layergroup-token'); +var LayergroupToken = require('../../../../lib/cartodb/models/layergroup-token'); var step = require('step'); var assert = require('../../../support/assert'); diff --git a/test/acceptance/ported/torque.js b/test/acceptance/ported/torque.js index ac4422cc..c148a521 100644 --- a/test/acceptance/ported/torque.js +++ b/test/acceptance/ported/torque.js @@ -7,7 +7,7 @@ var cartodbServer = require('../../../lib/cartodb/server'); var ServerOptions = require('./support/ported_server_options'); var BaseController = require('../../../lib/cartodb/controllers/base'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); describe('torque', function() { diff --git a/test/acceptance/ported/torque_boundaries.js b/test/acceptance/ported/torque_boundaries.js index fb88be52..1456c055 100644 --- a/test/acceptance/ported/torque_boundaries.js +++ b/test/acceptance/ported/torque_boundaries.js @@ -5,7 +5,7 @@ var cartodbServer = require('../../../lib/cartodb/server'); var ServerOptions = require('./support/ported_server_options'); var BaseController = require('../../../lib/cartodb/controllers/base'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); describe('torque boundary points', function() { diff --git a/test/acceptance/templates.js b/test/acceptance/templates.js index 74d34d82..92a95f60 100644 --- a/test/acceptance/templates.js +++ b/test/acceptance/templates.js @@ -23,7 +23,7 @@ var serverOptions = require(__dirname + '/../../lib/cartodb/server_options'); var server = new CartodbWindshaft(serverOptions); server.setMaxListeners(0); -var LayergroupToken = require('../support/layergroup-token'); +var LayergroupToken = require('../../lib/cartodb/models/layergroup-token'); describe('template_api', function() { server.layergroupAffectedTablesCache.cache.reset(); diff --git a/test/acceptance/turbo-carto/named-maps.js b/test/acceptance/turbo-carto/named-maps.js index 4597ea3c..063118aa 100644 --- a/test/acceptance/turbo-carto/named-maps.js +++ b/test/acceptance/turbo-carto/named-maps.js @@ -1,6 +1,6 @@ var assert = require('../../support/assert'); var step = require('step'); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); var testHelper = require('../../support/test_helper'); var CartodbWindshaft = require('../../../lib/cartodb/server'); var serverOptions = require('../../../lib/cartodb/server_options'); diff --git a/test/acceptance/widgets/named-maps.js b/test/acceptance/widgets/named-maps.js index eb5a0d29..e54ced23 100644 --- a/test/acceptance/widgets/named-maps.js +++ b/test/acceptance/widgets/named-maps.js @@ -10,7 +10,7 @@ var CartodbWindshaft = require('../../../lib/cartodb/server'); var serverOptions = require('../../../lib/cartodb/server_options'); var server = new CartodbWindshaft(serverOptions); -var LayergroupToken = require('../../support/layergroup-token'); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); describe('named-maps widgets', function() { diff --git a/test/support/test-client.js b/test/support/test-client.js index fe619531..cd7b4f60 100644 --- a/test/support/test-client.js +++ b/test/support/test-client.js @@ -7,7 +7,7 @@ var PSQL = require('cartodb-psql'); var _ = require('underscore'); var mapnik = require('windshaft').mapnik; -var LayergroupToken = require('./layergroup-token'); +var LayergroupToken = require('../../lib/cartodb/models/layergroup-token'); var assert = require('./assert'); var helper = require('./test_helper'); @@ -116,6 +116,15 @@ module.exports.SQL = { ONE_POINT: 'select 1 as cartodb_id, \'SRID=3857;POINT(0 0)\'::geometry the_geom_webmercator' }; +function resErr2errRes(callback) { + return (res, err) => { + if (err) { + return callback(err); + } + return callback(err, res); + }; +} + TestClient.prototype.getWidget = function(widgetName, params, callback) { var self = this; @@ -716,9 +725,9 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) { expectedResponse.headers['Content-Type'] = 'application/json; charset=utf-8'; } - assert.response(self.server, request, expectedResponse, this); + assert.response(self.server, request, expectedResponse, resErr2errRes(this)); }, - function finish(res, err) { + function finish(err, res) { if (err) { return callback(err); } @@ -869,9 +878,9 @@ TestClient.prototype.getStaticCenter = function (params, callback) { } }, params.response); - assert.response(self.server, request, expectedResponse, this); + assert.response(self.server, request, expectedResponse, resErr2errRes(this)); }, - function(res, err) { + function(err, res) { if (err) { return callback(err); } @@ -968,9 +977,9 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) { } }; - assert.response(self.server, request, expectedResponse, this); + assert.response(self.server, request, expectedResponse, resErr2errRes(this)); }, - function finish(res, err) { + function finish(err, res) { if (err) { return callback(err); } @@ -1063,9 +1072,9 @@ TestClient.prototype.getAttributes = function(params, callback) { } }; - assert.response(self.server, request, expectedResponse, this); + assert.response(self.server, request, expectedResponse, resErr2errRes(this)); }, - function finish(res, err) { + function finish(err, res) { if (err) { return callback(err); } diff --git a/test/unit/cartodb/base_controller.js b/test/unit/cartodb/base_controller.js index 462e1957..9bf21ccd 100644 --- a/test/unit/cartodb/base_controller.js +++ b/test/unit/cartodb/base_controller.js @@ -1,21 +1,21 @@ require('../../support/test_helper.js'); var assert = require('assert'); -var BaseController = require('../../../lib/cartodb/controllers/base'); +var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware'); -describe('BaseController', function() { +describe('error-middleware', function() { it('different formats for postgis plugin error returns 400 as status code', function() { var expectedStatusCode = 400; assert.equal( - BaseController.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"), + errorMiddleware.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"), expectedStatusCode, "Error status code for single line does not match" ); assert.equal( - BaseController.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"), + errorMiddleware.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"), expectedStatusCode, "Error status code for multiline/PSQL does not match" ); diff --git a/test/unit/cartodb/error_messages.test.js b/test/unit/cartodb/error_messages.test.js index 52441db2..bfe0b03a 100644 --- a/test/unit/cartodb/error_messages.test.js +++ b/test/unit/cartodb/error_messages.test.js @@ -2,7 +2,7 @@ require('../../support/test_helper'); var assert = require('assert'); -var BaseController = require('../../../lib/cartodb/controllers/base'); +var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware'); describe('error messages clean up', function() { @@ -15,7 +15,7 @@ describe('error messages clean up', function() { " encountered during parsing of layer 'layer0' in Layer" ].join('\n'); - var outMessage = BaseController.errorMessage(inMessage); + var outMessage = errorMiddleware.errorMessage(inMessage); assert.ok(outMessage.match('connect'), outMessage); assert.ok(!outMessage.match(/666/), outMessage); diff --git a/test/unit/cartodb/ported/tile_stats.test.js b/test/unit/cartodb/ported/tile_stats.test.js index b71f1384..3fbf93ec 100644 --- a/test/unit/cartodb/ported/tile_stats.test.js +++ b/test/unit/cartodb/ported/tile_stats.test.js @@ -41,7 +41,9 @@ describe('tile stats', function() { jsonp: function() {}, send: function() {} }; - layergroupController.finalizeGetTileOrGrid('Unsupported format png2', reqMock, resMock, null, null); + + var next = function () {}; + layergroupController.finalizeGetTileOrGrid('Unsupported format png2', reqMock, resMock, null, null, next); assert.ok(formatMatched, 'Format was never matched in increment method'); assert.equal(expectedCalls, 0, 'Unexpected number of calls to increment method'); @@ -74,7 +76,8 @@ describe('tile stats', function() { var layergroupController = new LayergroupController(); - layergroupController.finalizeGetTileOrGrid('Another error happened', reqMock, resMock, null, null); + var next = function () {}; + layergroupController.finalizeGetTileOrGrid('Another error happened', reqMock, resMock, null, null, next); assert.ok(formatMatched, 'Format was never matched in increment method'); assert.equal(expectedCalls, 0, 'Unexpected number of calls to increment method'); diff --git a/test/unit/cartodb/prepare-context.test.js b/test/unit/cartodb/prepare-context.test.js new file mode 100644 index 00000000..dbbfb8bf --- /dev/null +++ b/test/unit/cartodb/prepare-context.test.js @@ -0,0 +1,171 @@ +var assert = require('assert'); +var _ = require('underscore'); + +var RedisPool = require('redis-mpool'); +var cartodbRedis = require('cartodb-redis'); +var PgConnection = require('../../../lib/cartodb/backends/pg_connection'); +var AuthApi = require('../../../lib/cartodb/api/auth_api'); +var TemplateMaps = require('../../../lib/cartodb/backends/template_maps'); + +const cleanUpQueryParamsMiddleware = require('../../../lib/cartodb/middleware/context/clean-up-query-params'); +const authorizeMiddleware = require('../../../lib/cartodb/middleware/context/authorize'); +const dbConnSetupMiddleware = require('../../../lib/cartodb/middleware/context/db-conn-setup'); +const localsMiddleware = require('../../../lib/cartodb/middleware/context/locals'); + +var windshaft = require('windshaft'); + +describe('prepare-context', function() { + + var test_user = _.template(global.environment.postgres_auth_user, {user_id:1}); + var test_pubuser = global.environment.postgres.user; + var test_database = test_user + '_db'; + + let cleanUpQueryParams; + let dbConnSetup; + let authorize; + + before(function() { + var redisPool = new RedisPool(global.environment.redis); + var mapStore = new windshaft.storage.MapStore(); + var metadataBackend = cartodbRedis({pool: redisPool}); + var pgConnection = new PgConnection(metadataBackend); + var templateMaps = new TemplateMaps(redisPool); + var authApi = new AuthApi(pgConnection, metadataBackend, mapStore, templateMaps); + + cleanUpQueryParams = cleanUpQueryParamsMiddleware(); + authorize = authorizeMiddleware(authApi); + dbConnSetup = dbConnSetupMiddleware(pgConnection); + }); + + + it('can be found in server_options', function(){ + assert.ok(_.isFunction(authorize)); + assert.ok(_.isFunction(dbConnSetup)); + assert.ok(_.isFunction(cleanUpQueryParams)); + }); + + function prepareRequest(req) { + req.profiler = { + done: function() {} + }; + + return req; + } + + function prepareResponse(res) { + if(!res.locals) { + res.locals = {}; + } + res.locals.user = 'localhost'; + + return res; + } + + it('res.locals are created', function(done) { + let req = {}; + let res = {}; + + localsMiddleware(prepareRequest(req), prepareResponse(res), function(err) { + if ( err ) { done(err); return; } + assert.ok(res.hasOwnProperty('locals'), 'response has locals'); + done(); + }); + }); + + it('cleans up request', function(done){ + var req = {headers: { host:'localhost' }, query: {dbuser:'hacker',dbname:'secret'}}; + var res = {}; + + cleanUpQueryParams(prepareRequest(req), prepareResponse(res), function(err) { + if ( err ) { done(err); return; } + assert.ok(_.isObject(req.query), 'request has query'); + assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); + assert.ok(res.hasOwnProperty('locals'), 'response has locals'); + assert.ok(!res.locals.hasOwnProperty('interactivity'), 'response locals do not have interactivity'); + done(); + }); + }); + + it('sets dbname from redis metadata', function(done){ + var req = {headers: { host:'localhost' }, query: {} }; + var res = { set: function () {} }; + + dbConnSetup(prepareRequest(req), prepareResponse(res), function(err) { + if ( err ) { done(err); return; } + assert.ok(_.isObject(req.query), 'request has query'); + assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); + assert.ok(res.hasOwnProperty('locals'), 'response has locals'); + assert.ok(!res.locals.hasOwnProperty('interactivity'), 'response locals do not have interactivity'); + assert.equal(res.locals.dbname, test_database); + assert.ok(res.locals.dbuser === test_pubuser, 'could inject dbuser ('+res.locals.dbuser+')'); + done(); + }); + }); + + it('sets also dbuser for authenticated requests', function(done){ + var req = { headers: { host: 'localhost' }, query: { map_key: '1234' }}; + var res = { set: function () {} }; + + // FIXME: review authorize-pgconnsetup workflow, It might we are doing authorization twice. + authorize(prepareRequest(req), prepareResponse(res), function (err) { + if (err) { done(err); return; } + dbConnSetup(req, res, function(err) { + if ( err ) { done(err); return; } + assert.ok(_.isObject(req.query), 'request has query'); + assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); + assert.ok(res.hasOwnProperty('locals'), 'response has locals'); + assert.ok(!res.locals.hasOwnProperty('interactivity'), 'request params do not have interactivity'); + assert.equal(res.locals.dbname, test_database); + assert.equal(res.locals.dbuser, test_user); + + req = { + headers: { + host:'localhost' + }, + query: { + map_key: '1235' + } + }; + + res = { set: function () {} }; + + dbConnSetup(prepareRequest(req), prepareResponse(res), function() { + // wrong key resets params to no user + assert.ok(res.locals.dbuser === test_pubuser, 'could inject dbuser ('+res.locals.dbuser+')'); + done(); + }); + }); + }); + }); + + it('it should remove invalid params', function(done) { + var config = { + version: '1.3.0' + }; + var req = { + headers: { + host:'localhost' + }, + query: { + non_included: 'toberemoved', + api_key: 'test', + style: 'override', + config: config + } + }; + var res = {}; + + cleanUpQueryParams(prepareRequest(req), prepareResponse(res), function (err) { + if ( err ) { + return done(err); + } + + var query = res.locals; + assert.deepEqual(config, query.config); + assert.equal('test', query.api_key); + assert.equal(undefined, query.non_included); + done(); + }); + }); + +}); diff --git a/test/unit/cartodb/req2params.test.js b/test/unit/cartodb/req2params.test.js deleted file mode 100644 index 7b055e99..00000000 --- a/test/unit/cartodb/req2params.test.js +++ /dev/null @@ -1,129 +0,0 @@ -var assert = require('assert'); -var _ = require('underscore'); -require('../../support/test_helper'); - -var RedisPool = require('redis-mpool'); -var cartodbRedis = require('cartodb-redis'); -var PgConnection = require('../../../lib/cartodb/backends/pg_connection'); -var AuthApi = require('../../../lib/cartodb/api/auth_api'); -var TemplateMaps = require('../../../lib/cartodb/backends/template_maps'); - -var BaseController = require('../../../lib/cartodb/controllers/base'); -var windshaft = require('windshaft'); - -describe('req2params', function() { - - var test_user = _.template(global.environment.postgres_auth_user, {user_id:1}); - var test_pubuser = global.environment.postgres.user; - var test_database = test_user + '_db'; - - - var baseController; - before(function() { - var redisPool = new RedisPool(global.environment.redis); - var mapStore = new windshaft.storage.MapStore(); - var metadataBackend = cartodbRedis({pool: redisPool}); - var pgConnection = new PgConnection(metadataBackend); - var templateMaps = new TemplateMaps(redisPool); - var authApi = new AuthApi(pgConnection, metadataBackend, mapStore, templateMaps); - - baseController = new BaseController(authApi, pgConnection); - }); - - - it('can be found in server_options', function(){ - assert.ok(_.isFunction(baseController.req2params)); - }); - - function prepareRequest(req) { - req.profiler = { - done: function() {} - }; - req.context = { user: 'localhost' }; - return req; - } - - it('cleans up request', function(done){ - var req = {headers: { host:'localhost' }, query: {dbuser:'hacker',dbname:'secret'}}; - baseController.req2params(prepareRequest(req), function(err, req) { - if ( err ) { done(err); return; } - assert.ok(_.isObject(req.query), 'request has query'); - assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); - assert.ok(req.hasOwnProperty('params'), 'request has params'); - assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity'); - assert.equal(req.params.dbname, test_database, 'could forge dbname: '+ req.params.dbname); - assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')'); - done(); - }); - }); - - it('sets dbname from redis metadata', function(done){ - var req = {headers: { host:'localhost' }, query: {} }; - baseController.req2params(prepareRequest(req), function(err, req) { - if ( err ) { done(err); return; } - assert.ok(_.isObject(req.query), 'request has query'); - assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); - assert.ok(req.hasOwnProperty('params'), 'request has params'); - assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity'); - assert.equal(req.params.dbname, test_database); - assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')'); - done(); - }); - }); - - it('sets also dbuser for authenticated requests', function(done){ - var req = {headers: { host:'localhost' }, query: {map_key: '1234'} }; - baseController.req2params(prepareRequest(req), function(err, req) { - if ( err ) { done(err); return; } - assert.ok(_.isObject(req.query), 'request has query'); - assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); - assert.ok(req.hasOwnProperty('params'), 'request has params'); - assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity'); - assert.equal(req.params.dbname, test_database); - assert.equal(req.params.dbuser, test_user); - - req = { - headers: { - host:'localhost' - }, - query: { - map_key: '1235' - } - }; - baseController.req2params(prepareRequest(req), function(err, req) { - // wrong key resets params to no user - assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')'); - done(); - }); - }); - }); - - it('it should remove invalid params', function(done) { - var config = { - version: '1.3.0' - }; - var req = { - headers: { - host:'localhost' - }, - query: { - non_included: 'toberemoved', - api_key: 'test', - style: 'override', - config: config - } - }; - baseController.req2params(prepareRequest(req), function(err, req) { - if (err) { - return done(err); - } - var query = req.params; - assert.deepEqual(config, query.config); - assert.equal('test', query.api_key); - assert.equal(undefined, query.non_included); - assert.equal(undefined, query.style); - done(); - }); - }); - -});