Compare commits

..

161 Commits

Author SHA1 Message Date
Raul Ochoa
e9195967a4 Release 1.26.0 2015-01-27 17:50:13 +01:00
Raul Ochoa
30c6a390ac Upgrades windshaft to 0.35.0, supports mapconfig version 1.3.0 2015-01-27 17:49:08 +01:00
Raul Ochoa
3a97af767f Stubs next version 2015-01-26 17:33:22 +01:00
Raul Ochoa
57dd36a476 Release 1.25.0 2015-01-26 17:30:51 +01:00
Raul Ochoa
6ab6fd91e4 Merge pull request #248 from CartoDB/196-validate-layergroup-in-named-maps
Basic layergroup validation on named map creation/update
2015-01-26 17:04:48 +01:00
Raul Ochoa
c41c223b84 Merge pull request #249 from CartoDB/247-add-named-maps-surrogate-keys
Add named maps surrogate keys and call invalidation on template modification/deletion
2015-01-26 17:04:25 +01:00
Raul Ochoa
7e2be7b30f Document varnish configuration 2015-01-26 16:27:59 +01:00
Raul Ochoa
e690170689 More exhaustive layergroup validation:
- layers is an array and it's not empty
- layers has at least options
2015-01-26 15:51:10 +01:00
Raul Ochoa
81f1b0dcf8 Adds tests for named maps surrogate keys and for invalidation 2015-01-26 15:02:28 +01:00
Raul Ochoa
146a2b2606 Merge branch 'master' into 247-add-named-maps-surrogate-keys 2015-01-26 13:19:03 +01:00
Raul Ochoa
ff811ac1b5 Merge pull request #246 from CartoDB/238-drop-signed-maps
Drop signed maps
2015-01-26 13:13:27 +01:00
Raul Ochoa
11d9f5dd76 Basic layergroup validation on named map creation/update 2015-01-23 18:24:25 +01:00
Raul Ochoa
571a635fed Old style, avoid merge conflicts, missing history 2015-01-23 17:46:58 +01:00
Raul Ochoa
6e70518146 Split between old cache_enabled and new purge_enabled configuration 2015-01-23 17:46:16 +01:00
Raul Ochoa
fabb438cf0 Escape \b for the regex 2015-01-23 17:22:49 +01:00
Raul Ochoa
0abd6a2293 Adds check for surrogate key headers in template instances
p.s. it fixes instantiate template with params test
2015-01-23 17:02:13 +01:00
Raul Ochoa
272e8cd221 Adds Surrogate Keys to named maps 2015-01-23 16:37:38 +01:00
Raul Ochoa
885accdadf Adds varnish http port to the default configurations 2015-01-23 16:36:45 +01:00
Raul Ochoa
f5a3b77737 Make TemplateMaps to emit messages when adding/updating/deleting templates 2015-01-23 16:35:47 +01:00
Raul Ochoa
56abcfd2f4 Update documentation to remove references to signed maps 2015-01-23 11:33:58 +01:00
Raul Ochoa
ecd570323b Merge branch 'master' into 238-drop-signed-maps 2015-01-22 19:37:03 +01:00
Raul Ochoa
20eb92a3b1 Remove signedmaps and locks functionality as it is no longer needed 2015-01-22 19:28:59 +01:00
Raul Ochoa
8d22ed7594 Tests to validate template instantiation returns new instances with
default values if they are missing.
2015-01-22 18:38:42 +01:00
Raul Ochoa
3321987c33 Merge pull request #245 from CartoDB/apiUpdate
API doc update
2015-01-22 18:00:02 +01:00
Raul Ochoa
981be0edd5 Replace signed maps auth tests with template maps tests 2015-01-22 17:55:47 +01:00
Raul Ochoa
e8ab3a48c6 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_toknes, 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 the deleting it, resulting in
        in a new template
        * Templates already instantiated will be accessible thrught their
        layergroup so it is possible to continue requesting tiles/grids/etc.
 - Authorization is now handled by template maps
