Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9195967a4 | ||
|
|
30c6a390ac | ||
|
|
3a97af767f | ||
|
|
57dd36a476 | ||
|
|
6ab6fd91e4 | ||
|
|
c41c223b84 | ||
|
|
7e2be7b30f | ||
|
|
e690170689 | ||
|
|
81f1b0dcf8 | ||
|
|
146a2b2606 | ||
|
|
ff811ac1b5 | ||
|
|
11d9f5dd76 | ||
|
|
571a635fed | ||
|
|
6e70518146 | ||
|
|
fabb438cf0 | ||
|
|
0abd6a2293 | ||
|
|
272e8cd221 | ||
|
|
885accdadf | ||
|
|
f5a3b77737 | ||
|
|
56abcfd2f4 | ||
|
|
ecd570323b | ||
|
|
20eb92a3b1 | ||
|
|
8d22ed7594 | ||
|
|
3321987c33 | ||
|
|
981be0edd5 | ||
|
|
e8ab3a48c6 | ||
|
|
58a54de5a6 | ||
|
|
21b1cea5e8 | ||
|
|
64b5a64e1b | ||
|
|
f1b6be1ecb | ||
|
|
ac84fc569f | ||
|
|
4bdc43ff7c | ||
|
|
3afbbccfa2 | ||
|
|
8bc08d75b7 | ||
|
|
c14157acc2 | ||
|
|
595dac57a0 | ||
|
|
5632b19e16 | ||
|
|
007196555d | ||
|
|
62ffc05ef4 | ||
|
|
5962141114 | ||
|
|
7901a05b55 | ||
|
|
4c2a0ca048 | ||
|
|
b40c8e6624 | ||
|
|
97d3b1a03b | ||
|
|
fcea0c9b83 | ||
|
|
7ce8737e75 | ||
|
|
1d91f0fca9 | ||
|
|
23fe7fb0f7 | ||
|
|
1880b5d261 | ||
|
|
cf004322fd | ||
|
|
30d8f28221 | ||
|
|
caa05e779a | ||
|
|
f13fec13b8 | ||
|
|
a93f346948 | ||
|
|
48d44bada1 | ||
|
|
a20d08ddc8 | ||
|
|
4f18e31af5 | ||
|
|
41f6a172ee | ||
|
|
1776d31ba4 | ||
|
|
845ebcac15 | ||
|
|
45f73d4be8 | ||
|
|
ebdd71f342 | ||
|
|
597f8a7bab | ||
|
|
3f1aa9955b | ||
|
|
aad2a1e098 | ||
|
|
07fd7619bc | ||
|
|
96bcd14bb8 | ||
|
|
db9d350cae | ||
|
|
5914498027 | ||
|
|
180109e3aa | ||
|
|
929dac0df0 | ||
|
|
6cd9a53aa5 | ||
|
|
cd585dd657 | ||
|
|
2a150a6e7e | ||
|
|
902b7339d1 | ||
|
|
f84b907dc8 | ||
|
|
72cf5f8b04 | ||
|
|
e1d7852877 | ||
|
|
3f66c20616 | ||
|
|
a5f908d70e | ||
|
|
e9383a2f0c | ||
|
|
0453166326 | ||
|
|
670478e9ca | ||
|
|
1a9bc5550c | ||
|
|
25f7e58b3a | ||
|
|
839f8b062b | ||
|
|
eae1fbff8a | ||
|
|
d628b2de27 | ||
|
|
56e4cb765f | ||
|
|
21179c56d4 | ||
|
|
3da2830cfa | ||
|
|
873d6287c4 | ||
|
|
ea7eed4ad0 | ||
|
|
3b4b5ab298 | ||
|
|
2711c9b78c | ||
|
|
48d60821a7 | ||
|
|
076f4b441f | ||
|
|
e9eca83cd1 | ||
|
|
f6aa20b96d | ||
|
|
c15a384622 | ||
|
|
46c3bedd15 | ||
|
|
bc587f17de | ||
|
|
5905971178 | ||
|
|
afc7a7c956 | ||
|
|
bf970803ec | ||
|
|
3de473662f | ||
|
|
4bad92e3dd | ||
|
|
2089a299f1 | ||
|
|
10b1081960 | ||
|
|
c636a820d5 | ||
|
|
6c4bb59f06 | ||
|
|
97c55c1187 | ||
|
|
2c5db229c6 | ||
|
|
7c389a8010 | ||
|
|
c84ed0a4b4 | ||
|
|
1c638aa661 | ||
|
|
6325a23bb4 | ||
|
|
07abae30ba | ||
|
|
494e2f48d5 | ||
|
|
c7875b3f53 | ||
|
|
c88330f5f2 | ||
|
|
ff4ec19fff | ||
|
|
697f3473f6 | ||
|
|
439cd65050 | ||
|
|
79c7a559ad | ||
|
|
18d5315c2f | ||
|
|
06693aeac7 | ||
|
|
d5e6f9906c | ||
|
|
fac4de21de | ||
|
|
e3d0f0ec8f | ||
|
|
81c86019ab | ||
|
|
95c4a25bd2 | ||
|
|
d2f801c7d6 | ||
|
|
f72c4f28da | ||
|
|
a248fe5c4b | ||
|
|
e9495ccd84 | ||
|
|
cf5e34eae6 | ||
|
|
e52f583e20 | ||
|
|
98967cdf88 | ||
|
|
ceb1bb7f50 | ||
|
|
94c61cb959 | ||
|
|
20cb559714 | ||
|
|
2a49770fc0 | ||
|
|
4a71a9f7b5 | ||
|
|
b4b596ad8b | ||
|
|
a865bcfa1d | ||
|
|
3587cf5154 | ||
|
|
12b1d6e53b | ||
|
|
f4cb87f493 | ||
|
|
804088009e | ||
|
|
9f5faf7cf8 | ||
|
|
711c1a89ee | ||
|
|
30dd0604ca | ||
|
|
0b67eed92f | ||
|
|
9782cbae35 | ||
|
|
6ca4c0b23f | ||
|
|
a672ac66ae | ||
|
|
65c4ca01d0 | ||
|
|
3dad6e96e3 | ||
|
|
7009eb20f8 | ||
|
|
24cbd192aa |
24
.travis.yml
24
.travis.yml
@@ -1,27 +1,11 @@
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
|
||||
before_install:
|
||||
- sudo mv /etc/apt/sources.list.d/pgdg-source.list* /tmp
|
||||
- sudo apt-get -qq purge postgis* postgresql*
|
||||
- sudo apt-add-repository --yes ppa:cartodb/postgresql-9.3
|
||||
- sudo apt-add-repository --yes ppa:cartodb/gis
|
||||
- sudo rm -Rf /var/lib/postgresql /etc/postgresql
|
||||
- sudo apt-add-repository --yes ppa:mapnik/nightly-2.3
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y postgresql-9.3-postgis-2.1
|
||||
- sudo apt-get install -y postgresql-contrib-9.3
|
||||
- sudo apt-get install -q libprotobuf-dev protobuf-compiler
|
||||
- sudo apt-get install -q libmapnik-dev
|
||||
- sudo apt-get install -q mapnik-input-plugin-gdal mapnik-input-plugin-ogr mapnik-input-plugin-postgis
|
||||
- sudo apt-get install -y gdal-bin
|
||||
- echo -e "local\tall\tall\ttrust\nhost\tall\tall\t127.0.0.1/32\ttrust\nhost\tall\tall\t::1/128\ttrust" |sudo tee /etc/postgresql/9.3/main/pg_hba.conf
|
||||
- sudo service postgresql restart
|
||||
- sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev
|
||||
- createdb template_postgis
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
before_script:
|
||||
# Tell npm to use known registrars:
|
||||
# see http://blog.npmjs.org/post/78085451721/npms-self-signed-certificate-is-no-more
|
||||
- npm config set ca ""
|
||||
|
||||
env:
|
||||
- NPROCS=1 JOBS=1 PGUSER=postgres
|
||||
|
||||
|
||||
135
NEWS.md
135
NEWS.md
@@ -1,3 +1,138 @@
|
||||
1.26.0 -- 2015-01-27
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.35.0, supports mapconfig version `1.3.0`
|
||||
|
||||
|
||||
1.25.0 -- 2015-01-26
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- No more signed maps (#227 and #238)
|
||||
- Splits template maps endpoint into its own controller
|
||||
- Removes TemplateMaps dependency on SignedMaps
|
||||
- Token validation is done against the template
|
||||
- Template is always extended with default values for auth and placeholders
|
||||
- MapConfig is extended, in order to validate auth_tokens, with template info:
|
||||
- template name
|
||||
- template auth
|
||||
- No more locks to create, update or delete templates
|
||||
- Trusting in redis' hash semantics
|
||||
- Some tradeoffs:
|
||||
* A client having more templates than allowed by a race condition between limit (HLEN) check and creation (HSET)
|
||||
* Updating a template could happen while deleting it, resulting in a new template
|
||||
* Templates already instantiated will be accessible through their layergroup so it is possible to continue requesting tiles/grids/etc.
|
||||
- Authorisation is now handled by template maps
|
||||
- Template instantiation returns new instances with default values if they are missing
|
||||
|
||||
|
||||
New features:
|
||||
- Basic layergroup validation on named map creation/update (#196)
|
||||
- Add named maps surrogate keys and call invalidation on template modification/deletion (#247)
|
||||
- Extends TemplateMaps backend with EventEmitter
|
||||
- Emits for create, update and delete templates
|
||||
- VarnishHttpCacheBackend will invalidate a varnish instance via HTTP PURGE method
|
||||
- In the future there could be more backends, for instance to invalidate a CDN.
|
||||
- NamedMapsEntry has the responibility to generate a cache key for a named map
|
||||
- It probably should receive a template/named map instead of owner and template name
|
||||
- SurrogateKeysCache is resposible to tag responses with a header
|
||||
- It also is responsible for invalidations given an Invalidation Backend
|
||||
- In the future it could have several backends so it can invalidates different caches
|
||||
- SurrogateKeysCache is subscribed to TemplateMaps events to do the invalidations
|
||||
|
||||
|
||||
1.24.0 -- 2015-01-15
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.34.0 for retina support
|
||||
|
||||
|
||||
1.23.1 -- 2015-01-14
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Regenerate npm-shrinkwrap.json
|
||||
|
||||
|
||||
1.23.0 -- 2015-01-14
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.33.0
|
||||
|
||||
New features:
|
||||
- Sets HTTP renderer configuration in server_options
|
||||
|
||||
|
||||
1.22.0 -- 2015-01-13
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Health check endpoint
|
||||
|
||||
|
||||
1.21.2 -- 2014-12-15
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.32.4
|
||||
|
||||
|
||||
1.21.1 -- 2014-12-11
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.32.2
|
||||
|
||||
Bugfixes:
|
||||
- Closes fd for log files on `kill -HUP` (#230)
|
||||
|
||||
|
||||
|
||||
1.21.0 -- 2014-10-24
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Allow a different cache-control max-age for layergroup responses
|
||||
|
||||
|
||||
1.20.2 -- 2014-10-20
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.31.0
|
||||
|
||||
|
||||
1.20.1 -- 2014-10-17
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades redis-mpool to 0.3.0
|
||||
|
||||
|
||||
1.20.0 -- 2014-10-15
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Report to statsd the status of redis pools
|
||||
- Upgrades Windshaft to start reporting redis/renderers/mapnik pool metrics
|
||||
|
||||
Enhancements:
|
||||
- Share one redis-mpool across the application
|
||||
|
||||
|
||||
1.19.0 -- 2014-10-14
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Dropping support for npm <1.2.1
|
||||
npm-shrinkwrap.json is incompatible when generated with npm >=1.2.1 and consumed by npm <1.2.1
|
||||
- Upgrades windshaft to 0.28.2
|
||||
- Generates npm-shrinkwrap.json with npm >1.2.0
|
||||
|
||||
|
||||
1.18.2 -- 2014-10-13
|
||||
--------------------
|
||||
|
||||
|
||||
28
app.js
28
app.js
@@ -8,7 +8,8 @@
|
||||
*/
|
||||
|
||||
var path = require('path'),
|
||||
fs = require('fs')
|
||||
fs = require('fs'),
|
||||
RedisPool = require('redis-mpool')
|
||||
;
|
||||
|
||||
|
||||
@@ -31,7 +32,7 @@ var _ = require('underscore');
|
||||
global.environment = require(__dirname + '/config/environments/' + ENV);
|
||||
global.environment.api_hostname = require('os').hostname().split('.')[0];
|
||||
|
||||
global.log4js = require('log4js')
|
||||
global.log4js = require('log4js');
|
||||
log4js_config = {
|
||||
appenders: [],
|
||||
replaceConsole:true
|
||||
@@ -69,13 +70,25 @@ if ( global.environment.rollbar ) {
|
||||
log4js.configure(log4js_config, { cwd: __dirname });
|
||||
global.logger = log4js.getLogger();
|
||||
|
||||
var redisOpts = _.extend(global.environment.redis, { name: 'windshaft' }),
|
||||
redisPool = new RedisPool(redisOpts);
|
||||
|
||||
// Include cartodb_windshaft only _after_ the "global" variable is set
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/28
|
||||
var CartodbWindshaft = require('./lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('./lib/cartodb/server_options')();
|
||||
var CartodbWindshaft = require('./lib/cartodb/cartodb_windshaft'),
|
||||
serverOptions = require('./lib/cartodb/server_options')(redisPool);
|
||||
|
||||
ws = CartodbWindshaft(serverOptions);
|
||||
|
||||
if (global.statsClient) {
|
||||
redisPool.on('status', function(status) {
|
||||
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
|
||||
global.statsClient.gauge(keyPrefix + 'count', status.count);
|
||||
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
|
||||
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
|
||||
});
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good number if you have up to 1024 filedescriptors
|
||||
// 4 is good if you have max 32 filedescriptors
|
||||
@@ -103,8 +116,11 @@ process.on('SIGUSR2', function() {
|
||||
});
|
||||
|
||||
process.on('SIGHUP', function() {
|
||||
log4js.configure(log4js_config);
|
||||
console.log('Log files reloaded');
|
||||
global.log4js.clearAndShutdownAppenders(function() {
|
||||
global.log4js.configure(log4js_config);
|
||||
global.logger = log4js.getLogger();
|
||||
console.log('Log files reloaded');
|
||||
});
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function(err) {
|
||||
|
||||
@@ -40,7 +40,7 @@ var config = {
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: 'logs/node-windshaft.log'
|
||||
,log_filename: undefined
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'development_cartodb_user_<%= user_id %>'
|
||||
@@ -63,6 +63,7 @@ var config = {
|
||||
*/
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
@@ -86,7 +87,15 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
bufferSize: 64,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
]
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -103,11 +112,19 @@ var config = {
|
||||
// by 2 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 1, // idle time before dropping connection
|
||||
reapIntervalMillis: 1, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
}
|
||||
,sqlapi: {
|
||||
@@ -132,14 +149,25 @@ var config = {
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
port: 6082,
|
||||
port: 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -65,6 +65,7 @@ var config = {
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
@@ -80,7 +81,15 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
bufferSize: 64,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
]
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -97,11 +106,19 @@ var config = {
|
||||
// by 2 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 30000, // idle time before dropping connection
|
||||
reapIntervalMillis: 1000, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
}
|
||||
,sqlapi: {
|
||||
@@ -126,9 +143,12 @@ var config = {
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
port: 6082,
|
||||
port: 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
@@ -149,6 +169,14 @@ var config = {
|
||||
handler: 'inline'
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -57,6 +57,7 @@ var config = {
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
@@ -80,7 +81,15 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
bufferSize: 64,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
]
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -97,11 +106,19 @@ var config = {
|
||||
// by 2 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 30000, // idle time before dropping connection
|
||||
reapIntervalMillis: 1000, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
}
|
||||
,sqlapi: {
|
||||
@@ -126,9 +143,12 @@ var config = {
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
port: 6082,
|
||||
port: 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
@@ -149,6 +169,14 @@ var config = {
|
||||
handler: 'inline'
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -57,6 +57,7 @@ var config = {
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
@@ -80,7 +81,15 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
bufferSize: 64,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
]
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -97,11 +106,19 @@ var config = {
|
||||
// by 2 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 1, // idle time before dropping connection
|
||||
reapIntervalMillis: 1, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
}
|
||||
,sqlapi: {
|
||||
@@ -128,14 +145,25 @@ var config = {
|
||||
}
|
||||
,varnish: {
|
||||
host: '',
|
||||
port: null,
|
||||
port: null, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Kind of maps
|
||||
|
||||
Windshaft-CartoDB supports these kind of maps:
|
||||
Windshaft-CartoDB supports the following types of maps:
|
||||
|
||||
- [Temporary maps](#temporary-maps) (created by anyone)
|
||||
- [Detached maps](#detached-maps)
|
||||
@@ -12,14 +12,14 @@ Windshaft-CartoDB supports these kind of maps:
|
||||
## Temporary maps
|
||||
|
||||
Temporary maps have no owners and are anonymous in nature.
|
||||
There are two kind of temporary maps:
|
||||
There are two kinds of temporary maps:
|
||||
|
||||
- Detached maps (aka MultiLayer-API)
|
||||
- Inline maps
|
||||
|
||||
### Detached maps
|
||||
|
||||
Detached maps are maps which are configured with a request
|
||||
Detached maps are maps that are configured with a request
|
||||
obtaining a temporary token and then used by referencing
|
||||
the obtained token. The token expires automatically when unused.
|
||||
|
||||
|
||||
331
docs/Map-API.md
331
docs/Map-API.md
@@ -1,6 +1,6 @@
|
||||
## Maps API
|
||||
|
||||
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account and style them using CartoCSS. The API generates a XYZ based URL to fetch Web Mercator projected tiles using web clients like Leaflet, Google Maps, OpenLayers.
|
||||
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account, and you can apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
|
||||
|
||||
You can create two types of maps with the Maps API:
|
||||
|
||||
@@ -8,7 +8,7 @@ You can create two types of maps with the Maps API:
|
||||
Maps that can be created using your CartoDB public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CartoDB.js example]({{ '/cartodb-platform/cartodb-js.html' | prepend: site.baseurl }}).
|
||||
|
||||
- **Named maps**
|
||||
Maps that access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
|
||||
Maps that have access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
|
||||
|
||||
## Quickstart
|
||||
|
||||
@@ -16,7 +16,7 @@ You can create two types of maps with the Maps API:
|
||||
|
||||
Here is an example of how to create an anonymous map with JavaScript:
|
||||
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
var mapconfig = {
|
||||
"version": "1.0.1",
|
||||
"layers": [{
|
||||
@@ -37,19 +37,18 @@ $.ajax({
|
||||
url: 'http://documentation.cartodb.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'http://documentation.cartodb.com/api/v1/map/' + data.layergroupid + '{z}/{x}/{y}.png'
|
||||
var templateUrl = 'http://documentation.cartodb.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
|
||||
console.log(templateUrl);
|
||||
}
|
||||
})
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
### Named maps
|
||||
|
||||
Let's create a named map using some private tables in a CartoDB account.
|
||||
The following API call creates a map of European countries that have a white fill color:
|
||||
The following map config sets up a map of European countries that have a white fill color:
|
||||
|
||||
{% highlight javascript %}
|
||||
// mapconfig.json
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "test",
|
||||
@@ -67,38 +66,42 @@ The following API call creates a map of European countries that have a white fil
|
||||
}]
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
The map config needs to be sent to CartoDB's Map API using an authenticated call. Here we use a command line tool called `curl`. For more info about this tool see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl) or type ``man curl`` in bash. Using `curl` the call would look like:
|
||||
The map config needs to be sent to CartoDB's Map API using an authenticated call. Here we will use a command line tool called `curl`. For more info about this tool, see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl), or type ``man curl`` in bash. Using `curl`, and storing the config from above in a file `mapconfig.json`, the call would look like:
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
curl 'https://{account}.cartodb.com/api/v1/map/named?api_key=APIKEY' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
To get the `URL` to fetch the tiles you need to instantiate the map.
|
||||
To get the `URL` to fetch the tiles you need to instantiate the map, where `template_id` is the template name from the previous response.
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight bash %}
|
||||
curl 'http://{account}.cartodb.com/api/v1/map/named/test' -H 'Content-Type: application/json'
|
||||
{% endhighlight %}
|
||||
```bash
|
||||
curl -X POST 'http://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
The response will return JSON with properties for the `layergroupid` and the timestamp (`last_updated`) of the last data modification.
|
||||
The response will return JSON with properties for the `layergroupid`, `cdn_url`, and the timestamp (`last_updated`) of the last data modification.
|
||||
|
||||
Here is an example response:
|
||||
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"cdn_url": {
|
||||
"http": "ashbu.cartocdn.com",
|
||||
"https": "cartocdn-ashbu.global.ssl.fastly.net"
|
||||
},
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z"
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
You can use the `layergroupid` to instantiate a URL template for accessing tiles on the client. Here we use the `layergroupid` from the example response above in this URL template:
|
||||
|
||||
{% highlight bash %}
|
||||
http://documentation.cartodb.com/tiles/layergroup/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
{% endhighlight %}
|
||||
```bash
|
||||
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
## General Concepts
|
||||
|
||||
@@ -106,7 +109,7 @@ The following concepts are the same for every endpoint in the API except when it
|
||||
|
||||
### Auth
|
||||
|
||||
By default, users do not have access to private tables in CartoDB. In order to instantiate a map from private table data an API Key is required. Additionally, to include some endpoints an API Key must be included (e.g. creating a named map).
|
||||
By default, users do not have access to private tables in CartoDB. In order to instantiate a map from private table data, an API Key is required. Additionally, to include some endpoints, an API Key must be included (e.g. creating a named map).
|
||||
|
||||
To execute an authorized request, api_key=YOURAPIKEY should be added to the request URL. The param can be also passed as POST param. We **strongly advise** using HTTPS when you are performing requests that include your `api_key`.
|
||||
|
||||
@@ -114,13 +117,13 @@ To execute an authorized request, api_key=YOURAPIKEY should be added to the requ
|
||||
|
||||
Errors are reported using standard HTTP codes and extended information encoded in JSON with this format:
|
||||
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"errors": [
|
||||
"access forbidden to table TABLE"
|
||||
]
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
|
||||
|
||||
@@ -137,13 +140,13 @@ Anonymous maps allows you to instantiate a map given SQL and CartoCSS. It also a
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight html %}
|
||||
```html
|
||||
POST /api/v1/map
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"version": "1.0.1",
|
||||
"layers": [{
|
||||
@@ -156,7 +159,7 @@ POST /api/v1/map
|
||||
}
|
||||
}]
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.19.1/doc/MapConfig-1.1.0.md).
|
||||
|
||||
@@ -167,9 +170,9 @@ The response includes:
|
||||
- **layergroupid**
|
||||
The ID for that map, used to compose the URL for the tiles. The final URL is:
|
||||
|
||||
{% highlight html %}
|
||||
```html
|
||||
http://{account}.cartodb.com/api/v1/map/:layergroupid/{z}/{x}/{y}.png
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
- **updated_at**
|
||||
The ISO date of the last time the data involved in the query was updated.
|
||||
@@ -183,12 +186,12 @@ The response includes:
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
curl 'http://documentation.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
@@ -197,33 +200,33 @@ curl 'http://documentation.cartodb.com/api/v1/map' -H 'Content-Type: application
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
The tiles can be accessed using:
|
||||
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
For UTF grid tiles:
|
||||
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
For attributes defined in `attributes` section:
|
||||
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/attributes/:feature_id
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{ c: 1, d: 2 }
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
Notice UTF Grid and attributes endpoints need an intenger parameter, ``layer``. That number is the 0-based index of the layer inside the mapconfig. So in this case 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example mapconfig. If a second layer was available it could be returned with 1, a third layer with 2, etc.
|
||||
Notice UTF Grid and attributes endpoints need an integer parameter, ``layer``. That number is the 0-based index of the layer inside the mapconfig. So in this case 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example mapconfig. If a second layer was available it could be returned with 1, a third layer with 2, etc.
|
||||
|
||||
### Create JSONP
|
||||
|
||||
@@ -232,16 +235,13 @@ The JSONP endpoint is provided in order to allow web browsers access which don't
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
GET /api/v1/map?callback=method
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **auth_token** *(optional)*
|
||||
If the named map needs authorization.
|
||||
|
||||
- **config**
|
||||
- **config**
|
||||
Encoded JSON with the params for creating named maps (the variables defined in the template).
|
||||
|
||||
- **lmza**
|
||||
@@ -253,23 +253,30 @@ GET /api/v1/map?callback=method
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
{% highlight bash %}
|
||||
curl http://...
|
||||
{% endhighlight %}
|
||||
```bash
|
||||
curl "https://documentation.cartodb.com/api/v1/map?callback=callback&config=%7B%22version%22%3A%221.0.1%22%2C%22layers%22%3A%5B%7B%22type%22%3A%22cartodb%22%2C%22options%22%3A%7B%22sql%22%3A%22select+%2A+from+european_countries_e%22%2C%22cartocss%22%3A%22%23european_countries_e%7B+polygon-fill%3A+%23FF6600%3B+%7D%22%2C%22cartocss_version%22%3A%222.3.0%22%2C%22interactivity%22%3A%5B%22cartodb_id%22%5D%7D%7D%5D%7D"
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
{% highlight javascript %}
|
||||
{
|
||||
}
|
||||
{% endhighlight %}
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "d9034c133262dfb90285cea26c5c7ad7:0",
|
||||
cdn_url: {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
},
|
||||
last_updated: "1970-01-01T00:00:00.000Z"
|
||||
})
|
||||
```
|
||||
|
||||
### Remove
|
||||
|
||||
Anonymous maps cannot be removed by an API call. They will expire after about five minutes but sometimes longer. If an anonymous map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time returns to the map an attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
|
||||
|
||||
## Named Maps
|
||||
|
||||
Named maps are essentially the same as anonymous maps but the mapconfig is stored in the server and given a unique name. Two other big differences are that you can created named maps from private data and that users without an API Key can see them even though they are from that private data.
|
||||
Named maps are essentially the same as anonymous maps except the mapconfig is stored on the server and the map is given a unique name. Two other big differences are that you can create named maps from private data, and that users without an API Key can see them even though they are from that private data.
|
||||
|
||||
The main two differences compared to anonymous maps are:
|
||||
|
||||
@@ -277,23 +284,25 @@ The main two differences compared to anonymous maps are:
|
||||
This allows you to control who is able to see the map based on a token auth
|
||||
|
||||
- **templates**
|
||||
Since the mapconfig is static it can contain some variables so the client con modify the map appearance using those variables.
|
||||
Since the mapconfig is static it can contain some variables so the client can modify the map's appearance using those variables
|
||||
|
||||
Template maps are persistent with no preset expiration. They can only be created or deleted by a CartoDB user with a valid API_KEY (see auth section).
|
||||
Template maps are persistent with no preset expiration. They can only be created or deleted by a CartoDB user with a valid API_KEY (see [auth section](http://docs.cartodb.com/cartodb-platform/maps-api.html#auth)).
|
||||
|
||||
### Create
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight html %}
|
||||
```html
|
||||
POST /api/v1/map/named
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
<div class="code-title">template.json</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "template_name",
|
||||
@@ -328,27 +337,28 @@ POST /api/v1/map/named
|
||||
]
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
##### Arguments
|
||||
|
||||
- **name**: there can be at most 1 template with the same name for any user valid names start with a letter and only contains letter, numbers or underscores
|
||||
- **name**: There can be at most _one_ template with the same name for any user. Valid names start with a letter, and only contain letters, numbers, or underscores (_).
|
||||
- **auth**:
|
||||
- **method** `"token"` or `"open"` (the default if no `"method"` is given)
|
||||
- **method** `"token"` or `"open"` (the default if no `"method"` is given).
|
||||
- **valid_tokens** when `"method"` is set to `"token"`, the values listed here allow you to instantiate the named map.
|
||||
- **placeholders**: Variables not listed here are not substituted. Variable not provided at instantiation time trigger an error. A default is required for optional variables. Type specification is used for quoting, to avoid injections see template format section below.
|
||||
- **layergroup**: the layer list definition. This is the MapConfig explained in anonymous maps see https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.1.0.md
|
||||
- **layergroup**: the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.1.0.md) for more info.
|
||||
|
||||
#### Template Format
|
||||
|
||||
A templated `layergroup` allows using placeholders in the "cartocss" and "sql" elements of the "option" object in any "layer" of a layergroup configuration
|
||||
A templated `layergroup` allows the use of placeholders in the "cartocss" and "sql" elements of the "option" object in any "layer" of a `layergroup` configuration
|
||||
|
||||
Valid placeholder names start with a letter and can only contain letters, numbers or underscores. They have to be written between `<%=` and `%>` strings in order to be replaced.
|
||||
Valid placeholder names start with a letter and can only contain letters, numbers, or underscores. They have to be written between the `<%=` and `%>` strings in order to be replaced.
|
||||
|
||||
##### Example
|
||||
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
<%= my_color %>
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
The set of supported placeholders for a template will need to be explicitly defined with a specific type and default value for each.
|
||||
|
||||
@@ -366,19 +376,19 @@ Placeholder default values will be used whenever new values are not provided as
|
||||
When using templates, be very careful about your selections as they can give broad access to your data if they are defined losely.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
{% highlight html %}
|
||||
```html
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://docs.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
{% endhighlight %}
|
||||
'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"templateid":"name",
|
||||
"template_id":"name",
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
### Instantiate
|
||||
|
||||
@@ -387,21 +397,23 @@ Instantiating a map allows you to get the information needed to fetch tiles. Tha
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight html %}
|
||||
```html
|
||||
POST /api/v1/map/named/:template_name
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
#### Param
|
||||
|
||||
{% highlight javascript %}
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
|
||||
```javascript
|
||||
// params.json
|
||||
{
|
||||
color: "#ff0000",
|
||||
cartodb_id: 3
|
||||
"color": "#ff0000",
|
||||
"cartodb_id": 3
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
The fields you pass as `params.json` depend on the variables allowed by the named map. If there are variables missing it will raise an error (HTTP 400)
|
||||
The fields you pass as `params.json` depend on the variables allowed by the named map. If there are variables missing it will raise an error (HTTP 400).
|
||||
|
||||
- **auth_token** *optional* if the named map needs auth
|
||||
|
||||
@@ -412,27 +424,27 @@ You can initialize a template map by passing all of the required parameters in a
|
||||
Valid credentials will be needed if required by the template.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.json \
|
||||
'https://docs.cartodb.com/api/v1/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
{% endhighlight %}
|
||||
'https://documentation.cartodb.com/api/v1/map/named/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
<div class="code-title">Response</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated": "2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
<div class="code-title">Error</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see anonymous map section). However, you'll need to show the `auth_token`, if required by the template.
|
||||
|
||||
@@ -443,62 +455,62 @@ There is also a special endpoint to be able to initialize a map using JSONP (for
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name/jsonp
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **auth_token** *(optional)* If the named map needs auth
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
- **config** Encoded JSON with the params for creating named maps (the variables defined in the template)
|
||||
- **lmza** This attribute contains the same as config but LZMA compressed. It cannot be used at the same time than `config`.
|
||||
- **callback:** JSON callback name
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
{% highlight bash %}
|
||||
curl 'https://docs.cartodb.com/api/v1/map/named/:template_name/jsonp?auth_token=AUTH_TOKEN&callback=function_name&config=template_params_json'
|
||||
{% endhighlight %}
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map/named/:template_name/jsonp?auth_token=AUTH_TOKEN&callback=callback&config=template_params_json'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
{% highlight javascript %}
|
||||
callback(
|
||||
```javascript
|
||||
callback({
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
)
|
||||
{% endhighlight %}
|
||||
})
|
||||
```
|
||||
|
||||
This takes the `callback` function (required), `auth_token` if the template needs auth, and `config` which is the variable for the template (in cases where it has variables).
|
||||
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
The response is in this format:
|
||||
|
||||
{% highlight javascript %}
|
||||
jQuery17205720721024554223_1390996319118({
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
})
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight bash %}
|
||||
PUT /api/v1/map/:map_name
|
||||
{% endhighlight %}
|
||||
```bash
|
||||
PUT /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Same params used to create a map.
|
||||
- **api_key** is required
|
||||
|
||||
#### Response
|
||||
|
||||
@@ -511,31 +523,29 @@ Updating a named map removes all the named map instances so they need to be init
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://docs.cartodb.com/tiles/template/:template_name?api_key=APIKEY'
|
||||
{% endhighlight %}
|
||||
'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"template_id": "@template_name"
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
If any template has the same name, it will be updated.
|
||||
|
||||
If a template with the same name does NOT exist, a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"error": "error string here"
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
Updating a template map will also remove all signatures from previously initialized maps.
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
@@ -544,25 +554,29 @@ Delete the specified template map from the server and disables any previously in
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight bash %}
|
||||
DELETE /template/:template_name
|
||||
{% endhighlight %}
|
||||
```bash
|
||||
DELETE /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request">REQUEST</div>
|
||||
{% highlight bash %}
|
||||
curl -X DELETE 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
{% endhighlight %}
|
||||
```bash
|
||||
curl -X DELETE 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response would be issued. Otherwise a 4xx response with with an error will be returned:
|
||||
On success, a 204 (No Content) response would be issued. Otherwise a 4xx response with with an error will be returned.
|
||||
|
||||
### Listing Available Templates
|
||||
|
||||
@@ -571,9 +585,9 @@ This allows you to get a list of all available templates.
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
GET /api/v1/map/named/
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
@@ -582,34 +596,34 @@ GET /api/v1/map/named/
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
{% highlight bash %}
|
||||
curl -X GET 'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
|
||||
{% endhighlight %}
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
### Getting a Specific Template
|
||||
|
||||
This gets the definition of a template
|
||||
This gets the definition of a template.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
{% highlight bash %}
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
@@ -618,20 +632,43 @@ GET /api/v1/map/named/:template_name
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
{% highlight bash %}
|
||||
curl -X GET 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
{% endhighlight %}
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"template": {...} // see template.json above
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
{% highlight javascript %}
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
{% endhighlight %}
|
||||
```
|
||||
|
||||
### Use with CartoDB.js
|
||||
Named maps can be used with CartoDB.js by specifying a named map in a layer source as follows. Named maps are treated almost the same as other layer source types in most other ways.
|
||||
|
||||
```js
|
||||
var layerSource = {
|
||||
user_name: '{your_user_name}',
|
||||
type: 'namedmap',
|
||||
named_map: {
|
||||
name: '{template_name}',
|
||||
layers: [{
|
||||
layer_name: "layer1",
|
||||
interactivity: "column1, column2, ..."
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
cartodb.createLayer('map_dom_id',layerSource)
|
||||
.addTo(map_object);
|
||||
|
||||
```
|
||||
|
||||
See [CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js.html) methods [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetparamskey-value) and [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetauthtokenauthtoken).
|
||||
|
||||
@@ -25,4 +25,4 @@ Windshaft-CartoDB adds the following attributes in the response object
|
||||
|
||||
## Stats tag
|
||||
|
||||
Windshaft-CartoDB adds support for a ``stat_tag`` element in the multilayer configuration to help [stats](Redis-stats-format) gathering.
|
||||
Windshaft-CartoDB adds support for a ``stat_tag`` element in the multilayer configuration to help [stats](https://github.com/CartoDB/Windshaft-cartodb/wiki/Redis-stats-format) gathering.
|
||||
|
||||
@@ -5,13 +5,6 @@ layergroup configurations (instantiation).
|
||||
Template maps are persistent, can only be created and deleted by the
|
||||
CartoDB user showing a valid API_KEY.
|
||||
|
||||
Instantiating a signed template map would result in a [signed
|
||||
map](https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps)
|
||||
instance that would be signed with the same signature as the template.
|
||||
|
||||
Deleting a signed template results in deletion of all signatures created
|
||||
as a result of instantiation.
|
||||
|
||||
|
||||
# Template format
|
||||
|
||||
@@ -43,9 +36,6 @@ Placeholder default value will be used when not provided at
|
||||
instantiation time and could be used to test validity of the
|
||||
template by creating a default instance.
|
||||
|
||||
Additionally you'll be able to embed an authorization
|
||||
certificate that would be used to sign any instance of the template.
|
||||
|
||||
```js
|
||||
// template.json
|
||||
{
|
||||
@@ -56,7 +46,6 @@ certificate that would be used to sign any instance of the template.
|
||||
name: "template_name",
|
||||
// embedded authorization certificate
|
||||
auth: {
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps
|
||||
method: "token", // or "open" (the default if no "method" is given)
|
||||
// only (required and non empty) for "token" method
|
||||
valid_tokens: ["auth_token1","auth_token2"]
|
||||
@@ -92,7 +81,8 @@ certificate that would be used to sign any instance of the template.
|
||||
|
||||
# Creating a templated map
|
||||
|
||||
You can create a signed template map with a single call (for simplicity).
|
||||
You can create a template map with a single call (for simplicity).
|
||||
|
||||
You'd use a POST sending JSON data:
|
||||
|
||||
```sh
|
||||
@@ -121,10 +111,7 @@ Errors are in this form:
|
||||
|
||||
# Updating an existing template
|
||||
|
||||
Update of a template map implies removal all signatures from previous
|
||||
map instances.
|
||||
|
||||
You can update a signed template map with a PUT:
|
||||
You can update a template map with a PUT:
|
||||
|
||||
```sh
|
||||
curl -X PUT \
|
||||
@@ -243,13 +230,7 @@ or, on error:
|
||||
|
||||
You can then use the ``layergroupid`` for fetching tiles and grids as you do
|
||||
normally ( see https://github.com/CartoDB/Windshaft/wiki/Multilayer-API).
|
||||
But you'll still have to show the ``auth_token``, if required by the template
|
||||
(see https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps)
|
||||
|
||||
Instances of a signed template map will be signed with the same signature
|
||||
certificate associated with the template. Such certificate would contain
|
||||
a reference to the template identifier, so that it can be revoked every
|
||||
time the template is updated or deleted.
|
||||
But you'll still have to show the ``auth_token``, if required by the template.
|
||||
|
||||
### using JSONP
|
||||
There is also a special endpoint to be able to instanciate using JSONP (for old browsers)
|
||||
@@ -275,8 +256,6 @@ last_updated: "2014-01-27T17:41:03.021Z"
|
||||
```
|
||||
# Deleting a template map
|
||||
|
||||
Deletion of a template map will imply removal all instance signatures
|
||||
|
||||
You can delete a templated map with a DELETE to ``/template/:template_name``:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -26,12 +26,11 @@ Again, each inner timer may have several inner timers.
|
||||
- **affectedTables**: time to check what are the affected tables for adding the cache channel, see *addCacheChannel*
|
||||
- **authorize**: time to authorize a request, see *authorizedByAPIKey*, *authorizedByCert*, *authorizedBySigner*
|
||||
- **authorizedByAPIKey**: time to authorize using an API KEY
|
||||
- **authorizedByCert**: time to authorize a request by a cert, see [signed map](https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps)
|
||||
- **authorizedBySigner**: time to authorize a request for a [signed map](https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps)
|
||||
- **authorizedByCert**: time to authorize a template instantiation
|
||||
- **authorizedBySigner**: time to authorize a request with auth_token
|
||||
- **findLastUpdated**: time to retrieve the last update time for a list of tables, see *affectedTables*
|
||||
- **fingerPrint**: time to create a fingerprint for a signed map
|
||||
- **generateCacheChannel**: time to generate the headers for the cache channel based on the request, see *addCacheChannel*
|
||||
- **getSignerMapKey**: time to retrieve from redis the authorized key for a signed map
|
||||
- **getSignerMapKey**: time to retrieve from redis the authorized user for a template map
|
||||
- **getTablePrivacy**: time to retrieve from redis the privacy of a table
|
||||
- **getTemplate**: time to retrieve from redis the template for a map
|
||||
- **getUserMapKey**: time to retrieve from redis the user key for a map
|
||||
@@ -41,6 +40,5 @@ Again, each inner timer may have several inner timers.
|
||||
- **setDBAuth**: time to retrieve from redis and set db user and db password from a user
|
||||
- **setDBConn**: time to retrieve from redis and set db host and db name from a user
|
||||
- **setDBParams**: time to prepare all db params to be able to connect/query a database, see *setDBAuth* and *setDBConn*
|
||||
- **signMap**: time to sign in redis layergroup for a map, see signed maps
|
||||
- **tablePrivacy_getUserDBName**: time to retrieve from redis the database for a user
|
||||
|
||||
|
||||
32
lib/cartodb/cache/backend/varnish_http.js
vendored
Normal file
32
lib/cartodb/cache/backend/varnish_http.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
var request = require('request');
|
||||
|
||||
function VarnishHttpCacheBackend(host, port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
module.exports = VarnishHttpCacheBackend;
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
*/
|
||||
VarnishHttpCacheBackend.prototype.invalidate = function(cacheObject, callback) {
|
||||
request(
|
||||
{
|
||||
method: 'PURGE',
|
||||
url: 'http://' + this.host + ':' + this.port + '/key',
|
||||
headers: {
|
||||
'Invalidation-Match': '\\b' + cacheObject.key() + '\\b'
|
||||
}
|
||||
},
|
||||
function(err, response) {
|
||||
if (err || response.statusCode !== 204) {
|
||||
return callback(new Error('Unable to invalidate Varnish object'));
|
||||
}
|
||||
return callback(null);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = VarnishHttpCacheBackend;
|
||||
18
lib/cartodb/cache/model/named_maps_entry.js
vendored
Normal file
18
lib/cartodb/cache/model/named_maps_entry.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
var crypto = require('crypto');
|
||||
|
||||
function NamedMaps(owner, name) {
|
||||
this.namespace = 'n';
|
||||
this.owner = owner;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
module.exports = NamedMaps;
|
||||
|
||||
|
||||
NamedMaps.prototype.key = function() {
|
||||
return this.namespace + ':' + shortHashKey(this.owner + ':' + this.name);
|
||||
};
|
||||
|
||||
function shortHashKey(target) {
|
||||
return crypto.createHash('sha256').update(target).digest('base64').substring(0,6);
|
||||
}
|
||||
26
lib/cartodb/cache/surrogate_keys_cache.js
vendored
Normal file
26
lib/cartodb/cache/surrogate_keys_cache.js
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @param cacheBackend should respond to `invalidate(cacheObject, callback)` method
|
||||
* @constructor
|
||||
*/
|
||||
function SurrogateKeysCache(cacheBackend) {
|
||||
this.cacheBackend = cacheBackend;
|
||||
}
|
||||
|
||||
module.exports = SurrogateKeysCache;
|
||||
|
||||
|
||||
/**
|
||||
* @param response should respond to `header(key, value)` method
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
*/
|
||||
SurrogateKeysCache.prototype.tag = function(response, cacheObject) {
|
||||
response.header('Surrogate-Key', cacheObject.key());
|
||||
};
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
*/
|
||||
SurrogateKeysCache.prototype.invalidate = function(cacheObject, callback) {
|
||||
this.cacheBackend.invalidate(cacheObject, callback);
|
||||
};
|
||||
@@ -1,22 +1,16 @@
|
||||
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, Windshaft = require('windshaft')
|
||||
, redisPool = require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:cartodb'}))
|
||||
// TODO: instanciate cartoData with redisPool
|
||||
, cartoData = require('cartodb-redis')(global.environment.redis)
|
||||
, SignedMaps = require('./signed_maps.js')
|
||||
, TemplateMaps = require('./template_maps.js')
|
||||
, Cache = require('./cache_validator')
|
||||
, os = require('os')
|
||||
, HealthCheck = require('./monitoring/health_check')
|
||||
;
|
||||
|
||||
if ( ! process.env['PGAPPNAME'] )
|
||||
process.env['PGAPPNAME']='cartodb_tiler';
|
||||
|
||||
var CartodbWindshaft = function(serverOptions) {
|
||||
var debug = global.environment.debug;
|
||||
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
if ( global.environment.statsd ) {
|
||||
@@ -26,6 +20,11 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
var redisPool = serverOptions.redis.pool
|
||||
|| require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:cartodb'}));
|
||||
|
||||
var cartoData = require('cartodb-redis')({pool: redisPool});
|
||||
|
||||
if(serverOptions.cache_enabled) {
|
||||
console.log("cache invalidation enabled, varnish on ", serverOptions.varnish_host, ' ', serverOptions.varnish_port);
|
||||
Cache.init(serverOptions.varnish_host, serverOptions.varnish_port, serverOptions.varnish_secret);
|
||||
@@ -41,7 +40,7 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
err = new Error("map state cannot be changed by unauthenticated request!");
|
||||
}
|
||||
callback(err, req);
|
||||
}
|
||||
};
|
||||
|
||||
// This is for Templated maps
|
||||
//
|
||||
@@ -49,11 +48,31 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
//
|
||||
var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)';
|
||||
|
||||
serverOptions.signedMaps = new SignedMaps(redisPool);
|
||||
var templateMapsOpts = {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
};
|
||||
var templateMaps = new TemplateMaps(redisPool, templateMapsOpts);
|
||||
serverOptions.templateMaps = templateMaps;
|
||||
|
||||
var SurrogateKeysCache = require('./cache/surrogate_keys_cache'),
|
||||
NamedMapsCacheEntry = require('./cache/model/named_maps_entry'),
|
||||
VarnishHttpCacheBackend = require('./cache/backend/varnish_http'),
|
||||
varnishHttpCacheBackend = new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port),
|
||||
surrogateKeysCache = new SurrogateKeysCache(varnishHttpCacheBackend);
|
||||
|
||||
if (serverOptions.varnish_purge_enabled) {
|
||||
function invalidateNamedMap(owner, templateName) {
|
||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
|
||||
if (err) {
|
||||
console.warn('Cache: surrogate key invalidation failed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, invalidateNamedMap);
|
||||
});
|
||||
}
|
||||
var templateMaps = new TemplateMaps(redisPool, serverOptions.signedMaps, templateMapsOpts);
|
||||
|
||||
// boot
|
||||
var ws = new Windshaft.Server(serverOptions);
|
||||
@@ -64,7 +83,7 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
var version = wsversion();
|
||||
version.windshaft_cartodb = require('../../package.json').version;
|
||||
return version;
|
||||
}
|
||||
};
|
||||
|
||||
var ws_sendResponse = ws.sendResponse;
|
||||
// GET routes for which we don't want to request any caching.
|
||||
@@ -133,6 +152,20 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
ws_sendError.apply(this, arguments);
|
||||
};
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
var TemplateMapsController = require('./controllers/template_maps'),
|
||||
templateMapsController = new TemplateMapsController(
|
||||
ws, serverOptions, templateMaps, cartoData, template_baseurl, surrogateKeysCache, NamedMapsCacheEntry
|
||||
);
|
||||
templateMapsController.register(ws);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
/**
|
||||
* Helper to allow access to the layer to be used in the maps infowindow popup.
|
||||
*/
|
||||
@@ -198,473 +231,32 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
);
|
||||
});
|
||||
|
||||
// ---- Template maps interface starts @{
|
||||
var healthCheck = new HealthCheck(cartoData, Windshaft.tilelive);
|
||||
ws.get('/health', function(req, res) {
|
||||
var healthConfig = global.environment.health || {};
|
||||
|
||||
ws.userByReq = function(req) {
|
||||
return serverOptions.userByReq(req);
|
||||
}
|
||||
|
||||
// Add a template
|
||||
ws.post(template_baseurl, function(req, res) {
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can create templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
var next = this;
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
|
||||
throw new Error('template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
if ( err ) throw err;
|
||||
// NOTE: might omit "cdbuser" if == dbowner ...
|
||||
return { template_id: cdbuser + '@' + tpl_id };
|
||||
},
|
||||
function finish(err, response){
|
||||
if ( req.profiler ) {
|
||||
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
if (err){
|
||||
response = { error: ''+err };
|
||||
var statusCode = 400;
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
if (!!healthConfig.enabled) {
|
||||
var startTime = Date.now();
|
||||
healthCheck.check(healthConfig, function(err, result) {
|
||||
var ok = !err;
|
||||
var response = {
|
||||
enabled: true,
|
||||
ok: ok,
|
||||
elapsed: Date.now() - startTime,
|
||||
result: result
|
||||
};
|
||||
if (err) {
|
||||
response.err = err.message;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'POST TEMPLATE', err);
|
||||
} else {
|
||||
ws.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
res.send(response, ok ? 200 : 503);
|
||||
|
||||
// Update a template
|
||||
ws.put(template_baseurl + '/:template_id', function(req, res) {
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can list templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
|
||||
throw new Error('template PUT data must be of type application/json');
|
||||
template = req.body;
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
err = new Error("Invalid template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
if ( err ) throw err;
|
||||
return { template_id: cdbuser + '@' + tpl_id };
|
||||
},
|
||||
function finish(err, response){
|
||||
if ( req.profiler ) {
|
||||
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'PUT TEMPLATE', err);
|
||||
} else {
|
||||
ws.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Get a specific template
|
||||
ws.get(template_baseurl + '/:template_id', function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated users can get template maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
var err = new Error("Cannot get template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val){
|
||||
if ( err ) throw err;
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
function finish(err, response){
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'GET TEMPLATE', err);
|
||||
} else {
|
||||
ws.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Delete a specific template
|
||||
ws.del(template_baseurl + '/:template_id', function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated users can delete template maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
var err = new Error("Cannot find template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val){
|
||||
if ( err ) throw err;
|
||||
return { status: 'ok' };
|
||||
},
|
||||
function finish(err, response){
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'DELETE TEMPLATE', err);
|
||||
} else {
|
||||
ws.sendResponse(res, ['', 204]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Get a list of owned templates
|
||||
ws.get(template_baseurl, function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var cdbuser = ws.userByReq(req);
|
||||
Step(
|
||||
function checkPerms(){
|
||||
serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can list templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
if ( err ) throw err;
|
||||
// NOTE: might omit "cbduser" if == dbowner ...
|
||||
var ids = _.map(tpl_ids, function(id) { return cdbuser + '@' + id; })
|
||||
return { template_ids: ids };
|
||||
},
|
||||
function finish(err, response){
|
||||
var statusCode = 200;
|
||||
if (err){
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'GET TEMPLATE LIST', err);
|
||||
} else {
|
||||
ws.sendResponse(res, [response, statusCode]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
ws.setDBParams = function(cdbuser, params, callback) {
|
||||
Step(
|
||||
function setAuth() {
|
||||
serverOptions.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if ( err ) throw err;
|
||||
serverOptions.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
ws.options(template_baseurl + '/:template_id', function(req, res) {
|
||||
ws.doCORS(res, "Content-Type");
|
||||
return next();
|
||||
});
|
||||
|
||||
// Instantiate a template
|
||||
function instanciateTemplate(req, res, template_params, callback) {
|
||||
ws.doCORS(res);
|
||||
var that = this;
|
||||
var response = {};
|
||||
var template;
|
||||
var signedMaps = serverOptions.signedMaps;
|
||||
var layergroup;
|
||||
var layergroupid;
|
||||
var fakereq; // used for call to createLayergroup
|
||||
var cdbuser = ws.userByReq(req);
|
||||
// Format of template_id: [<template_owner>]@<template_id>
|
||||
var tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] && tpl_id[0] != cdbuser ) {
|
||||
var err = new Error('Cannot instanciate map of user "'
|
||||
+ tpl_id[0] + '" on database of user "'
|
||||
+ cdbuser + '"')
|
||||
err.http_status = 403;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
var auth_token = req.query.auth_token;
|
||||
Step(
|
||||
function getTemplate(){
|
||||
templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function checkAuthorized(err, data) {
|
||||
if ( req.profiler ) req.profiler.done('getTemplate');
|
||||
if ( err ) throw err;
|
||||
if ( ! data ) {
|
||||
err = new Error("Template '" + tpl_id + "' of user '" + cdbuser + "' not found");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
template = data;
|
||||
var cert = templateMaps.getTemplateCertificate(template);
|
||||
var authorized = false;
|
||||
try {
|
||||
// authorizedByCert will throw if unauthorized
|
||||
authorized = signedMaps.authorizedByCert(cert, auth_token);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
err = new Error('Unauthorized template instanciation');
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
/*if ( (! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json') && req.query.callback === undefined) {
|
||||
throw new Error('template POST data must be of type application/json, it is instead ');
|
||||
}*/
|
||||
//var template_params = req.body;
|
||||
if ( req.profiler ) req.profiler.done('authorizedByCert');
|
||||
return templateMaps.instance(template, template_params);
|
||||
},
|
||||
function prepareParams(err, instance){
|
||||
if ( req.profiler ) req.profiler.done('TemplateMaps_instance');
|
||||
if ( err ) throw err;
|
||||
layergroup = instance;
|
||||
fakereq = { query: {}, params: {}, headers: _.clone(req.headers),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
ws.setDBParams(cdbuser, fakereq.params, this);
|
||||
},
|
||||
function setApiKey(err){
|
||||
if ( req.profiler ) req.profiler.done('setDBParams');
|
||||
if ( err ) throw err;
|
||||
cartoData.getUserMapKey(cdbuser, this);
|
||||
},
|
||||
function createLayergroup(err, val) {
|
||||
if ( req.profiler ) req.profiler.done('getUserMapKey');
|
||||
if ( err ) throw err;
|
||||
fakereq.params.api_key = val;
|
||||
ws.createLayergroup(layergroup, fakereq, this);
|
||||
},
|
||||
function signLayergroup(err, resp) {
|
||||
// NOTE: createLayergroup uses profiler.start()/end() internally
|
||||
//if ( req.profiler ) req.profiler.done('createLayergroup');
|
||||
if ( err ) throw err;
|
||||
response = resp;
|
||||
var signer = cdbuser;
|
||||
var map_id = response.layergroupid.split(':')[0]; // dropping last_updated
|
||||
var crt_id = template.auth_id; // check ?
|
||||
if ( ! crt_id ) {
|
||||
var errmsg = "Template '" + tpl_id + "' of user '" + cdbuser + "' has no signature";
|
||||
// Is this really illegal ?
|
||||
// Maybe we could just return an unsigned layergroupid
|
||||
// in this case...
|
||||
err = new Error(errmsg);
|
||||
err.http_status = 403; // Forbidden, we refuse to respond to this
|
||||
throw err;
|
||||
}
|
||||
signedMaps.signMap(signer, map_id, crt_id, this);
|
||||
},
|
||||
function prepareResponse(err) {
|
||||
if ( req.profiler ) req.profiler.done('signMap');
|
||||
if ( err ) throw err;
|
||||
//console.log("Response from createLayergroup: "); console.dir(response);
|
||||
// Add the signature part to the token!
|
||||
var tplhash = templateMaps.fingerPrint(template).substring(0,8);
|
||||
if ( req.profiler ) req.profiler.done('fingerPrint');
|
||||
response.layergroupid = cdbuser + '@' + tplhash + '@' + response.layergroupid;
|
||||
return response;
|
||||
},
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
function finish_instanciation(err, response, res, req) {
|
||||
if ( req.profiler ) {
|
||||
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
if (err) {
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
if(debug) {
|
||||
response.stack = err.stack;
|
||||
}
|
||||
ws.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
|
||||
});
|
||||
} else {
|
||||
ws.sendResponse(res, [response, 200]);
|
||||
res.send({enabled: false, ok: true}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
ws.post(template_baseurl + '/:template_id', function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
Step(
|
||||
function() {
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json') {
|
||||
throw new Error('template POST data must be of type application/json, it is instead ');
|
||||
}
|
||||
instanciateTemplate(req, res, req.body, this);
|
||||
}, function(err, response) {
|
||||
finish_instanciation(err, response, res, req);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* jsonp endpoint, allows to instanciate a template with a json call.
|
||||
* callback query argument is mandartoy
|
||||
*/
|
||||
ws.get(template_baseurl + '/:template_id/jsonp', function(req, res) {
|
||||
if ( req.profiler && req.profiler.statsd_client) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
}
|
||||
Step(
|
||||
function() {
|
||||
if ( req.query.callback === undefined || req.query.callback.length === 0) {
|
||||
throw new Error('callback parameter should be present and be a function name');
|
||||
}
|
||||
var config = {};
|
||||
if(req.query.config) {
|
||||
try {
|
||||
config = JSON.parse(req.query.config);
|
||||
} catch(e) {
|
||||
throw new Error('badformed config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
instanciateTemplate(req, res, config, this);
|
||||
}, function(err, response) {
|
||||
finish_instanciation(err, response, res, req);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// ---- Template maps interface ends @}
|
||||
|
||||
return ws;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CartodbWindshaft;
|
||||
|
||||
478
lib/cartodb/controllers/template_maps.js
Normal file
478
lib/cartodb/controllers/template_maps.js
Normal file
@@ -0,0 +1,478 @@
|
||||
var Step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
function TemplateMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache,
|
||||
NamedMapsCacheEntry) {
|
||||
this.app = app;
|
||||
this.serverOptions = serverOptions;
|
||||
this.templateMaps = templateMaps;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.templateBaseUrl = templateBaseUrl;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.NamedMapsCacheEntry = NamedMapsCacheEntry;
|
||||
}
|
||||
|
||||
module.exports = TemplateMapsController;
|
||||
|
||||
|
||||
TemplateMapsController.prototype.register = function(app) {
|
||||
app.get(this.templateBaseUrl + '/:template_id/jsonp', this.jsonp.bind(this));
|
||||
app.post(this.templateBaseUrl, this.create.bind(this));
|
||||
app.put(this.templateBaseUrl + '/:template_id', this.update.bind(this));
|
||||
app.get(this.templateBaseUrl + '/:template_id', this.retrieve.bind(this));
|
||||
app.del(this.templateBaseUrl + '/:template_id', this.destroy.bind(this));
|
||||
app.get(this.templateBaseUrl, this.list.bind(this));
|
||||
app.options(this.templateBaseUrl + '/:template_id', this.options.bind(this));
|
||||
app.post(this.templateBaseUrl + '/:template_id', this.instantiate.bind(this));
|
||||
};
|
||||
|
||||
// Add a template
|
||||
TemplateMapsController.prototype.create = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = self.serverOptions.userByReq(req);
|
||||
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can create templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
|
||||
throw new Error('template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
if ( err ) throw err;
|
||||
// NOTE: might omit "cdbuser" if == dbowner ...
|
||||
return { template_id: cdbuser + '@' + tpl_id };
|
||||
},
|
||||
function finish(err, response){
|
||||
if ( req.profiler ) {
|
||||
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
if (err){
|
||||
response = { error: ''+err };
|
||||
var statusCode = 400;
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'POST TEMPLATE', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Update a template
|
||||
TemplateMapsController.prototype.update = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = this.serverOptions.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can list templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
|
||||
throw new Error('template PUT data must be of type application/json');
|
||||
template = req.body;
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
err = new Error("Invalid template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
if ( err ) throw err;
|
||||
return { template_id: cdbuser + '@' + tpl_id };
|
||||
},
|
||||
function finish(err, response){
|
||||
if ( req.profiler ) {
|
||||
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'PUT TEMPLATE', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Get a specific template
|
||||
TemplateMapsController.prototype.retrieve = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = this.serverOptions.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated users can get template maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
var err = new Error("Cannot get template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val){
|
||||
if ( err ) throw err;
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
function finish(err, response){
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'GET TEMPLATE', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Delete a specific template
|
||||
TemplateMapsController.prototype.destroy = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = this.serverOptions.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated users can delete template maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
var err = new Error("Cannot find template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val){
|
||||
if ( err ) throw err;
|
||||
return { status: 'ok' };
|
||||
},
|
||||
function finish(err, response){
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'DELETE TEMPLATE', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, ['', 204]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Get a list of owned templates
|
||||
TemplateMapsController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = this.serverOptions.userByReq(req);
|
||||
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can list templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
if ( err ) throw err;
|
||||
// NOTE: might omit "cbduser" if == dbowner ...
|
||||
var ids = _.map(tpl_ids, function(id) { return cdbuser + '@' + id; });
|
||||
return { template_ids: ids };
|
||||
},
|
||||
function finish(err, response){
|
||||
var statusCode = 200;
|
||||
if (err){
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'GET TEMPLATE LIST', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, [response, statusCode]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TemplateMapsController.prototype.instantiate = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
Step(
|
||||
function() {
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json') {
|
||||
throw new Error('template POST data must be of type application/json, it is instead ');
|
||||
}
|
||||
self.instantiateTemplate(req, res, req.body, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TemplateMapsController.prototype.options = function(req, res) {
|
||||
this.app.doCORS(res, "Content-Type");
|
||||
return next();
|
||||
};
|
||||
|
||||
/**
|
||||
* jsonp endpoint, allows to instantiate a template with a json call.
|
||||
* callback query argument is mandatory
|
||||
*/
|
||||
TemplateMapsController.prototype.jsonp = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
}
|
||||
Step(
|
||||
function() {
|
||||
if ( req.query.callback === undefined || req.query.callback.length === 0) {
|
||||
throw new Error('callback parameter should be present and be a function name');
|
||||
}
|
||||
var config = {};
|
||||
if(req.query.config) {
|
||||
try {
|
||||
config = JSON.parse(req.query.config);
|
||||
} catch(e) {
|
||||
throw new Error('badformed config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
self.instantiateTemplate(req, res, config, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Instantiate a template
|
||||
TemplateMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var template;
|
||||
var layergroup;
|
||||
var fakereq; // used for call to createLayergroup
|
||||
var cdbuser = self.serverOptions.userByReq(req);
|
||||
// Format of template_id: [<template_owner>]@<template_id>
|
||||
var tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] && tpl_id[0] != cdbuser ) {
|
||||
var err = new Error('Cannot instanciate map of user "'
|
||||
+ tpl_id[0] + '" on database of user "'
|
||||
+ cdbuser + '"');
|
||||
err.http_status = 403;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
var auth_token = req.query.auth_token;
|
||||
Step(
|
||||
function getTemplate(){
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function checkAuthorized(err, templateValue) {
|
||||
if ( req.profiler ) req.profiler.done('getTemplate');
|
||||
if ( err ) throw err;
|
||||
if ( ! templateValue ) {
|
||||
err = new Error("Template '" + tpl_id + "' of user '" + cdbuser + "' not found");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
|
||||
template = templateValue;
|
||||
|
||||
var authorized = false;
|
||||
try {
|
||||
authorized = self.templateMaps.isAuthorized(template, auth_token);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
err = new Error('Unauthorized template instanciation');
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByCert');
|
||||
}
|
||||
|
||||
return self.templateMaps.instance(template, template_params);
|
||||
},
|
||||
function prepareParams(err, instance){
|
||||
if ( req.profiler ) req.profiler.done('TemplateMaps_instance');
|
||||
if ( err ) throw err;
|
||||
layergroup = instance;
|
||||
fakereq = { query: {}, params: {}, headers: _.clone(req.headers),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
self.setDBParams(cdbuser, fakereq.params, this);
|
||||
},
|
||||
function setApiKey(err){
|
||||
if ( req.profiler ) req.profiler.done('setDBParams');
|
||||
if ( err ) throw err;
|
||||
self.metadataBackend.getUserMapKey(cdbuser, this);
|
||||
},
|
||||
function createLayergroup(err, val) {
|
||||
if ( req.profiler ) req.profiler.done('getUserMapKey');
|
||||
if ( err ) throw err;
|
||||
fakereq.params.api_key = val;
|
||||
self.app.createLayergroup(layergroup, fakereq, this);
|
||||
},
|
||||
function prepareResponse(err, layergroup) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
var tplhash = self.templateMaps.fingerPrint(template).substring(0,8);
|
||||
layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid;
|
||||
|
||||
self.surrogateKeysCache.tag(res, new self.NamedMapsCacheEntry(cdbuser, template.name));
|
||||
|
||||
return layergroup;
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
TemplateMapsController.prototype.finish_instantiation = function(err, response, res, req) {
|
||||
if ( req.profiler ) {
|
||||
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
if (err) {
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
if(global.environment.debug) {
|
||||
response.stack = err.stack;
|
||||
}
|
||||
this.app.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
|
||||
} else {
|
||||
this.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
};
|
||||
|
||||
TemplateMapsController.prototype.setDBParams = function(cdbuser, params, callback) {
|
||||
var self = this;
|
||||
Step(
|
||||
function setAuth() {
|
||||
self.serverOptions.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if ( err ) throw err;
|
||||
self.serverOptions.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
90
lib/cartodb/monitoring/health_check.js
Normal file
90
lib/cartodb/monitoring/health_check.js
Normal file
@@ -0,0 +1,90 @@
|
||||
var _ = require('underscore'),
|
||||
dot = require('dot'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
Step = require('step');
|
||||
|
||||
function HealthCheck(metadataBackend, tilelive) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.tilelive = tilelive;
|
||||
}
|
||||
|
||||
module.exports = HealthCheck;
|
||||
|
||||
|
||||
var mapnikOptions = {
|
||||
query: {
|
||||
metatile: 1,
|
||||
poolSize: 4,
|
||||
bufferSize: 64
|
||||
},
|
||||
protocol: 'mapnik:',
|
||||
slashes: true,
|
||||
xml: null
|
||||
};
|
||||
|
||||
var xmlTemplate = dot.template(fs.readFileSync(path.resolve(__dirname, 'map-config.xml'), 'utf-8'));
|
||||
|
||||
HealthCheck.prototype.check = function(config, callback) {
|
||||
|
||||
var self = this,
|
||||
startTime,
|
||||
result = {
|
||||
redis: {
|
||||
ok: false
|
||||
},
|
||||
mapnik: {
|
||||
ok: false
|
||||
},
|
||||
tile: {
|
||||
ok: false
|
||||
}
|
||||
};
|
||||
mapnikXmlParams = config;
|
||||
|
||||
Step(
|
||||
function getDBParams() {
|
||||
startTime = Date.now();
|
||||
self.metadataBackend.getAllUserDBParams(config.username, this);
|
||||
},
|
||||
function loadMapnik(err, dbParams) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
result.redis = {
|
||||
ok: !err,
|
||||
elapsed: Date.now() - startTime,
|
||||
size: Object.keys(dbParams).length
|
||||
};
|
||||
mapnikOptions.xml = xmlTemplate(mapnikXmlParams);
|
||||
|
||||
startTime = Date.now();
|
||||
self.tilelive.load(mapnikOptions, this);
|
||||
},
|
||||
function getTile(err, source) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
result.mapnik = {
|
||||
ok: !err,
|
||||
elapsed: Date.now() - startTime
|
||||
};
|
||||
|
||||
startTime = Date.now();
|
||||
source.getTile(config.z, config.x, config.y, this);
|
||||
},
|
||||
function handleTile(err, tile) {
|
||||
result.tile = {
|
||||
ok: !err
|
||||
};
|
||||
|
||||
if (tile) {
|
||||
result.tile.elapsed = Date.now() - startTime;
|
||||
result.tile.size = tile.length;
|
||||
}
|
||||
|
||||
callback(err, result);
|
||||
}
|
||||
);
|
||||
};
|
||||
4
lib/cartodb/monitoring/map-config.xml
Normal file
4
lib/cartodb/monitoring/map-config.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<Map
|
||||
background-color="#c33"
|
||||
srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
|
||||
</Map>
|
||||
@@ -1,10 +1,9 @@
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, cartoData = require('cartodb-redis')(global.environment.redis)
|
||||
, Cache = require('./cache_validator')
|
||||
, QueryTablesApi = require('./api/query_tables_api')
|
||||
, crypto = require('crypto')
|
||||
, LZMA = require('lzma').LZMA;
|
||||
, LZMA = require('lzma').LZMA
|
||||
;
|
||||
|
||||
// This is for backward compatibility with 1.3.3
|
||||
@@ -16,16 +15,35 @@ if ( _.isUndefined(global.environment.sqlapi.domain) ) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function(){
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'sql',
|
||||
'geom_type',
|
||||
'cache_buster',
|
||||
'cache_policy',
|
||||
'callback',
|
||||
'interactivity',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'style',
|
||||
'style_version',
|
||||
'style_convert',
|
||||
'config',
|
||||
'scale_factor'
|
||||
];
|
||||
|
||||
var lzmaWorker = new LZMA();
|
||||
|
||||
var queryTablesApi = new QueryTablesApi();
|
||||
module.exports = function(redisPool) {
|
||||
var redisOpts = redisPool ? {pool: redisPool} : global.environment.redis;
|
||||
var cartoData = require('cartodb-redis')(redisOpts),
|
||||
lzmaWorker = new LZMA(),
|
||||
queryTablesApi = new QueryTablesApi();
|
||||
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
cache_ttl: 60000, // milliseconds
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
cache_ttl: 60000, // milliseconds
|
||||
metatile: 4,
|
||||
bufferSize: 64,
|
||||
statsInterval: 60000
|
||||
});
|
||||
|
||||
var me = {
|
||||
@@ -61,13 +79,19 @@ module.exports = function(){
|
||||
},
|
||||
statsd: global.environment.statsd,
|
||||
renderCache: {
|
||||
ttl: rendererConfig.cache_ttl
|
||||
ttl: rendererConfig.cache_ttl,
|
||||
statsInterval: rendererConfig.statsInterval
|
||||
},
|
||||
renderer: {
|
||||
http: rendererConfig.http
|
||||
},
|
||||
redis: global.environment.redis,
|
||||
enable_cors: global.environment.enable_cors,
|
||||
varnish_host: global.environment.varnish.host,
|
||||
varnish_port: global.environment.varnish.port,
|
||||
varnish_http_port: global.environment.varnish.http_port,
|
||||
varnish_secret: global.environment.varnish.secret,
|
||||
varnish_purge_enabled: global.environment.varnish.purge_enabled,
|
||||
cache_enabled: global.environment.cache_enabled,
|
||||
log_format: global.environment.log_format,
|
||||
useProfiler: global.environment.useProfiler
|
||||
@@ -77,6 +101,9 @@ module.exports = function(){
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
me.redis.unwatchOnRelease = false;
|
||||
|
||||
// Re-use redisPool
|
||||
me.redis.pool = redisPool;
|
||||
|
||||
/* This whole block is about generating X-Cache-Channel { */
|
||||
|
||||
// TODO: review lifetime of elements of this cache
|
||||
@@ -338,7 +365,7 @@ module.exports = function(){
|
||||
if ( req.query && req.query.cache_policy == 'persist' ) {
|
||||
res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
|
||||
} else {
|
||||
var ttl = global.environment.varnish.ttl || 86400;
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
}
|
||||
res.header('Last-Modified', (new Date()).toUTCString());
|
||||
@@ -468,10 +495,6 @@ module.exports = function(){
|
||||
|
||||
// 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
|
||||
@@ -490,11 +513,20 @@ module.exports = function(){
|
||||
var auth_token = req.params.auth_token;
|
||||
|
||||
//console.log("Checking authorization from signer " + signer + " for resource " + layergroup_id + " with auth_token " + auth_token);
|
||||
var mapStore = req.app.mapStore;
|
||||
if (!mapStore) {
|
||||
throw new Error('Unable to retrieve map configuration token');
|
||||
}
|
||||
|
||||
mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var authorized = me.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
|
||||
callback(null, authorized ? signer : null);
|
||||
});
|
||||
|
||||
me.signedMaps.isAuthorized(signer, layergroup_id, auth_token,
|
||||
function(err, authorized) {
|
||||
callback(err, authorized ? signer : null);
|
||||
});
|
||||
};
|
||||
|
||||
// Check if a request is authorized by api_key
|
||||
@@ -652,9 +684,7 @@ module.exports = function(){
|
||||
return;
|
||||
}
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var good_query = ['sql', 'geom_type', 'cache_buster', 'cache_policy', 'callback', 'interactivity', 'map_key', 'api_key', 'auth_token', 'style', 'style_version', 'style_convert', 'config' ];
|
||||
var bad_query = _.difference(_.keys(req.query), good_query);
|
||||
var bad_query = _.difference(_.keys(req.query), REQUEST_QUERY_PARAMS_WHITELIST);
|
||||
|
||||
_.each(bad_query, function(key){ delete req.query[key]; });
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
var crypto = require('crypto');
|
||||
var Step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
var debug = global.environment ? global.environment.debug : undefined;
|
||||
|
||||
// Class handling map signatures and user certificates
|
||||
//
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps
|
||||
//
|
||||
// @param redis_pool an instance of a "redis-mpool"
|
||||
// See https://github.com/CartoDB/node-redis-mpool
|
||||
// Needs version 0.x.x of the API.
|
||||
//
|
||||
function SignedMaps(redis_pool) {
|
||||
this.redis_pool = redis_pool;
|
||||
|
||||
// Database containing signatures
|
||||
// TODO: allow configuring ?
|
||||
// NOTE: currently it is the same as
|
||||
// the one containing layergroups
|
||||
this.db_signatures = 0;
|
||||
|
||||
//
|
||||
// Map signatures in redis are reference to signature certificates
|
||||
// We have the following datastores:
|
||||
//
|
||||
// 1. User certificates: set of per-user authorization certificates
|
||||
// 2. Map signatures: set of per-map certificate references
|
||||
// 3. Certificate applications: set of per-certificate signed maps
|
||||
|
||||
// User certificates (HASH:crt_id->crt_val)
|
||||
this.key_map_crt = "map_crt|<%= signer %>";
|
||||
|
||||
// Map signatures (SET:crt_id)
|
||||
this.key_map_sig = "map_sig|<%= signer %>|<%= map_id %>";
|
||||
|
||||
// Certificates applications (SET:map_id)
|
||||
//
|
||||
// Everytime a map is signed, the map identifier (layergroup_id)
|
||||
// is added to this set. The purpose of this set is to drop
|
||||
// all map signatures when a certificate is removed
|
||||
//
|
||||
this.key_crt_sig = "crt_sig|<%= signer %>|<%= crt_id %>";
|
||||
|
||||
};
|
||||
|
||||
var o = SignedMaps.prototype;
|
||||
|
||||
//--------------- PRIVATE METHODS --------------------------------
|
||||
|
||||
o._acquireRedis = function(callback) {
|
||||
this.redis_pool.acquire(this.db_signatures, callback);
|
||||
};
|
||||
|
||||
o._releaseRedis = function(client) {
|
||||
this.redis_pool.release(this.db_signatures, client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal function to communicate with redis
|
||||
*
|
||||
* @param redisFunc - the redis function to execute
|
||||
* @param redisArgs - the arguments for the redis function in an array
|
||||
* @param callback - function to pass results too.
|
||||
*/
|
||||
o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
var redisClient;
|
||||
var that = this;
|
||||
var db = that.db_signatures;
|
||||
|
||||
Step(
|
||||
function getRedisClient() {
|
||||
that.redis_pool.acquire(db, this);
|
||||
},
|
||||
function executeQuery(err, data) {
|
||||
if ( err ) throw err;
|
||||
redisClient = data;
|
||||
redisArgs.push(this);
|
||||
redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs);
|
||||
},
|
||||
function releaseRedisClient(err, data) {
|
||||
if ( ! _.isUndefined(redisClient) ) that.redis_pool.release(db, redisClient);
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
o._getAuthMethod = function(auth) {
|
||||
return auth.method || 'open';
|
||||
};
|
||||
|
||||
//--------------- PUBLIC API -------------------------------------
|
||||
|
||||
/// Check formal validity of a certificate
|
||||
//
|
||||
/// Return an Error instance if invalid, null otherwise
|
||||
///
|
||||
o.checkInvalidCertificate = function(cert) {
|
||||
//console.log("Checking cert: "); console.dir(cert);
|
||||
if ( cert.version !== "0.0.1" ) {
|
||||
return new Error("Unsupported certificate version " + cert.version);
|
||||
}
|
||||
|
||||
if ( ! cert.auth ) {
|
||||
console.log("Cert is : "); console.dir(cert);
|
||||
return new Error("No certificate authorization");
|
||||
}
|
||||
|
||||
var method = this._getAuthMethod(cert.auth);
|
||||
|
||||
switch ( method ) {
|
||||
case 'open':
|
||||
break;
|
||||
case 'token':
|
||||
if ( ! _.isArray(cert.auth.valid_tokens) )
|
||||
return new Error("Invalid 'token' authentication: missing valid_tokens");
|
||||
if ( ! cert.auth.valid_tokens.length )
|
||||
return new Error("Invalid 'token' authentication: no valid_tokens");
|
||||
break;
|
||||
default:
|
||||
return new Error("Unsupported authentication method: " + cert.auth.method);
|
||||
break;
|
||||
}
|
||||
|
||||
return null; // all valid
|
||||
}
|
||||
|
||||
// Check if the given certificate authorizes waiver of "auth"
|
||||
o.authorizedByCert = function(cert, auth) {
|
||||
auth = _.isArray(auth) ? auth : [auth];
|
||||
|
||||
var err = this.checkInvalidCertificate(cert);
|
||||
if ( err ) throw err;
|
||||
|
||||
var method = this._getAuthMethod(cert.auth);
|
||||
|
||||
// Open authentication certificates are always authorized
|
||||
if ( method === 'open' ) return true;
|
||||
|
||||
// Token based authentication requires valid token
|
||||
if ( method === 'token' ) {
|
||||
return _.intersection(cert.auth.valid_tokens, auth).length > 0;
|
||||
}
|
||||
|
||||
throw new Error("Unsupported authentication method: " + cert.auth.method);
|
||||
};
|
||||
|
||||
// Check if shown credential are authorized to access a map
|
||||
// by the given signer.
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param map_id a layergroup_id
|
||||
// @param auth an authentication token, or undefined if none
|
||||
// (can still be authorized by signature)
|
||||
//
|
||||
// @param callback function(Error, Boolean)
|
||||
//
|
||||
o.isAuthorized = function(signer, map_id, auth, callback) {
|
||||
var that = this;
|
||||
var redisClient;
|
||||
var db = that.db_signatures;
|
||||
var authorized = false;
|
||||
var certificate_id_list;
|
||||
var missing_certificates = [];
|
||||
if ( debug ) {
|
||||
console.log("Check auth from signer '" + signer + "' on map '" + map_id + "' with auth '" + auth + "'");
|
||||
}
|
||||
Step(
|
||||
function getRedisClient() {
|
||||
that.redis_pool.acquire(db, this);
|
||||
},
|
||||
function getMapSignatures(err, client) {
|
||||
if ( err ) throw err;
|
||||
redisClient = client;
|
||||
var map_sig_key = _.template(that.key_map_sig, {signer:signer, map_id:map_id});
|
||||
redisClient.SMEMBERS(map_sig_key, this);
|
||||
//that._redisCmd('SMEMBERS', [ map_sig_key ], this);
|
||||
},
|
||||
function getCertificates(err, crt_lst) {
|
||||
if ( err ) throw err;
|
||||
if ( debug ) {
|
||||
console.log("Map '" + map_id + "' is signed by " + crt_lst.length + " certificates of user '" + signer);
|
||||
}
|
||||
certificate_id_list = crt_lst;
|
||||
if ( ! crt_lst.length ) {
|
||||
// No certs, avoid calling redis with short args list.
|
||||
// Next step expects a list of certificate values so
|
||||
// we directly send the empty list.
|
||||
return crt_lst;
|
||||
}
|
||||
var map_crt_key = _.template(that.key_map_crt, {signer:signer});
|
||||
//that._redisCmd('HMGET', [ map_crt_key ].concat(crt_lst), this);
|
||||
redisClient.HMGET(map_crt_key, crt_lst, this);
|
||||
},
|
||||
function checkCertificates(err, certs) {
|
||||
if ( err ) throw err;
|
||||
for (var i=0; i<certs.length; ++i) {
|
||||
var crt_id = certificate_id_list[i];
|
||||
if ( _.isNull(certs[i]) ) {
|
||||
missing_certificates.push(crt_id);
|
||||
continue;
|
||||
}
|
||||
var cert;
|
||||
try {
|
||||
//console.log("cert " + crt_id + ": " + certs[i]);
|
||||
cert = JSON.parse(certs[i]);
|
||||
authorized = that.authorizedByCert(cert, auth);
|
||||
} catch (err) {
|
||||
console.log("Certificate " + certificate_id_list[i] + " by user '" + signer + "' is malformed: " + err);
|
||||
continue;
|
||||
}
|
||||
if ( authorized ) {
|
||||
if ( debug ) {
|
||||
console.log("Access to map '" + map_id + "' authorized by cert '"
|
||||
+ certificate_id_list[i] + "' of user '" + signer + "'");
|
||||
}
|
||||
//console.dir(cert);
|
||||
break; // no need to further check certs
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( missing_certificates.length ) {
|
||||
console.log("WARNING: map '" + map_id + "' is signed by '" + signer
|
||||
+ "' with " + missing_certificates.length
|
||||
+ " missing certificates: "
|
||||
+ missing_certificates + " (TODO: give cleanup instructions)");
|
||||
}
|
||||
if ( redisClient ) that.redis_pool.release(db, redisClient);
|
||||
callback(err, authorized);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Add an authorization certificate from a user.
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param cert certificate object, see
|
||||
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps
|
||||
//
|
||||
// @param callback function(err, crt_id) return certificate id
|
||||
//
|
||||
// TODO: allow for requesting error when certificate already exists ?
|
||||
//
|
||||
o.addCertificate = function(signer, cert, callback) {
|
||||
var crt_val = JSON.stringify(cert);
|
||||
var crt_id = crypto.createHash('md5').update(crt_val).digest('hex');
|
||||
|
||||
var usr_crt_key = _.template(this.key_map_crt, {signer:signer});
|
||||
this._redisCmd('HSET', [ usr_crt_key, crt_id, crt_val ], function(err, created) {
|
||||
// NOTE: created would be 0 if the field already existed, 1 otherwise
|
||||
callback(err, crt_id);
|
||||
});
|
||||
};
|
||||
|
||||
// Remove an authorization certificate of a user, also removing
|
||||
// any signature made with the certificate.
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param crt_id certificate identifier, as returned by addCertificate
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.delCertificate = function(signer, crt_id, callback) {
|
||||
var db = this.db_signatures;
|
||||
var crt_sig_key = _.template(this.key_crt_sig, {signer:signer, crt_id:crt_id});
|
||||
var signed_map_list;
|
||||
var redis_client;
|
||||
var that = this;
|
||||
Step (
|
||||
function getRedisClient() {
|
||||
that._acquireRedis(this);
|
||||
},
|
||||
function removeCertificate(err, data) {
|
||||
if ( err ) throw err;
|
||||
redis_client = data;
|
||||
// Remove the certificate (would be enough to stop authorizing uses)
|
||||
var usr_crt_key = _.template(that.key_map_crt, {signer:signer});
|
||||
redis_client.HDEL(usr_crt_key, crt_id, this);
|
||||
},
|
||||
function getMapSignatures(err, deleted) {
|
||||
if ( err ) throw err;
|
||||
if ( ! deleted ) {
|
||||
// debugging (how can this be possible?)
|
||||
console.log("WARNING: authorization certificate '" + crt_id
|
||||
+ "' by user '" + signer + "' did not exist on delete request");
|
||||
}
|
||||
// Get all signatures by this certificate
|
||||
redis_client.SMEMBERS(crt_sig_key, this);
|
||||
},
|
||||
function delMapSignaturesReference(err, map_id_list) {
|
||||
if ( err ) throw err;
|
||||
signed_map_list = map_id_list;
|
||||
if ( debug ) {
|
||||
console.log("Certificate '" + crt_id + "' from user '" + signer
|
||||
+ "' was used to sign " + signed_map_list.length + " maps");
|
||||
}
|
||||
redis_client.DEL(crt_sig_key, this);
|
||||
},
|
||||
function delMapSignatures(err) {
|
||||
if ( err ) throw err;
|
||||
var crt_sig_key = _.template(that.key_crt_sig, {signer:signer, crt_id:crt_id});
|
||||
var tx = redis_client.MULTI();
|
||||
for (var i=0; i<signed_map_list.length; ++i) {
|
||||
var map_id = signed_map_list[i];
|
||||
var map_sig_key = _.template(that.key_map_sig, {signer:signer, map_id:map_id});
|
||||
//console.log("Queuing removal of '" + crt_id + "' from '" + map_sig_key + "'");
|
||||
tx.SREM( map_sig_key, crt_id )
|
||||
}
|
||||
tx.EXEC(this);
|
||||
},
|
||||
function reportTransaction(err, rets) {
|
||||
if ( err ) throw err;
|
||||
if ( debug ) {
|
||||
for (var i=0; i<signed_map_list.length; ++i) {
|
||||
var ret = rets[i];
|
||||
if ( ! ret ) {
|
||||
console.log("No signature with certificate '" + crt_id
|
||||
+ "' of user '" + signer + "' found in map '"
|
||||
+ signed_map_list[i] + "'");
|
||||
} else {
|
||||
console.log("Signature with certificate '" + crt_id
|
||||
+ "' of user '" + signer + "' removed from map '"
|
||||
+ signed_map_list[i] + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( ! _.isUndefined(redis_client) ) {
|
||||
that._releaseRedis(redis_client);
|
||||
}
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Sign a map with a certificate reference
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param map_id a layergroup_id
|
||||
// @param crt_id signature certificate identifier
|
||||
//
|
||||
// @param callback function(Error)
|
||||
//
|
||||
o.signMap = function(signer, map_id, crt_id, callback) {
|
||||
var that = this;
|
||||
Step(
|
||||
function addMapSignature() {
|
||||
var map_sig_key = _.template(that.key_map_sig, {signer:signer, map_id:map_id});
|
||||
if ( debug ) {
|
||||
console.log("Adding " + crt_id + " to " + map_sig_key);
|
||||
}
|
||||
that._redisCmd('SADD', [ map_sig_key, crt_id ], this);
|
||||
},
|
||||
function addCertificateUsage(err) {
|
||||
// Add the map to the set of maps signed by the given cert
|
||||
if ( err ) throw err;
|
||||
var crt_sig_key = _.template(that.key_crt_sig, {signer:signer, crt_id:crt_id});
|
||||
that._redisCmd('SADD', [ crt_sig_key, map_id ], this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Sign a map with a full certificate
|
||||
//
|
||||
// @param signer a signer name (cartodb username)
|
||||
// @param map_id a layergroup_id
|
||||
// @param cert_id signature certificate identifier
|
||||
//
|
||||
// @param callback function(Error, String) return certificate id
|
||||
//
|
||||
o.addSignature = function(signer, map_id, cert, callback) {
|
||||
var that = this;
|
||||
var certificate_id;
|
||||
Step(
|
||||
function addCertificate() {
|
||||
that.addCertificate(signer, cert, this);
|
||||
},
|
||||
function signMap(err, cert_id) {
|
||||
if ( err ) throw err;
|
||||
if ( ! cert_id ) throw new Error("addCertificate returned no certificate id");
|
||||
certificate_id = cert_id;
|
||||
that.signMap(signer, map_id, cert_id, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err, certificate_id);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = SignedMaps;
|
||||
@@ -3,6 +3,11 @@ var crypto = require('crypto'),
|
||||
_ = require('underscore'),
|
||||
dot = require('dot');
|
||||
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
// Class handling map templates
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps
|
||||
@@ -11,16 +16,16 @@ var crypto = require('crypto'),
|
||||
// See https://github.com/CartoDB/node-redis-mpool
|
||||
// Needs version 0.x.x of the API.
|
||||
//
|
||||
// @param signed_maps an instance of a "signed_maps" class,
|
||||
// See signed_maps.js
|
||||
//
|
||||
// @param opts TemplateMap options. Supported elements:
|
||||
// 'max_user_templates' limit on the number of per-user
|
||||
//
|
||||
//
|
||||
function TemplateMaps(redis_pool, signed_maps, opts) {
|
||||
function TemplateMaps(redis_pool, opts) {
|
||||
if (!(this instanceof TemplateMaps)) return new TemplateMaps();
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
this.redis_pool = redis_pool;
|
||||
this.signed_maps = signed_maps;
|
||||
this.opts = opts || {};
|
||||
|
||||
// Database containing templates
|
||||
@@ -36,8 +41,6 @@ function TemplateMaps(redis_pool, signed_maps, opts) {
|
||||
// We have the following datastores:
|
||||
//
|
||||
// 1. User templates: set of per-user map templates
|
||||
// NOTE: each template would have an associated auth
|
||||
// reference, see signed_maps.js
|
||||
|
||||
// User templates (HASH:tpl_id->tpl_val)
|
||||
this.key_usr_tpl = dot.template("map_tpl|{{=it.owner}}");
|
||||
@@ -48,6 +51,11 @@ function TemplateMaps(redis_pool, signed_maps, opts) {
|
||||
this.lock_ttl = this.opts['lock_ttl'] || 5000;
|
||||
}
|
||||
|
||||
util.inherits(TemplateMaps, EventEmitter);
|
||||
|
||||
module.exports = TemplateMaps;
|
||||
|
||||
|
||||
var o = TemplateMaps.prototype;
|
||||
|
||||
//--------------- PRIVATE METHODS --------------------------------
|
||||
@@ -56,14 +64,6 @@ o._userTemplateLimit = function() {
|
||||
return this.opts['max_user_templates'] || 0;
|
||||
};
|
||||
|
||||
o._acquireRedis = function(callback) {
|
||||
this.redis_pool.acquire(this.db_signatures, callback);
|
||||
};
|
||||
|
||||
o._releaseRedis = function(client) {
|
||||
this.redis_pool.release(this.db_signatures, client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal function to communicate with redis
|
||||
*
|
||||
@@ -93,35 +93,6 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
// @param callback function(err, obtained)
|
||||
o._obtainTemplateLock = function(owner, tpl_id, callback) {
|
||||
var that = this,
|
||||
lockKey = this.key_usr_tpl_lck({owner:owner});
|
||||
Step (
|
||||
function obtainLock() {
|
||||
that._redisCmd('HGET', [lockKey, tpl_id], this);
|
||||
},
|
||||
function checkLock(err, lockTime) {
|
||||
if (err) { throw err; }
|
||||
|
||||
var _newLockTime = Date.now();
|
||||
if (!lockTime || ((_newLockTime - lockTime) > that.lock_ttl)) {
|
||||
that._redisCmd('HSET', [lockKey, tpl_id, _newLockTime], this);
|
||||
} else {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' is locked");
|
||||
}
|
||||
},
|
||||
function finish(err, hsetValue) {
|
||||
callback(err, !!hsetValue);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// @param callback function(err, deleted)
|
||||
o._releaseTemplateLock = function(owner, tpl_id, callback) {
|
||||
this._redisCmd('HDEL', [this.key_usr_tpl_lck({owner:owner}), tpl_id], callback);
|
||||
};
|
||||
|
||||
var _reValidIdentifier = /^[a-zA-Z][0-9a-zA-Z_]*$/;
|
||||
o._checkInvalidTemplate = function(template) {
|
||||
if ( template.version != '0.0.1' ) {
|
||||
@@ -135,6 +106,11 @@ o._checkInvalidTemplate = function(template) {
|
||||
return new Error("Invalid characters in template name '" + tplname + "'");
|
||||
}
|
||||
|
||||
var invalidError = isInvalidLayergroup(template.layergroup);
|
||||
if (invalidError) {
|
||||
return invalidError;
|
||||
}
|
||||
|
||||
var placeholders = template.placeholders || {};
|
||||
|
||||
var placeholderKeys = Object.keys(placeholders);
|
||||
@@ -152,29 +128,62 @@ o._checkInvalidTemplate = function(template) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check certificate validity
|
||||
var cert = this.getTemplateCertificate(template);
|
||||
var err = this.signed_maps.checkInvalidCertificate(cert);
|
||||
if ( err ) return err;
|
||||
var auth = template.auth || {};
|
||||
|
||||
// TODO: run more checks over template format ?
|
||||
switch ( auth.method ) {
|
||||
case 'open':
|
||||
break;
|
||||
case 'token':
|
||||
if ( ! _.isArray(auth.valid_tokens) )
|
||||
return new Error("Invalid 'token' authentication: missing valid_tokens");
|
||||
if ( ! auth.valid_tokens.length )
|
||||
return new Error("Invalid 'token' authentication: no valid_tokens");
|
||||
break;
|
||||
default:
|
||||
return new Error("Unsupported authentication method: " + auth.method);
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
function isInvalidLayergroup(layergroup) {
|
||||
if (!layergroup) {
|
||||
return new Error('Missing layergroup');
|
||||
}
|
||||
|
||||
var layers = layergroup.layers;
|
||||
|
||||
if (!_.isArray(layers) || layers.length === 0) {
|
||||
return new Error('Missing or empty layers array from layergroup config');
|
||||
}
|
||||
|
||||
var invalidLayers = layers
|
||||
.map(function(layer, layerIndex) {
|
||||
return layer.options ? null : layerIndex;
|
||||
})
|
||||
.filter(function(layerIndex) {
|
||||
return layerIndex !== null;
|
||||
});
|
||||
|
||||
if (invalidLayers.length) {
|
||||
return new Error('Missing `options` in layergroup config for layers: ' + invalidLayers.join(', '));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function templateDefaults(template) {
|
||||
var templateAuth = _.defaults({}, template.auth || {}, {
|
||||
method: 'open'
|
||||
});
|
||||
return _.defaults({ auth: templateAuth }, template, {
|
||||
placeholders: {}
|
||||
});
|
||||
}
|
||||
|
||||
//--------------- PUBLIC API -------------------------------------
|
||||
|
||||
// Extract a signature certificate from a template
|
||||
//
|
||||
// The certificate will be ready to be passed to
|
||||
// SignedMaps.addCertificate or SignedMaps.authorizedByCert
|
||||
//
|
||||
o.getTemplateCertificate = function(template) {
|
||||
return {
|
||||
version: '0.0.1',
|
||||
template_id: template.name,
|
||||
auth: template.auth
|
||||
};
|
||||
};
|
||||
|
||||
// Add a template
|
||||
//
|
||||
// NOTE: locks user+template_name or fails
|
||||
@@ -188,102 +197,58 @@ o.getTemplateCertificate = function(template) {
|
||||
// Return template identifier (only valid for given user)
|
||||
//
|
||||
o.addTemplate = function(owner, template, callback) {
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
if ( invalidError ) {
|
||||
callback(invalidError);
|
||||
return;
|
||||
}
|
||||
var tplname = template.name;
|
||||
var self = this;
|
||||
|
||||
// Procedure:
|
||||
//
|
||||
// - Check against limit
|
||||
// 0. Obtain a lock for user+template_name, fail if impossible
|
||||
// 1. Check no other template exists with the same name
|
||||
// 2. Install certificate extracted from template, extending
|
||||
// it to contain a name to properly salt things out.
|
||||
// 3. Modify the template object to reference certificate by id
|
||||
// 4. Install template
|
||||
// 5. Release lock
|
||||
//
|
||||
//
|
||||
template = templateDefaults(template);
|
||||
|
||||
var usr_tpl_key = this.key_usr_tpl({owner:owner});
|
||||
var gotLock = false;
|
||||
var that = this;
|
||||
var limit = that._userTemplateLimit();
|
||||
Step(
|
||||
function checkLimit() {
|
||||
if ( ! limit ) return 0;
|
||||
that._redisCmd('HLEN', [ usr_tpl_key ], this);
|
||||
},
|
||||
// try to obtain a lock
|
||||
function obtainLock(err, len) {
|
||||
if ( err ) throw err;
|
||||
if ( limit && len >= limit ) {
|
||||
throw new Error("User '" + owner + "' reached limit on number of templates (" + len + "/" + limit + ")");
|
||||
}
|
||||
that._obtainTemplateLock(owner, tplname, this);
|
||||
},
|
||||
function getExistingTemplate(err, locked) {
|
||||
if ( err ) throw err;
|
||||
if ( ! locked ) {
|
||||
// Already locked
|
||||
throw new Error("Template '" + tplname + "' of user '" + owner + "' is locked");
|
||||
}
|
||||
gotLock = true;
|
||||
that._redisCmd('HEXISTS', [ usr_tpl_key, tplname ], this);
|
||||
},
|
||||
function installCertificate(err, exists) {
|
||||
if ( err ) throw err;
|
||||
if ( exists ) {
|
||||
throw new Error("Template '" + tplname + "' of user '" + owner + "' already exists");
|
||||
}
|
||||
var cert = that.getTemplateCertificate(template);
|
||||
that.signed_maps.addCertificate(owner, cert, this);
|
||||
},
|
||||
function installTemplate(err, crt_id) {
|
||||
if ( err ) throw err;
|
||||
template.auth_id = crt_id;
|
||||
var tpl_val = JSON.stringify(template);
|
||||
that._redisCmd('HSET', [ usr_tpl_key, tplname, tpl_val ], this);
|
||||
},
|
||||
function releaseLock(err, newfield) {
|
||||
if ( ! err && ! newfield ) {
|
||||
console.log("ERROR: addTemplate overridden existing template '"
|
||||
+ tplname + "' of '" + owner
|
||||
+ "' -- HSET returned " + overridden + ": someone added it without locking ?");
|
||||
// TODO: how to recover this ?!
|
||||
}
|
||||
|
||||
if ( err && ! gotLock ) throw err;
|
||||
|
||||
// release the lock
|
||||
var next = this;
|
||||
that._releaseTemplateLock(owner, tplname, function(e, d) {
|
||||
if ( e ) {
|
||||
console.log("Error removing lock on template '" + tplname
|
||||
+ "' of user '" + owner + "': " + e);
|
||||
} else if ( ! d ) {
|
||||
console.log("ERROR: lock on template '" + tplname
|
||||
+ "' of user '" + owner + "' externally removed during insert!");
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err, tplname);
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
if ( invalidError ) {
|
||||
return callback(invalidError);
|
||||
}
|
||||
|
||||
var templateName = template.name;
|
||||
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
|
||||
var limit = this._userTemplateLimit();
|
||||
|
||||
Step(
|
||||
function checkLimit() {
|
||||
if ( ! limit ) {
|
||||
return 0;
|
||||
}
|
||||
self._redisCmd('HLEN', [ userTemplatesKey ], this);
|
||||
},
|
||||
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
if ( limit && numberOfTemplates >= limit ) {
|
||||
throw new Error("User '" + owner + "' reached limit on number of templates " +
|
||||
"("+ numberOfTemplates + "/" + limit + ")");
|
||||
}
|
||||
self._redisCmd('HSETNX', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
function validateInstallation(err, wasSet) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
if ( ! wasSet ) {
|
||||
throw new Error("Template '" + templateName + "' of user '" + owner + "' already exists");
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('add', owner, templateName, template);
|
||||
}
|
||||
|
||||
callback(err, templateName, template);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Delete a template
|
||||
//
|
||||
// NOTE: locks user+template_name or fails
|
||||
//
|
||||
// Also deletes associated authentication certificate, which
|
||||
// in turn deletes all instance signatures
|
||||
//
|
||||
// @param owner cartodb username of the template owner
|
||||
//
|
||||
// @param tpl_id template identifier as returned
|
||||
@@ -292,82 +257,28 @@ o.addTemplate = function(owner, template, callback) {
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.delTemplate = function(owner, tpl_id, callback) {
|
||||
var usr_tpl_key = this.key_usr_tpl({owner:owner});
|
||||
var gotLock = false;
|
||||
var that = this;
|
||||
Step(
|
||||
// try to obtain a lock
|
||||
function obtainLock() {
|
||||
that._obtainTemplateLock(owner, tpl_id, this);
|
||||
},
|
||||
function getExistingTemplate(err, locked) {
|
||||
if ( err ) throw err;
|
||||
if ( ! locked ) {
|
||||
// Already locked
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' is locked");
|
||||
}
|
||||
gotLock = true;
|
||||
that._redisCmd('HGET', [ usr_tpl_key, tpl_id ], this);
|
||||
},
|
||||
function delCertificate(err, tplval) {
|
||||
if ( err ) throw err;
|
||||
if ( ! tplval ) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
var tpl = JSON.parse(tplval);
|
||||
if ( ! tpl.auth_id ) {
|
||||
// not sure this is an error, in case we'll ever
|
||||
// allow unsigned templates...
|
||||
console.log("ERROR: installed template '" + tpl_id
|
||||
+ "' of user '" + owner + "' has no auth_id reference: "); console.dir(tpl);
|
||||
return null;
|
||||
}
|
||||
var next = this;
|
||||
that.signed_maps.delCertificate(owner, tpl.auth_id, function(err) {
|
||||
if ( err ) {
|
||||
var msg = "ERROR: could not delete certificate '"
|
||||
+ tpl.auth_id + "' associated with template '"
|
||||
+ tpl_id + "' of user '" + owner + "': " + err;
|
||||
// I'm actually not sure we want this event to be fatal
|
||||
// (avoiding a deletion of the template itself)
|
||||
next(new Error(msg));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
function delTemplate(err) {
|
||||
if ( err ) throw err;
|
||||
that._redisCmd('HDEL', [ usr_tpl_key, tpl_id ], this);
|
||||
},
|
||||
function releaseLock(err, deleted) {
|
||||
if ( ! err && ! deleted ) {
|
||||
console.log("ERROR: template '" + tpl_id
|
||||
+ "' of user '" + owner + "' externally removed during delete!");
|
||||
}
|
||||
var self = this;
|
||||
Step(
|
||||
function deleteTemplate() {
|
||||
self._redisCmd('HDEL', [ self.key_usr_tpl({ owner:owner }), tpl_id ], this);
|
||||
},
|
||||
function handleDeletion(err, deleted) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!deleted) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('delete', owner, tpl_id);
|
||||
}
|
||||
|
||||
if ( ! gotLock ) {
|
||||
if ( err ) throw err;
|
||||
return null;
|
||||
}
|
||||
|
||||
// release the lock
|
||||
var next = this;
|
||||
that._releaseTemplateLock(owner, tpl_id, function(e, d) {
|
||||
if ( e ) {
|
||||
console.log("Error removing lock on template '" + tpl_id
|
||||
+ "' of user '" + owner + "': " + e);
|
||||
} else if ( ! d ) {
|
||||
console.log("ERROR: lock on template '" + tpl_id
|
||||
+ "' of user '" + owner + "' externally removed during delete!");
|
||||
callback(err);
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
// Update a template
|
||||
@@ -387,104 +298,54 @@ o.delTemplate = function(owner, tpl_id, callback) {
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
var self = this;
|
||||
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
if ( invalidError ) {
|
||||
callback(invalidError);
|
||||
return;
|
||||
}
|
||||
template = templateDefaults(template);
|
||||
|
||||
var tplname = template.name;
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
|
||||
if ( tpl_id != tplname ) {
|
||||
callback(new Error("Cannot update name of a map template ('" + tpl_id + "' != '" + tplname + "')"));
|
||||
return;
|
||||
}
|
||||
|
||||
var usr_tpl_key = this.key_usr_tpl({owner:owner});
|
||||
var gotLock = false;
|
||||
var that = this;
|
||||
Step(
|
||||
// try to obtain a lock
|
||||
function obtainLock() {
|
||||
that._obtainTemplateLock(owner, tpl_id, this);
|
||||
},
|
||||
function getExistingTemplate(err, locked) {
|
||||
if ( err ) throw err;
|
||||
if ( ! locked ) {
|
||||
// Already locked
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' is locked");
|
||||
}
|
||||
gotLock = true;
|
||||
that._redisCmd('HGET', [ usr_tpl_key, tpl_id ], this);
|
||||
},
|
||||
function delOldCertificate(err, tplval) {
|
||||
if ( err ) throw err;
|
||||
if ( ! tplval ) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '"
|
||||
+ owner +"' does not exist");
|
||||
}
|
||||
var tpl = JSON.parse(tplval);
|
||||
if ( ! tpl.auth_id ) {
|
||||
// not sure this is an error, in case we'll ever
|
||||
// allow unsigned templates...
|
||||
console.log("ERROR: installed template '" + tpl_id
|
||||
+ "' of user '" + owner + "' has no auth_id reference: "); console.dir(tpl);
|
||||
return null;
|
||||
}
|
||||
var next = this;
|
||||
that.signed_maps.delCertificate(owner, tpl.auth_id, function(err) {
|
||||
if ( err ) {
|
||||
var msg = "ERROR: could not delete certificate '"
|
||||
+ tpl.auth_id + "' associated with template '"
|
||||
+ tpl_id + "' of user '" + owner + "': " + err;
|
||||
// I'm actually not sure we want this event to be fatal
|
||||
// (avoiding a deletion of the template itself)
|
||||
next(new Error(msg));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
function installNewCertificate(err) {
|
||||
if ( err ) throw err;
|
||||
var cert = that.getTemplateCertificate(template);
|
||||
that.signed_maps.addCertificate(owner, cert, this);
|
||||
},
|
||||
function updTemplate(err, crt_id) {
|
||||
if ( err ) throw err;
|
||||
template.auth_id = crt_id;
|
||||
var tpl_val = JSON.stringify(template);
|
||||
that._redisCmd('HSET', [ usr_tpl_key, tplname, tpl_val ], this);
|
||||
},
|
||||
function releaseLock(err, newfield) {
|
||||
if ( ! err && newfield ) {
|
||||
console.log("ERROR: template '" + tpl_id
|
||||
+ "' of user '" + owner + "' externally removed during update!");
|
||||
}
|
||||
|
||||
if ( ! gotLock ) {
|
||||
if ( err ) throw err;
|
||||
return null;
|
||||
}
|
||||
|
||||
// release the lock
|
||||
var next = this;
|
||||
that._releaseTemplateLock(owner, tpl_id, function(e, d) {
|
||||
if ( e ) {
|
||||
console.log("Error removing lock on template '" + tpl_id
|
||||
+ "' of user '" + owner + "': " + e);
|
||||
} else if ( ! d ) {
|
||||
console.log("ERROR: lock on template '" + tpl_id
|
||||
+ "' of user '" + owner + "' externally removed during update!");
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
if ( invalidError ) {
|
||||
return callback(invalidError);
|
||||
}
|
||||
);
|
||||
|
||||
var templateName = template.name;
|
||||
|
||||
if ( tpl_id != templateName ) {
|
||||
return callback(new Error("Cannot update name of a map template ('" + tpl_id + "' != '" + templateName + "')"));
|
||||
}
|
||||
|
||||
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
|
||||
|
||||
Step(
|
||||
function getExistingTemplate() {
|
||||
self._redisCmd('HGET', [ userTemplatesKey, tpl_id ], this);
|
||||
},
|
||||
function updateTemplate(err, currentTemplate) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!currentTemplate) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
self._redisCmd('HSET', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
function handleTemplateUpdate(err, didSetNewField) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (didSetNewField) {
|
||||
console.warn('New template created on update operation');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('update', owner, templateName, template);
|
||||
}
|
||||
|
||||
callback(err, template);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// List user templates
|
||||
@@ -509,20 +370,43 @@ o.listTemplates = function(owner, callback) {
|
||||
// Return full template definition
|
||||
//
|
||||
o.getTemplate = function(owner, tpl_id, callback) {
|
||||
var that = this;
|
||||
Step(
|
||||
function getTemplate() {
|
||||
that._redisCmd('HGET', [ that.key_usr_tpl({owner:owner}), tpl_id ], this);
|
||||
},
|
||||
function parseTemplate(err, tpl_val) {
|
||||
if ( err ) throw err;
|
||||
// Should we strip auth_id ?
|
||||
return JSON.parse(tpl_val);
|
||||
},
|
||||
function finish(err, tpl) {
|
||||
callback(err, tpl);
|
||||
var self = this;
|
||||
Step(
|
||||
function getTemplate() {
|
||||
self._redisCmd('HGET', [ self.key_usr_tpl({owner:owner}), tpl_id ], this);
|
||||
},
|
||||
function parseTemplate(err, tpl_val) {
|
||||
if ( err ) throw err;
|
||||
return JSON.parse(tpl_val);
|
||||
},
|
||||
function finish(err, tpl) {
|
||||
callback(err, tpl);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
o.isAuthorized = function(template, authTokens) {
|
||||
if (!template) {
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
authTokens = _.isArray(authTokens) ? authTokens : [authTokens];
|
||||
|
||||
var templateAuth = template.auth;
|
||||
|
||||
if (!templateAuth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (templateAuth.method === 'open') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (templateAuth.method === 'token') {
|
||||
return _.intersection(templateAuth.valid_tokens, authTokens).length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Perform placeholder substitutions on a template
|
||||
@@ -594,6 +478,13 @@ o.instance = function(template, params) {
|
||||
if ( lyropt.sql) lyropt.sql = _replaceVars(lyropt.sql, all_params);
|
||||
// Anything else ?
|
||||
}
|
||||
|
||||
// extra information about the template
|
||||
layergroup.template = {
|
||||
name: template.name,
|
||||
auth: template.auth
|
||||
};
|
||||
|
||||
return layergroup;
|
||||
};
|
||||
|
||||
@@ -604,5 +495,3 @@ o.fingerPrint = function(template) {
|
||||
.digest('hex')
|
||||
;
|
||||
};
|
||||
|
||||
module.exports = TemplateMaps;
|
||||
|
||||
2385
npm-shrinkwrap.json
generated
2385
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "1.18.2",
|
||||
"version": "1.26.0",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -25,14 +25,14 @@
|
||||
"node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0",
|
||||
"underscore" : "~1.6.0",
|
||||
"dot": "~1.0.2",
|
||||
"windshaft": "https://github.com/CartoDB/Windshaft/tarball/0.28.1",
|
||||
"windshaft": "https://github.com/CartoDB/Windshaft/tarball/0.35.0",
|
||||
"step": "~0.0.5",
|
||||
"request": "~2.9.203",
|
||||
"cartodb-redis": "https://github.com/CartoDB/node-cartodb-redis/tarball/0.11.0",
|
||||
"cartodb-psql": "https://github.com/CartoDB/node-cartodb-psql/tarball/0.4.0",
|
||||
"redis-mpool": "https://github.com/CartoDB/node-redis-mpool/tarball/0.1.0",
|
||||
"redis-mpool": "https://github.com/CartoDB/node-redis-mpool/tarball/0.3.0",
|
||||
"lzma": "~1.3.7",
|
||||
"log4js": "~0.6.17",
|
||||
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
|
||||
"rollbar": "~0.3.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -43,5 +43,9 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make check"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8 <0.11",
|
||||
"npm": ">=1.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
211
test/acceptance/cache/surrogate_keys_invalidation.js
vendored
Normal file
211
test/acceptance/cache/surrogate_keys_invalidation.js
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
var assert = require('../../support/assert');
|
||||
var redis = require('redis');
|
||||
var Step = require('step');
|
||||
|
||||
var helper = require(__dirname + '/../../support/test_helper');
|
||||
|
||||
var SqlApiEmulator = require(__dirname + '/../../support/SQLAPIEmu.js');
|
||||
|
||||
var NamedMapsCacheEntry = require(__dirname + '/../../../lib/cartodb/cache/model/named_maps_entry');
|
||||
var SurrogateKeysCache = require(__dirname + '/../../../lib/cartodb/cache/surrogate_keys_cache');
|
||||
|
||||
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/cartodb_windshaft');
|
||||
var ServerOptions = require(__dirname + '/../../../lib/cartodb/server_options');
|
||||
var serverOptions = ServerOptions();
|
||||
|
||||
|
||||
suite('templates surrogate keys', function() {
|
||||
|
||||
var redisClient,
|
||||
sqlApiServer,
|
||||
server;
|
||||
|
||||
var templateOwner = 'localhost',
|
||||
templateName = 'acceptance',
|
||||
expectedTemplateId = templateOwner + '@' + templateName,
|
||||
template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry as the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
expectedBody = { template_id: expectedTemplateId };
|
||||
|
||||
suiteSetup(function(done) {
|
||||
// Enable Varnish purge for tests
|
||||
serverOptions.varnish_purge_enabled = true;
|
||||
|
||||
server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
sqlApiServer = new SqlApiEmulator(global.environment.sqlapi.port, done);
|
||||
|
||||
redisClient = redis.createClient(global.environment.redis.port);
|
||||
});
|
||||
|
||||
var surrogateKeysCacheInvalidateFn = SurrogateKeysCache.prototype.invalidate;
|
||||
|
||||
beforeEach(function(done) {
|
||||
var postTemplateRequest = {
|
||||
url: '/tiles/template?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
|
||||
Step(
|
||||
function postTemplate() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
postTemplateRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function rePostTemplate(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, expectedBody);
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("update template calls surrogate keys invalidation", function(done) {
|
||||
var cacheEntryKey;
|
||||
var surrogateKeysCacheInvalidateMethodInvoked = false;
|
||||
SurrogateKeysCache.prototype.invalidate = function(cacheEntry) {
|
||||
cacheEntryKey = cacheEntry.key();
|
||||
surrogateKeysCacheInvalidateMethodInvoked = true;
|
||||
};
|
||||
|
||||
Step(
|
||||
function putValidTemplate() {
|
||||
var updateTemplateRequest = {
|
||||
url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234',
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
updateTemplateRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkValidUpdate(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, expectedBody);
|
||||
|
||||
assert.ok(surrogateKeysCacheInvalidateMethodInvoked);
|
||||
assert.equal(cacheEntryKey, new NamedMapsCacheEntry(templateOwner, templateName).key());
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.keys("map_*|localhost", function(err, keys) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.del(keys, function(err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("delete template calls surrogate keys invalidation", function(done) {
|
||||
|
||||
var cacheEntryKey;
|
||||
var surrogateKeysCacheInvalidateMethodInvoked = false;
|
||||
SurrogateKeysCache.prototype.invalidate = function(cacheEntry) {
|
||||
cacheEntryKey = cacheEntry.key();
|
||||
surrogateKeysCacheInvalidateMethodInvoked = true;
|
||||
};
|
||||
|
||||
Step(
|
||||
function putValidTemplate() {
|
||||
var deleteTemplateRequest = {
|
||||
url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
deleteTemplateRequest,
|
||||
{
|
||||
status: 204
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkValidUpdate(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
assert.ok(surrogateKeysCacheInvalidateMethodInvoked);
|
||||
assert.equal(cacheEntryKey, new NamedMapsCacheEntry(templateOwner, templateName).key());
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
SurrogateKeysCache.prototype.invalidate = surrogateKeysCacheInvalidateFn;
|
||||
done();
|
||||
});
|
||||
|
||||
suiteTeardown(function(done) {
|
||||
sqlApiServer.close(done);
|
||||
});
|
||||
|
||||
});
|
||||
72
test/acceptance/health_check.js
Normal file
72
test/acceptance/health_check.js
Normal file
@@ -0,0 +1,72 @@
|
||||
var helper = require(__dirname + '/../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
suite('health checks', function () {
|
||||
|
||||
beforeEach(function (done) {
|
||||
global.environment.health = {
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
done();
|
||||
});
|
||||
|
||||
var healthCheckRequest = {
|
||||
url: '/health',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
test('returns 200 and ok=true with enabled configuration', function (done) {
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
console.log(res.body);
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.ok(parsed.enabled);
|
||||
assert.ok(parsed.ok);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('fails for invalid user because it is not in redis', function (done) {
|
||||
global.environment.health.username = 'invalid';
|
||||
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 503
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.equal(parsed.enabled, true);
|
||||
assert.equal(parsed.ok, false);
|
||||
|
||||
assert.equal(parsed.result.redis.ok, false);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1324,6 +1324,49 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
|
||||
);
|
||||
});
|
||||
|
||||
var layergroupTtlRequest = {
|
||||
url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify({
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
})),
|
||||
method: 'GET',
|
||||
headers: {host: 'localhost'}
|
||||
};
|
||||
var layergroupTtlResponseExpectation = {
|
||||
status: 200
|
||||
};
|
||||
|
||||
test("cache control for layergroup default value", function(done) {
|
||||
global.environment.varnish.layergroupTtl = null;
|
||||
|
||||
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
|
||||
function(res) {
|
||||
assert.equal(res.headers['cache-control'], 'public,max-age=86400,must-revalidate');
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("cache control for layergroup uses configuration for max-age", function(done) {
|
||||
var layergroupTtl = 300;
|
||||
global.environment.varnish.layergroupTtl = layergroupTtl;
|
||||
|
||||
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
|
||||
function(res) {
|
||||
assert.equal(res.headers['cache-control'], 'public,max-age=' + layergroupTtl + ',must-revalidate');
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
suiteTeardown(function(done) {
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ var querystring = require('querystring');
|
||||
var semver = require('semver');
|
||||
var Step = require('step');
|
||||
var strftime = require('strftime');
|
||||
var NamedMapsCacheEntry = require(__dirname + '/../../lib/cartodb/cache/model/named_maps_entry');
|
||||
var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js');
|
||||
var redis_stats_db = 5;
|
||||
|
||||
@@ -57,6 +58,29 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
}
|
||||
};
|
||||
|
||||
function makeTemplate(templateName) {
|
||||
return {
|
||||
version: '0.0.1',
|
||||
name: templateName || 'acceptance1',
|
||||
auth: { method: 'open' },
|
||||
layergroup: {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator from test_table limit 2 offset 2',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function extendDefaultsTemplate(template) {
|
||||
return _.extend({}, template, {auth: {method: 'open'}, placeholders: {}});
|
||||
}
|
||||
|
||||
test("can add template, returning id", function(done) {
|
||||
|
||||
var errors = [];
|
||||
@@ -118,15 +142,12 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
if ( m.match(/^map_(tpl|crt)|/) )
|
||||
return m;
|
||||
});
|
||||
if ( todrop.length != 2 ) {
|
||||
if ( todrop.length !== 1 ) {
|
||||
errors.push(new Error("Unexpected keys in redis: " + todrop));
|
||||
} else {
|
||||
if ( todrop.indexOf('map_tpl|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_tpl|localhost' key in redis"));
|
||||
}
|
||||
if ( todrop.indexOf('map_crt|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_crt|localhost' key in redis"));
|
||||
}
|
||||
}
|
||||
redis_client.del(todrop, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
@@ -457,15 +478,12 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
if ( m.match(/^map_(tpl|crt)|/) )
|
||||
return m;
|
||||
});
|
||||
if ( todrop.length != 2 ) {
|
||||
if ( todrop.length !== 1 ) {
|
||||
errors.push(new Error("Unexpected keys in redis: " + todrop));
|
||||
} else {
|
||||
if ( todrop.indexOf('map_tpl|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_tpl|localhost' key in redis"));
|
||||
}
|
||||
if ( todrop.indexOf('map_crt|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_crt|localhost' key in redis"));
|
||||
}
|
||||
}
|
||||
redis_client.del(todrop, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
@@ -492,7 +510,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
url: '/tiles/template?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(template_acceptance1)
|
||||
data: JSON.stringify(makeTemplate())
|
||||
}
|
||||
assert.response(server, post_request, {},
|
||||
function(res) { next(null, res); });
|
||||
@@ -530,7 +548,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
url: '/tiles/template/unexistent/?api_key=1234',
|
||||
method: 'PUT',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(template_acceptance1)
|
||||
data: JSON.stringify(makeTemplate())
|
||||
}
|
||||
var next = this;
|
||||
assert.response(server, put_request, {},
|
||||
@@ -548,7 +566,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
url: '/tiles/template/' + tpl_id + '/?api_key=1234',
|
||||
method: 'PUT',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(template_acceptance1)
|
||||
data: JSON.stringify(makeTemplate())
|
||||
}
|
||||
var next = this;
|
||||
assert.response(server, put_request, {},
|
||||
@@ -572,15 +590,12 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
if ( m.match(/^map_(tpl|crt)|/) )
|
||||
return m;
|
||||
});
|
||||
if ( todrop.length != 2 ) {
|
||||
if ( todrop.length !== 1 ) {
|
||||
errors.push(new Error("Unexpected keys in redis: " + todrop));
|
||||
} else {
|
||||
if ( todrop.indexOf('map_tpl|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_tpl|localhost' key in redis"));
|
||||
}
|
||||
if ( todrop.indexOf('map_crt|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_crt|localhost' key in redis"));
|
||||
}
|
||||
}
|
||||
redis_client.del(todrop, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
@@ -607,7 +622,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
url: '/tiles/template?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(template_acceptance1)
|
||||
data: JSON.stringify(makeTemplate())
|
||||
}
|
||||
assert.response(server, post_request, {},
|
||||
function(res) { next(null, res); });
|
||||
@@ -653,7 +668,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('template'),
|
||||
"Missing 'template' from response body: " + res.body);
|
||||
assert.deepEqual(template_acceptance1, parsed.template);
|
||||
assert.deepEqual(extendDefaultsTemplate(makeTemplate()), parsed.template);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -664,15 +679,12 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
if ( m.match(/^map_(tpl|crt)|/) )
|
||||
return m;
|
||||
});
|
||||
if ( todrop.length != 2 ) {
|
||||
if ( todrop.length !== 1 ) {
|
||||
errors.push(new Error("Unexpected keys in redis: " + todrop));
|
||||
} else {
|
||||
if ( todrop.indexOf('map_tpl|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_tpl|localhost' key in redis"));
|
||||
}
|
||||
if ( todrop.indexOf('map_crt|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_crt|localhost' key in redis"));
|
||||
}
|
||||
}
|
||||
redis_client.del(todrop, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
@@ -699,7 +711,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
url: '/tiles/template?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(template_acceptance1)
|
||||
data: JSON.stringify(makeTemplate())
|
||||
}
|
||||
assert.response(server, post_request, {},
|
||||
function(res) { next(null, res); });
|
||||
@@ -728,7 +740,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('template'),
|
||||
"Missing 'template' from response body: " + res.body);
|
||||
assert.deepEqual(template_acceptance1, parsed.template);
|
||||
assert.deepEqual(extendDefaultsTemplate(makeTemplate()), parsed.template);
|
||||
var del_request = {
|
||||
url: '/tiles/template/' + tpl_id,
|
||||
method: 'DELETE',
|
||||
@@ -1011,16 +1023,10 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
assert.response(server, get_request, {},
|
||||
function(res) { next(null, res); });
|
||||
},
|
||||
function checkTileDeleted(err, res) {
|
||||
function checkTileAvailable(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(res.statusCode, 403,
|
||||
'Unexpected statusCode fetch tile after signature revokal: '
|
||||
+ res.statusCode + ':' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('error'),
|
||||
"Missing 'error' from response body: " + res.body);
|
||||
assert.ok(parsed.error.match(/permission denied/i),
|
||||
'Unexpected error for unauthorized access : ' + parsed.error);
|
||||
assert.equal(res.statusCode, 200, 'Tile should be accessible');
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -1230,16 +1236,10 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
assert.response(server, get_request, {},
|
||||
function(res) { next(null, res); });
|
||||
},
|
||||
function checkTileDeleted(err, res) {
|
||||
function checkTorqueTileAvailable(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(res.statusCode, 403,
|
||||
'Unexpected statusCode fetch tile after signature revokal: '
|
||||
+ res.statusCode + ':' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('error'),
|
||||
"Missing 'error' from response body: " + res.body);
|
||||
assert.ok(parsed.error.match(/permission denied/i),
|
||||
'Unexpected error for unauthorized access : ' + parsed.error);
|
||||
assert.equal(res.statusCode, 200, 'Torque tile should be accessible');
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -1426,16 +1426,10 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
assert.response(server, get_request, {},
|
||||
function(res) { next(null, res); });
|
||||
},
|
||||
function checkTileDeleted(err, res) {
|
||||
function checkLayerAttributesAvailable(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(res.statusCode, 403,
|
||||
'Unexpected statusCode fetch tile after signature revokal: '
|
||||
+ res.statusCode + ':' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('error'),
|
||||
"Missing 'error' from response body: " + res.body);
|
||||
assert.ok(parsed.error.match(/permission denied/i),
|
||||
'Unexpected error for unauthorized access : ' + parsed.error);
|
||||
assert.equal(res.statusCode, 200, 'Layer attributes should be accessible');
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -1597,6 +1591,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
|
||||
helper.checkCache(res);
|
||||
helper.checkSurrogateKey(res, new NamedMapsCacheEntry('localhost', template_acceptance_open.name).key());
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -1612,10 +1607,9 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
version: '0.0.1',
|
||||
name: 'acceptance_open_jsonp_params',
|
||||
auth: { method: 'open' },
|
||||
/*
|
||||
placeholders: {
|
||||
color: { type: "css_color", default: "red" }
|
||||
},*/
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
@@ -1668,9 +1662,11 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
function checkInstanciation(err, res)
|
||||
{
|
||||
if ( err ) throw err;
|
||||
console.log(err, res.body, res.headers);
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
|
||||
helper.checkNoCache(res);
|
||||
helper.checkCache(res);
|
||||
helper.checkSurrogateKey(res, new NamedMapsCacheEntry('localhost', template_acceptance_open.name).key());
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -1852,6 +1848,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
assert.ok(parsed.hasOwnProperty('layergroupid'),
|
||||
"Missing 'layergroupid' from response body: " + res.body);
|
||||
layergroupid = parsed.layergroupid;
|
||||
helper.checkSurrogateKey(res, new NamedMapsCacheEntry('localhost', template_acceptance2.name).key());
|
||||
return null;
|
||||
},
|
||||
function updateTemplate(err, res)
|
||||
@@ -1896,6 +1893,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
assert.ok(parsed.hasOwnProperty('layergroupid'),
|
||||
"Missing 'layergroupid' from response body: " + res.body);
|
||||
assert.ok(layergroupid != parsed.layergroupid);
|
||||
helper.checkSurrogateKey(res, new NamedMapsCacheEntry('localhost', template_acceptance2.name).key());
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -1906,15 +1904,12 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio
|
||||
if ( m.match(/^map_(tpl|crt)|/) )
|
||||
return m;
|
||||
});
|
||||
if ( todrop.length != 2 ) {
|
||||
if ( todrop.length !== 1 ) {
|
||||
errors.push(new Error("Unexpected keys in redis: " + todrop));
|
||||
} else {
|
||||
if ( todrop.indexOf('map_tpl|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_tpl|localhost' key in redis"));
|
||||
}
|
||||
if ( todrop.indexOf('map_crt|localhost') == -1 ) {
|
||||
errors.push(new Error("Missing 'map_crt|localhost' key in redis"));
|
||||
}
|
||||
}
|
||||
redis_client.del(todrop, function(err) {
|
||||
if ( err ) errors.push(err.message);
|
||||
|
||||
@@ -50,10 +50,16 @@ function checkCache(res) {
|
||||
assert.ok(res.headers.hasOwnProperty('last-modified'));
|
||||
}
|
||||
|
||||
function checkSurrogateKey(res, expectedKey) {
|
||||
assert.ok(res.headers.hasOwnProperty('surrogate-key'));
|
||||
assert.equal(res.headers['surrogate-key'], expectedKey);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
lzma_compress_to_base64: lzma_compress_to_base64,
|
||||
checkNoCache: checkNoCache,
|
||||
checkSurrogateKey: checkSurrogateKey,
|
||||
checkCache: checkCache
|
||||
};
|
||||
|
||||
|
||||
29
test/unit/cartodb/cache/model/named_maps_entry.test.js
vendored
Normal file
29
test/unit/cartodb/cache/model/named_maps_entry.test.js
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var NamedMapsCacheEntry = require('../../../../../lib/cartodb/cache/model/named_maps_entry');
|
||||
|
||||
suite('cache named_maps_entry', function() {
|
||||
|
||||
var namedMapOwner = 'foo',
|
||||
namedMapName = 'wadus_name',
|
||||
namedMapsCacheEntry = new NamedMapsCacheEntry(namedMapOwner, namedMapName),
|
||||
entryKey = namedMapsCacheEntry.key();
|
||||
|
||||
test('key is a string', function() {
|
||||
assert.ok(_.isString(entryKey));
|
||||
});
|
||||
|
||||
test('key is 8 chars length', function() {
|
||||
assert.equal(entryKey.length, 8);
|
||||
var entryKeyParts = entryKey.split(':');
|
||||
assert.equal(entryKeyParts.length, 2);
|
||||
assert.equal(entryKeyParts[0], 'n');
|
||||
});
|
||||
|
||||
test('key is name spaced for named maps', function() {
|
||||
var entryKeyParts = entryKey.split(':');
|
||||
assert.equal(entryKeyParts.length, 2);
|
||||
assert.equal(entryKeyParts[0], 'n');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
var assert = require('assert')
|
||||
//, _ = require('underscore')
|
||||
, RedisPool = require('redis-mpool')
|
||||
, SignedMaps = require('../../../lib/cartodb/signed_maps.js')
|
||||
, test_helper = require('../../support/test_helper')
|
||||
, Step = require('step')
|
||||
, tests = module.exports = {};
|
||||
|
||||
suite('signed_maps', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redis_pool = RedisPool(global.environment.redis);
|
||||
|
||||
test('can sign map with open and token-based auth', function(done) {
|
||||
var smap = new SignedMaps(redis_pool);
|
||||
assert.ok(smap);
|
||||
var sig = 'sig1';
|
||||
var map = 'map1';
|
||||
var tok = 'tok1';
|
||||
var crt = {
|
||||
version:'0.0.1',
|
||||
layergroup_id:map,
|
||||
auth: {}
|
||||
};
|
||||
var crt1_id; // by token
|
||||
var crt2_id; // open
|
||||
Step(
|
||||
function() {
|
||||
smap.isAuthorized(sig,map,tok,this);
|
||||
},
|
||||
function checkAuthFailure1(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(!authorized, "unexpectedly authorized");
|
||||
crt.auth.method = 'token';
|
||||
crt.auth.valid_tokens = [tok];
|
||||
smap.addSignature(sig, map, crt, this)
|
||||
},
|
||||
function getCert1(err, id) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(id, "undefined signature id");
|
||||
crt1_id = id; // keep note of it
|
||||
//console.log("Certificate 1 is " + crt1_id);
|
||||
smap.isAuthorized(sig,map,'',this);
|
||||
},
|
||||
function checkAuthFailure2(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(!authorized, "unexpectedly authorized");
|
||||
smap.isAuthorized(sig,map,tok,this);
|
||||
},
|
||||
function checkAuthSuccess1(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(authorized, "unauthorized :(");
|
||||
crt.auth.method = 'open';
|
||||
delete crt.auth.valid_tokens;
|
||||
smap.addSignature(sig, map, crt, this)
|
||||
},
|
||||
function getCert2(err, id) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(id, "undefined signature id");
|
||||
crt2_id = id; // keep note of it
|
||||
//console.log("Certificate 2 is " + crt2_id);
|
||||
smap.isAuthorized(sig,map,'arbitrary',this);
|
||||
},
|
||||
function checkAuthSuccess2_delCert2(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(authorized, "unauthorized :(");
|
||||
var next = this;
|
||||
smap.delCertificate(sig, crt2_id, function(e) {
|
||||
if (e) next(e);
|
||||
else smap.isAuthorized(sig,map,'arbitrary',next);
|
||||
});
|
||||
},
|
||||
function checkAuthFailure3_delCert2(err, authorized) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(!authorized, "unexpectedly authorized");
|
||||
smap.delCertificate(sig, crt1_id, this);
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('can validate certificates', function(done) {
|
||||
var smap = new SignedMaps(redis_pool);
|
||||
assert.ok(smap);
|
||||
Step(
|
||||
function invalidVersion() {
|
||||
var cert = { version: '-1' };
|
||||
var err = smap.checkInvalidCertificate(cert);
|
||||
assert.ok(err);
|
||||
assert.equal(err.message, "Unsupported certificate version -1");
|
||||
return null;
|
||||
},
|
||||
function invalidTokenAuth() {
|
||||
var cert = { version: '0.0.1', auth: { method:'token', valid_token:[] } };
|
||||
var err = smap.checkInvalidCertificate(cert);
|
||||
assert.ok(err);
|
||||
assert.equal(err.message, "Invalid 'token' authentication: missing valid_tokens");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -1,31 +1,42 @@
|
||||
var assert = require('assert')
|
||||
//, _ = require('underscore')
|
||||
, RedisPool = require('redis-mpool')
|
||||
, SignedMaps = require('../../../lib/cartodb/signed_maps.js')
|
||||
, TemplateMaps = require('../../../lib/cartodb/template_maps.js')
|
||||
, test_helper = require('../../support/test_helper')
|
||||
, Step = require('step')
|
||||
, _ = require('underscore')
|
||||
, tests = module.exports = {};
|
||||
|
||||
suite('template_maps', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redis_pool = RedisPool(global.environment.redis);
|
||||
var signed_maps = new SignedMaps(redis_pool);
|
||||
|
||||
var wadusLayer = {
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var validTemplate = {
|
||||
version:'0.0.1',
|
||||
name: 'first',
|
||||
auth: {},
|
||||
layergroup: {}
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
var owner = 'me';
|
||||
|
||||
test('does not accept template with unsupported version', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'6.6.6',
|
||||
name:'k', auth: {}, layergroup: {} };
|
||||
name:'k', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
Step(
|
||||
function() {
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
@@ -42,10 +53,10 @@ suite('template_maps', function() {
|
||||
});
|
||||
|
||||
test('does not accept template with missing name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
auth: {}, layergroup: {} };
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
Step(
|
||||
function() {
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
@@ -62,10 +73,10 @@ suite('template_maps', function() {
|
||||
});
|
||||
|
||||
test('does not accept template with invalid name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
auth: {}, layergroup: {} };
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var invalidnames = [ "ab|", "a b", "a@b", "1ab", "_x", "", " x", "x " ];
|
||||
var testNext = function() {
|
||||
if ( ! invalidnames.length ) { done(); return; }
|
||||
@@ -88,11 +99,11 @@ suite('template_maps', function() {
|
||||
});
|
||||
|
||||
test('does not accept template with invalid placeholder name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "valid", placeholders: {},
|
||||
auth: {}, layergroup: {} };
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var invalidnames = [ "ab|", "a b", "a@b", "1ab", "_x", "", " x", "x " ];
|
||||
var testNext = function() {
|
||||
if ( ! invalidnames.length ) { done(); return; }
|
||||
@@ -116,11 +127,11 @@ suite('template_maps', function() {
|
||||
});
|
||||
|
||||
test('does not accept template with missing placeholder default', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "valid", placeholders: { v: {} },
|
||||
auth: {}, layergroup: {} };
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with missing placeholder default"));
|
||||
@@ -136,11 +147,11 @@ suite('template_maps', function() {
|
||||
});
|
||||
|
||||
test('does not accept template with missing placeholder type', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "valid", placeholders: { v: { default:1 } },
|
||||
auth: {}, layergroup: {} };
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with missing placeholder type"));
|
||||
@@ -158,11 +169,11 @@ suite('template_maps', function() {
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/128
|
||||
test('does not accept template with invalid token auth (undefined tokens)',
|
||||
function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "invalid_auth1", placeholders: { },
|
||||
auth: { method: 'token' }, layergroup: {} };
|
||||
auth: { method: 'token' }, layergroup: {layers:[wadusLayer]} };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with invalid token auth (undefined tokens)"));
|
||||
@@ -178,12 +189,12 @@ suite('template_maps', function() {
|
||||
});
|
||||
|
||||
test('add, get and delete a valid template', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var expected_failure = false;
|
||||
var tpl_id;
|
||||
var tpl = { version:'0.0.1',
|
||||
name: 'first', auth: {}, layergroup: {} };
|
||||
name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
Step(
|
||||
function() {
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
@@ -204,7 +215,7 @@ suite('template_maps', function() {
|
||||
},
|
||||
function delTemplate(err, got_tpl) {
|
||||
if ( err ) throw err;
|
||||
assert.deepEqual(got_tpl, tpl);
|
||||
assert.deepEqual(got_tpl, _.extend({}, tpl, {auth: {method: 'open'}, placeholders: {}}));
|
||||
tmap.delTemplate('me', tpl_id, this);
|
||||
},
|
||||
function finish(err) {
|
||||
@@ -214,12 +225,12 @@ suite('template_maps', function() {
|
||||
});
|
||||
|
||||
test('add multiple templates, list them', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var expected_failure = false;
|
||||
var tpl1 = { version:'0.0.1', name: 'first', auth: {}, layergroup: {} };
|
||||
var tpl1 = { version:'0.0.1', name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var tpl1_id;
|
||||
var tpl2 = { version:'0.0.1', name: 'second', auth: {}, layergroup: {} };
|
||||
var tpl2 = { version:'0.0.1', name: 'second', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var tpl2_id;
|
||||
Step(
|
||||
function addTemplate1() {
|
||||
@@ -273,14 +284,14 @@ suite('template_maps', function() {
|
||||
});
|
||||
|
||||
test('update templates', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var expected_failure = false;
|
||||
var owner = 'me';
|
||||
var tpl = { version:'0.0.1',
|
||||
name: 'first',
|
||||
auth: { method: 'open' },
|
||||
layergroup: {}
|
||||
layergroup: {layers:[wadusLayer]}
|
||||
};
|
||||
var tpl_id;
|
||||
Step(
|
||||
@@ -333,7 +344,7 @@ suite('template_maps', function() {
|
||||
});
|
||||
|
||||
test('instanciate templates', function() {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps);
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
|
||||
var tpl1 = {
|
||||
@@ -431,11 +442,11 @@ suite('template_maps', function() {
|
||||
|
||||
// Can set a limit on the number of user templates
|
||||
test('can limit number of user templates', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, signed_maps, {
|
||||
var tmap = new TemplateMaps(redis_pool, {
|
||||
max_user_templates: 2
|
||||
});
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1', auth: {}, layergroup: {} };
|
||||
var tpl = { version:'0.0.1', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var expectErr = false;
|
||||
var idMe = [];
|
||||
var idYou = [];
|
||||
@@ -511,88 +522,4 @@ suite('template_maps', function() {
|
||||
);
|
||||
});
|
||||
|
||||
var redisCmdFunc = TemplateMaps.prototype._redisCmd;
|
||||
|
||||
function runWithRedisStubbed(stubbedCommands, func) {
|
||||
TemplateMaps.prototype._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
redisFunc = redisFunc.toLowerCase();
|
||||
if (stubbedCommands.hasOwnProperty(redisFunc)) {
|
||||
callback(null, stubbedCommands[redisFunc]);
|
||||
} else {
|
||||
throw 'Unknown command';
|
||||
}
|
||||
};
|
||||
|
||||
func();
|
||||
|
||||
TemplateMaps.prototype._redisCmd = redisCmdFunc;
|
||||
}
|
||||
|
||||
test('_obtainTemplateLock with no previous value, happy case', function(done) {
|
||||
runWithRedisStubbed({hget: null, hset: 1}, function() {
|
||||
var templateMaps = new TemplateMaps(redis_pool, signed_maps);
|
||||
|
||||
templateMaps._obtainTemplateLock(owner, validTemplate.name, function(err, gotLock) {
|
||||
assert.ok(!err);
|
||||
assert.ok(gotLock);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('_obtainTemplateLock no lock for non expired ttl, simulates obtaining two locks at same time', function(done) {
|
||||
runWithRedisStubbed({hget: Date.now()}, function() {
|
||||
var templateMaps = new TemplateMaps(redis_pool, signed_maps);
|
||||
|
||||
templateMaps._obtainTemplateLock(owner, validTemplate.name, function(err, gotLock) {
|
||||
assert.ok(!!err);
|
||||
assert.equal(gotLock, false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('_obtainTemplateLock no lock for non expired ttl, last millisecond of valid ttl', function(done) {
|
||||
var nowValue = Date.now(),
|
||||
nowFunc = Date.now;
|
||||
Date.now = function() {
|
||||
return nowValue;
|
||||
};
|
||||
var lockTtl = 1000;
|
||||
runWithRedisStubbed({hget: Date.now() - lockTtl, hset: true}, function() {
|
||||
var templateMaps = new TemplateMaps(redis_pool, signed_maps, {lock_ttl: lockTtl});
|
||||
|
||||
templateMaps._obtainTemplateLock(owner, validTemplate.name, function(err, gotLock) {
|
||||
assert.ok(!!err);
|
||||
assert.equal(gotLock, false);
|
||||
|
||||
Date.now = nowFunc;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('_obtainTemplateLock gets lock for expired ttl, first millisecond of invalid ttl', function(done) {
|
||||
var nowValue = Date.now(),
|
||||
nowFunc = Date.now;
|
||||
Date.now = function() {
|
||||
return nowValue;
|
||||
};
|
||||
var lockTtl = 1000;
|
||||
runWithRedisStubbed({hget: Date.now() - lockTtl - 1, hset: true}, function() {
|
||||
var templateMaps = new TemplateMaps(redis_pool, signed_maps, {lock_ttl: lockTtl});
|
||||
|
||||
templateMaps._obtainTemplateLock(owner, validTemplate.name, function(err, gotLock) {
|
||||
assert.ok(!err);
|
||||
assert.ok(gotLock);
|
||||
|
||||
Date.now = nowFunc;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
95
test/unit/cartodb/template_maps_auth.test.js
Normal file
95
test/unit/cartodb/template_maps_auth.test.js
Normal file
@@ -0,0 +1,95 @@
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps');
|
||||
var test_helper = require('../../support/test_helper');
|
||||
var Step = require('step');
|
||||
var tests = module.exports = {};
|
||||
|
||||
suite('template_maps_auth', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis),
|
||||
templateMaps = new TemplateMaps(redisPool, {max_user_templates: 1000});
|
||||
|
||||
function makeTemplate(method, validTokens) {
|
||||
var template = {
|
||||
name: 'wadus_template',
|
||||
auth: {
|
||||
method: method
|
||||
}
|
||||
};
|
||||
|
||||
if (method === 'token') {
|
||||
template.auth.valid_tokens = validTokens || [];
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
var methodToken = 'token',
|
||||
methodOpen = 'open';
|
||||
|
||||
var tokenFoo = 'foo',
|
||||
tokenBar = 'bar';
|
||||
|
||||
var authorizationTestScenarios = [
|
||||
{
|
||||
desc: 'open method is always authorized',
|
||||
template: makeTemplate(methodOpen),
|
||||
token: undefined,
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
desc: 'token method is authorized for valid token',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: tokenFoo,
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
desc: 'token method not authorized for invalid token',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: tokenBar,
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'token method is authorized for valid token array',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: [tokenFoo],
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
desc: 'token method not authorized for invalid token array',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: [tokenBar],
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'wadus method not authorized',
|
||||
template: makeTemplate('wadus', [tokenFoo]),
|
||||
token: tokenFoo,
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'undefined template result in not authorized',
|
||||
template: undefined,
|
||||
token: tokenFoo,
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'undefined template auth result in not authorized',
|
||||
template: {},
|
||||
token: tokenFoo,
|
||||
expected: false
|
||||
}
|
||||
];
|
||||
|
||||
authorizationTestScenarios.forEach(function(testScenario) {
|
||||
test(testScenario.desc, function(done) {
|
||||
var debugMessage = testScenario.expected ? 'should be authorized' : 'unexpectedly authorized';
|
||||
var result = templateMaps.isAuthorized(testScenario.template, testScenario.token);
|
||||
assert.equal(result, testScenario.expected, debugMessage);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
106
test/unit/cartodb/template_maps_defaults.test.js
Normal file
106
test/unit/cartodb/template_maps_defaults.test.js
Normal file
@@ -0,0 +1,106 @@
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var test_helper = require('../../support/test_helper');
|
||||
var _ = require('underscore');
|
||||
|
||||
suite('template_maps', function() {
|
||||
|
||||
var redisPool = new RedisPool(global.environment.redis),
|
||||
templateMaps = new TemplateMaps(redisPool);
|
||||
|
||||
var owner = 'me';
|
||||
var templateName = 'wadus';
|
||||
|
||||
|
||||
var defaultTemplate = {
|
||||
version:'0.0.1',
|
||||
name: templateName,
|
||||
layergroup: {
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function makeTemplate(auth, placeholders) {
|
||||
return _.extend({}, defaultTemplate, {
|
||||
auth: auth,
|
||||
placeholders: placeholders
|
||||
});
|
||||
}
|
||||
|
||||
var defaultAuth = {
|
||||
method: 'open'
|
||||
};
|
||||
|
||||
var authTokenSample = {
|
||||
method: 'token',
|
||||
valid_tokens: ['wadus_token']
|
||||
};
|
||||
|
||||
var placeholdersSample = {
|
||||
wadus: {
|
||||
type: 'number',
|
||||
default: 1
|
||||
}
|
||||
};
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'default auth and placeholders values',
|
||||
template: defaultTemplate,
|
||||
expected: {
|
||||
auth: defaultAuth,
|
||||
placeholders: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'default placeholders but specified auth',
|
||||
template: makeTemplate(authTokenSample),
|
||||
expected: {
|
||||
auth: authTokenSample,
|
||||
placeholders: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'default auth but specified placeholders',
|
||||
template: makeTemplate(undefined, placeholdersSample),
|
||||
expected: {
|
||||
auth: defaultAuth,
|
||||
placeholders: placeholdersSample
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'specified auth and placeholders',
|
||||
template: makeTemplate(authTokenSample, placeholdersSample),
|
||||
expected: {
|
||||
auth: authTokenSample,
|
||||
placeholders: placeholdersSample
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
test('adding template returns a new instance with ' + testScenario.desc, function(done) {
|
||||
|
||||
templateMaps.addTemplate(owner, testScenario.template, function(err, templateId, template) {
|
||||
assert.ok(!err, 'Unexpected error adding template: ' + (err && err.message));
|
||||
assert.ok(testScenario.template !== template, 'template instances should be different');
|
||||
assert.equal(template.name, templateName);
|
||||
assert.deepEqual(template.auth, testScenario.expected.auth);
|
||||
assert.deepEqual(template.placeholders, testScenario.expected.placeholders);
|
||||
|
||||
templateMaps.delTemplate(owner, templateName, done);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
112
test/unit/cartodb/valid_template_maps.test.js
Normal file
112
test/unit/cartodb/valid_template_maps.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var test_helper = require('../../support/test_helper');
|
||||
var _ = require('underscore');
|
||||
|
||||
suite('template_maps', function() {
|
||||
|
||||
var redisPool = new RedisPool(global.environment.redis),
|
||||
templateMaps = new TemplateMaps(redisPool);
|
||||
|
||||
var owner = 'me';
|
||||
var templateName = 'wadus';
|
||||
|
||||
|
||||
var defaultTemplate = {
|
||||
version:'0.0.1',
|
||||
name: templateName
|
||||
};
|
||||
|
||||
function makeTemplate(layers) {
|
||||
var layergroup = {
|
||||
layers: layers
|
||||
};
|
||||
return _.extend({}, defaultTemplate, {
|
||||
layergroup: layergroup
|
||||
});
|
||||
}
|
||||
|
||||
var layerWithMissingOptions = {},
|
||||
minimumValidLayer = {
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'Missing layers array does not validate',
|
||||
template: makeTemplate(),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing or empty layers array from layergroup config'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'Empty layers array does not validate',
|
||||
template: makeTemplate([]),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing or empty layers array from layergroup config'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'Layer with missing options does not validate',
|
||||
template: makeTemplate([
|
||||
layerWithMissingOptions
|
||||
]),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing `options` in layergroup config for layers: 0'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'Multiple layers report invalid layer',
|
||||
template: makeTemplate([
|
||||
minimumValidLayer,
|
||||
layerWithMissingOptions
|
||||
]),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing `options` in layergroup config for layers: 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'default auth but specified placeholders',
|
||||
template: makeTemplate([
|
||||
minimumValidLayer
|
||||
]),
|
||||
expected: {
|
||||
isValid: true,
|
||||
message: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
test(testScenario.desc, function(done) {
|
||||
|
||||
templateMaps.addTemplate(owner, testScenario.template, function(err) {
|
||||
|
||||
if (testScenario.expected.isValid) {
|
||||
|
||||
assert.ok(!err);
|
||||
templateMaps.delTemplate(owner, templateName, done);
|
||||
|
||||
} else {
|
||||
|
||||
assert.ok(err);
|
||||
assert.equal(err.message, testScenario.expected.message);
|
||||
done();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user