The req2params method is doing too many things, this is an initial step to do fewer things in that method.
254 lines
7.5 KiB
JavaScript
254 lines
7.5 KiB
JavaScript
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;
|
|
}
|
|
|
|
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) {
|
|
res.set(headers);
|
|
}
|
|
|
|
res.status(status);
|
|
|
|
if (!Buffer.isBuffer(body) && typeof body === 'object') {
|
|
if (req.query && req.query.callback) {
|
|
res.jsonp(body);
|
|
} else {
|
|
res.json(body);
|
|
}
|
|
} else {
|
|
res.send(body);
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|
|
// jshint maxcomplexity:6
|
|
|
|
BaseController.prototype.sendError = function(req, res, err, label) {
|
|
var allErrors = Array.isArray(err) ? err : [err];
|
|
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');
|
|
}
|
|
|
|
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)) {
|
|
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;
|
|
}
|