2015-01-22 15:40:40 +01:00
Andy Eschbacher
58a54de5a6 Added section on CartoDB.js use 2015-01-21 09:29:57 -05:00
Andy Eschbacher
21b1cea5e8 updates 2015-01-21 09:19:50 -05:00
Raul Ochoa
64b5a64e1b Add templateMaps to serveroptions for the time being 2015-01-21 11:44:06 +01:00
Raul Ochoa
f1b6be1ecb Merge branch 'master' into 238-drop-signed-maps 2015-01-21 11:42:42 +01:00
Raul Ochoa
ac84fc569f Merge pull request #244 from CartoDB/docs-anonymous-maps-jsonp-example
Add jsonp example for anonymous maps
2015-01-21 11:25:37 +01:00
Raul Ochoa
4bdc43ff7c Put back curl command 2015-01-21 11:21:16 +01:00
Raul Ochoa
3afbbccfa2 Add jsonp example for anonymous maps 2015-01-21 11:08:34 +01:00
Raul Ochoa
8bc08d75b7 Separate signed maps instantiation 2015-01-20 18:40:56 +01:00
Raul Ochoa
c14157acc2 Moves template routing 2015-01-20 18:16:09 +01:00
Raul Ochoa
595dac57a0 Moves setDBParams into controller 2015-01-20 18:14:10 +01:00
Raul Ochoa
5632b19e16 Remove unused functionality from app 2015-01-20 18:13:36 +01:00
Raul Ochoa
007196555d Use userByReq from serverOptions 2015-01-20 18:12:24 +01:00
Raul Ochoa
62ffc05ef4 Move template map instantiation into controller 2015-01-20 17:57:53 +01:00
Raul Ochoa
5962141114 Moves template options to controller 2015-01-20 17:45:47 +01:00
Raul Ochoa
7901a05b55 List templates moved into controller 2015-01-20 17:39:33 +01:00
Raul Ochoa
4c2a0ca048 Delete moved to controller 2015-01-20 17:34:23 +01:00
Raul Ochoa
b40c8e6624 Retrieve template moved to controller 2015-01-20 17:17:06 +01:00
Raul Ochoa
97d3b1a03b Move update template to controller 2015-01-20 17:07:55 +01:00
Raul Ochoa
fcea0c9b83 Move template creation to controller 2015-01-20 16:58:12 +01:00
Raul Ochoa
7ce8737e75 Initial split template maps endpoint into its own controller 2015-01-20 16:56:06 +01:00
Raul Ochoa
1d91f0fca9 Stubs next version 2015-01-15 17:37:51 +01:00
Raul Ochoa
23fe7fb0f7 Release 1.24.0 2015-01-15 17:36:48 +01:00
Raul Ochoa
1880b5d261 Merge pull request #243 from CartoDB/retina-support
Retina support for mapnik layers
2015-01-15 17:33:54 +01:00
Raul Ochoa
cf004322fd Upgrades windshaft to 0.34.0 for retina support 2015-01-15 17:21:24 +01:00
Raul Ochoa
30d8f28221 Use retina branch from windshaft 2015-01-14 18:26:28 +01:00
Raul Ochoa
caa05e779a Add scale_factor param as valid one 2015-01-14 18:11:13 +01:00
Raul Ochoa
f13fec13b8 Stubs next version 2015-01-14 16:42:41 +01:00
Raul Ochoa
a93f346948 Release 1.23.1 2015-01-14 16:41:46 +01:00
Raul Ochoa
48d44bada1 Regenerate npm-shrinkwrap.json 2015-01-14 16:18:31 +01:00
Raul Ochoa
a20d08ddc8 Stubs next version 2015-01-14 15:52:20 +01:00
Raul Ochoa
4f18e31af5 Release 1.23.0 2015-01-14 15:50:59 +01:00
Raul Ochoa
41f6a172ee Merge pull request #242 from CartoDB/static-controllers
Upgrade windshaft for static previews
2015-01-14 15:45:22 +01:00
Raul Ochoa
1776d31ba4 Upgrades windshaft to 0.33.0 2015-01-14 15:32:59 +01:00
Raul Ochoa
845ebcac15 Merge branch 'master' into static-controllers
Conflicts:
	npm-shrinkwrap.json
2015-01-13 12:45:37 +01:00
Raul Ochoa
45f73d4be8 Stubs next version 2015-01-13 12:19:28 +01:00
Raul Ochoa
ebdd71f342 Release 1.22.0 2015-01-13 12:18:10 +01:00
Raul Ochoa
597f8a7bab Merge pull request #240 from CartoDB/health-check
Add healthcheck endpoint
2015-01-13 12:12:34 +01:00
Raul Ochoa
3f1aa9955b Remove query tables api dependency from health check 2015-01-13 12:09:02 +01:00
Raul Ochoa
aad2a1e098 Regenerate npm-shrinkwrap.json 2015-01-13 12:01:34 +01:00
Alejandro Martínez
07fd7619bc Merge remote-tracking branch 'origin/master' into health-check
Conflicts:
	NEWS.md
	npm-shrinkwrap.json
	package.json
