Merge remote-tracking branch 'origin/master' into mvt-path-conf

This commit is contained in:
David Manzanares
2017-10-10 17:53:28 +02:00
49 changed files with 903 additions and 687 deletions
+13 -12
View File
@@ -19,22 +19,22 @@ function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) {
module.exports = AuthApi;
// Check if a request is authorized by a signer
// Check if the user is authorized by a signer
//
// @param req express request object
// @param res express response object
// @param callback function(err, signed_by) signed_by will be
// null if the request is not signed by anyone
// or will be a string cartodb username otherwise.
//
AuthApi.prototype.authorizedBySigner = function(req, callback) {
if ( ! req.params.token || ! req.params.signer ) {
AuthApi.prototype.authorizedBySigner = function(res, callback) {
if ( ! res.locals.token || ! res.locals.signer ) {
return callback(null, false); // no signer requested
}
var self = this;
var layergroup_id = req.params.token;
var auth_token = req.params.auth_token;
var layergroup_id = res.locals.token;
var auth_token = res.locals.auth_token;
this.mapStore.load(layergroup_id, function(err, mapConfig) {
if (err) {
@@ -84,11 +84,12 @@ AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
* Check access authorization
*
* @param req - standard req object. Importantly contains table and host information
* @param res - standard res object. Contains the auth parameters in locals
* @param callback function(err, allowed) is access allowed not?
*/
AuthApi.prototype.authorize = function(req, callback) {
AuthApi.prototype.authorize = function(req, res, callback) {
var self = this;
var user = req.context.user;
var user = res.locals.user;
step(
function () {
@@ -101,11 +102,11 @@ AuthApi.prototype.authorize = function(req, callback) {
// if not authorized by api_key, continue
if (!authorized) {
// not authorized by api_key, check if authorized by signer
return self.authorizedBySigner(req, this);
return self.authorizedBySigner(res, this);
}
// authorized by api key, login as the given username and stop
self.pgConnection.setDBAuth(user, req.params, function(err) {
self.pgConnection.setDBAuth(user, res.locals, function(err) {
callback(err, true); // authorized (or error)
});
},
@@ -120,7 +121,7 @@ AuthApi.prototype.authorize = function(req, callback) {
// if no signer name was given, let dbparams and
// PostgreSQL do the rest.
//
if ( ! req.params.signer ) {
if ( ! res.locals.signer ) {
return callback(null, true); // authorized so far
}
@@ -128,7 +129,7 @@ AuthApi.prototype.authorize = function(req, callback) {
return callback(null, false);
}
self.pgConnection.setDBAuth(user, req.params, function(err) {
self.pgConnection.setDBAuth(user, res.locals, function(err) {
req.profiler.done('setDBAuth');
callback(err, true); // authorized (or error)
});
+2 -4
View File
@@ -24,7 +24,7 @@ module.exports = DataviewBackend;
DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, params, callback) {
var dataviewName = params.dataviewName;
var dataviewName = params.dataviewName;
step(
function getMapConfig() {
mapConfigProvider.getMapConfig(this);
@@ -113,9 +113,7 @@ function getOverrideParams(params, ownFilter) {
return overrideParams;
}
DataviewBackend.prototype.search = function (mapConfigProvider, user, params, callback) {
var dataviewName = params.dataviewName;
DataviewBackend.prototype.search = function (mapConfigProvider, user, dataviewName, params, callback) {
step(
function getMapConfig() {
mapConfigProvider.getMapConfig(this);
+17 -12
View File
@@ -10,8 +10,10 @@ var BaseController = require('./base');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
function AnalysesController(authApi, pgConnection) {
BaseController.call(this, authApi, pgConnection);
function AnalysesController(prepareContext) {
BaseController.call(this);
this.prepareContext = prepareContext;
}
util.inherits(AnalysesController, BaseController);
@@ -19,7 +21,13 @@ util.inherits(AnalysesController, BaseController);
module.exports = AnalysesController;
AnalysesController.prototype.register = function(app) {
app.get(app.base_url_mapconfig + '/analyses/catalog', cors(), userMiddleware, this.catalog.bind(this));
app.get(
app.base_url_mapconfig + '/analyses/catalog',
cors(),
userMiddleware,
this.prepareContext,
this.catalog.bind(this)
);
};
AnalysesController.prototype.sendResponse = function(req, res, resource) {
@@ -27,17 +35,13 @@ AnalysesController.prototype.sendResponse = function(req, res, resource) {
this.send(req, res, resource, 200);
};
AnalysesController.prototype.catalog = function(req, res) {
AnalysesController.prototype.catalog = function (req, res, next) {
var self = this;
var username = req.context.user;
var username = res.locals.user;
step(
function reqParams() {
self.req2params(req, this);
},
function catalogQuery(err) {
assert.ifError(err);
var pg = new PSQL(dbParamsFromReqParams(req.params));
function catalogQuery() {
var pg = new PSQL(dbParamsFromReqParams(res.locals));
getMetadata(username, pg, this);
},
function prepareResponse(err, results) {
@@ -80,7 +84,8 @@ AnalysesController.prototype.catalog = function(req, res) {
err = new Error('Unauthorized');
err.http_status = 401;
}
self.sendError(req, res, err);
next(req, res, err);
} else {
self.sendResponse(req, res, { catalog: catalogWithTables });
}
+1 -266
View File
@@ -1,130 +1,12 @@
var assert = require('assert');
var _ = require('underscore');
var step = require('step');
var debug = require('debug')('windshaft:cartodb');
// Whitelist query parameters and attach format
var REQUEST_QUERY_PARAMS_WHITELIST = [
'config',
'map_key',
'api_key',
'auth_token',
'callback',
'zoom',
'lon',
'lat',
// analysis
'filters' // json
];
function BaseController(authApi, pgConnection) {
this.authApi = authApi;
this.pgConnection = pgConnection;
function BaseController() {
}
module.exports = BaseController;
// jshint maxcomplexity:8
/**
* Whitelist input and get database name & default geometry type from
* subdomain/user metadata held in CartoDB Redis
* @param req - standard express request obj. Should have host & table
* @param callback
*/
BaseController.prototype.req2params = function(req, callback){
var self = this;
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
if (Array.isArray(req.context.allowedQueryParams)) {
allowedQueryParams = allowedQueryParams.concat(req.context.allowedQueryParams);
}
req.query = _.pick(req.query, allowedQueryParams);
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
var user = req.context.user;
if ( req.params.token ) {
// Token might match the following patterns:
// - {user}@{tpl_id}@{token}:{cache_buster}
var tksplit = req.params.token.split(':');
req.params.token = tksplit[0];
if ( tksplit.length > 1 ) {
req.params.cache_buster= tksplit[1];
}
tksplit = req.params.token.split('@');
if ( tksplit.length > 1 ) {
req.params.signer = tksplit.shift();
if ( ! req.params.signer ) {
req.params.signer = user;
}
else if ( req.params.signer !== user ) {
var err = new Error(
'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"'
);
err.http_status = 403;
req.profiler.done('req2params');
callback(err);
return;
}
if ( tksplit.length > 1 ) {
/*var template_hash = */tksplit.shift(); // unused
}
req.params.token = tksplit.shift();
}
}
// bring all query values onto req.params object
_.extend(req.params, req.query);
req.profiler.done('req2params.setup');
step(
function getPrivacy(){
self.authApi.authorize(req, this);
},
function validateAuthorization(err, authorized) {
req.profiler.done('authorize');
assert.ifError(err);
if(!authorized) {
err = new Error("Sorry, you are unauthorized (permission denied)");
err.http_status = 403;
throw err;
}
return null;
},
function getDatabase(err){
assert.ifError(err);
self.pgConnection.setDBConn(user, req.params, this);
},
function finishSetup(err) {
if ( err ) {
req.profiler.done('req2params');
return callback(err, req);
}
// Add default database connection parameters
// if none given
_.defaults(req.params, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
req.profiler.done('req2params');
callback(null, req);
}
);
};
// jshint maxcomplexity:6
// jshint maxcomplexity:9
BaseController.prototype.send = function(req, res, body, status, headers) {
if (req.params.dbhost) {
res.set('X-Served-By-DB-Host', req.params.dbhost);
}
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
if (headers) {
@@ -152,150 +34,3 @@ BaseController.prototype.send = function(req, res, body, status, headers) {
}
};
// jshint maxcomplexity:6
BaseController.prototype.sendError = function(req, res, err, label) {
var allErrors = Array.isArray(err) ? err : [err];
allErrors = populateTimeoutErrors(allErrors);
label = label || 'UNKNOWN';
err = allErrors[0] || new Error(label);
allErrors[0] = err;
var statusCode = findStatusCode(err);
if (err.message === 'Tile does not exist' && req.params.format === 'mvt') {
statusCode = 204;
}
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
// If a callback was requested, force status to 200
if (req.query && req.query.callback) {
statusCode = 200;
}
var errorResponseBody = {
errors: allErrors.map(errorMessage),
errors_with_context: allErrors.map(errorMessageWithContext)
};
this.send(req, res, errorResponseBody, statusCode);
};
function stripConnectionInfo(message) {
// Strip connection info, if any
return message
// See https://github.com/CartoDB/Windshaft/issues/173
.replace(/Connection string: '[^']*'\n\s/im, '')
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
.replace(/is the server.*encountered/im, 'encountered');
}
var ERROR_INFO_TO_EXPOSE = {
message: true,
layer: true,
type: true,
analysis: true,
subtype: true
};
function shouldBeExposed (prop) {
return !!ERROR_INFO_TO_EXPOSE[prop];
}
function errorMessage(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
return stripConnectionInfo(message);
}
function errorMessageWithContext(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
var error = {
type: err.type || 'unknown',
message: stripConnectionInfo(message),
};
for (var prop in err) {
// type & message are properties from Error's prototype and will be skipped
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
error[prop] = err[prop];
}
}
return error;
}
module.exports.errorMessage = errorMessage;
function findStatusCode(err) {
var statusCode;
if ( err.http_status ) {
statusCode = err.http_status;
} else {
statusCode = statusFromErrorMessage('' + err);
}
return statusCode;
}
module.exports.findStatusCode = findStatusCode;
function statusFromErrorMessage(errMsg) {
// Find an appropriate statusCode based on message
// jshint maxcomplexity:7
var statusCode = 400;
if ( -1 !== errMsg.indexOf('permission denied') ) {
statusCode = 403;
}
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
statusCode = 403;
}
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
statusCode = 400;
}
else if ( -1 !== errMsg.indexOf('does not exist') ) {
if ( -1 !== errMsg.indexOf(' role ') ) {
statusCode = 403; // role 'xxx' does not exist
} else if ( errMsg.match(/function .* does not exist/) ) {
statusCode = 400; // invalid SQL (SQL function does not exist)
} else {
statusCode = 404;
}
}
return statusCode;
}
function isRenderTimeoutError (err) {
return err.message === 'Render timed out';
}
function isDatasourceTimeoutError (err) {
return err.message && err.message.match(/canceling statement due to statement timeout/i);
}
function isTimeoutError (err) {
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
}
function populateTimeoutErrors (errors) {
return errors.map(function (error) {
if (isRenderTimeoutError(error)) {
error.subtype = 'render';
}
if (isDatasourceTimeoutError(error)) {
error.subtype = 'datasource';
}
if (isTimeoutError(error)) {
error.message = 'You are over platform\'s limits. Please contact us to know more details';
error.type = 'limit';
error.http_status = 429;
}
return error;
});
}
+124 -100
View File
@@ -28,9 +28,9 @@ var QueryTables = require('cartodb-query-tables');
* @param {AnalysisBackend} analysisBackend
* @constructor
*/
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
function LayergroupController(prepareContext, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
BaseController.call(this, authApi, pgConnection);
BaseController.call(this);
this.pgConnection = pgConnection;
this.mapStore = mapStore;
@@ -43,39 +43,65 @@ function LayergroupController(authApi, pgConnection, mapStore, tileBackend, prev
this.dataviewBackend = new DataviewBackend(analysisBackend);
this.analysisStatusBackend = new AnalysisStatusBackend();
this.prepareContext = prepareContext;
}
util.inherits(LayergroupController, BaseController);
module.exports = LayergroupController;
LayergroupController.prototype.register = function(app) {
app.get(app.base_url_mapconfig +
'/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware,
this.tile.bind(this));
app.get(
app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format',
cors(),
userMiddleware,
this.prepareContext,
this.tile.bind(this)
);
app.get(app.base_url_mapconfig +
'/:token/:z/:x/:y.:format', cors(), userMiddleware,
this.tile.bind(this));
app.get(
app.base_url_mapconfig + '/:token/:z/:x/:y.:format',
cors(),
userMiddleware,
this.prepareContext,
this.tile.bind(this)
);
app.get(app.base_url_mapconfig +
'/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
this.layer.bind(this));
app.get(
app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)',
cors(),
userMiddleware,
validateLayerRouteMiddleware,
this.prepareContext,
this.layer.bind(this)
);
app.get(app.base_url_mapconfig +
'/:token/:layer/attributes/:fid', cors(), userMiddleware,
this.attributes.bind(this));
app.get(
app.base_url_mapconfig + '/:token/:layer/attributes/:fid',
cors(),
userMiddleware,
this.prepareContext,
this.attributes.bind(this)
);
app.get(app.base_url_mapconfig +
'/static/center/:token/:z/:lat/:lng/:width/:height.:format',
cors(), userMiddleware, allowQueryParams(['layer']),
this.center.bind(this));
app.get(
app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format',
cors(),
userMiddleware,
allowQueryParams(['layer']),
this.prepareContext,
this.center.bind(this)
);
app.get(app.base_url_mapconfig +
'/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
cors(), userMiddleware, allowQueryParams(['layer']),
this.bbox.bind(this));
app.get(
app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
cors(),
userMiddleware,
allowQueryParams(['layer']),
this.prepareContext,
this.bbox.bind(this)
);
// Undocumented/non-supported API endpoint methods.
// Use at your own peril.
@@ -98,6 +124,7 @@ LayergroupController.prototype.register = function(app) {
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataview.bind(this)
);
@@ -106,6 +133,7 @@ LayergroupController.prototype.register = function(app) {
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataview.bind(this)
);
@@ -114,6 +142,7 @@ LayergroupController.prototype.register = function(app) {
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataviewSearch.bind(this)
);
@@ -122,30 +151,32 @@ LayergroupController.prototype.register = function(app) {
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataviewSearch.bind(this)
);
app.get(app.base_url_mapconfig +
'/:token/analysis/node/:nodeId', cors(), userMiddleware,
this.analysisNodeStatus.bind(this));
app.get(
app.base_url_mapconfig + '/:token/analysis/node/:nodeId',
cors(),
userMiddleware,
this.prepareContext,
this.analysisNodeStatus.bind(this)
);
};
LayergroupController.prototype.analysisNodeStatus = function(req, res) {
LayergroupController.prototype.analysisNodeStatus = function(req, res, next) {
var self = this;
step(
function setupParams() {
self.req2params(req, this);
},
function retrieveNodeStatus(err) {
assert.ifError(err);
self.analysisStatusBackend.getNodeStatus(req.params, this);
function retrieveNodeStatus() {
self.analysisStatusBackend.getNodeStatus(res.locals, this);
},
function finish(err, nodeStatus, stats) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'GET NODE STATUS');
err.label = 'GET NODE STATUS';
next(err);
} else {
self.sendResponse(req, res, nodeStatus, 200, {
'Cache-Control': 'public,max-age=5',
@@ -156,54 +187,50 @@ LayergroupController.prototype.analysisNodeStatus = function(req, res) {
);
};
LayergroupController.prototype.dataview = function(req, res) {
LayergroupController.prototype.dataview = function(req, res, next) {
var self = this;
step(
function setupParams() {
self.req2params(req, this);
},
function retrieveDataview(err) {
assert.ifError(err);
function retrieveDataview() {
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, req.context.user, self.userLimitsApi, req.params
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
);
self.dataviewBackend.getDataview(
mapConfigProvider,
res.locals.user,
res.locals,
this
);
self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
},
function finish(err, dataview, stats) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'GET DATAVIEW');
err.label = 'GET DATAVIEW';
next(err);
} else {
self.sendResponse(req, res, dataview, 200);
}
}
);
};
LayergroupController.prototype.dataviewSearch = function(req, res) {
LayergroupController.prototype.dataviewSearch = function(req, res, next) {
var self = this;
step(
function setupParams() {
self.req2params(req, this);
},
function searchDataview(err) {
assert.ifError(err);
function searchDataview() {
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, req.context.user, self.userLimitsApi, req.params
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
);
self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this);
self.dataviewBackend.search(mapConfigProvider, res.locals.user, req.params.dataviewName, res.locals, this);
},
function finish(err, searchResult, stats) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'GET DATAVIEW SEARCH');
err.label = 'GET DATAVIEW SEARCH';
next(err);
} else {
self.sendResponse(req, res, searchResult, 200);
}
@@ -212,28 +239,24 @@ LayergroupController.prototype.dataviewSearch = function(req, res) {
};
LayergroupController.prototype.attributes = function(req, res) {
LayergroupController.prototype.attributes = function(req, res, next) {
var self = this;
req.profiler.start('windshaft.maplayer_attribute');
step(
function setupParams() {
self.req2params(req, this);
},
function retrieveFeatureAttributes(err) {
assert.ifError(err);
function retrieveFeatureAttributes() {
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, req.context.user, self.userLimitsApi, req.params
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
);
self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
self.attributesBackend.getFeatureAttributes(mapConfigProvider, res.locals, false, this);
},
function finish(err, tile, stats) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'GET ATTRIBUTES');
err.label = 'GET ATTRIBUTES';
next(err);
} else {
self.sendResponse(req, res, tile, 200);
}
@@ -243,37 +266,30 @@ LayergroupController.prototype.attributes = function(req, res) {
};
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
LayergroupController.prototype.tile = function(req, res) {
LayergroupController.prototype.tile = function(req, res, next) {
req.profiler.start('windshaft.map_tile');
this.tileOrLayer(req, res);
this.tileOrLayer(req, res, next);
};
// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
LayergroupController.prototype.layer = function(req, res, next) {
if (req.params.token === 'static') {
return next();
}
req.profiler.start('windshaft.maplayer_tile');
this.tileOrLayer(req, res);
this.tileOrLayer(req, res, next);
};
LayergroupController.prototype.tileOrLayer = function (req, res) {
LayergroupController.prototype.tileOrLayer = function (req, res, next) {
var self = this;
step(
function mapController$prepareParams() {
self.req2params(req, this);
},
function mapController$getTileOrGrid(err) {
assert.ifError(err);
function mapController$getTileOrGrid() {
self.tileBackend.getTile(
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
req.params, this
);
},
function mapController$finalize(err, tile, headers, stats) {
req.profiler.add(stats);
self.finalizeGetTileOrGrid(err, req, res, tile, headers);
self.finalizeGetTileOrGrid(err, req, res, tile, headers, next);
}
);
};
@@ -284,7 +300,7 @@ function getStatusCode(tile, format){
// This function is meant for being called as the very last
// step by all endpoints serving tiles or grids
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) {
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers, next) {
var supportedFormats = {
grid_json: true,
json_torque: true,
@@ -313,7 +329,9 @@ LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, t
}
err.message = errMsg;
this.sendError(req, res, err, 'TILE RENDER');
err.label = 'TILE RENDER';
next(err);
global.statsClient.increment('windshaft.tiles.error');
global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
} else {
@@ -323,42 +341,38 @@ LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, t
}
};
LayergroupController.prototype.bbox = function(req, res) {
LayergroupController.prototype.bbox = function(req, res, next) {
this.staticMap(req, res, +req.params.width, +req.params.height, {
west: +req.params.west,
north: +req.params.north,
east: +req.params.east,
south: +req.params.south
});
}, null, next);
};
LayergroupController.prototype.center = function(req, res) {
LayergroupController.prototype.center = function(req, res, next) {
this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
lng: +req.params.lng,
lat: +req.params.lat
});
}, next);
};
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) {
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center, next) {
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
req.params.layer = 'all';
req.params.format = 'png';
req.params.format = req.params.format || 'png';
res.locals.layer = res.locals.layer || 'all';
var self = this;
step(
function reqParams() {
self.req2params(req, this);
},
function getImage(err) {
assert.ifError(err);
function getImage() {
if (center) {
self.previewBackend.getImage(
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
format, width, height, zoom, center, this);
} else {
self.previewBackend.getImage(
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
format, width, height, zoom /* bounds */, this);
}
},
@@ -367,7 +381,8 @@ LayergroupController.prototype.staticMap = function(req, res, width, height, zoo
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'STATIC_MAP');
err.label = 'STATIC_MAP';
next(err);
} else {
res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
self.sendResponse(req, res, image, 200);
@@ -385,18 +400,18 @@ LayergroupController.prototype.sendResponse = function(req, res, body, status, h
// Set Last-Modified header
var lastUpdated;
if (req.params.cache_buster) {
if (res.locals.cache_buster) {
// Assuming cache_buster is a timestamp
lastUpdated = new Date(parseInt(req.params.cache_buster));
lastUpdated = new Date(parseInt(res.locals.cache_buster));
} else {
lastUpdated = new Date();
}
res.set('Last-Modified', lastUpdated.toUTCString());
var dbName = req.params.dbname;
var dbName = res.locals.dbname;
step(
function getAffectedTables() {
self.getAffectedTables(req.context.user, dbName, req.params.token, this);
self.getAffectedTables(res.locals.user, dbName, res.locals.token, this);
},
function sendResponse(err, affectedTables) {
req.profiler.done('affectedTables');
@@ -472,3 +487,12 @@ LayergroupController.prototype.getAffectedTables = function(user, dbName, layerg
callback
);
};
function validateLayerRouteMiddleware(req, res, next) {
if (req.params.token === 'static') {
return next('route');
}
next();
}
+71 -49
View File
@@ -20,7 +20,6 @@ var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
/**
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
@@ -34,11 +33,11 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/cr
* @param {StatsBackend} statsBackend
* @constructor
*/
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
function MapController(prepareContext, pgConnection, templateMaps, mapBackend, metadataBackend,
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter,
statsBackend) {
BaseController.call(this, authApi, pgConnection);
BaseController.call(this);
this.pgConnection = pgConnection;
this.templateMaps = templateMaps;
@@ -52,6 +51,7 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata
this.resourceLocator = new ResourceLocator(global.environment);
this.statsBackend = statsBackend;
this.prepareContext = prepareContext;
}
util.inherits(MapController, BaseController);
@@ -60,38 +60,60 @@ module.exports = MapController;
MapController.prototype.register = function(app) {
app.get(app.base_url_mapconfig, cors(), userMiddleware, this.createGet.bind(this));
app.post(app.base_url_mapconfig, cors(), userMiddleware, this.createPost.bind(this));
app.get(app.base_url_templated + '/:template_id/jsonp', cors(), userMiddleware, this.jsonp.bind(this));
app.post(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.instantiate.bind(this));
app.get(
app.base_url_mapconfig,
cors(),
userMiddleware,
this.prepareContext,
this.createGet.bind(this)
);
app.post(
app.base_url_mapconfig,
cors(),
userMiddleware,
this.prepareContext,
this.createPost.bind(this)
);
app.get(
app.base_url_templated + '/:template_id/jsonp',
cors(),
userMiddleware,
this.prepareContext,
this.jsonp.bind(this)
);
app.post(
app.base_url_templated + '/:template_id',
cors(),
userMiddleware,
this.prepareContext,
this.instantiate.bind(this)
);
app.options(app.base_url_mapconfig, cors('Content-Type'));
};
MapController.prototype.createGet = function(req, res){
MapController.prototype.createGet = function(req, res, next){
req.profiler.start('windshaft.createmap_get');
this.create(req, res, function createGet$prepareConfig(err, req) {
assert.ifError(err);
if ( ! req.params.config ) {
this.create(req, res, function createGet$prepareConfig(req, config) {
if ( ! config ) {
throw new Error('layergroup GET needs a "config" parameter');
}
return JSON.parse(req.params.config);
});
return JSON.parse(config);
}, next);
};
MapController.prototype.createPost = function(req, res) {
MapController.prototype.createPost = function(req, res, next) {
req.profiler.start('windshaft.createmap_post');
this.create(req, res, function createPost$prepareConfig(err, req) {
assert.ifError(err);
this.create(req, res, function createPost$prepareConfig(req) {
if (!req.is('application/json')) {
throw new Error('layergroup POST data must be of type application/json');
}
return req.body;
});
}, next);
};
MapController.prototype.instantiate = function(req, res) {
MapController.prototype.instantiate = function(req, res, next) {
req.profiler.start('windshaft-cartodb.instance_template_post');
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
@@ -99,10 +121,10 @@ MapController.prototype.instantiate = function(req, res) {
return callback(new Error('Template POST data must be of type application/json'));
}
return callback(null, req.body);
});
}, next);
};
MapController.prototype.jsonp = function(req, res) {
MapController.prototype.jsonp = function(req, res, next) {
req.profiler.start('windshaft-cartodb.instance_template_get');
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
@@ -121,10 +143,10 @@ MapController.prototype.jsonp = function(req, res) {
}
return callback(err, templateParams);
});
}, next);
};
MapController.prototype.create = function(req, res, prepareConfigFn) {
MapController.prototype.create = function(req, res, prepareConfigFn, next) {
var self = this;
var mapConfig;
@@ -132,35 +154,36 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
var context = {};
step(
function setupParams(){
self.req2params(req, this);
function prepareConfig () {
const requestMapConfig = prepareConfigFn(req, res.locals.config);
return requestMapConfig;
},
prepareConfigFn,
function prepareAdapterMapConfig(err, requestMapConfig) {
assert.ifError(err);
context.analysisConfiguration = {
user: req.context.user,
user: res.locals.user,
db: {
host: req.params.dbhost,
port: req.params.dbport,
dbname: req.params.dbname,
user: req.params.dbuser,
pass: req.params.dbpassword
host: res.locals.dbhost,
port: res.locals.dbport,
dbname: res.locals.dbname,
user: res.locals.dbuser,
pass: res.locals.dbpassword
},
batch: {
username: req.context.user,
apiKey: req.params.api_key
username: res.locals.user,
apiKey: res.locals.api_key
}
};
self.mapConfigAdapter.getMapConfig(req.context.user, requestMapConfig, req.params, context, this);
self.mapConfigAdapter.getMapConfig(res.locals.user, requestMapConfig, res.locals, context, this);
},
function createLayergroup(err, requestMapConfig) {
assert.ifError(err);
var datasource = context.datasource || Datasource.EmptyDatasource();
mapConfig = new MapConfig(requestMapConfig, datasource);
self.mapBackend.createLayergroup(
mapConfig, req.params,
new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params),
mapConfig,
res.locals,
new CreateLayergroupMapConfigProvider(mapConfig, res.locals.user, self.userLimitsApi, res.locals),
this
);
},
@@ -188,11 +211,12 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
err = error;
}
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
err.label = 'ANONYMOUS LAYERGROUP';
next(err);
} else {
var analysesResults = context.analysesResults || [];
self.addDataviewsAndWidgetsUrls(req.context.user, layergroup, mapConfig.obj());
self.addAnalysesMetadata(req.context.user, layergroup, analysesResults, true);
self.addDataviewsAndWidgetsUrls(res.locals.user, layergroup, mapConfig.obj());
self.addAnalysesMetadata(res.locals.user, layergroup, analysesResults, true);
addContextMetadata(layergroup, mapConfig.obj(), context);
res.set('X-Layergroup-Id', layergroup.layergroupid);
self.send(req, res, layergroup, 200);
@@ -212,17 +236,14 @@ function addContextMetadata(layergroup, mapConfig, context) {
}
}
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) {
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn, next) {
var self = this;
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
var mapConfigProvider;
var mapConfig;
step(
function setupParams(){
self.req2params(req, this);
},
function getTemplateParams() {
prepareParamsFn(this);
},
@@ -237,8 +258,8 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
cdbuser,
req.params.template_id,
templateParams,
req.query.auth_token,
req.params
res.locals.auth_token,
res.locals
);
mapConfigProvider.getMapConfig(this);
},
@@ -259,7 +280,8 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
},
function finishTemplateInstantiation(err, layergroup) {
if (err) {
self.sendError(req, res, err, 'NAMED MAP LAYERGROUP');
err.label = 'NAMED MAP LAYERGROUP';
next(err);
} else {
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
@@ -282,7 +304,7 @@ MapController.prototype.afterLayergroupCreate =
function(req, res, mapconfig, layergroup, analysesResults, callback) {
var self = this;
var username = req.context.user;
var username = res.locals.user;
var tasksleft = 2; // redis key and affectedTables
var errors = [];
@@ -323,7 +345,7 @@ function(req, res, mapconfig, layergroup, analysesResults, callback) {
}
});
var dbName = req.params.dbname;
var dbName = res.locals.dbname;
var layergroupId = layergroup.layergroupid;
var dbConnection;
+44 -35
View File
@@ -10,9 +10,9 @@ var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
var allowQueryParams = require('../middleware/allow-query-params');
function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
surrogateKeysCache, tablesExtentApi, metadataBackend) {
BaseController.call(this, authApi, pgConnection);
BaseController.call(this);
this.namedMapProviderCache = namedMapProviderCache;
this.tileBackend = tileBackend;
@@ -20,6 +20,7 @@ function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileB
this.surrogateKeysCache = surrogateKeysCache;
this.tablesExtentApi = tablesExtentApi;
this.metadataBackend = metadataBackend;
this.prepareContext = prepareContext;
}
util.inherits(NamedMapsController, BaseController);
@@ -27,18 +28,26 @@ util.inherits(NamedMapsController, BaseController);
module.exports = NamedMapsController;
NamedMapsController.prototype.register = function(app) {
app.get(app.base_url_templated +
'/:template_id/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
this.tile.bind(this));
app.get(
app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)',
cors(),
userMiddleware,
this.prepareContext,
this.tile.bind(this)
);
app.get(app.base_url_mapconfig +
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware,
app.get(
app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
cors(),
userMiddleware,
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
this.staticMap.bind(this));
this.prepareContext,
this.staticMap.bind(this)
);
};
NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) {
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(req.context.user, namedMapProvider.getTemplateName()));
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(res.locals.user, namedMapProvider.getTemplateName()));
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
@@ -75,24 +84,20 @@ NamedMapsController.prototype.sendResponse = function(req, res, resource, header
);
};
NamedMapsController.prototype.tile = function(req, res) {
NamedMapsController.prototype.tile = function(req, res, next) {
var self = this;
var cdbUser = req.context.user;
var cdbUser = res.locals.user;
var namedMapProvider;
step(
function reqParams() {
self.req2params(req, this);
},
function getNamedMapProvider(err) {
assert.ifError(err);
function getNamedMapProvider() {
self.namedMapProviderCache.get(
cdbUser,
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
res.locals,
this
);
},
@@ -104,7 +109,8 @@ NamedMapsController.prototype.tile = function(req, res) {
function handleImage(err, tile, headers, stats) {
req.profiler.add(stats);
if (err) {
self.sendError(req, res, err, 'NAMED_MAP_TILE');
err.label = 'NAMED_MAP_TILE';
next(err);
} else {
self.sendResponse(req, res, tile, headers, namedMapProvider);
}
@@ -112,28 +118,24 @@ NamedMapsController.prototype.tile = function(req, res) {
);
};
NamedMapsController.prototype.staticMap = function(req, res) {
NamedMapsController.prototype.staticMap = function(req, res, next) {
var self = this;
var cdbUser = req.context.user;
var cdbUser = res.locals.user;
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
req.params.format = 'png';
req.params.layer = 'all';
res.locals.format = req.params.format || 'png';
res.locals.layer = res.locals.layer || 'all';
var namedMapProvider;
step(
function reqParams() {
self.req2params(req, this);
},
function getNamedMapProvider(err) {
assert.ifError(err);
function getNamedMapProvider() {
self.namedMapProviderCache.get(
cdbUser,
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
res.locals,
this
);
},
@@ -141,12 +143,12 @@ NamedMapsController.prototype.staticMap = function(req, res) {
assert.ifError(err);
namedMapProvider = _namedMapProvider;
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, namedMapProvider, this);
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, res.locals, namedMapProvider, this);
},
function prepareImageOptions(err) {
assert.ifError(err);
self.getStaticImageOptions(cdbUser, req.params, namedMapProvider, this);
self.getStaticImageOptions(cdbUser, res.locals, namedMapProvider, this);
},
function getImage(err, imageOpts) {
assert.ifError(err);
@@ -180,7 +182,8 @@ NamedMapsController.prototype.staticMap = function(req, res) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'STATIC_VIZ_MAP');
err.label = 'STATIC_VIZ_MAP';
next(err);
} else {
self.sendResponse(req, res, image, headers, namedMapProvider);
}
@@ -188,7 +191,13 @@ NamedMapsController.prototype.staticMap = function(req, res) {
);
};
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (user, req, namedMapProvider, callback) {
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (
user,
req,
params,
namedMapProvider,
callback
) {
var self = this;
namedMapProvider.getTemplate(function (err, template) {
if (err) {
@@ -213,7 +222,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us
}
// overwrites 'all' default filter
req.params.layer = layerVisibilityFilter.join(',');
params.layer = layerVisibilityFilter.join(',');
// recreates the provider
self.namedMapProviderCache.get(
@@ -221,7 +230,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
params,
callback
);
});
+60 -26
View File
@@ -15,8 +15,8 @@ var userMiddleware = require('../middleware/user');
* @param {TemplateMaps} templateMaps
* @constructor
*/
function NamedMapsAdminController(authApi, pgConnection, templateMaps) {
BaseController.call(this, authApi, pgConnection);
function NamedMapsAdminController(authApi, templateMaps) {
BaseController.call(this);
this.authApi = authApi;
this.templateMaps = templateMaps;
@@ -26,19 +26,52 @@ util.inherits(NamedMapsAdminController, BaseController);
module.exports = NamedMapsAdminController;
NamedMapsAdminController.prototype.register = function(app) {
app.post(app.base_url_templated, cors(), userMiddleware, this.create.bind(this));
app.put(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.update.bind(this));
app.get(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.retrieve.bind(this));
app.delete(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.destroy.bind(this));
app.get(app.base_url_templated, cors(), userMiddleware, this.list.bind(this));
app.options(app.base_url_templated + '/:template_id', cors('Content-Type'));
NamedMapsAdminController.prototype.register = function (app) {
app.post(
app.base_url_templated + '/',
cors(),
userMiddleware,
this.create.bind(this)
);
app.put(
app.base_url_templated + '/:template_id',
cors(),
userMiddleware,
this.update.bind(this)
);
app.get(
app.base_url_templated + '/:template_id',
cors(),
userMiddleware,
this.retrieve.bind(this)
);
app.delete(
app.base_url_templated + '/:template_id',
cors(),
userMiddleware,
this.destroy.bind(this)
);
app.get(
app.base_url_templated + '/',
cors(),
userMiddleware,
this.list.bind(this)
);
app.options(
app.base_url_templated + '/:template_id',
cors('Content-Type')
);
};
NamedMapsAdminController.prototype.create = function(req, res) {
NamedMapsAdminController.prototype.create = function(req, res, next) {
var self = this;
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
step(
function checkPerms(){
@@ -55,14 +88,14 @@ NamedMapsAdminController.prototype.create = function(req, res) {
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self, req, res, 'POST TEMPLATE')
finishFn(self, req, res, 'POST TEMPLATE', null, next)
);
};
NamedMapsAdminController.prototype.update = function(req, res) {
NamedMapsAdminController.prototype.update = function(req, res, next) {
var self = this;
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
var template;
var tpl_id;
@@ -84,16 +117,16 @@ NamedMapsAdminController.prototype.update = function(req, res) {
return { template_id: tpl_id };
},
finishFn(self, req, res, 'PUT TEMPLATE')
finishFn(self, req, res, 'PUT TEMPLATE', null, next)
);
};
NamedMapsAdminController.prototype.retrieve = function(req, res) {
NamedMapsAdminController.prototype.retrieve = function(req, res, next) {
var self = this;
req.profiler.start('windshaft-cartodb.get_template');
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
var tpl_id;
step(
function checkPerms(){
@@ -118,16 +151,16 @@ NamedMapsAdminController.prototype.retrieve = function(req, res) {
delete tpl_val.auth_id;
return { template: tpl_val };
},
finishFn(self, req, res, 'GET TEMPLATE')
finishFn(self, req, res, 'GET TEMPLATE', null, next)
);
};
NamedMapsAdminController.prototype.destroy = function(req, res) {
NamedMapsAdminController.prototype.destroy = function(req, res, next) {
var self = this;
req.profiler.start('windshaft-cartodb.delete_template');
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
var tpl_id;
step(
function checkPerms(){
@@ -144,15 +177,15 @@ NamedMapsAdminController.prototype.destroy = function(req, res) {
assert.ifError(err);
return '';
},
finishFn(self, req, res, 'DELETE TEMPLATE', 204)
finishFn(self, req, res, 'DELETE TEMPLATE', 204, next)
);
};
NamedMapsAdminController.prototype.list = function(req, res) {
NamedMapsAdminController.prototype.list = function(req, res, next) {
var self = this;
req.profiler.start('windshaft-cartodb.get_template_list');
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
step(
function checkPerms(){
@@ -168,14 +201,15 @@ NamedMapsAdminController.prototype.list = function(req, res) {
assert.ifError(err);
return { template_ids: tpl_ids };
},
finishFn(self, req, res, 'GET TEMPLATE LIST')
finishFn(self, req, res, 'GET TEMPLATE LIST', null, next)
);
};
function finishFn(controller, req, res, description, status) {
function finishFn(controller, req, res, description, status, next) {
return function finish(err, response){
if (err) {
controller.sendError(req, res, err, description);
err.label = description;
next(err);
} else {
controller.send(req, res, response, status || 200);
}
+1 -1
View File
@@ -3,7 +3,7 @@ module.exports = function allowQueryParams(params) {
throw new Error('allowQueryParams must receive an Array of params');
}
return function allowQueryParamsMiddleware(req, res, next) {
req.context.allowedQueryParams = params;
res.locals.allowedQueryParams = params;
next();
};
};
@@ -0,0 +1,20 @@
module.exports = function authorizeMiddleware (authApi) {
return function (req, res, next) {
req.profiler.done('req2params.setup');
authApi.authorize(req, res, (err, authorized) => {
req.profiler.done('authorize');
if (err) {
return next(err);
}
if(!authorized) {
err = new Error("Sorry, you are unauthorized (permission denied)");
err.http_status = 403;
return next(err);
}
return next();
});
};
};
@@ -0,0 +1,32 @@
const _ = require('underscore');
// Whitelist query parameters and attach format
const REQUEST_QUERY_PARAMS_WHITELIST = [
'config',
'map_key',
'api_key',
'auth_token',
'callback',
'zoom',
'lon',
'lat',
// analysis
'filters' // json
];
module.exports = function cleanUpQueryParamsMiddleware () {
return function cleanUpQueryParams (req, res, next) {
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
if (Array.isArray(res.locals.allowedQueryParams)) {
allowedQueryParams = allowedQueryParams.concat(res.locals.allowedQueryParams);
}
req.query = _.pick(req.query, allowedQueryParams);
// bring all query values onto res.locals object
_.extend(res.locals, req.query);
next();
};
};
@@ -0,0 +1,31 @@
const _ = require('underscore');
module.exports = function dbConnSetupMiddleware(pgConnection) {
return function dbConnSetup(req, res, next) {
const user = res.locals.user;
pgConnection.setDBConn(user, res.locals, (err) => {
if (err) {
if (err.message && -1 !== err.message.indexOf('name not found')) {
err.http_status = 404;
}
req.profiler.done('req2params');
return next(err);
}
// Add default database connection parameters
// if none given
_.defaults(res.locals, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
res.set('X-Served-By-DB-Host', res.locals.dbhost);
req.profiler.done('req2params');
next(null);
});
};
};
+15
View File
@@ -0,0 +1,15 @@
const locals = require('./locals');
const cleanUpQueryParams = require('./clean-up-query-params');
const layergroupToken = require('./layergroup-token');
const authorize = require('./authorize');
const dbConnSetup = require('./db-conn-setup');
module.exports = function prepareContextMiddleware(authApi, pgConnection) {
return [
locals,
cleanUpQueryParams(),
layergroupToken,
authorize(authApi),
dbConnSetup(pgConnection)
];
};
@@ -0,0 +1,32 @@
var LayergroupToken = require('../../models/layergroup-token');
module.exports = function layergroupTokenMiddleware(req, res, next) {
if (!res.locals.token) {
return next();
}
var user = res.locals.user;
var layergroupToken = LayergroupToken.parse(res.locals.token);
res.locals.token = layergroupToken.token;
res.locals.cache_buster = layergroupToken.cacheBuster;
if (layergroupToken.signer) {
res.locals.signer = layergroupToken.signer;
if (!res.locals.signer) {
res.locals.signer = user;
} else if (res.locals.signer !== user) {
var err = new Error(`Cannot use map signature of user "${res.locals.signer}" on db of user "${user}"`);
err.type = 'auth';
err.http_status = 403;
if (req.query && req.query.callback) {
err.http_status = 200;
}
req.profiler.done('req2params');
return next(err);
}
}
return next();
};
+8
View File
@@ -0,0 +1,8 @@
const _ = require('underscore');
module.exports = function localsMiddleware(req, res, next) {
_.extend(res.locals, req.params);
next();
};
+172
View File
@@ -0,0 +1,172 @@
const _ = require('underscore');
const debug = require('debug')('windshaft:cartodb:error-middleware');
module.exports = function errorMiddleware (/* options */) {
return function error (err, req, res, next) {
// jshint unused:false
// jshint maxcomplexity:9
var allErrors = Array.isArray(err) ? err : [err];
allErrors = populateTimeoutErrors(allErrors);
const label = err.label || 'UNKNOWN';
err = allErrors[0] || new Error(label);
allErrors[0] = err;
var statusCode = findStatusCode(err);
if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
statusCode = 204;
}
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
// If a callback was requested, force status to 200
if (req.query && req.query.callback) {
statusCode = 200;
}
var errorResponseBody = {
errors: allErrors.map(errorMessage),
errors_with_context: allErrors.map(errorMessageWithContext)
};
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
res.status(statusCode);
if (req.query && req.query.callback) {
res.jsonp(errorResponseBody);
} else {
res.json(errorResponseBody);
}
try {
// May throw due to dns, see
// See http://github.com/CartoDB/Windshaft/issues/166
req.profiler.sendStats();
} catch (err) {
debug("error sending profiling stats: " + err);
}
};
};
function isRenderTimeoutError (err) {
return err.message === 'Render timed out';
}
function isDatasourceTimeoutError (err) {
return err.message && err.message.match(/canceling statement due to statement timeout/i);
}
function isTimeoutError (err) {
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
}
function populateTimeoutErrors (errors) {
return errors.map(function (error) {
if (isRenderTimeoutError(error)) {
error.subtype = 'render';
}
if (isDatasourceTimeoutError(error)) {
error.subtype = 'datasource';
}
if (isTimeoutError(error)) {
error.message = 'You are over platform\'s limits. Please contact us to know more details';
error.type = 'limit';
error.http_status = 429;
}
return error;
});
}
function findStatusCode(err) {
var statusCode;
if ( err.http_status ) {
statusCode = err.http_status;
} else {
statusCode = statusFromErrorMessage('' + err);
}
return statusCode;
}
module.exports.findStatusCode = findStatusCode;
function statusFromErrorMessage(errMsg) {
// Find an appropriate statusCode based on message
// jshint maxcomplexity:7
var statusCode = 400;
if ( -1 !== errMsg.indexOf('permission denied') ) {
statusCode = 403;
}
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
statusCode = 403;
}
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
statusCode = 400;
}
else if ( -1 !== errMsg.indexOf('does not exist') ) {
if ( -1 !== errMsg.indexOf(' role ') ) {
statusCode = 403; // role 'xxx' does not exist
} else if ( errMsg.match(/function .* does not exist/) ) {
statusCode = 400; // invalid SQL (SQL function does not exist)
} else {
statusCode = 404;
}
}
return statusCode;
}
function errorMessage(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
return stripConnectionInfo(message);
}
module.exports.errorMessage = errorMessage;
function stripConnectionInfo(message) {
// Strip connection info, if any
return message
// See https://github.com/CartoDB/Windshaft/issues/173
.replace(/Connection string: '[^']*'\n\s/im, '')
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
.replace(/is the server.*encountered/im, 'encountered');
}
var ERROR_INFO_TO_EXPOSE = {
message: true,
layer: true,
type: true,
analysis: true,
subtype: true
};
function shouldBeExposed (prop) {
return !!ERROR_INFO_TO_EXPOSE[prop];
}
function errorMessageWithContext(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
var error = {
type: err.type || 'unknown',
message: stripConnectionInfo(message),
};
for (var prop in err) {
// type & message are properties from Error's prototype and will be skipped
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
error[prop] = err[prop];
}
}
return error;
}
+2 -2
View File
@@ -1,8 +1,8 @@
'use strict';
var LZMA = require('lzma').LZMA;
const LZMA = require('lzma').LZMA;
var lzmaWorker = new LZMA();
const lzmaWorker = new LZMA();
module.exports = function lzmaMiddleware(req, res, next) {
if (!req.query.hasOwnProperty('lzma')) {
+8 -1
View File
@@ -2,6 +2,13 @@ var CdbRequest = require('../models/cdb_request');
var cdbRequest = new CdbRequest();
module.exports = function userMiddleware(req, res, next) {
req.context.user = cdbRequest.userByReq(req);
res.locals.user = cdbRequest.userByReq(req);
// avoid a req.params.user equals to undefined
// overwrites res.locals.user
if(!req.params.user) {
delete req.params.user;
}
next();
};
+29
View File
@@ -0,0 +1,29 @@
/**
* @param {String} token might match the following pattern: {user}@{tpl_id}@{token}:{cache_buster}
*/
function parse(token) {
var signer, cacheBuster;
var tokenSplit = token.split(':');
token = tokenSplit[0];
if (tokenSplit.length > 1) {
cacheBuster = tokenSplit[1];
}
tokenSplit = token.split('@');
if ( tokenSplit.length > 1 ) {
signer = tokenSplit.shift();
if ( tokenSplit.length > 1 ) {
/*var template_hash = */tokenSplit.shift(); // unused
}
token = tokenSplit.shift();
}
return {
token: token,
signer: signer,
cacheBuster: cacheBuster
};
}
module.exports.parse = parse;
+16 -9
View File
@@ -4,8 +4,6 @@ var RedisPool = require('redis-mpool');
var cartodbRedis = require('cartodb-redis');
var _ = require('underscore');
var lzmaMiddleware = require('./middleware/lzma');
var controller = require('./controllers');
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
@@ -46,6 +44,11 @@ var MapConfigAdapter = require('./models/mapconfig/adapter');
var StatsBackend = require('./backends/stats');
const lzmaMiddleware = require('./middleware/lzma');
const errorMiddleware = require('./middleware/error-middleware');
const prepareContextMiddleware = require('./middleware/context');
module.exports = function(serverOptions) {
// Make stats client globally accessible
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
@@ -209,12 +212,16 @@ module.exports = function(serverOptions) {
var versions = getAndValidateVersions(serverOptions);
const prepareContext = typeof serverOptions.req2params === 'function' ?
serverOptions.req2params :
prepareContextMiddleware(authApi, pgConnection);
/*******************************************************************************************************************
* Routing
******************************************************************************************************************/
new controller.Layergroup(
authApi,
prepareContext,
pgConnection,
mapStore,
tileBackend,
@@ -227,7 +234,7 @@ module.exports = function(serverOptions) {
).register(app);
new controller.Map(
authApi,
prepareContext,
pgConnection,
templateMaps,
mapBackend,
@@ -240,8 +247,7 @@ module.exports = function(serverOptions) {
).register(app);
new controller.NamedMaps(
authApi,
pgConnection,
prepareContext,
namedMapProviderCache,
tileBackend,
previewBackend,
@@ -250,9 +256,9 @@ module.exports = function(serverOptions) {
metadataBackend
).register(app);
new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app);
new controller.NamedMapsAdmin(authApi, templateMaps).register(app);
new controller.Analyses(authApi, pgConnection).register(app);
new controller.Analyses(prepareContext).register(app);
new controller.ServerInfo(versions).register(app);
@@ -260,6 +266,8 @@ module.exports = function(serverOptions) {
* END Routing
******************************************************************************************************************/
app.use(errorMiddleware());
return app;
};
@@ -354,7 +362,6 @@ function bootstrap(opts) {
app.use(bodyParser.json());
app.use(function bootstrap$prepareRequestResponse(req, res, next) {
req.context = req.context || {};
req.profiler = new Profiler({
statsd_client: global.statsClient,
profile: opts.useProfiler