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 2efa11b6..657f8545 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,10 @@ ## 3.12.11 Released 2017-mm-dd +Bugfixes: + - Bounding box parameter ignored in static named maps #735. + + ## 3.12.10 Released 2017-09-18 - Upgrades windshaft to [3.3.2](https://github.com/CartoDB/windshaft/releases/tag/3.3.2). diff --git a/app.js b/app.js index 1220e8f6..9270cac0 100755 --- a/app.js +++ b/app.js @@ -2,14 +2,20 @@ var http = require('http'); var https = require('https'); var path = require('path'); var fs = require('fs'); - var _ = require('underscore'); +var semver = require('semver'); // jshint undef:false var log = console.log.bind(console); var logError = console.error.bind(console); // jshint undef:true +var nodejsVersion = process.versions.node; +if (!semver.satisfies(nodejsVersion, '>=6.9.0')) { + logError(`Node version ${nodejsVersion} is not supported, please use Node.js 6.9 or higher.`); + process.exit(1); +} + var argv = require('yargs') .usage('Usage: $0 [options]') .help('h') diff --git a/lib/cartodb/controllers/analyses.js b/lib/cartodb/controllers/analyses.js index 7760cc32..ebe9c007 100644 --- a/lib/cartodb/controllers/analyses.js +++ b/lib/cartodb/controllers/analyses.js @@ -10,16 +10,27 @@ 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); module.exports = AnalysesController; -AnalysesController.prototype.register = function(app) { - app.get(app.base_url_mapconfig + '/analyses/catalog', cors(), userMiddleware, this.catalog.bind(this)); +AnalysesController.prototype.register = function(router) { + router.use( + cors(), + userMiddleware + ); + + router.get( + '/analyses/catalog', + this.prepareContext, + this.catalog.bind(this) + ); }; AnalysesController.prototype.sendResponse = function(req, res, resource) { @@ -27,16 +38,12 @@ 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; step( - function reqParams() { - self.req2params(req, this); - }, - function catalogQuery(err) { - assert.ifError(err); + function catalogQuery() { var pg = new PSQL(dbParamsFromReqParams(req.params)); getMetadata(username, pg, this); }, @@ -80,7 +87,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 f74702e9..7462f6ba 100644 --- a/lib/cartodb/controllers/base.js +++ b/lib/cartodb/controllers/base.js @@ -1,156 +1,10 @@ -var assert = require('assert'); - -var _ = require('underscore'); -var step = require('step'); var debug = require('debug')('windshaft:cartodb'); -var LZMA = require('lzma').LZMA; -var lzmaWorker = new LZMA(); - -// 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:10 -/** - * 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; - - if ( req.query.lzma ) { - - // Decode (from base64) - var lzma = new Buffer(req.query.lzma, 'base64') - .toString('binary') - .split('') - .map(function(c) { - return c.charCodeAt(0) - 128; - }); - - - // Decompress - lzmaWorker.decompress( - lzma, - function(result) { - req.profiler.done('lzma'); - try { - delete req.query.lzma; - _.extend(req.query, JSON.parse(result)); - self.req2params(req, callback); - } catch (err) { - req.profiler.done('req2params'); - callback(new Error('Error parsing lzma as JSON: ' + err)); - } - } - ); - return; - } - - 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) { @@ -184,150 +38,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 f787c426..2172f6c3 100644 --- a/lib/cartodb/controllers/layergroup.js +++ b/lib/cartodb/controllers/layergroup.js @@ -6,7 +6,6 @@ var BaseController = require('./base'); var cors = require('../middleware/cors'); var userMiddleware = require('../middleware/user'); -var layergroupTokenMiddleware = require('../middleware/layergroup-token'); var allowQueryParams = require('../middleware/allow-query-params'); var DataviewBackend = require('../backends/dataview'); @@ -29,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; @@ -44,41 +43,58 @@ 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(router) { + router.use( + cors(), + userMiddleware + ); -LayergroupController.prototype.register = function(app) { - app.get(app.base_url_mapconfig + - '/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware, layergroupTokenMiddleware, - this.tile.bind(this)); + router.get( + '/:token/:z/:x/:y@:scale_factor?x.:format', + this.prepareContext, + this.tile.bind(this) + ); - app.get(app.base_url_mapconfig + - '/:token/:z/:x/:y.:format', cors(), userMiddleware, layergroupTokenMiddleware, - this.tile.bind(this)); + router.get( + '/:token/:z/:x/:y.:format', + this.prepareContext, + this.tile.bind(this) + ); - app.get(app.base_url_mapconfig + - '/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware, layergroupTokenMiddleware, - this.layer.bind(this)); + router.get( + '/:token/:layer/:z/:x/:y.(:format)', + validateLayerRouteMiddleware, + this.prepareContext, + this.layer.bind(this) + ); - app.get(app.base_url_mapconfig + - '/:token/:layer/attributes/:fid', cors(), userMiddleware, layergroupTokenMiddleware, - this.attributes.bind(this)); + router.get( + '/:token/:layer/attributes/:fid', + this.prepareContext, + this.attributes.bind(this) + ); - app.get(app.base_url_mapconfig + + router.get( '/static/center/:token/:z/:lat/:lng/:width/:height.:format', - cors(), userMiddleware, allowQueryParams(['layer']), - layergroupTokenMiddleware, - this.center.bind(this)); + allowQueryParams(['layer']), + this.prepareContext, + this.center.bind(this) + ); - app.get(app.base_url_mapconfig + + router.get( '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', - cors(), userMiddleware, allowQueryParams(['layer']), - layergroupTokenMiddleware, - this.bbox.bind(this)); + allowQueryParams(['layer']), + this.prepareContext, + this.bbox.bind(this) + ); // Undocumented/non-supported API endpoint methods. // Use at your own peril. @@ -96,64 +112,53 @@ LayergroupController.prototype.register = function(app) { 'q' // widgets search ]; - app.get( - app.base_url_mapconfig + '/:token/dataview/:dataviewName', - cors(), - userMiddleware, + router.get( + '/:token/dataview/:dataviewName', allowQueryParams(allowedDataviewQueryParams), - layergroupTokenMiddleware, + this.prepareContext, this.dataview.bind(this) ); - app.get( - app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName', - cors(), - userMiddleware, + router.get( + '/:token/:layer/widget/:dataviewName', allowQueryParams(allowedDataviewQueryParams), - layergroupTokenMiddleware, + this.prepareContext, this.dataview.bind(this) ); - app.get( - app.base_url_mapconfig + '/:token/dataview/:dataviewName/search', - cors(), - userMiddleware, + router.get( + '/:token/dataview/:dataviewName/search', allowQueryParams(allowedDataviewQueryParams), - layergroupTokenMiddleware, + this.prepareContext, this.dataviewSearch.bind(this) ); - app.get( - app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName/search', - cors(), - userMiddleware, + router.get( + '/:token/:layer/widget/:dataviewName/search', allowQueryParams(allowedDataviewQueryParams), - layergroupTokenMiddleware, + this.prepareContext, this.dataviewSearch.bind(this) ); - app.get(app.base_url_mapconfig + - '/:token/analysis/node/:nodeId', cors(), userMiddleware, - layergroupTokenMiddleware, - this.analysisNodeStatus.bind(this)); + router.get('/:token/analysis/node/:nodeId', + 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); + function retrieveNodeStatus() { self.analysisStatusBackend.getNodeStatus(req.params, 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', @@ -164,16 +169,11 @@ 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 ); @@ -183,25 +183,20 @@ LayergroupController.prototype.dataview = function(req, res) { 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 ); @@ -211,7 +206,8 @@ LayergroupController.prototype.dataviewSearch = function(req, res) { 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); } @@ -220,18 +216,13 @@ 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 ); @@ -241,7 +232,8 @@ LayergroupController.prototype.attributes = function(req, res) { 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); } @@ -251,29 +243,22 @@ 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), req.params, this @@ -281,14 +266,14 @@ LayergroupController.prototype.tileOrLayer = function (req, res) { }, 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); } ); }; // 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, @@ -317,7 +302,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 { @@ -327,35 +314,31 @@ 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 - }); + }, 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.layer = req.params.layer || 'all'; + req.params.format = req.params.format || 'png'; 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), @@ -371,7 +354,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); @@ -476,3 +460,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..661cdb39 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); + this.create(req, res, function createGet$prepareConfig(req) { if ( ! req.params.config ) { throw new Error('layergroup GET needs a "config" parameter'); } return JSON.parse(req.params.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,10 +154,10 @@ MapController.prototype.create = function(req, res, prepareConfigFn) { var context = {}; step( - function setupParams(){ - self.req2params(req, this); + function prepareConfig () { + const requestMapConfig = prepareConfigFn(req); + return requestMapConfig; }, - prepareConfigFn, function prepareAdapterMapConfig(err, requestMapConfig) { assert.ifError(err); context.analysisConfiguration = { @@ -188,7 +210,8 @@ 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()); @@ -212,7 +235,7 @@ 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; @@ -220,9 +243,6 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn var mapConfigProvider; var mapConfig; step( - function setupParams(){ - self.req2params(req, this); - }, function getTemplateParams() { prepareParamsFn(this); }, @@ -259,7 +279,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; diff --git a/lib/cartodb/controllers/named_maps.js b/lib/cartodb/controllers/named_maps.js index bde3827d..3898b0c7 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,13 +28,22 @@ 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, allowQueryParams(['layer']), - this.staticMap.bind(this)); + app.get( + app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', + cors(), + userMiddleware, + allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']), + this.prepareContext, + this.staticMap.bind(this) + ); }; NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) { @@ -74,18 +84,14 @@ 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 namedMapProvider; step( - function reqParams() { - self.req2params(req, this); - }, - function getNamedMapProvider(err) { - assert.ifError(err); + function getNamedMapProvider() { self.namedMapProviderCache.get( cdbUser, req.params.template_id, @@ -103,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); } @@ -111,22 +118,18 @@ 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 format = req.params.format === 'jpg' ? 'jpeg' : 'png'; - req.params.format = 'png'; - req.params.layer = 'all'; + req.params.format = req.params.format || 'png'; + req.params.layer = req.params.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, @@ -179,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); } diff --git a/lib/cartodb/controllers/named_maps_admin.js b/lib/cartodb/controllers/named_maps_admin.js index 08ad5322..a4a126d5 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,16 +26,22 @@ 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 (router) { + router.options('/:template_id', cors('Content-Type')); + + router.use( + cors(), + userMiddleware + ); + + router.post('/', this.create.bind(this)); + router.put('/:template_id', this.update.bind(this)); + router.get('/:template_id', this.retrieve.bind(this)); + router.delete('/:template_id', this.destroy.bind(this)); + router.get('/', this.list.bind(this)); }; -NamedMapsAdminController.prototype.create = function(req, res) { +NamedMapsAdminController.prototype.create = function(req, res, next) { var self = this; var cdbuser = req.context.user; @@ -55,11 +61,11 @@ 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; @@ -84,11 +90,11 @@ 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'); @@ -118,11 +124,11 @@ 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'); @@ -144,11 +150,11 @@ 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'); @@ -168,14 +174,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/context/authorize.js b/lib/cartodb/middleware/context/authorize.js new file mode 100644 index 00000000..bf95a78a --- /dev/null +++ b/lib/cartodb/middleware/context/authorize.js @@ -0,0 +1,27 @@ +const _ = require('underscore'); + +module.exports = function authorizeMiddleware (authApi) { + return function (req, res, next) { + // FIXME: Temporary hack to share data between middlewares. Express overrides req.params to + // parse url params to an object and it's performed after matching path and controller. + res.locals = {}; + _.extend(res.locals, req.params); + + req.profiler.done('req2params.setup'); + + authApi.authorize(req, (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..7b0b56eb --- /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(req.context.allowedQueryParams)) { + allowedQueryParams = allowedQueryParams.concat(req.context.allowedQueryParams); + } + + req.query = _.pick(req.query, allowedQueryParams); + + // bring all query values onto req.params object + _.extend(req.params, 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..97efb77d --- /dev/null +++ b/lib/cartodb/middleware/context/db-conn-setup.js @@ -0,0 +1,39 @@ +const _ = require('underscore'); + +module.exports = function dbConnSetupMiddleware(pgConnection) { + return function (req, res, next) { + const user = req.context.user; + + // FIXME: this function shouldn't be able to change `req.params`. It should return an + // object with the user's conf and it should be merge with default here. + pgConnection.setDBConn(user, req.params, (err) => { + if (err) { + if (err.message && -1 !== err.message.indexOf('name not found')) { + err.http_status = 404; + } + req.profiler.done('req2params'); + return next(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 + }); + + // FIXME: Temporary hack to share data between middlewares. Express overrides req.params to + // parse url params to an object and it's performed after matching path and controller. + if (!res.locals) { + res.locals = {}; + } + _.defaults(res.locals, req.params); + + req.profiler.done('req2params'); + + next(null, req); + }); + }; +}; diff --git a/lib/cartodb/middleware/context/index.js b/lib/cartodb/middleware/context/index.js new file mode 100644 index 00000000..9d87b6ee --- /dev/null +++ b/lib/cartodb/middleware/context/index.js @@ -0,0 +1,13 @@ +const cleanUpQueryParams = require('./clean-up-query-params'); +const parseTokenParam = require('./parse-token-param'); +const authorize = require('./authorize'); +const dbConnSetup = require('./db-conn-setup'); + +module.exports = function prepareContextMiddleware(authApi, pgConnection) { + return [ + cleanUpQueryParams(), + parseTokenParam(), + authorize(authApi), + dbConnSetup(pgConnection) + ]; +}; diff --git a/lib/cartodb/middleware/context/parse-token-param.js b/lib/cartodb/middleware/context/parse-token-param.js new file mode 100644 index 00000000..e2a0c2c5 --- /dev/null +++ b/lib/cartodb/middleware/context/parse-token-param.js @@ -0,0 +1,47 @@ +module.exports = function parseTokenParamMiddleware () { + return function parseTokenParam (req, res, next) { + // jshint maxcomplexity:7 + if (!req.params.token) { + return next(); + } + + var user = req.context.user; + + // 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'); + + return next(err); + } + + // skip template hash + if (tksplit.length > 1) { + tksplit.shift(); + } + + req.params.token = tksplit.shift(); + } + + next(); + }; +}; diff --git a/lib/cartodb/middleware/error-middleware.js b/lib/cartodb/middleware/error-middleware.js new file mode 100644 index 00000000..71f6c411 --- /dev/null +++ b/lib/cartodb/middleware/error-middleware.js @@ -0,0 +1,176 @@ +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) + }; + + if (res.locals && res.locals.dbhost) { + res.set('X-Served-By-DB-Host', res.locals.dbhost); + } + + 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 new file mode 100644 index 00000000..6655cdeb --- /dev/null +++ b/lib/cartodb/middleware/lzma.js @@ -0,0 +1,30 @@ +'use strict'; + +const LZMA = require('lzma').LZMA; + +const lzmaWorker = new LZMA(); + +module.exports = function lzmaMiddleware(req, res, next) { + if (!req.query.hasOwnProperty('lzma')) { + return next(); + } + + // Decode (from base64) + var lzma = new Buffer(req.query.lzma, 'base64') + .toString('binary') + .split('') + .map(function(c) { + return c.charCodeAt(0) - 128; + }); + + // Decompress + lzmaWorker.decompress(lzma, function(result) { + try { + delete req.query.lzma; + Object.assign(req.query, JSON.parse(result)); + next(); + } catch (err) { + next(new Error('Error parsing lzma as JSON: ' + err)); + } + }); +}; diff --git a/lib/cartodb/server.js b/lib/cartodb/server.js index d9302276..51841dca 100644 --- a/lib/cartodb/server.js +++ b/lib/cartodb/server.js @@ -44,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); @@ -206,12 +211,18 @@ module.exports = function(serverOptions) { var versions = getAndValidateVersions(serverOptions); + const prepareContext = typeof serverOptions.req2params === 'function' ? + serverOptions.req2params : + prepareContextMiddleware(authApi, pgConnection); + /******************************************************************************************************************* * Routing ******************************************************************************************************************/ + const routerLayergroup = express.Router(); + new controller.Layergroup( - authApi, + prepareContext, pgConnection, mapStore, tileBackend, @@ -221,10 +232,12 @@ module.exports = function(serverOptions) { userLimitsApi, layergroupAffectedTablesCache, analysisBackend - ).register(app); + ).register(routerLayergroup); + + app.use(app.base_url_mapconfig, routerLayergroup); new controller.Map( - authApi, + prepareContext, pgConnection, templateMaps, mapBackend, @@ -237,8 +250,7 @@ module.exports = function(serverOptions) { ).register(app); new controller.NamedMaps( - authApi, - pgConnection, + prepareContext, namedMapProviderCache, tileBackend, previewBackend, @@ -247,9 +259,13 @@ module.exports = function(serverOptions) { metadataBackend ).register(app); - new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app); + const namedMapsAdminRouter = express.Router(); + new controller.NamedMapsAdmin(authApi, templateMaps).register(namedMapsAdminRouter); + app.use(app.base_url_templated, namedMapsAdminRouter); - new controller.Analyses(authApi, pgConnection).register(app); + const analysisRouter = express.Router(); + new controller.Analyses(prepareContext).register(analysisRouter); + app.use(app.base_url_mapconfig, analysisRouter); new controller.ServerInfo(versions).register(app); @@ -257,6 +273,8 @@ module.exports = function(serverOptions) { * END Routing ******************************************************************************************************************/ + app.use(errorMiddleware()); + return app; }; @@ -364,6 +382,8 @@ function bootstrap(opts) { next(); }); + app.use(lzmaMiddleware); + // temporary measure until we upgrade to newer version expressjs so we can check err.status app.use(function(err, req, res, next) { if (err) { diff --git a/package.json b/package.json index 9c70ad72..02c94e52 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "queue-async": "~1.0.7", "redis-mpool": "0.4.1", "request": "~2.79.0", + "semver": "~5.3.0", "step": "~0.0.6", "step-profiler": "~0.3.0", "turbo-carto": "0.19.2", @@ -52,7 +53,6 @@ "moment": "~2.18.1", "nock": "~2.11.0", "redis": "~0.12.1", - "semver": "~1.1.4", "strftime": "~0.8.2" }, "scripts": { diff --git a/test/acceptance/analysis/analysis-layers-use-cases.js b/test/acceptance/analysis/analysis-layers-use-cases.js index 83c2f535..2f087626 100644 --- a/test/acceptance/analysis/analysis-layers-use-cases.js +++ b/test/acceptance/analysis/analysis-layers-use-cases.js @@ -5,34 +5,34 @@ var TestClient = require('../../support/test-client'); var dot = require('dot'); var debug = require('debug')('windshaft:cartodb:test'); -describe('analysis-layers use cases', function() { +describe('analysis-layers use cases', function () { - var multitypeStyleTemplate = dot.template([ - "#points['mapnik::geometry_type'=1] {", - " marker-fill-opacity: {{=it._opacity}};", - " marker-line-color: #FFF;", - " marker-line-width: 0.5;", - " marker-line-opacity: {{=it._opacity}};", - " marker-placement: point;", - " marker-type: ellipse;", - " marker-width: 8;", - " marker-fill: {{=it._color}};", - " marker-allow-overlap: true;", - "}", - "#lines['mapnik::geometry_type'=2] {", - " line-color: {{=it._color}};", - " line-width: 2;", - " line-opacity: {{=it._opacity}};", - "}", - "#polygons['mapnik::geometry_type'=3] {", - " polygon-fill: {{=it._color}};", - " polygon-opacity: {{=it._opacity}};", - " line-color: #FFF;", - " line-width: 0.5;", - " line-opacity: {{=it._opacity}};", - "}" - ].join('\n')); + var multitypeStyleTemplate = dot.template( + `#points['mapnik::geometry_type'=1] { + marker-fill-opacity: {{=it._opacity}}; + marker-line-color: #FFF; + marker-line-width: 0.5; + marker-line-opacity: {{=it._opacity}}; + marker-placement: point; + marker-type: ellipse; + marker-width: 8; + marker-fill: {{=it._color}}; + marker-allow-overlap: true; + } + #lines['mapnik::geometry_type'=2] { + line-color: {{=it._color}}; + line-width: 2; + line-opacity: {{=it._opacity}}; + } + #polygons['mapnik::geometry_type'=3] { + polygon-fill: {{=it._color}}; + polygon-opacity: {{=it._opacity}}; + line-color: #FFF; + line-width: 0.5; + line-opacity: {{=it._opacity}}; + }` + ); function cartocss(color, opacity) { @@ -47,18 +47,53 @@ describe('analysis-layers use cases', function() { version: '1.5.0', layers: layers, dataviews: dataviews || {}, - analysis: analysis || [] + analyses: analysis || [] }; } - function analysisDef(analysis) { - return JSON.stringify(analysis); - } - var DEFAULT_MULTITYPE_STYLE = cartocss(); var TILE_ANALYSIS_TABLES = { z: 14, x: 8023, y: 6177 }; + var pointInPolygonDef = { + id: 'a1', + type: 'point-in-polygon', + params: { + points_source: { + type: 'source', + params: { + query: 'select * from analysis_rent_listings' + } + }, + polygons_source: { + type: 'buffer', + params: { + source: { + type: 'source', + params: { + query: 'select * from analysis_banks' + } + }, + radius: 250 + } + } + } + }; + + var bufferDef = { + id: 'b1', + type: 'buffer', + params: { + source: { + type: 'source', + params: { + query: 'select * from analysis_banks' + } + }, + radius: 250 + } + }; + var useCases = [ { desc: '1 mapnik layer', @@ -68,7 +103,7 @@ describe('analysis-layers use cases', function() { { type: 'cartodb', options: { - sql: "select * from analysis_rent_listings", + sql: 'select * from analysis_rent_listings', cartocss: DEFAULT_MULTITYPE_STYLE, cartocss_version: '2.3.0' } @@ -83,7 +118,7 @@ describe('analysis-layers use cases', function() { { type: 'cartodb', options: { - sql: "select * from analysis_banks", + sql: 'select * from analysis_banks', cartocss: cartocss('#2167AB'), cartocss_version: '2.3.0' } @@ -91,7 +126,7 @@ describe('analysis-layers use cases', function() { { type: 'cartodb', options: { - sql: "select * from analysis_rent_listings", + sql: 'select * from analysis_rent_listings', cartocss: DEFAULT_MULTITYPE_STYLE, cartocss_version: '2.3.0' } @@ -105,30 +140,27 @@ describe('analysis-layers use cases', function() { { type: 'cartodb', options: { - sql: "select * from analysis_rent_listings", + sql: 'select * from analysis_rent_listings', cartocss: DEFAULT_MULTITYPE_STYLE, cartocss_version: '2.3.0' } }, { - type: 'analysis', + type: 'cartodb', options: { - def: analysisDef({ - "type": "buffer", - "params": { - "source": { - "type": "source", - "params": { - "query": "select * from analysis_banks" - } - }, - "radius": 250 - } - }), - cartocss: cartocss('black', 0.5) + source: { + id: 'b1' + }, + cartocss: DEFAULT_MULTITYPE_STYLE, + cartocss_version: '2.3.0' } } - ]) + ], + {}, + [ + bufferDef + ] + ) }, { @@ -137,531 +169,115 @@ describe('analysis-layers use cases', function() { { type: 'cartodb', options: { - sql: "select * from analysis_rent_listings", + sql: 'select * from analysis_rent_listings', cartocss: DEFAULT_MULTITYPE_STYLE, cartocss_version: '2.3.0' } }, { - type: 'analysis', + type: 'cartodb', options: { - def: analysisDef({ - "type": "point-in-polygon", - "params": { - "pointsSource": { - "type": "source", - "params": { - "query": "select * from analysis_rent_listings" - } - }, - "polygonsSource": { - "type": "buffer", - "params": { - "source": { - "type": "source", - "params": { - "query": "select * from analysis_banks" - } - }, - "radius": 250 - } - } - } - }), - cartocss: cartocss('green', 1.0) + source: { + id: 'a1' + }, + cartocss: DEFAULT_MULTITYPE_STYLE, + cartocss_version: '2.3.0' } } - ]) + ], + {}, + [ + pointInPolygonDef + ] + ) }, { desc: 'point-in-polygon from buffer atm-machines and rent listings + rent listings', - mapConfig: mapConfig([ - { - type: 'analysis', - options: { - def: analysisDef({ - "type": "point-in-polygon", - "params": { - "pointsSource": { - "type": "source", - "params": { - "query": "select * from analysis_rent_listings" - } - }, - "polygonsSource": { - "type": "buffer", - "params": { - "source": { - "type": "source", - "params": { - "query": "select * from analysis_banks" - } - }, - "radius": 250 - } - } - } - }), - cartocss: cartocss('green', 1.0) - } - }, - { - type: 'cartodb', - options: { - sql: "select * from analysis_rent_listings", - cartocss: DEFAULT_MULTITYPE_STYLE, - cartocss_version: '2.3.0' - } - } - ]) - }, - - { - desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings', - mapConfig: mapConfig([ - { - type: 'cartodb', - options: { - sql: "select * from analysis_rent_listings", - cartocss: DEFAULT_MULTITYPE_STYLE, - cartocss_version: '2.3.0' - } - }, - { - type: 'analysis', - options: { - def: analysisDef({ - "type": "buffer", - "params": { - "source": { - "type": "source", - "params": { - "query": "select * from analysis_banks" - } - }, - "radius": 300 - } - }), - cartocss: cartocss('magenta', 0.5) - } - }, - { - type: 'analysis', - options: { - def: analysisDef({ - "type": "point-in-polygon", - "params": { - "pointsSource": { - "type": "source", - "params": { - "query": "select * from analysis_rent_listings" - } - }, - "polygonsSource": { - "type": "buffer", - "params": { - "source": { - "type": "source", - "params": { - "query": "select * from analysis_banks" - } - }, - "radius": 300 - } - } - } - }), - cartocss: cartocss('green', 1.0) - } - } - ]) - }, - - { - skip: true, - desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings', - mapConfig: mapConfig([ - { - type: 'cartodb', - options: { - "source": { id: "a" }, - "cartocss": DEFAULT_MULTITYPE_STYLE, - "cartocss_version": "2.3.0" - } - }, - { - type: 'cartodb', - options: { - "source": { id: "b1" }, - "cartocss": cartocss('green', 1.0), - "cartocss_version": "2.3.0" - } - }, - { - type: 'cartodb', - options: { - "source": { id: "b2" }, - "cartocss": cartocss('magenta', 0.5), - "cartocss_version": "2.3.0" - } - } - ], - [ - { - id: "b2", - options: { - def: analysisDef({ - "type": "count-in-polygon", - "id": "a0", - "params": { - "columnName": 'count_airbnb', - "pointsSource": { - "type": "source", - "params": { - query: "select * from analysis_rent_listings" - }, - dataviews: { - price_histogram: { - type: 'histogram', - options: { - column: 'price' - } - } - } - }, - "polygonsSource": { - "id": "b1", - "type": "buffer", - "params": { - "source": { - "id": "b0", - "type": "source", - "params": { - query: "select * from analysis_banks" - } - }, - "radius": 250 - }, - dataviews: { - bank_category: { - type: 'aggregation', - options: { - column: 'bank' - } - } - } - } - }, - dataviews: { - count_histogram: { - type: 'histogram', - options: { - column: 'count_airbnb' - } - } - } - }), - cartocss: cartocss('green', 1.0) - } - } - ]) - }, - - { - skip: true, - desc: 'I. Distribution centers', mapConfig: mapConfig( - // layers [ { type: 'cartodb', options: { - "source": { id: "b0" }, - "cartocss": [ - "#distribution_centers {", - " marker-fill-opacity: 1.0;", - " marker-line-color: #FFF;", - " marker-line-width: 0.5;", - " marker-line-opacity: 0.7;", - " marker-placement: point;", - " marker-type: ellipse;", - " marker-width: 8;", - " marker-fill: blue;", - " marker-allow-overlap: true;", - "}" - ].join('\n'), - "cartocss_version": "2.3.0" + source: { + id: 'a1' + }, + cartocss: DEFAULT_MULTITYPE_STYLE, + cartocss_version: '2.3.0' } }, { type: 'cartodb', options: { - "source": { id: "a0" }, - "cartocss": [ - "#shops {", - " marker-fill-opacity: 1.0;", - " marker-line-color: #FFF;", - " marker-line-width: 0.5;", - " marker-line-opacity: 0.7;", - " marker-placement: point;", - " marker-type: ellipse;", - " marker-width: 8;", - " marker-fill: red;", - " marker-allow-overlap: true;", - "}" - ].join('\n'), - "cartocss_version": "2.3.0" - } - }, - { - type: 'cartodb', - options: { - "source": { id: "a1" }, - "cartocss": [ - "#routing {", - " line-color: ramp([routing_time], colorbrewer(Reds));", - " line-width: ramp([routing_time], 2, 8);", - " line-opacity: 1.0;", - "}" - ].join('\n'), - "cartocss_version": "2.3.0" + sql: 'select * from analysis_rent_listings', + cartocss: DEFAULT_MULTITYPE_STYLE, + cartocss_version: '2.3.0' } } ], - // dataviews - { - distribution_center_name_category: { - source: { id: 'b0' }, - type: 'aggregation', - options: { - column: 'name' - } - }, - time_histogram: { - source: { id: 'a1' }, - type: 'histogram', - options: { - column: 'routing_time' - } - }, - distance_histogram: { - source: { id: 'a1' }, - type: 'histogram', - options: { - column: 'routing_distance' - } - } - }, - // analysis + {}, [ - { - id: 'a1', - type: 'routing-n-to-n', - params: { - // distanceColumn: 'routing_distance', - // timeColumn: 'routing_time', - originSource: { - id: 'b0', - type: 'source', - params: { - query: 'select * from distribution_centers' - } - }, - destinationSource: { - id: 'a0', - type: 'source', - params: { - query: 'select * from shops' - } - } - } - } + pointInPolygonDef ] ) }, { - skip: true, - desc: 'II. Population analysis', + desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings', mapConfig: mapConfig( - // layers - [ - { - type: 'cartodb', - options: { - "source": { id: "a2" }, - "cartocss": [ - "#count_in_polygon {", - " polygon-opacity: 1.0", - " line-color: #FFF;", - " line-width: 0.5;", - " line-opacity: 0.7", - " polygon-fill: ramp([estimated_people], colorbrewer(Reds));", - "}" - ].join('\n'), - "cartocss_version": "2.3.0" - } - }, - { - type: 'cartodb', - options: { - "source": { id: "a0" }, - "cartocss": DEFAULT_MULTITYPE_STYLE, - "cartocss_version": "2.3.0" - } - } - ], - // dataviews - { - total_population_formula: { - "source": { id: "a3" }, - type: 'formula', - options: { - column: 'total_population', - operation: 'sum' - } - }, - people_histogram: { // this injects a range filter at `a2` node output - "source": { id: "a2" }, - type: 'histogram', - options: { - column: 'estimated_people' - } - }, - subway_line_category: { // this injects a category filter at `a0` node output - "source": { id: "a0" }, - type: 'aggregation', - options: { - column: 'subway_line' - } - } - }, - // analysis - [ - { - id: 'a3', - // this will union the polygons, produce just one polygon, and calculate the total population for it - type: 'total-population', - params: { - columnName: 'total_population', - source: { - id: 'a2', - type: 'estimated-population', - params: { - columnName: 'estimated_people', - source: { - id: 'a1', - type: 'trade-area', - params: { - source: { - "id": "a0", - "type": "source", - "params": { - query: "select * from subway_stops" - } - }, - kind: 'walk', - time: 300 - } - } - } - } - } - } - ]) - }, - - { - skip: true, - desc: 'III. Point in polygon', - mapConfig: mapConfig( - // layers [ { type: 'cartodb', options: { - "source": { id: "a1" }, - "cartocss": [ - "#count_in_polygon {", - " polygon-opacity: 1.0", - " line-color: #FFF;", - " line-width: 0.5;", - " line-opacity: 0.7", - " polygon-fill: ramp([count_people], colorbrewer(Reds));", - "}" - ].join('\n'), - "cartocss_version": "2.3.0" + sql: 'select * from analysis_rent_listings', + cartocss: DEFAULT_MULTITYPE_STYLE, + cartocss_version: '2.3.0' + } + }, + { + type: 'cartodb', + options: { + source: { + id: 'a1' + }, + cartocss: DEFAULT_MULTITYPE_STYLE, + cartocss_version: '2.3.0' + } + }, + { + type: 'cartodb', + options: { + source: { + id: 'b1' + }, + cartocss: DEFAULT_MULTITYPE_STYLE, + cartocss_version: '2.3.0' } } ], - // dataviews - { - age_histogram: { - "source": { id: "a0" }, - type: 'histogram', - options: { - column: 'age' - } - }, - income_histogram: { - "source": { id: "a0" }, - type: 'histogram', - options: { - column: 'income' - } - }, - gender_category: { - "source": { id: "a0" }, - type: 'aggregation', - options: { - column: 'gender' - } - } - }, - // analysis + {}, [ - { - "id": "a1", - "type": "count-in-polygon", - "params": { - "columnName": 'count_people', - "pointsSource": { - "id": 'a0', - "type": "source", - "params": { - query: "select the_geom, age, gender, income from people" - } - }, - "polygonsSource": { - "id": "b0", - "type": "source", - "params": { - query: "select * from postal_codes" - } - } - } - } + bufferDef, + pointInPolygonDef ] ) } - ]; - useCases.forEach(function(useCase, imageIdx) { + useCases.forEach(function (useCase) { if (!!useCase.skip) { - debug(JSON.stringify(useCase.mapConfig, null, 4)); + return debug(JSON.stringify(useCase.mapConfig, null, 4)); } - it.skip('should implement use case: "' + useCase.desc + '"', function(done) { + it(`should implement use case: '${useCase.desc}'`, function (done) { var testClient = new TestClient(useCase.mapConfig, 1234); var tile = useCase.tile || TILE_ANALYSIS_TABLES; - testClient.getTile(tile.z, tile.x, tile.y, function(err, res, image) { + testClient.getTile(tile.z, tile.x, tile.y, function (err, res, image) { assert.ok(!err, err); - image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png'); + //image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png'); assert.equal(image.width(), 256); diff --git a/test/acceptance/named_maps_static_view.js b/test/acceptance/named_maps_static_view.js index c7ddff2f..b7af4197 100644 --- a/test/acceptance/named_maps_static_view.js +++ b/test/acceptance/named_maps_static_view.js @@ -192,12 +192,36 @@ describe('named maps static view', function() { } getStaticMap({ zoom: 3 }, function(err, img) { assert.ok(!err); - img.save('/tmp/static.png'); assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done); }); }); }); + it('should return override bbox', function (done) { + var view = { + bounds: { + west: 0, + south: 0, + east: 45, + north: 45 + }, + zoom: 4, + center: { + lng: 40, + lat: 20 + } + }; + templateMaps.addTemplate(username, createTemplate(view), function (err) { + if (err) { + return done(err); + } + getStaticMap({ bbox: '0,45,90,45' }, function(err, img) { + assert.ok(!err); + assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), IMAGE_TOLERANCE, done); + }); + }); + }); + it('should allow to select the layers to render', function (done) { var view = { bounds: { diff --git a/test/acceptance/ported/support/ported_server_options.js b/test/acceptance/ported/support/ported_server_options.js index a2302288..af299664 100644 --- a/test/acceptance/ported/support/ported_server_options.js +++ b/test/acceptance/ported/support/ported_server_options.js @@ -47,7 +47,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'); diff --git a/test/fixtures/previews/populated_places_simple_reduced-override-bbox.png b/test/fixtures/previews/populated_places_simple_reduced-override-bbox.png new file mode 100644 index 00000000..f7fee54c Binary files /dev/null and b/test/fixtures/previews/populated_places_simple_reduced-override-bbox.png differ 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/lzmaMiddleware.test.js b/test/unit/cartodb/lzmaMiddleware.test.js new file mode 100644 index 00000000..9a41030a --- /dev/null +++ b/test/unit/cartodb/lzmaMiddleware.test.js @@ -0,0 +1,36 @@ +var assert = require('assert'); +var testHelper = require('../../support/test_helper'); + +var lzmaMiddleware = require('../../../lib/cartodb/middleware/lzma'); + +describe('lzma-middleware', function() { + + it('it should extend params with decoded lzma', function(done) { + var qo = { + config: { + version: '1.3.0' + } + }; + testHelper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) { + var req = { + headers: { + host:'localhost' + }, + query: { + api_key: 'test', + lzma: data + } + }; + lzmaMiddleware(req, {}, function(err) { + if ( err ) { + return done(err); + } + var query = req.query; + assert.deepEqual(qo.config, query.config); + assert.equal('test', query.api_key); + done(); + }); + }); + }); + +}); 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..a9119c13 --- /dev/null +++ b/test/unit/cartodb/prepare-context.test.js @@ -0,0 +1,151 @@ +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'); + +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() {} + }; + req.context = { user: 'localhost' }; + req.params = {}; + return req; + } + + it('cleans up request', function(done){ + var req = {headers: { host:'localhost' }, query: {dbuser:'hacker',dbname:'secret'}}; + var res = {}; + + cleanUpQueryParams(prepareRequest(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(req.hasOwnProperty('params'), 'request has params'); + assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity'); + done(); + }); + }); + + it('sets dbname from redis metadata', function(done){ + var req = {headers: { host:'localhost' }, query: {}, locals: {} }; + var res = {}; + + dbConnSetup(prepareRequest(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(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' }, locals: {} }; + var res = {}; + + // FIXME: review authorize-pgconnsetup workflow, It might we are doing authorization twice. + authorize(prepareRequest(req), 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(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' + }, + locals: {} + }; + + dbConnSetup(prepareRequest(req), res, 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 + }, + locals: {} + }; + var res = {}; + + cleanUpQueryParams(prepareRequest(req), res, function (err) { + 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); + done(); + }); + }); + +}); diff --git a/test/unit/cartodb/req2params.test.js b/test/unit/cartodb/req2params.test.js deleted file mode 100644 index 6293199e..00000000 --- a/test/unit/cartodb/req2params.test.js +++ /dev/null @@ -1,132 +0,0 @@ -var assert = require('assert'); -var _ = require('underscore'); -var test_helper = 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 extend params with decoded lzma', function(done) { - var qo = { - config: { - version: '1.3.0' - } - }; - test_helper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) { - var req = { - headers: { - host:'localhost' - }, - query: { - non_included: 'toberemoved', - api_key: 'test', - style: 'override', - lzma: data - } - }; - baseController.req2params(prepareRequest(req), function(err, req) { - if ( err ) { - return done(err); - } - var query = req.params; - assert.deepEqual(qo.config, query.config); - assert.equal('test', query.api_key); - assert.equal(undefined, query.non_included); - done(); - }); - }); - }); - -}); diff --git a/yarn.lock b/yarn.lock index fa2928f4..624dde8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,6 +32,15 @@ ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^5.1.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" @@ -53,8 +62,8 @@ ansi-styles@^2.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" aproba@^1.0.3: - version "1.1.2" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" are-we-there-yet@~1.1.2: version "1.1.4" @@ -105,7 +114,11 @@ aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" -aws4@^1.2.1: +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" @@ -150,6 +163,18 @@ boom@2.x.x: dependencies: hoek "2.x.x" +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -393,6 +418,12 @@ cryptiles@2.x.x: dependencies: boom "2.x.x" +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + d3-queue@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-2.0.3.tgz#07fbda3acae5358a9c5299aaf880adf0953ed2c2" @@ -426,8 +457,8 @@ debug@^1.0.4: ms "2.0.0" debug@^2.2.0: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: ms "2.0.0" @@ -441,7 +472,7 @@ deep-eql@^0.1.3: dependencies: type-detect "0.1.1" -deep-equal@^1.0.0, deep-equal@~1.0.1: +deep-equal@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -453,17 +484,6 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" -define-properties@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" - dependencies: - foreach "^2.0.5" - object-keys "^1.0.8" - -defined@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -550,24 +570,6 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.5.0: - version "1.8.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.2.tgz#25103263dc4decbda60e0c737ca32313518027ee" - dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" - -es-to-primitive@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" - dependencies: - is-callable "^1.1.1" - is-date-object "^1.0.1" - is-symbol "^1.0.1" - es6-promise@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.1.2.tgz#795e25ceb47f7babb263d151afbedd92d18e6a07" @@ -645,7 +647,7 @@ express@~4.13.3: utils-merge "1.0.0" vary "~1.0.1" -extend@~3.0.0: +extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -653,6 +655,10 @@ extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -679,16 +685,6 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" -for-each@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" - dependencies: - is-function "~1.0.0" - -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -701,6 +697,14 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + forwarded@~0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -730,10 +734,6 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.0.2, function-bind@^1.1.1, function-bind@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -821,7 +821,7 @@ glob@^6.0.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.5, glob@^7.1.1, glob@~7.1.2: +glob@^7.0.5, glob@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -872,6 +872,10 @@ har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + har-validator@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" @@ -888,6 +892,13 @@ har-validator@~4.2.1: ajv "^4.9.1" har-schema "^1.0.5" +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -902,13 +913,7 @@ has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" -has@^1.0.1, has@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" - dependencies: - function-bind "^1.0.2" - -hawk@~3.1.3: +hawk@3.1.3, hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" dependencies: @@ -917,6 +922,15 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + hiredis@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/hiredis/-/hiredis-0.5.0.tgz#db03a98becd7003d13c260043aceecfacdf59b87" @@ -928,6 +942,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoek@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + hosted-git-info@^2.1.4: version "2.5.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" @@ -957,6 +975,14 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + husl@^6.0.1: version "6.0.6" resolved "https://registry.yarnpkg.com/husl/-/husl-6.0.6.tgz#f71b3e45d2000d6406432a9cc17a4b7e0c5b800d" @@ -1002,24 +1028,12 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" -is-callable@^1.1.1, is-callable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" dependencies: number-is-nan "^1.0.0" -is-function@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" - is-my-json-valid@^2.12.4: version "2.16.1" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" @@ -1033,16 +1047,6 @@ is-property@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - dependencies: - has "^1.0.1" - -is-symbol@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" - is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -1118,6 +1122,10 @@ jshint@~2.9.4: shelljs "0.3.x" strip-json-comments "1.0.x" +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -1331,7 +1339,7 @@ mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" -mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.6, mime-types@~2.1.7: +mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.17, mime-types@~2.1.6, mime-types@~2.1.7: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" dependencies: @@ -1359,7 +1367,7 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^1.2.0, minimist@~1.2.0: +minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -1448,18 +1456,18 @@ nock@~2.11.0: mkdirp "^0.5.0" propagate "0.3.x" -node-pre-gyp@~0.6.30, node-pre-gyp@~0.6.36, node-pre-gyp@~0.6.37: - version "0.6.37" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.37.tgz#3c872b236b2e266e4140578fe1ee88f693323a05" +node-pre-gyp@~0.6.30, node-pre-gyp@~0.6.36, node-pre-gyp@~0.6.38: + version "0.6.38" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz#e92a20f83416415bb4086f6d1fb78b3da73d113d" dependencies: + hawk "3.1.3" mkdirp "^0.5.1" nopt "^4.0.1" npmlog "^4.0.2" rc "^1.1.7" - request "^2.81.0" + request "2.81.0" rimraf "^2.6.1" semver "^5.3.0" - tape "^4.6.3" tar "^2.2.1" tar-pack "^3.4.0" @@ -1502,7 +1510,7 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -oauth-sign@~0.8.1: +oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" @@ -1514,14 +1522,6 @@ object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" -object-inspect@~1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.3.0.tgz#5b1eb8e6742e2ee83342a637034d844928ba2f6d" - -object-keys@^1.0.8: - version "1.0.11" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" - object-keys@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" @@ -1601,10 +1601,6 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-parse@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" - path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -1621,6 +1617,10 @@ performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + pg-connection-string@0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7" @@ -1776,6 +1776,10 @@ qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + queue-async@~1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/queue-async/-/queue-async-1.0.7.tgz#22ae0a1dac4a92f5bcd4634f993c682a2a810945" @@ -1872,7 +1876,7 @@ repeat-string@^1.5.2: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" -request@2.x, request@^2.55.0, request@^2.69.0, request@^2.81.0: +request@2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" dependencies: @@ -1899,6 +1903,33 @@ request@2.x, request@^2.55.0, request@^2.69.0, request@^2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" +request@2.x, request@^2.55.0, request@^2.69.0: + version "2.82.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.82.0.tgz#2ba8a92cd7ac45660ea2b10a53ae67cd247516ea" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.2" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + request@~2.79.0: version "2.79.0" resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" @@ -1936,18 +1967,6 @@ resolve@1.1.x: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" - dependencies: - path-parse "^1.0.5" - -resumer@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" - dependencies: - through "~2.3.4" - right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -1966,7 +1985,7 @@ rimraf@~2.4.0: dependencies: glob "^6.0.1" -safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -1982,10 +2001,6 @@ semver@4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" -semver@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-1.1.4.tgz#2e5a4e72bab03472cc97f72753b4508912ef5540" - semver@~4.3.3: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" @@ -1994,6 +2009,10 @@ semver@~5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + send@0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/send/-/send-0.13.1.tgz#a30d5f4c82c8a9bae9ad00a1d9b1bdbe6f199ed7" @@ -2058,6 +2077,12 @@ sntp@1.x.x: dependencies: hoek "2.x.x" +sntp@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b" + dependencies: + hoek "4.x.x" + source-map@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" @@ -2111,11 +2136,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" "sqlite3@2.x || 3.x": - version "3.1.11" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-3.1.11.tgz#84421b181b3e9f4939b7a3d712263d7946cfc96d" + version "3.1.12" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-3.1.12.tgz#2b3a14b17162e39e8aa6e1e2487a41d0795396d8" dependencies: nan "~2.7.0" - node-pre-gyp "~0.6.37" + node-pre-gyp "~0.6.38" srs@1.x: version "1.2.0" @@ -2167,14 +2192,6 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string.prototype.trim@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" - dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.0" - function-bind "^1.0.2" - string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -2185,7 +2202,7 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" -stringstream@~0.0.4: +stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -2225,24 +2242,6 @@ supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -tape@^4.6.3: - version "4.8.0" - resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e" - dependencies: - deep-equal "~1.0.1" - defined "~1.0.0" - for-each "~0.3.2" - function-bind "~1.1.0" - glob "~7.1.2" - has "~1.0.1" - inherits "~2.0.3" - minimist "~1.2.0" - object-inspect "~1.3.0" - resolve "~1.4.0" - resumer "~0.0.0" - string.prototype.trim "~1.1.2" - through "~2.3.8" - tar-pack@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" @@ -2271,7 +2270,7 @@ through2@~0.2.3: readable-stream "~1.1.9" xtend "~2.1.1" -through@2, through@~2.3.4, through@~2.3.8: +through@2: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -2308,9 +2307,9 @@ torque.js@~2.11.0: dependencies: carto CartoDB/carto#0.15.1-cdb1 -tough-cookie@~2.3.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" +tough-cookie@~2.3.0, tough-cookie@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" dependencies: punycode "^1.4.1" @@ -2401,7 +2400,7 @@ utils-merge@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" -uuid@^3.0.0: +uuid@^3.0.0, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"