2015-01-13 11:55:54 +01:00
Alejandro Martínez
96bcd14bb8 Remove PostgreSQL from health checks
This way the health checks will only check for Redis and Mapnik
initialization.
An empty tile without layers or datasources is generated.
2015-01-13 11:29:19 +01:00
Alejandro Martínez
db9d350cae Add healthcheck configuration examples 2015-01-12 16:00:39 +01:00
Raul Ochoa
5914498027 Add canvas dependencies for travis 2015-01-02 17:26:30 +01:00
Raul Ochoa
180109e3aa Update npm-shrinkwrap.json 2015-01-02 15:36:19 +01:00
Raul Ochoa
929dac0df0 Merge branch 'master' into static-controllers 2014-12-23 14:16:33 +01:00
Raul Ochoa
6cd9a53aa5 Merge pull request #237 from CartoDB/templateid_typo
Update Map-API.md
2014-12-23 14:15:51 +01:00
Carlos Matallín
cd585dd657 Update Map-API.md 2014-12-23 14:06:02 +01:00
javi santana
2a150a6e7e Merge pull request #236 from CartoDB/typoFixes
Fixed typos, etc.
2014-12-18 10:17:32 +01:00
Andy Eschbacher
902b7339d1 Fixed typos, etc. 2014-12-17 15:31:18 -05:00
Raul Ochoa
f84b907dc8 Merge branch 'master' into static-controllers
Conflicts:
	npm-shrinkwrap.json
	package.json
2014-12-16 19:05:39 +01:00
Raul Ochoa
72cf5f8b04 Stubs next version 2014-12-15 16:08:18 +01:00
Raul Ochoa
e1d7852877 Release 1.21.2 2014-12-15 16:07:00 +01:00
Raul Ochoa
3f66c20616 Upgrades windshaft to 0.32.4 2014-12-15 16:05:58 +01:00
Raul Ochoa
a5f908d70e Merge branch 'master' into static-controllers
Conflicts:
	npm-shrinkwrap.json
	package.json
2014-12-12 16:53:38 +01:00
Raul Ochoa
e9383a2f0c Stubs next version 2014-12-11 10:24:22 +01:00
Raul Ochoa
0453166326 Release 1.21.1 2014-12-11 10:22:56 +01:00
Raul Ochoa
670478e9ca Merge pull request #234 from CartoDB/upgrade-windshaft
Upgrade windshaft
2014-12-11 10:21:27 +01:00
Raul Ochoa
1a9bc5550c Make windshaft version visible in news 2014-12-11 10:17:12 +01:00
Raul Ochoa
25f7e58b3a Re-generate npm-shrinkwrap.json 2014-12-11 10:16:05 +01:00
Raul Ochoa
839f8b062b Changes to windshaft tagged version 2014-12-11 10:12:40 +01:00
Raul Ochoa
eae1fbff8a Upgrades windshaft 2014-12-10 20:25:27 +01:00
Raul Ochoa
d628b2de27 Merge branch 'master' into static-controllers
Conflicts:
	npm-shrinkwrap.json
2014-12-02 18:24:41 +01:00
Raul Ochoa
56e4cb765f Merge pull request #231 from CartoDB/230-close-logfile-fd-on-kill-hup
Closes fd for log files on `kill -HUP`
2014-12-02 17:27:10 +01:00
Raul Ochoa
21179c56d4 Merge branch 'master' into static-controllers
Conflicts:
	npm-shrinkwrap.json
	package.json
2014-12-02 17:14:42 +01:00
Raul Ochoa
3da2830cfa Merge branch 'master' into 230-close-logfile-fd-on-kill-hup
Conflicts:
	npm-shrinkwrap.json
