2011-09-05 07:00:41 +08:00
|
|
|
var _ = require('underscore')
|
|
|
|
, Step = require('step')
|
2013-11-16 02:14:00 +08:00
|
|
|
, cartoData = require('cartodb-redis')(global.environment.redis)
|
2012-09-24 23:57:39 +08:00
|
|
|
, Cache = require('./cache_validator')
|
|
|
|
, mapnik = require('mapnik')
|
2013-03-13 23:45:15 +08:00
|
|
|
, crypto = require('crypto')
|
|
|
|
, request = require('request')
|
2013-03-23 01:55:59 +08:00
|
|
|
, LZMA = require('lzma/lzma_worker.js').LZMA
|
2012-09-24 23:57:39 +08:00
|
|
|
;
|
2011-09-05 07:00:41 +08:00
|
|
|
|
2013-08-21 16:11:30 +08:00
|
|
|
// This is for backward compatibility with 1.3.3
|
2013-09-12 22:19:01 +08:00
|
|
|
if ( _.isUndefined(global.environment.sqlapi.domain) ) {
|
2013-09-12 21:36:50 +08:00
|
|
|
global.environment.sqlapi.domain = global.environment.sqlapi.host;
|
|
|
|
}
|
2013-08-21 16:11:30 +08:00
|
|
|
|
2011-09-05 07:00:41 +08:00
|
|
|
module.exports = function(){
|
2013-02-25 23:53:57 +08:00
|
|
|
|
|
|
|
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
2013-06-28 23:58:11 +08:00
|
|
|
cache_ttl: 60000, // milliseconds
|
2013-02-25 23:53:57 +08:00
|
|
|
metatile: 4,
|
|
|
|
bufferSize: 64
|
|
|
|
});
|
|
|
|
|
2011-09-05 07:00:41 +08:00
|
|
|
var me = {
|
|
|
|
base_url: '/tiles/:table',
|
2013-02-13 01:53:16 +08:00
|
|
|
base_url_notable: '/tiles',
|
2012-09-20 00:52:13 +08:00
|
|
|
grainstore: {
|
|
|
|
datasource: global.environment.postgres,
|
2012-09-24 23:57:39 +08:00
|
|
|
cachedir: global.environment.millstone.cache_basedir,
|
2013-06-26 22:26:02 +08:00
|
|
|
mapnik_version: global.environment.mapnik_version || mapnik.versions.mapnik,
|
2013-12-05 18:11:13 +08:00
|
|
|
default_layergroup_ttl: 7200, // seconds (default is 300)
|
|
|
|
gc_prob: 0.01 // default is 0.01 TODO: make configurable via env config
|
2012-09-20 00:52:13 +08:00
|
|
|
},
|
2013-02-25 23:53:57 +08:00
|
|
|
mapnik: {
|
|
|
|
metatile: rendererConfig.metatile,
|
|
|
|
bufferSize: rendererConfig.bufferSize
|
|
|
|
},
|
|
|
|
renderCache: {
|
|
|
|
ttl: rendererConfig.cache_ttl
|
|
|
|
},
|
2011-09-20 09:27:23 +08:00
|
|
|
redis: global.environment.redis,
|
2011-10-13 21:22:54 +08:00
|
|
|
enable_cors: global.environment.enable_cors,
|
2012-05-02 02:00:14 +08:00
|
|
|
varnish_host: global.environment.varnish.host,
|
|
|
|
varnish_port: global.environment.varnish.port,
|
2012-05-03 02:32:54 +08:00
|
|
|
cache_enabled: global.environment.cache_enabled,
|
2013-07-16 21:59:34 +08:00
|
|
|
log_format: global.environment.log_format,
|
|
|
|
useProfiler: global.environment.useProfiler
|
2012-05-03 02:32:54 +08:00
|
|
|
};
|
|
|
|
|
2012-10-09 17:45:57 +08:00
|
|
|
// Be nice and warn if configured mapnik version
|
|
|
|
// is != instaled mapnik version
|
|
|
|
if ( mapnik.versions.mapnik != me.grainstore.mapnik_version ) {
|
|
|
|
console.warn("WARNING: detected mapnik version ("
|
|
|
|
+ mapnik.versions.mapnik + ") != configured mapnik version ("
|
|
|
|
+ me.grainstore.mapnik_version + ")");
|
|
|
|
}
|
|
|
|
|
2013-03-13 23:45:15 +08:00
|
|
|
/* This whole block is about generating X-Cache-Channel { */
|
|
|
|
|
|
|
|
// TODO: review lifetime of elements of this cache
|
|
|
|
// NOTE: by-token indices should only be dropped when
|
|
|
|
// the corresponding layegroup is dropped, because
|
|
|
|
// we have no SQL after layer creation.
|
|
|
|
me.channelCache = {};
|
|
|
|
|
2013-03-14 01:41:37 +08:00
|
|
|
// Run a query through the SQL api
|
|
|
|
me.sqlQuery = function (username, api_key, sql, callback) {
|
2013-03-13 23:45:15 +08:00
|
|
|
var api = global.environment.sqlapi;
|
|
|
|
|
|
|
|
// build up api string
|
2013-11-13 00:41:10 +08:00
|
|
|
var sqlapi = api.protocol + '://' + username;
|
|
|
|
if ( api.domain ) sqlapi += '.' + api.domain;
|
|
|
|
sqlapi += ':' + api.port + '/api/' + api.version + '/sql'
|
2013-03-13 23:45:15 +08:00
|
|
|
|
2013-03-14 01:41:37 +08:00
|
|
|
var qs = { q: sql }
|
2013-03-13 23:45:15 +08:00
|
|
|
|
|
|
|
// add api_key if given
|
2013-03-14 01:41:37 +08:00
|
|
|
if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; }
|
2013-03-13 23:45:15 +08:00
|
|
|
|
|
|
|
// call sql api
|
|
|
|
request.get({url:sqlapi, qs:qs, json:true}, function(err, res, body){
|
|
|
|
if (err){
|
2013-12-18 19:59:26 +08:00
|
|
|
console.log('ERROR connecting to SQL API on ' + sqlapi + ': ' + err);
|
2013-03-14 01:41:37 +08:00
|
|
|
callback(err);
|
2013-03-13 23:45:15 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (res.statusCode != 200) {
|
|
|
|
var msg = res.body.error ? res.body.error : res.body;
|
2013-06-11 16:26:35 +08:00
|
|
|
callback(new Error(msg));
|
|
|
|
console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg);
|
2013-03-13 23:45:15 +08:00
|
|
|
return;
|
2013-09-12 21:36:50 +08:00
|
|
|
}
|
2013-03-14 01:41:37 +08:00
|
|
|
callback(null, body.rows);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2013-05-30 22:48:40 +08:00
|
|
|
//
|
|
|
|
// Invoke callback with number of milliseconds since
|
|
|
|
// last update in any of the given tables
|
|
|
|
//
|
2013-03-14 01:41:37 +08:00
|
|
|
me.findLastUpdated = function (username, api_key, tableNames, callback) {
|
2013-03-21 18:39:55 +08:00
|
|
|
var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname::name = any (\'{'
|
|
|
|
+ tableNames.join(',') + '}\')';
|
2013-03-14 01:41:37 +08:00
|
|
|
|
|
|
|
// call sql api
|
|
|
|
me.sqlQuery(username, api_key, sql, function(err, rows){
|
|
|
|
if (err){
|
|
|
|
var msg = err.message ? err.message : err;
|
|
|
|
callback(new Error('could not find last updated timestamp: ' + msg));
|
|
|
|
return;
|
|
|
|
}
|
2013-03-21 18:39:55 +08:00
|
|
|
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
|
|
|
|
var last_updated = 0;
|
|
|
|
if(rows.length !== 0) {
|
|
|
|
last_updated = rows[0].max || 0;
|
|
|
|
}
|
2013-05-30 22:48:40 +08:00
|
|
|
callback(null, last_updated*1000);
|
2013-03-14 01:41:37 +08:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
me.affectedTables = function (username, api_key, sql, callback) {
|
|
|
|
|
2013-04-23 23:12:10 +08:00
|
|
|
// Replace mapnik tokens
|
|
|
|
sql = sql.replace(RegExp('!bbox!', 'g'), 'ST_MakeEnvelope(0,0,0,0)')
|
|
|
|
.replace(RegExp('!pixel_width!', 'g'), '1')
|
|
|
|
.replace(RegExp('!pixel_height!', 'g'), '1')
|
|
|
|
;
|
|
|
|
|
|
|
|
// Pass to CDB_QueryTables
|
|
|
|
sql = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)';
|
2013-03-14 01:41:37 +08:00
|
|
|
|
|
|
|
// call sql api
|
|
|
|
me.sqlQuery(username, api_key, sql, function(err, rows){
|
|
|
|
if (err){
|
|
|
|
var msg = err.message ? err.message : err;
|
|
|
|
callback(new Error('could not fetch source tables: ' + msg));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var qtables = rows[0].cdb_querytables;
|
2013-03-13 23:45:15 +08:00
|
|
|
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
|
2013-03-14 01:41:37 +08:00
|
|
|
tableNames = tableNames.split(',');
|
2013-03-13 23:45:15 +08:00
|
|
|
callback(null, tableNames);
|
|
|
|
});
|
2013-03-14 01:41:37 +08:00
|
|
|
};
|
2013-03-13 23:45:15 +08:00
|
|
|
|
|
|
|
me.buildCacheChannel = function (dbName, tableNames){
|
2013-03-14 01:41:37 +08:00
|
|
|
return dbName + ':' + tableNames.join(',');
|
2013-03-13 23:45:15 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
me.generateMD5 = function(data){
|
|
|
|
var hash = crypto.createHash('md5');
|
|
|
|
hash.update(data);
|
|
|
|
return hash.digest('hex');
|
|
|
|
}
|
|
|
|
|
|
|
|
me.generateCacheChannel = function(req, callback){
|
|
|
|
|
|
|
|
// use key to call sql api with sql request if present, else
|
|
|
|
// just return dbname and table name base key
|
|
|
|
var dbName = req.params.dbname;
|
|
|
|
|
|
|
|
var cacheKey = [ dbName ];
|
|
|
|
if ( req.params.token ) cacheKey.push(req.params.token);
|
|
|
|
else if ( req.params.sql ) cacheKey.push( me.generateMD5(req.params.sql) );
|
|
|
|
cacheKey = cacheKey.join(':');
|
|
|
|
|
|
|
|
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
|
|
|
|
callback(null, me.channelCache[cacheKey]);
|
|
|
|
return;
|
2013-09-12 21:36:50 +08:00
|
|
|
}
|
2013-07-15 19:48:06 +08:00
|
|
|
else if ( req.params.token ) {
|
|
|
|
// cached cache channel for token-based access should be constructed
|
|
|
|
// at cache creation time
|
|
|
|
callback(new Error('missing channel cache for token ' + req.params.token));
|
2013-03-13 23:45:15 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( ! req.params.sql && ! req.params.token ) {
|
2013-03-14 01:41:37 +08:00
|
|
|
var cacheChannel = me.buildCacheChannel(dbName, [req.params.table]);
|
2013-03-13 23:45:15 +08:00
|
|
|
// not worth caching this
|
|
|
|
callback(null, cacheChannel);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( ! req.params.sql ) {
|
|
|
|
callback(new Error("this request doesn't need an X-Cache-Channel generated"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var dbName = req.params.dbname;
|
2014-01-09 22:36:16 +08:00
|
|
|
var username = this.userByReq(req);
|
2013-03-13 23:45:15 +08:00
|
|
|
|
|
|
|
// strip out windshaft/mapnik inserted sql if present
|
|
|
|
var sql = req.params.sql.match(/^\((.*)\)\sas\scdbq$/);
|
|
|
|
sql = (sql != null) ? sql[1] : req.params.sql;
|
|
|
|
|
|
|
|
me.affectedTables(username, req.params.map_key, sql, function(err, tableNames) {
|
|
|
|
if ( err ) { callback(err); return; }
|
|
|
|
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
|
|
|
|
me.channelCache[cacheKey] = cacheChannel; // store for caching
|
|
|
|
callback(null, cacheChannel);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2012-09-25 00:57:48 +08:00
|
|
|
// Set the cache chanel info to invalidate the cache on the frontend server
|
|
|
|
//
|
|
|
|
// @param req The request object.
|
|
|
|
// The function will have no effect unless req.res exists.
|
|
|
|
// It is expected that req.params contains 'table' and 'dbname'
|
|
|
|
//
|
|
|
|
// @param cb function(err, channel) will be called when ready.
|
|
|
|
// the channel parameter will be null if nothing was added
|
|
|
|
//
|
|
|
|
me.addCacheChannel = function(req, cb) {
|
|
|
|
// skip non-GET requests, or requests for which there's no response
|
|
|
|
if ( req.method != 'GET' || ! req.res ) { cb(null, null); return; }
|
|
|
|
var res = req.res;
|
2013-03-13 17:36:28 +08:00
|
|
|
var cache_policy = req.query.cache_policy;
|
2013-07-08 18:13:45 +08:00
|
|
|
if ( req.params.token ) cache_policy = 'persist';
|
2013-03-13 17:36:28 +08:00
|
|
|
if ( cache_policy == 'persist' ) {
|
|
|
|
res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
|
|
|
|
} else {
|
|
|
|
var ttl = global.environment.varnish.ttl || 86400;
|
|
|
|
res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public');
|
|
|
|
}
|
2013-03-13 23:45:15 +08:00
|
|
|
|
2013-07-15 19:14:06 +08:00
|
|
|
// Set Last-Modified header
|
2013-07-15 19:48:06 +08:00
|
|
|
var lastUpdated;
|
|
|
|
if ( req.params.cache_buster ) {
|
|
|
|
// Assuming cache_buster is a timestamp
|
|
|
|
// FIXME: store lastModified in the cache channel instead
|
|
|
|
lastUpdated = new Date(parseInt(req.params.cache_buster));
|
|
|
|
} else {
|
|
|
|
lastUpdated = new Date();
|
|
|
|
}
|
|
|
|
res.header('Last-Modified', lastUpdated.toUTCString());
|
2013-07-15 18:02:54 +08:00
|
|
|
|
2013-03-13 23:45:15 +08:00
|
|
|
me.generateCacheChannel(req, function(err, channel){
|
2013-03-13 17:36:28 +08:00
|
|
|
if ( ! err ) {
|
|
|
|
res.header('X-Cache-Channel', channel);
|
2013-09-12 21:36:50 +08:00
|
|
|
cb(null, channel);
|
2012-10-24 15:40:05 +08:00
|
|
|
} else {
|
2013-03-13 17:36:28 +08:00
|
|
|
console.log('ERROR generating cache channel: ' + ( err.message ? err.message : err ));
|
|
|
|
// TODO: evaluate if we should bubble up the error instead
|
|
|
|
cb(null, 'ERROR');
|
2012-10-24 15:40:05 +08:00
|
|
|
}
|
2012-09-25 00:57:48 +08:00
|
|
|
});
|
2013-03-13 23:45:15 +08:00
|
|
|
};
|
|
|
|
|
2013-04-02 19:30:49 +08:00
|
|
|
me.afterLayergroupCreate = function(req, mapconfig, response, callback) {
|
2013-03-13 23:45:15 +08:00
|
|
|
var token = response.layergroupid;
|
|
|
|
|
2013-12-17 00:34:58 +08:00
|
|
|
var username = this.userByReq(req);
|
2013-04-12 23:28:34 +08:00
|
|
|
|
|
|
|
var tasksleft = 2; // redis key and affectedTables
|
|
|
|
var errors = [];
|
|
|
|
|
|
|
|
var done = function(err) {
|
|
|
|
if ( err ) {
|
|
|
|
errors.push('' + err);
|
|
|
|
}
|
|
|
|
if ( ! --tasksleft ) {
|
|
|
|
err = errors.length ? new Error(errors.join('\n')) : null;
|
|
|
|
callback(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't wait for the mapview count increment to
|
|
|
|
// take place before proceeding. Error will be logged
|
|
|
|
// asyncronously
|
2013-06-04 19:29:36 +08:00
|
|
|
cartoData.incMapviewCount(username, mapconfig.stat_tag, function(err) {
|
2013-09-19 21:34:03 +08:00
|
|
|
if (req.profiler) req.profiler.done('incMapviewCount');
|
2013-04-12 23:28:34 +08:00
|
|
|
if ( err ) console.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
|
2013-03-13 23:45:15 +08:00
|
|
|
var sql = [];
|
|
|
|
_.each(mapconfig.layers, function(lyr) {
|
|
|
|
sql.push(lyr.options.sql);
|
|
|
|
});
|
|
|
|
sql = sql.join(';');
|
|
|
|
|
|
|
|
var dbName = req.params.dbname;
|
2014-01-09 23:46:47 +08:00
|
|
|
var usr = this.userByReq(req);
|
2013-03-13 23:45:15 +08:00
|
|
|
var key = req.params.map_key;
|
|
|
|
|
|
|
|
var cacheKey = dbName + ':' + token;
|
|
|
|
|
|
|
|
me.affectedTables(usr, key, sql, function(err, tableNames) {
|
2013-09-19 21:34:03 +08:00
|
|
|
if (req.profiler) req.profiler.done('affectedTables');
|
2013-03-14 01:41:37 +08:00
|
|
|
|
2013-04-12 23:28:34 +08:00
|
|
|
if ( err ) { done(err); return; }
|
2013-03-13 23:45:15 +08:00
|
|
|
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
|
|
|
|
me.channelCache[cacheKey] = cacheChannel; // store for caching
|
2013-03-14 01:41:37 +08:00
|
|
|
// find last updated
|
|
|
|
me.findLastUpdated(usr, key, tableNames, function(err, lastUpdated) {
|
2013-09-19 21:34:03 +08:00
|
|
|
if (req.profiler) req.profiler.done('findLastUpdated');
|
2013-04-12 23:28:34 +08:00
|
|
|
if ( err ) { done(err); return; }
|
2013-04-04 19:15:50 +08:00
|
|
|
response.layergroupid = response.layergroupid + ':' + lastUpdated; // use epoch
|
|
|
|
response.last_updated = new Date(lastUpdated).toISOString(); // TODO: use ISO format
|
2013-04-12 23:28:34 +08:00
|
|
|
done(null);
|
2013-03-14 01:41:37 +08:00
|
|
|
});
|
2013-03-13 23:45:15 +08:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/* X-Cache-Channel generation } */
|
2012-09-25 00:57:48 +08:00
|
|
|
|
2013-12-17 01:24:35 +08:00
|
|
|
me.re_userFromHost = new RegExp(
|
|
|
|
global.environment.user_from_host ||
|
|
|
|
'^([^\\.]+)\\.' // would extract "strk" from "strk.cartodb.com"
|
|
|
|
);
|
|
|
|
|
2013-12-17 00:34:58 +08:00
|
|
|
me.userByReq = function(req) {
|
2013-12-17 01:24:35 +08:00
|
|
|
var host = req.headers.host;
|
|
|
|
var mat = host.match(this.re_userFromHost);
|
|
|
|
if ( ! mat ) {
|
|
|
|
console.error("ERROR: user pattern '" + this.re_userFromHost
|
|
|
|
+ "' does not match hostname '" + host + "'");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// console.log("Matches: "); console.dir(mat);
|
|
|
|
if ( ! mat.length === 2 ) {
|
|
|
|
console.error("ERROR: pattern '" + this.re_userFromHost
|
|
|
|
+ "' gave unexpected matches against '" + host + "': " + mat);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return mat[1];
|
2013-12-06 20:32:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set db authentication parameters to those of the given username
|
|
|
|
//
|
|
|
|
// @param username the cartodb username, mapped to a database username
|
|
|
|
// via CartodbRedis metadata records
|
|
|
|
//
|
|
|
|
// @param params the parameters to set auth options into
|
|
|
|
// added params are: "dbuser" and "dbpassword"
|
|
|
|
//
|
|
|
|
// @param callback function(err)
|
|
|
|
//
|
|
|
|
me.setDBAuth = function(username, params, callback) {
|
|
|
|
|
|
|
|
var user_params = {};
|
|
|
|
var auth_user = global.environment.postgres_auth_user;
|
|
|
|
var auth_pass = global.environment.postgres_auth_pass;
|
|
|
|
Step(
|
|
|
|
function getId() {
|
|
|
|
cartoData.getUserId(username, this);
|
|
|
|
},
|
|
|
|
function(err, user_id) {
|
|
|
|
if (err) throw err;
|
|
|
|
user_params['user_id'] = user_id;
|
|
|
|
var dbuser = _.template(auth_user, user_params);
|
|
|
|
_.extend(params, {dbuser:dbuser});
|
|
|
|
|
|
|
|
// skip looking up user_password if postgres_auth_pass
|
|
|
|
// doesn't contain the "user_password" label
|
|
|
|
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) return null;
|
|
|
|
|
|
|
|
cartoData.getUserDBPass(username, this);
|
|
|
|
},
|
|
|
|
function(err, user_password) {
|
|
|
|
if (err) throw err;
|
|
|
|
user_params['user_password'] = user_password;
|
|
|
|
if ( auth_pass ) {
|
|
|
|
var dbpass = _.template(auth_pass, user_params);
|
|
|
|
_.extend(params, {dbpassword:dbpass});
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
function finish(err) {
|
|
|
|
callback(err);
|
|
|
|
}
|
|
|
|
);
|
2013-12-18 00:35:12 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
// Set db connection parameters to those for the given username
|
|
|
|
//
|
|
|
|
// @param dbowner cartodb username of database owner,
|
|
|
|
// mapped to a database username
|
|
|
|
// via CartodbRedis metadata records
|
|
|
|
//
|
|
|
|
// @param params the parameters to set connection options into
|
|
|
|
// added params are: "dbname", "dbhost"
|
|
|
|
//
|
|
|
|
// @param callback function(err)
|
|
|
|
//
|
|
|
|
me.setDBConn = function(dbowner, params, callback) {
|
|
|
|
Step(
|
|
|
|
function getDatabaseHost(){
|
|
|
|
cartoData.getUserDBHost(dbowner, this);
|
|
|
|
},
|
|
|
|
function getDatabase(err, data){
|
|
|
|
if(err) throw err;
|
|
|
|
if ( data ) _.extend(params, {dbhost:data});
|
|
|
|
cartoData.getUserDBName(dbowner, this);
|
|
|
|
},
|
|
|
|
function getGeometryType(err, data){
|
|
|
|
if (err) throw err;
|
|
|
|
if ( data ) _.extend(params, {dbname:data});
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
function finish(err) {
|
|
|
|
callback(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Check if a request is authorized by a signer
|
|
|
|
//
|
|
|
|
// Any existing signature for the given request will verified
|
|
|
|
// for authorization to this specific request (may require auth_token)
|
|
|
|
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps
|
|
|
|
//
|
|
|
|
// @param req express request 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.
|
|
|
|
//
|
|
|
|
me.authorizedBySigner = function(req, callback)
|
|
|
|
{
|
|
|
|
if ( ! req.params.token || ! req.params.signer ) {
|
|
|
|
//console.log("No signature provided"); // debugging
|
|
|
|
callback(null, null); // no signer requested
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var signer = req.params.signer;
|
|
|
|
var layergroup_id = req.params.token;
|
|
|
|
var auth_token = req.params.auth_token;
|
|
|
|
|
|
|
|
console.log("Checking authorization from signer " + signer + " for resource " + layergroup_id + " with auth_token " + auth_token);
|
|
|
|
|
|
|
|
me.signedMaps.isAuthorized(signer, layergroup_id, auth_token,
|
|
|
|
function(err, authorized) {
|
|
|
|
callback(err, authorized ? signer : null);
|
|
|
|
});
|
|
|
|
};
|
2013-12-06 20:32:37 +08:00
|
|
|
|
2013-12-17 18:43:56 +08:00
|
|
|
// Check if a request is authorized by api_key
|
|
|
|
//
|
|
|
|
// @param req express request object
|
|
|
|
// @param callback function(err, authorized)
|
|
|
|
//
|
|
|
|
me.authorizedByAPIKey = function(req, callback)
|
|
|
|
{
|
|
|
|
var user = me.userByReq(req);
|
|
|
|
Step(
|
|
|
|
function (){
|
|
|
|
cartoData.getUserMapKey(user, this);
|
|
|
|
},
|
|
|
|
function checkApiKey(err, val){
|
|
|
|
if (err) throw err;
|
|
|
|
|
|
|
|
var valid = 0;
|
|
|
|
if ( val ) {
|
|
|
|
if ( val == req.query.map_key ) valid = 1;
|
|
|
|
else if ( val == req.query.api_key ) valid = 1;
|
|
|
|
// check also in request body
|
|
|
|
else if ( req.body && req.body.map_key && val == req.body.map_key ) valid = 1;
|
|
|
|
else if ( req.body && req.body.api_key && val == req.body.api_key ) valid = 1;
|
|
|
|
}
|
|
|
|
return valid;
|
|
|
|
},
|
|
|
|
function finish(err, authorized) {
|
|
|
|
callback(err, authorized);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2013-11-16 02:14:00 +08:00
|
|
|
/**
|
2013-12-06 20:32:37 +08:00
|
|
|
* Check access authorization
|
2013-11-16 02:14:00 +08:00
|
|
|
*
|
|
|
|
* @param req - standard req object. Importantly contains table and host information
|
2013-12-06 20:32:37 +08:00
|
|
|
* @param callback function(err, allowed) is access allowed not?
|
2013-11-16 02:14:00 +08:00
|
|
|
*/
|
2013-12-06 20:32:37 +08:00
|
|
|
me.authorize = function(req, callback) {
|
2013-11-16 02:14:00 +08:00
|
|
|
var that = this;
|
2013-12-17 00:34:58 +08:00
|
|
|
var user = me.userByReq(req);
|
2013-11-16 02:14:00 +08:00
|
|
|
|
|
|
|
Step(
|
2013-12-17 17:56:12 +08:00
|
|
|
function (){
|
2013-12-17 18:43:56 +08:00
|
|
|
that.authorizedByAPIKey(req, this);
|
2013-11-16 02:14:00 +08:00
|
|
|
},
|
2013-12-17 18:43:56 +08:00
|
|
|
function checkApiKey(err, authorized){
|
2013-11-16 02:14:00 +08:00
|
|
|
if (err) throw err;
|
|
|
|
|
2013-12-06 20:32:37 +08:00
|
|
|
// if not authorized by api_key, continue
|
2013-12-18 00:35:12 +08:00
|
|
|
if (authorized !== 1) {
|
|
|
|
// not authorized by api_key,
|
|
|
|
// check if authorized by signer
|
|
|
|
that.authorizedBySigner(req, this);
|
|
|
|
return;
|
|
|
|
}
|
2013-12-06 20:32:37 +08:00
|
|
|
|
2013-12-17 00:34:58 +08:00
|
|
|
// authorized by api key, login as the given username and stop
|
|
|
|
that.setDBAuth(user, req.params, function(err) {
|
2013-12-06 20:32:37 +08:00
|
|
|
callback(err, true); // authorized (or error)
|
|
|
|
});
|
|
|
|
},
|
2013-12-18 00:35:12 +08:00
|
|
|
function checkSignAuthorized(err, signed_by){
|
|
|
|
if (err) throw err;
|
|
|
|
|
|
|
|
if ( ! signed_by ) {
|
|
|
|
// request not authorized by signer, continue
|
|
|
|
// to check map privacy
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Authorized by "signed_by" !
|
|
|
|
that.setDBAuth(signed_by, req.params, function(err) {
|
|
|
|
callback(err, true); // authorized (or error)
|
|
|
|
});
|
|
|
|
},
|
2013-12-06 20:32:37 +08:00
|
|
|
function getDatabase(err){
|
2013-11-16 02:14:00 +08:00
|
|
|
if (err) throw err;
|
2013-12-06 20:32:37 +08:00
|
|
|
// NOTE: only used to get to table privacy
|
2013-12-17 00:34:58 +08:00
|
|
|
cartoData.getUserDBName(user, this);
|
2013-11-16 02:14:00 +08:00
|
|
|
},
|
2013-12-06 20:32:37 +08:00
|
|
|
function getPrivacy(err, dbname){
|
2013-11-16 02:14:00 +08:00
|
|
|
if (err) throw err;
|
2013-12-06 20:32:37 +08:00
|
|
|
cartoData.getTablePrivacy(dbname, req.params.table, this);
|
2013-11-16 02:14:00 +08:00
|
|
|
},
|
2013-12-06 20:32:37 +08:00
|
|
|
function(err, privacy){
|
|
|
|
callback(err, privacy);
|
2013-11-16 02:14:00 +08:00
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2012-05-03 02:32:54 +08:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
me.req2params = function(req, callback){
|
|
|
|
|
2013-03-23 01:55:59 +08:00
|
|
|
if ( req.query.lzma ) {
|
|
|
|
|
|
|
|
// TODO: check ?
|
|
|
|
//console.log("type of req.query.lzma is " + typeof(req.query.lzma));
|
|
|
|
|
2013-04-19 22:16:20 +08:00
|
|
|
// Decode (from base64)
|
2013-04-24 21:10:58 +08:00
|
|
|
var lzma = (new Buffer(req.query.lzma, 'base64').toString('binary')).split('').map(function(c) { return c.charCodeAt(0) - 128 })
|
2013-03-23 01:55:59 +08:00
|
|
|
|
|
|
|
// Decompress
|
|
|
|
LZMA.decompress(
|
|
|
|
lzma,
|
|
|
|
function(result) {
|
2013-07-16 21:59:34 +08:00
|
|
|
if (req.profiler) req.profiler.done('LZMA decompress');
|
2013-03-23 01:55:59 +08:00
|
|
|
try {
|
2013-04-24 21:10:58 +08:00
|
|
|
delete req.query.lzma
|
|
|
|
_.extend(req.query, JSON.parse(result))
|
2013-03-23 01:55:59 +08:00
|
|
|
me.req2params(req, callback);
|
2013-09-12 21:36:50 +08:00
|
|
|
} catch (err) {
|
2013-03-23 01:55:59 +08:00
|
|
|
callback(new Error('Error parsing lzma as JSON: ' + err));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
function(percent) { // progress
|
|
|
|
//console.log("LZMA decompression " + percent + "%");
|
|
|
|
}
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-05-03 02:32:54 +08:00
|
|
|
// Whitelist query parameters and attach format
|
2013-12-18 00:35:12 +08:00
|
|
|
var good_query = ['sql', 'geom_type', 'cache_buster', 'cache_policy', 'callback', 'interactivity', 'map_key', 'api_key', 'auth_token', 'style', 'style_version', 'style_convert', 'config' ];
|
2012-05-03 02:32:54 +08:00
|
|
|
var bad_query = _.difference(_.keys(req.query), good_query);
|
|
|
|
|
|
|
|
_.each(bad_query, function(key){ delete req.query[key]; });
|
|
|
|
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
|
|
|
|
2013-04-04 19:15:50 +08:00
|
|
|
if ( req.params.token ) {
|
|
|
|
//console.log("Request parameters include token " + req.params.token);
|
|
|
|
var tksplit = req.params.token.split(':');
|
|
|
|
req.params.token = tksplit[0];
|
|
|
|
if ( tksplit.length > 1 ) req.params.cache_buster= tksplit[1];
|
2013-12-06 20:32:37 +08:00
|
|
|
tksplit = req.params.token.split('@');
|
|
|
|
if ( tksplit.length > 1 ) {
|
2014-01-09 22:36:16 +08:00
|
|
|
req.params.signer = this.userByReq(req);
|
2013-12-18 00:35:12 +08:00
|
|
|
if ( tksplit[0] ) req.params.signer = tksplit[0];
|
2013-12-06 20:32:37 +08:00
|
|
|
req.params.token = tksplit[1];
|
2013-12-18 00:35:12 +08:00
|
|
|
//console.log("Request for token " + req.params.token + " with signature from " + req.params.signer);
|
2013-12-06 20:32:37 +08:00
|
|
|
}
|
2013-04-04 19:15:50 +08:00
|
|
|
}
|
|
|
|
|
2012-05-03 02:32:54 +08:00
|
|
|
// bring all query values onto req.params object
|
|
|
|
_.extend(req.params, req.query);
|
|
|
|
|
|
|
|
// for cartodb, ensure interactivity is cartodb_id or user specified
|
2013-04-06 00:11:36 +08:00
|
|
|
req.params.interactivity = req.params.interactivity || 'cartodb_id';
|
2012-05-03 02:32:54 +08:00
|
|
|
|
2012-09-03 20:54:23 +08:00
|
|
|
req.params.processXML = function(req, xml, callback) {
|
2013-11-12 00:23:10 +08:00
|
|
|
|
|
|
|
// Replace dbuser
|
2013-11-16 02:14:00 +08:00
|
|
|
var dbuser = req.params.dbuser || global.environment.postgres.user;
|
2013-06-17 23:24:09 +08:00
|
|
|
if ( ! me.rx_dbuser ) me.rx_dbuser = /(<Parameter name="user"><!\[CDATA\[)[^\]]*(]]><\/Parameter>)/g;
|
2012-09-05 21:41:22 +08:00
|
|
|
xml = xml.replace(me.rx_dbuser, "$1" + dbuser + "$2");
|
2013-11-11 07:50:03 +08:00
|
|
|
|
2013-11-12 00:23:10 +08:00
|
|
|
// Replace dbpass
|
2013-11-16 02:14:00 +08:00
|
|
|
var dbpass = req.params.dbpassword || global.environment.postgres.password;
|
2013-11-11 07:50:03 +08:00
|
|
|
if ( ! me.rx_dbpass ) me.rx_dbpass = /(<Parameter name="password"><!\[CDATA\[)[^\]]*(]]><\/Parameter>)/g;
|
|
|
|
xml = xml.replace(me.rx_dbpass, "$1" + dbpass + "$2");
|
|
|
|
|
2013-11-12 00:23:10 +08:00
|
|
|
// Replace or set dbhost
|
2013-11-16 02:14:00 +08:00
|
|
|
var dbhost = req.params.dbhost || global.environment.postgres.host;
|
2013-11-12 00:23:10 +08:00
|
|
|
if ( ! me.rx_dbhost ) me.rx_dbhost = /(<Parameter name="host"><!\[CDATA\[)[^\]]*(]]><\/Parameter>)/g;
|
|
|
|
xml = xml.replace(me.rx_dbhost, "$1" + dbhost + "$2");
|
|
|
|
|
2012-09-03 20:54:23 +08:00
|
|
|
callback(null, xml);
|
|
|
|
}
|
|
|
|
|
2012-09-25 00:57:48 +08:00
|
|
|
var that = this;
|
|
|
|
|
2013-07-16 21:59:34 +08:00
|
|
|
if (req.profiler) req.profiler.done('req2params.setup');
|
|
|
|
|
2013-12-17 00:34:58 +08:00
|
|
|
var user = me.userByReq(req);
|
2013-12-06 20:32:37 +08:00
|
|
|
|
2012-05-03 02:32:54 +08:00
|
|
|
Step(
|
|
|
|
function getPrivacy(){
|
2013-11-16 02:14:00 +08:00
|
|
|
me.authorize(req, this);
|
2012-05-03 02:32:54 +08:00
|
|
|
},
|
|
|
|
function gatekeep(err, data){
|
2013-12-06 20:32:37 +08:00
|
|
|
if (req.profiler) req.profiler.done('authorize');
|
2012-05-03 02:32:54 +08:00
|
|
|
if(err) throw err;
|
2012-09-06 02:16:55 +08:00
|
|
|
if(data === "0") throw new Error("Sorry, you are unauthorized (permission denied)");
|
2012-05-03 02:32:54 +08:00
|
|
|
return data;
|
|
|
|
},
|
|
|
|
function getDatabase(err, data){
|
|
|
|
if(err) throw err;
|
2013-12-18 00:35:12 +08:00
|
|
|
that.setDBConn(user, req.params, this);
|
2012-05-03 02:32:54 +08:00
|
|
|
},
|
2013-12-18 00:35:12 +08:00
|
|
|
function getGeometryType(err){
|
2013-11-11 23:55:51 +08:00
|
|
|
if (req.profiler) req.profiler.done('cartoData.getDatabase');
|
2012-05-03 02:32:54 +08:00
|
|
|
if (err) throw err;
|
2013-12-18 00:35:12 +08:00
|
|
|
cartoData.getTableGeometryType(req.params.dbname, req.params.table, this);
|
2012-05-03 02:32:54 +08:00
|
|
|
},
|
|
|
|
function finishSetup(err, data){
|
2013-11-11 23:55:51 +08:00
|
|
|
if (req.profiler) req.profiler.done('cartoData.getGeometryType');
|
2012-09-25 00:57:48 +08:00
|
|
|
if ( err ) { callback(err, req); return; }
|
|
|
|
|
2012-05-03 02:32:54 +08:00
|
|
|
if (!_.isNull(data))
|
|
|
|
_.extend(req.params, {geom_type: data});
|
|
|
|
|
2013-03-13 17:36:28 +08:00
|
|
|
that.addCacheChannel(req, function(err) {
|
2013-11-11 23:55:51 +08:00
|
|
|
if (req.profiler) req.profiler.done('addCacheChannel');
|
2012-09-25 00:57:48 +08:00
|
|
|
callback(err, req);
|
|
|
|
});
|
2012-05-03 02:32:54 +08:00
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Little helper method to get the current list of infowindow variables and return to client
|
|
|
|
* @param req
|
|
|
|
* @param callback
|
|
|
|
*/
|
|
|
|
me.getInfowindow = function(req, callback){
|
|
|
|
var that = this;
|
2013-12-17 17:56:12 +08:00
|
|
|
var user = me.userByReq(req);
|
2012-05-03 02:32:54 +08:00
|
|
|
|
|
|
|
Step(
|
|
|
|
function(){
|
2013-12-17 17:56:12 +08:00
|
|
|
// TODO: if this step really needed ?
|
2012-05-03 02:32:54 +08:00
|
|
|
that.req2params(req, this);
|
|
|
|
},
|
2013-12-17 17:56:12 +08:00
|
|
|
function getDatabase(err){
|
|
|
|
if (err) throw err;
|
|
|
|
cartoData.getUserDBName(user, this);
|
|
|
|
},
|
|
|
|
function getInfowindow(err, dbname){
|
|
|
|
if (err) throw err;
|
|
|
|
cartoData.getTableInfowindow(dbname, req.params.table, this);
|
|
|
|
},
|
2012-05-03 02:32:54 +08:00
|
|
|
function(err, data){
|
2013-12-17 17:56:12 +08:00
|
|
|
callback(err, data);
|
2012-05-03 02:32:54 +08:00
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2013-12-17 00:17:55 +08:00
|
|
|
/**
|
|
|
|
* Little helper method to get map metadata and return to client
|
|
|
|
* @param req
|
|
|
|
* @param callback
|
|
|
|
*/
|
|
|
|
me.getMapMetadata = function(req, callback){
|
|
|
|
var that = this;
|
2013-12-17 17:56:12 +08:00
|
|
|
var user = me.userByReq(req);
|
2013-12-17 00:17:55 +08:00
|
|
|
|
|
|
|
Step(
|
|
|
|
function(){
|
2013-12-17 17:56:12 +08:00
|
|
|
// TODO: if this step really needed ?
|
2013-12-17 00:17:55 +08:00
|
|
|
that.req2params(req, this);
|
|
|
|
},
|
2013-12-17 17:56:12 +08:00
|
|
|
function getDatabase(err){
|
|
|
|
if (err) throw err;
|
|
|
|
cartoData.getUserDBName(user, this);
|
|
|
|
},
|
|
|
|
function getMapMetadata(err, dbname){
|
|
|
|
if (err) throw err;
|
|
|
|
cartoData.getTableMapMetadata(dbname, req.params.table, this);
|
|
|
|
},
|
2013-12-17 00:17:55 +08:00
|
|
|
function(err, data){
|
2013-12-17 17:56:12 +08:00
|
|
|
callback(err, data);
|
2013-12-17 00:17:55 +08:00
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2012-05-03 02:32:54 +08:00
|
|
|
/**
|
|
|
|
* Helper to clear out tile cache on request
|
|
|
|
* @param req
|
|
|
|
* @param callback
|
|
|
|
*/
|
|
|
|
me.flushCache = function(req, Cache, callback){
|
|
|
|
var that = this;
|
|
|
|
|
|
|
|
Step(
|
2013-03-16 02:25:13 +08:00
|
|
|
function getParams(){
|
|
|
|
// this is mostly to compute req.params.dbname
|
2012-05-03 02:32:54 +08:00
|
|
|
that.req2params(req, this);
|
|
|
|
},
|
2013-03-16 02:25:13 +08:00
|
|
|
function flushInternalCache(err){
|
|
|
|
// TODO: implement this, see
|
|
|
|
// http://github.com/Vizzuality/Windshaft-cartodb/issues/73
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
function flushVarnishCache(err){
|
|
|
|
if (err) { callback(err); return; }
|
2012-10-05 22:55:58 +08:00
|
|
|
if(Cache) {
|
|
|
|
Cache.invalidate_db(req.params.dbname, req.params.table);
|
|
|
|
}
|
2012-05-03 02:32:54 +08:00
|
|
|
callback(null, true);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
return me;
|
2011-10-13 21:22:54 +08:00
|
|
|
}();
|