2014-12-02 17:09:50 +01:00
Raul Ochoa
873d6287c4 Merge pull request #232 from CartoDB/upgrade-windshaft
Upgrades windshaft to 0.32.1
2014-12-02 17:05:42 +01:00
Raul Ochoa
ea7eed4ad0 Upgrades windshaft to 0.32.1 2014-12-02 16:37:41 +01:00
Raul Ochoa
3b4b5ab298 Closes fd for log files on kill -HUP. Fixes #230 2014-12-02 15:05:28 +01:00
Raul Ochoa
2711c9b78c Fix typo 2014-12-02 11:00:36 +01:00
Raul Ochoa
48d60821a7 Exposes http renderer config 2014-12-01 18:43:40 +01:00
Raul Ochoa
076f4b441f Regenerates npm-shrinkwrap.json, will work on 0.8.x issues later 2014-11-27 12:16:52 +01:00
Raul Ochoa
e9eca83cd1 Fixes windshaft version 2014-11-27 12:07:29 +01:00
Raul Ochoa
f6aa20b96d Regenerate npm-shrinkwrap.json using millstone 0.6.14 2014-11-27 11:51:23 +01:00
Raul Ochoa
c15a384622 Regenerate npm-shrinkwrap.json as it seems to be different now 2014-11-27 03:18:22 +01:00
Raul Ochoa
46c3bedd15 Regenerate npm-shrinkwrap.json 2014-11-27 03:06:25 +01:00
Raul Ochoa
bc587f17de Adds proj4 in npm-shrinkwrap.json 2014-11-27 02:16:00 +01:00
Raul Ochoa
5905971178 Fix url for stats wiki 2014-11-25 14:39:18 +01:00
Raul Ochoa
afc7a7c956 Merge branch 'master' into static-controllers 2014-11-25 13:20:30 +01:00
Raul Ochoa
bf970803ec Defaults logging to stdout in development config example 2014-11-25 13:19:04 +01:00
Raul Ochoa
3de473662f Use static-controller branch from windshaft 2014-11-25 12:53:35 +01:00
Raul Ochoa
4bad92e3dd Improve named maps 2014-11-21 15:19:41 +01:00
Raul Ochoa
2089a299f1 Fix json 2014-11-18 11:33:33 +01:00
Raul Ochoa
10b1081960 Change highlighting to ``` 2014-11-14 17:23:15 +01:00
Raul Ochoa
c636a820d5 Fix named maps instantiation example 2014-11-13 14:14:24 +01:00
Raul Ochoa
6c4bb59f06 First tests, not all ready. WIP 2014-11-05 15:42:28 +01:00
Raul Ochoa
97c55c1187 Removes console.log 2014-11-05 15:42:16 +01:00
Raul Ochoa
2c5db229c6 Merge pull request #229 from CartoDB/pgraster
Add default configuration for raster overviews
2014-11-05 15:06:18 +01:00
Raul Ochoa
7c389a8010 Health check endpoint 2014-11-05 15:06:01 +01:00
Raul Ochoa
c84ed0a4b4 Merge branch 'master' into pgraster
Conflicts:
	npm-shrinkwrap.json
	package.json
2014-11-05 14:54:59 +01:00
Raul Ochoa
1c638aa661 Upgrades windshaft to 0.32.0 2014-11-05 14:48:59 +01:00
Raul Ochoa
6325a23bb4 Adds default value for raster overviews configuration 2014-11-05 12:48:26 +01:00
Raul Ochoa
07abae30ba Stubs next version 2014-10-24 17:08:27 +02:00
Raul Ochoa
494e2f48d5 Release 1.21.0 2014-10-24 17:07:36 +02:00
Raul Ochoa
c7875b3f53 Merge pull request #226 from CartoDB/layergroup-ttl
Allow a different cache-control max-age for layergroup responses
2014-10-24 16:55:30 +02:00
Raul Ochoa
c88330f5f2 Allow a different cache-control max-age for layergroup responses 2014-10-24 16:05:41 +02:00
Raul Ochoa
ff4ec19fff Fix documentation typos, endpoints and replace all /tiles with /api/v1/map 2014-10-21 19:18:34 +02:00
Raul Ochoa
697f3473f6 Use pgraster branch from windshaft 2014-10-21 14:46:53 +02:00
Raul Ochoa
439cd65050 Stubs next version 2014-10-20 17:52:47 +02:00
Raul Ochoa
79c7a559ad Release 1.20.2 2014-10-20 17:51:16 +02:00
Raul Ochoa
18d5315c2f Regenerate npm-shrinkwrap.json 2014-10-20 17:26:33 +02:00
Raul Ochoa
06693aeac7 Upgrades windshaft to 0.31.0 2014-10-20 17:24:23 +02:00
Raul Ochoa
d5e6f9906c Stubs next version 2014-10-17 14:23:19 +02:00
Raul Ochoa
fac4de21de Release 1.20.1 2014-10-17 14:21:51 +02:00
Raul Ochoa
e3d0f0ec8f Adds default value for redis returnToHead configuration 2014-10-17 14:18:14 +02:00
Raul Ochoa
81c86019ab Upgrades redis-mpool to 0.3.0 2014-10-17 14:15:59 +02:00
Raul Ochoa
95c4a25bd2 Stubs next version 2014-10-15 17:30:49 +02:00
Raul Ochoa
d2f801c7d6 Release 1.20.0 2014-10-15 17:29:56 +02:00
Raul Ochoa
f72c4f28da Prepend also the app name 2014-10-15 17:16:29 +02:00
Raul Ochoa
a248fe5c4b Prepend redis-pool in statsd key for redis-mpool status 2014-10-15 17:08:56 +02:00
Raul Ochoa
e9495ccd84 Merge pull request #225 from CartoDB/upgrade-windshaft
Upgrades Windshaft to report redis/renderers/mapnik pool metrics
2014-10-15 16:55:40 +02:00
Raul Ochoa
cf5e34eae6 Upgrades Windshaft to start reporting redis/renderers/mapnik pool metrics 2014-10-15 16:45:49 +02:00
Raul Ochoa
e52f583e20 Merge pull request #224 from CartoDB/one-redis-mpool
Use only one redis-mpool across the whole app
2014-10-15 16:31:02 +02:00
Raul Ochoa
98967cdf88 Be more clear about the effect of the statusInterval number 2014-10-15 16:25:35 +02:00
Raul Ochoa
ceb1bb7f50 Document what emitter.statusInterval is. 2014-10-15 16:20:51 +02:00
Raul Ochoa
94c61cb959 Update to release versions 2014-10-15 12:31:40 +02:00
Raul Ochoa
20cb559714 Merge branch 'master' into one-redis-mpool 2014-10-15 11:44:38 +02:00
Raul Ochoa
2a49770fc0 Merge pull request #223 from CartoDB/remove-travis-mapnik
Remove self-signed certificates workaround
2014-10-15 11:44:19 +02:00
Raul Ochoa
4a71a9f7b5 Remove self-signed certificates workaround 2014-10-15 11:06:16 +02:00
Raul Ochoa
b4b596ad8b Merge pull request #222 from CartoDB/remove-travis-mapnik
Let's use travis postgresql addon instead of manually installed one
2014-10-15 10:53:27 +02:00
Raul Ochoa
a865bcfa1d Let's use travis postgresql addon instead of manually installed one 2014-10-15 10:48:00 +02:00
Raul Ochoa
3587cf5154 Merge pull request #221 from CartoDB/remove-travis-mapnik
Reduces travis dependencies installation
2014-10-15 00:41:15 +02:00
Raul Ochoa
12b1d6e53b Reduces travis dependencies installation 2014-10-14 23:03:08 +02:00
Raul Ochoa
f4cb87f493 Adds default values for slow queries in redis 2014-10-14 22:14:34 +02:00
Raul Ochoa
804088009e Report to statsd the status of redis pools 2014-10-14 22:12:35 +02:00
Raul Ochoa
9f5faf7cf8 Server options to instantiate cartodb-redis with redis configuration if pool is not provided 2014-10-14 21:19:44 +02:00
Raul Ochoa
711c1a89ee Merge branch 'master' into one-redis-mpool 2014-10-14 18:23:17 +02:00
Raul Ochoa
30dd0604ca Stubs next version 2014-10-14 15:44:58 +02:00
Raul Ochoa
0b67eed92f Release 1.19.0 2014-10-14 15:12:17 +02:00
Raul Ochoa
9782cbae35 Merge pull request #220 from CartoDB/upgrade-npm-shrinkwrap
Generates npm-shrinkwrap.json with npm >1.2.0
2014-10-14 14:50:54 +02:00
Raul Ochoa
6ca4c0b23f Generates npm-shrinkwrap.json with npm >1.2.0 2014-10-14 14:17:54 +02:00
Raul Ochoa
a672ac66ae Upgrades windshaft to 0.28.2 2014-10-14 14:05:50 +02:00
Raul Ochoa
65c4ca01d0 Stubs next version 2014-10-13 16:29:31 +02:00
Raul Ochoa
3dad6e96e3 Merge branch 'master' into one-redis-mpool 2014-10-07 12:47:10 +02:00
Raul Ochoa
7009eb20f8 Check style fixes 2014-09-24 11:42:53 +02:00
Raul Ochoa
24cbd192aa Share one redis-mpool across the application 2014-09-24 11:42:36 +02:00
35 changed files with 3924 additions and 2425 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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.

View File

@@ -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).

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View 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;

View 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);
}

View 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);
};

View File

@@ -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;

View 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);
}
);
};

View 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);
}
);
};

View File

@@ -0,0 +1,4 @@
<Map
background-color="#c33"
srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
</Map>

View File

@@ -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

View File

@@ -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;

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View 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);
});
});

View 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();
}
);
});
});

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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
};

View 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');
});
});

View File

@@ -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);
}
);
});
});

View File

@@ -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();
});
});
});
});

View 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();
})
});
});

View 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);
});
});
});
});

View 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();
}
});
});
});
});