Compare commits

..

325 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
6c3e2513d5 Release 1.18.2 2014-10-13 16:28:15 +02:00
Raul Ochoa
cc50364453 Upgrades windshaft to 0.28.1 2014-10-13 16:19:32 +02:00
Raul Ochoa
774104b34e Defaults resultSet to object if undefined in QueryTablesApi 2014-10-13 15:24:14 +02:00
Raul Ochoa
5a3e427249 Stubs next version 2014-10-13 11:50:21 +02:00
Raul Ochoa
a9e2f95fdb Release 1.18.1 2014-10-13 11:48:57 +02:00
Raul Ochoa
d89b2986fd Allow to add more node.js' threadpool workers via process.env.UV_THREADPOOL_SIZE 2014-10-08 16:50:35 +02:00
Raul Ochoa
91229dd1e1 Merge pull request #219 from strk/master-ignores
Ignore files produced by "make check"
2014-10-08 10:41:06 +02:00
Sandro Santilli
f52c65ea25 Ignore files produced by "make check" 2014-10-08 10:33:19 +02:00
Raul Ochoa
3dad6e96e3 Merge branch 'master' into one-redis-mpool 2014-10-07 12:47:10 +02:00
Raul Ochoa
8fd6e22dc4 Stubs next version 2014-10-03 17:05:20 +02:00
Raul Ochoa
474198b4e1 Release 1.18.0 2014-10-03 17:03:50 +02:00
Raul Ochoa
fbdafcb7b7 Comes back to use mapnik 2.3.x based on cartodb/node-mapnik@1.4.15-cdb from windshaft@0.28.0 2014-10-03 16:57:04 +02:00
Raul Ochoa
9b0232c48d Stubs next version 2014-10-01 20:28:47 +02:00
Raul Ochoa
723a184086 Release 1.17.2 2014-10-01 20:27:11 +02:00
Raul Ochoa
db34a4ffff Stubs next version 2014-09-30 15:09:56 +02:00
Raul Ochoa
0b896fb935 Release 1.17.1 2014-09-30 15:08:26 +02:00
Raul Ochoa
e0331ec022 Upgrades mocha 2014-09-30 15:03:14 +02:00
Raul Ochoa
e31ef916f0 Upgrades windshaft to 0.27.1 which downgrades node-mapnik to 1.4.10 2014-09-30 15:02:24 +02:00
Raul Ochoa
55669f88ff Updates news 2014-09-26 11:21:28 +02:00
Raul Ochoa
c13dbc9a57 Merge pull request #214 from CartoDB/CDB-4008
TTL for template locks so they are not kept forever
2014-09-26 11:16:05 +02:00
Raul Ochoa
e8e03585ff Adds dot.js dependency 2014-09-25 19:17:02 +02:00
Raul Ochoa
b4bee864d2 Lock now considers the creation time and compares against a ttl so
a lock is not keep forever in case of failure.

Pending: lazy removal of expired locks.
2014-09-25 19:00:35 +02:00
Raul Ochoa
b41d1e84da Stubs next version 2014-09-25 16:17:10 +02:00
Raul Ochoa
b5d5d7c2b0 Release 1.17.0 2014-09-25 16:15:51 +02:00
Raul Ochoa
3e571b4ce8 Use object.keys to iterate over objects 2014-09-25 12:17:32 +02:00
Raul Ochoa
fb8fd5121e Do not expose internal implementation 2014-09-25 12:16:34 +02:00
Raul Ochoa
ac2a3243b5 Don't cache regexes and avoid the _re hack 2014-09-25 12:04:52 +02:00
Raul Ochoa
1c10b8193b Adds dot to compile templates 2014-09-24 19:17:51 +02:00
Raul Ochoa
abf0fa1b32 Remove unused var 2014-09-24 19:12:43 +02:00
Raul Ochoa
4c5bc13c7f Check style fixes 2014-09-24 19:11:53 +02:00
Raul Ochoa
c33b81e3ad Merge pull request #213 from CartoDB/mapnik-2.3.x
Starts using mapnik 2.3.x by upgrading windshaft dependency
2014-09-24 16:26:21 +02:00
Raul Ochoa
f88f0b5019 Modify travis configuration to be able to compile node-mapnik with mapnik 2.3.x 2014-09-24 16:02:31 +02:00
Raul Ochoa
3b96f0d535 Starts using mapnik 2.3.x via windshaft upgrade 2014-09-24 15:54:13 +02:00
Raul Ochoa
243672ead5 Merge branch 'master' into mapnik-2.3.x 2014-09-24 15:27:41 +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
Raul Ochoa
9d36ae293c Run check before any assert so server is stopped 2014-09-18 19:07:15 +02:00
Raul Ochoa
9496d83d1c Adds poolSize configuration for mapnik 2014-09-18 19:06:45 +02:00
Raul Ochoa
18233e9ea1 Upgrades windshaft and cartodb-redis 2014-09-18 19:05:20 +02:00
Raul Ochoa
6523c4bbbb Fixes template.json in documentation to be a valid JSON 2014-09-03 12:34:28 +02:00
Raul Ochoa
5bafbdfaa0 Changes mapnik dependency in windshaft module to use node-mapnik 1.4.13
which is based on mapnik-2.3.x
2014-08-29 17:03:31 +02:00
Raul Ochoa
7afa869833 Use double quote to be consistent 2014-08-29 16:48:28 +02:00
Raul Ochoa
0d32036523 Merge pull request #211 from CartoDB/CDB-3659
Remove some metrics
2014-08-26 13:44:21 +02:00
Raul Ochoa
a084ec19ff Update news 2014-08-26 12:23:56 +02:00
Raul Ochoa
7faff8f887 Removes cors metric 2014-08-26 11:53:59 +02:00
Raul Ochoa
f406001315 Removes get_infowindow and get_map_metadata metrics 2014-08-25 19:15:31 +02:00
Raul Ochoa
2b2020b43b Removes getTableGeometryType metric 2014-08-25 19:09:54 +02:00
Raul Ochoa
4886e4b34a Merge pull request #210 from CartoDB/improve-query-tables-api
Removes duplicated code in query_tables_api
2014-08-22 13:17:02 +02:00
Raul Ochoa
65e0364d37 Removes duplicated code in query_tables_api 2014-08-22 12:52:05 +02:00
Raul Ochoa
13e16e0a26 Merge pull request #209 from CartoDB/scale_denominator-support
Supports `!scale_denominator!` dynamic param in SQL queries
2014-08-22 10:30:14 +02:00
Raul Ochoa
965e1cd0c4 Supports !scale_denominator! dynamic param in SQL queries 2014-08-22 10:16:39 +02:00
Raul Ochoa
3750b67110 Merge pull request #208 from CartoDB/CDB-1843
Upgrades windshaft
2014-08-19 19:06:25 +02:00
Raul Ochoa
7e1f48f212 CDB-1843 upgrades windshaft 2014-08-19 18:42:48 +02:00
Raul Ochoa
42457df2c1 Stubs next version 2014-08-19 14:28:24 +02:00
Raul Ochoa
56d8f1acfe Release 1.16.1 2014-08-19 14:26:19 +02:00
Raul Ochoa
56ac4acc9f Upgrades cartodb-redis 2014-08-19 14:25:45 +02:00
javi santana
d91b0f1af5 fixed missing comma 2014-08-19 09:25:45 +02:00
Raul Ochoa
f986516379 Stubs next version 2014-08-18 15:29:47 +02:00
Raul Ochoa
fa59255556 Release 1.16.0 2014-08-18 15:28:38 +02:00
Raul Ochoa
ab1c11faf2 Merge pull request #207 from CartoDB/upgrade-windshaft
Upgrade windshaft
2014-08-18 15:26:58 +02:00
Raul Ochoa
307f220de4 Updates news 2014-08-18 15:14:01 +02:00
Raul Ochoa
50c8a2dc69 Defaults mapnik version for test 2014-08-18 14:48:45 +02:00
Raul Ochoa
3f726dc4c5 Upgrades windshaft 2014-08-18 14:41:29 +02:00
Raul Ochoa
105d50f1f4 Merge pull request #206 from CartoDB/upgrade-dependencies
Upgrades dependencies
2014-08-15 01:51:16 +02:00
Raul Ochoa
a3a5964926 Upgrades dependencies 2014-08-14 19:54:45 +02:00
Raul Ochoa
6a8cff6fcd Merge branch 'remove-mapnik-dependency'
Conflicts:
	NEWS.md
2014-08-14 19:26:52 +02:00
Raul Ochoa
b42e9c80f7 Adds windshaft's mapnik dependency to npm-shrinkwrap.json 2014-08-14 18:56:33 +02:00
Raul Ochoa
23a7684208 Removes mapnik dependency as it now relies on Windshaft to check mapnik version 2014-08-14 18:27:54 +02:00
Raul Ochoa
c52c245b9d Merge pull request #204 from CartoDB/CDB-3686
Configurable QueryTablesAPI to call directly postgresql
2014-08-14 13:44:07 +02:00
Raul Ochoa
4555b2107a Updates news 2014-08-14 13:32:22 +02:00
Raul Ochoa
49b0120c6d Formats NEWS 2014-08-14 13:32:04 +02:00
Raul Ochoa
f2541d8cae Merge branch 'master' into CDB-3686
Conflicts:
	package.json
2014-08-13 15:31:22 +02:00
Raul Ochoa
d1fb792709 Stubs next version 2014-08-13 15:27:16 +02:00
Raul Ochoa
86fb58155a Release 1.15.0 2014-08-13 15:26:25 +02:00
Raul Ochoa
d3c656893c Merge pull request #202 from CartoDB/CDB-3135
Upgrades dependencies to get redis-mpool improvements
2014-08-13 15:24:59 +02:00
Raul Ochoa
713e394e7b Slow pool configuration in example configurations 2014-08-13 15:15:30 +02:00
Raul Ochoa
40acf533ae Specifies name in the redis pool 2014-08-13 15:12:46 +02:00
Raul Ochoa
13fdfc602e Upgrades dependencies 2014-08-13 15:10:58 +02:00
Raul Ochoa
8255f3eb51 Upgrades cartodb-psql dependency 2014-08-11 20:18:27 +02:00
Raul Ochoa
e7ab71c606 Merge branch 'master' into CDB-3686 2014-08-11 12:19:11 +02:00
Raul Ochoa
3eab0d6349 Stubs next version 2014-08-07 12:35:38 +02:00
Raul Ochoa
58047fac17 Release 1.14.0 2014-08-07 12:34:49 +02:00
Raul Ochoa
6e4144d015 Prepares next release 2014-08-07 12:33:53 +02:00
Raul Ochoa
8b0fda8d89 Merge pull request #201 from CartoDB/CDB-3664
Upgrades windshaft (and grainstore) to be able to specify the tile format
2014-08-07 12:20:01 +02:00
Raul Ochoa
2ed656ca0d Upgrades windshaft (and grainstore) to be able to specify the tile
format, see: https://github.com/mapnik/mapnik/wiki/OutputFormats
2014-08-07 01:57:21 +02:00
Raul Ochoa
5cf79c82bb Configurable QueryTablesAPI to call directly postgresql using cartodb-psql
or to keep using a request to the SQL API
2014-08-06 21:48:08 +02:00
Raul Ochoa
d1373bec66 Improves SQL query for affected tables and last updated time 2014-08-04 17:48:59 +02:00
Raul Ochoa
325a0503cb Merge branch 'master' into cdb 2014-08-04 14:14:53 +02:00
Raul Ochoa
fa72f52ad4 Merge pull request #200 from CartoDB/CDB-3686
Affected tables and last updated time for a query into a single SQL API request
2014-08-04 14:14:00 +02:00
Raul Ochoa
528815a564 Updates news 2014-08-04 13:24:44 +02:00
Raul Ochoa
dabcba9f5f Merge branch 'master' into CDB-3686 2014-08-04 13:11:40 +02:00
Raul Ochoa
5d9afc18f5 Merge tag '1.13.1' into cdb 2014-08-04 13:11:01 +02:00
Raul Ochoa
995dabc9b7 Stubs next version 2014-08-04 13:05:08 +02:00
Raul Ochoa
36145542af Release 1.13.1 2014-08-04 13:04:13 +02:00
Raul Ochoa
06eca6525a Merge pull request #199 from CartoDB/CDB-3657
CDB-3657 Adds profiler as JSON to the header
2014-08-04 13:02:27 +02:00
Raul Ochoa
414673b347 CDB-3657 Adds profiler as JSON to the header 2014-08-04 12:53:15 +02:00
Raul Ochoa
a9767c049f CDB-3686 Adds support for per mil tolerance when comparing images as in Mac OS X some results from ImageMagick are a bit odd 2014-08-04 12:35:54 +02:00
Raul Ochoa
507a6a8979 CDB-3686 Style changes 2014-08-04 01:32:49 +02:00
Raul Ochoa
73d1db3bd2 CDB-3686 Adds support for per mil tolerance when comparing images as in Mac OS X some results from ImageMagick are a bit odd 2014-08-04 01:30:24 +02:00
Raul Ochoa
9b5921e8e1 CDB-3686 Fixes expected queries based on changes to request table names and last updated time in one request 2014-08-04 01:29:23 +02:00
Raul Ochoa
799a999148 CDB-3686 Makes SQL API emulator to handle new query with both names and updated time for affected tables. 2014-08-04 01:28:30 +02:00
Raul Ochoa
eafe3af13e Fixes reference to redis-mpool 2014-08-01 18:27:55 +02:00
Raul Ochoa
4e420c2f33 Merge tag '1.13.0' into CDB-3686 2014-07-30 18:25:10 +02:00
Raul Ochoa
654b3ad6d3 Fixes reference to redis-mpool 2014-07-30 18:23:45 +02:00
Raul Ochoa
9f8d73a1df Removes duplicated file 2014-07-30 18:17:14 +02:00
Raul Ochoa
f6e0b4ca9f Merge branch 'master' of https://github.com/cartodb/windshaft-cartodb into CDB-3686 2014-07-30 18:13:47 +02:00
Raul Ochoa
1dbad1f0b8 Stubs next version 2014-07-30 18:11:32 +02:00
Raul Ochoa
8f9e19e3e2 Fix date in NEWS 2014-07-30 18:10:28 +02:00
Raul Ochoa
b1a0b5e235 Release 1.13.0 2014-07-30 18:08:07 +02:00
Raul Ochoa
bce13944c3 Merge pull request #198 from CartoDB/multiuser
Support for multiple schemas, multiple auth tokens and public user from redis
2014-07-30 17:56:05 +02:00
Raul Ochoa
c8fc3d1e7a Updates to correct version of step profiler 2014-07-30 16:23:03 +02:00
Raul Ochoa
e6f7b9c1f9 Adds news about changes in multiuser branch 2014-07-30 16:08:45 +02:00
Raul Ochoa
552ebaaaac Upgrades Windshaft to version 0.22.0 2014-07-30 15:25:15 +02:00
Raul Ochoa
6019fb2ca3 Merge pull request #197 from CartoDB/CDB-3678
[CDB-3678] Creates api_hostname global variable
2014-07-30 15:09:58 +02:00
Raul Ochoa
3af45e1a32 Moves calls to SQL API to its own entity.
Groups affected tables and last updated time for affected tables into one request.
2014-07-30 13:46:46 +02:00
Raul Ochoa
75088c89d3 Style fixes 2014-07-30 13:45:53 +02:00
Luis Bosque
2c1d46f159 [CDB-3678] Creates api_hostname global variable 2014-07-29 14:54:35 +02:00
javi santana
15b9a1f34b fixed documentation 2014-07-24 13:01:35 +02:00
Carlos Matallín
5c70dd0557 run tests 2014-07-21 11:11:21 +02:00
Carlos Matallín
dc0acdbee1 Update Map-API.md 2014-07-21 10:18:15 +02:00
Carlos Matallín
ae01047e8c Merge pull request #195 from matallo/master
move maps api doc
2014-07-09 14:24:27 +02:00
Carlos Matallín
1b7c2a0208 move maps api doc 2014-07-09 14:23:53 +02:00
Carlos Matallín
a8b01f523a Merge pull request #194 from matallo/master
update doc
2014-07-09 14:20:43 +02:00
Carlos Matallín
23cbad8ba6 update doc 2014-07-09 14:19:25 +02:00
Carlos Matallín
984e0f6e83 Rename Map-API.md to Map-API-internal.md 2014-07-09 13:13:21 +02:00
Raul Ochoa
67df6a4d73 Adds support for several auth tokens 2014-07-08 10:35:45 +02:00
Raul Ochoa
f756b9d77f Removes search_path param 2014-07-04 12:18:35 +02:00
Raul Ochoa
0dfd51f81a Adds host to redis setup as it does not make sense to continue if there is no host in redis. 2014-07-04 11:47:44 +02:00
Raul Ochoa
bfdcee3772 Retrieving db public user from redis. It uses a new multiget method from cartodb-redis 2014-07-03 21:39:47 +02:00
Raul Ochoa
470aea22d9 Sets full search_path 2014-07-03 10:24:37 +02:00
Raul Ochoa
32e4c26c95 Sets origin for grainstore in shrinkwrap 2014-07-02 19:34:28 +02:00
Raul Ochoa
6a34568935 Forcing grainstore version in shrinkwrap 2014-07-02 19:16:06 +02:00
javi
3548106a6c changed branch for windshaft 2014-06-27 23:42:03 +02:00
javi
3806ad8843 Merge remote-tracking branch 'origin/CDB-2891-search_path' into multiuser 2014-06-27 09:10:39 +02:00
Raul Ochoa
037ce2dc12 CDB-2891 Exposes username as search_path in params 2014-06-27 00:48:48 +01:00
javi
338c0bcdbe use regclass instead table name to look for last_updated in CDB_tablemetadata 2014-06-26 15:00:55 +02:00
Raul Ochoa
bc3baf3094 CDB-3256 Prepares 1.12.1 release 2014-06-24 16:26:57 +02:00
Raul Ochoa
8a91b5cfb5 CDB-3256 Fixes test related to cache in templated layergroup creation 2014-06-24 16:05:54 +02:00
Raul Ochoa
4cf1ddd6fc CDB-3256 Adds response and method references to fake request object 2014-06-24 15:52:47 +02:00
Raul Ochoa
cb781aeb00 CDB-3256 Prepares 1.12.0 Release 2014-06-24 14:24:14 +02:00
Raul Ochoa
2dd03e21e1 CDB-3256 fix test and adds a couple more of tests for testing the no-cache scenarios 2014-06-24 13:13:00 +02:00
Raul Ochoa
055bacbad7 Sets PGUSER environment variable 2014-06-24 12:39:57 +02:00
Raul Ochoa
46ae6d1fe4 Changes travis configuration to be similar to windshaft one 2014-06-24 12:39:46 +02:00
Raul Ochoa
5e73b12cf5 CDB-3256 adds headers based on affected tables when creating a layergroup via HTTP GET 2014-06-24 12:16:30 +02:00
Sandro Santilli
86c6f3eeac Wrap all json strings and string values in double-quotes 2014-06-09 12:19:16 +02:00
Raul Ochoa
8922ae3a45 adds document about metrics being tracked 2014-05-29 13:10:46 +02:00
Raul Ochoa
318e22e9fa Merge commit '4738b880a6c29a6d10dda3ad178f35a54bd576d3'
Conflicts:
	NEWS.md
	package.json
2014-05-07 19:07:20 +02:00
Raul Ochoa
4738b880a6 Prepares release 1.10.3 2014-05-07 18:28:10 +02:00
Sandro Santilli
49829f8935 Set default PostgreSQL application name to "cartodb_tiler" 2014-05-07 16:19:22 +02:00
Sandro Santilli
8e9d72982a Refuse to start if log_filename points to a non-existing directory
Closes #189
2014-05-07 11:03:25 +02:00
Raul Ochoa
d2f0180475 Merge remote-tracking branch 'rochoa/master' 2014-04-22 11:40:48 +02:00
Raul Ochoa
4da0b1e07c CDB-2096 Configures the CWD for log4js logger. 2014-04-22 10:52:59 +02:00
Sandro Santilli
5a4a35b665 Fix documentation for redis.max setting
Closes #192
2014-04-16 17:53:42 +02:00
Raul Ochoa
248cb4bd76 Removes unused dependency. 2014-04-11 15:14:59 +02:00
Sandro Santilli
140001f036 Update release document 2014-04-09 09:14:20 +02:00
Sandro Santilli
3917cac800 Add 1.10.2 section 2014-04-08 10:00:41 +02:00
Sandro Santilli
ee37da5b35 Prepare for 1.10.3 2014-04-08 10:00:10 +02:00
Sandro Santilli
882ec65ba0 Use signer's map_key when contacting sql-api
Includes testcase.
Fixes #188
2014-04-08 09:44:49 +02:00
Sandro Santilli
bbd4db6ddb Fix show_style tool broken since 1.8.1 2014-03-31 12:53:48 +02:00
Sandro Santilli
312194228a Stop duplicating global.environment as global.settings 2014-03-28 18:47:59 +01:00
Sandro Santilli
5c1125900b Add support for log_filename directive, reopen logfile on SIGHUP 2014-03-28 18:05:18 +01:00
Sandro Santilli
08b8741282 Reload log files on SIGUSR2
This is an attempt to play more nicely with logrotate
2014-03-28 17:06:44 +01:00
Sandro Santilli
e8367b765a Add persist_connection setting in .example configs 2014-03-24 17:40:43 +01:00
Sandro Santilli
91cd0df7b3 Typo in comment 2014-03-24 17:03:32 +01:00
Sandro Santilli
dff0a2aa1f Merge branch 'b1.10'
Fixes bogus caching of failing jsonp responses
2014-03-21 15:17:43 +01:00
Sandro Santilli
5f30b9e798 Add an example of a slow mapconfig (using lots of data) 2014-03-20 18:19:30 +01:00
Sandro Santilli
7c892de7b1 Prepare for 1.11.0 2014-03-20 17:11:06 +01:00
46 changed files with 5824 additions and 2233 deletions

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ config/environments/*.js
tools/munin/windshaft.conf
logs/
pids/
redis.pid
test.log

View File

@@ -1,15 +1,13 @@
addons:
postgresql: "9.3"
before_install:
- sudo apt-add-repository --yes ppa:mapnik/v2.1.0
- sudo apt-get update -q
- sudo apt-get install -q libmapnik-dev
- sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev
- createdb template_postgis
- psql -c "CREATE EXTENSION postgis" template_postgis
# 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
- NPROCS=1 JOBS=1 PGUSER=postgres
language: node_js
node_js:

View File

@@ -1,13 +1,20 @@
1. Ensure proper version in package.json
2. Ensure NEWS section exists for the new version, review it, add release date
3. Drop npm-shrinkwrap.json
4. Run npm install
5. Test (make check or npm test), fix if broken before proceeding
6. Run npm shrinkwrap
7. Set "from" in npm-shrinkwrap.json for known packages
(windshaft, node-varnish, grainstore...)
8. Commit package.json, npm-shrinwrap.json, NEWS
9. git tag -a Major.Minor.Patch # use NEWS section as content
10. Announce
11. Stub NEWS/package for next version
1. Test (make clean all check), fix if broken before proceeding
2. Ensure proper version in package.json
3. Ensure NEWS section exists for the new version, review it, add release date
4. Drop npm-shrinkwrap.json
5. Run npm shrinkwrap to recreate npm-shrinkwrap.json
6. Commit package.json, npm-shrinwrap.json, NEWS
7. git tag -a Major.Minor.Patch # use NEWS section as content
8. Announce on cartodb@googlegroups.com
9. Stub NEWS/package for next version
Versions:
Bugfix releases increment Patch component of version.
Feature releases increment Minor and set Patch to zero.
If backward compatibility is broken, increment Major and
set to zero Minor and Patch.
Branches named 'b<Major>.<Minor>' are kept for any critical
fix that might need to be shipped before next feature release
is ready.

287
NEWS.md
View File

@@ -1,8 +1,291 @@
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
--------------------
Bug fixes:
- Defaults resultSet to object if undefined in QueryTablesApi
Announcements:
- Upgrades windshaft to 0.28.1
1.18.1 -- 2014-10-13
--------------------
New features:
- Allow to add more node.js' threadpool workers via process.env.UV_THREADPOOL_SIZE
1.18.0 -- 2014-10-03
--------------------
Announcements:
- Comes back to use mapnik 2.3.x based on cartodb/node-mapnik@1.4.15-cdb from windshaft@0.28.0
1.17.2 -- 2014-10-01
--------------------
Announcements:
- Upgrades windshaft to 0.27.2 which downgrades node-mapnik to 0.7.26-cdb1
1.17.1 -- 2014-09-30
--------------------
Announcements:
- Upgrades windshaft to 0.27.1 which downgrades node-mapnik to 1.4.10
Enhancements:
- TTL for template locks so they are not kept forever
- Upgrades mocha
1.17.0 -- 2014-09-25
--------------------
New features:
- Starts using mapnik 2.3.x
Enhancements:
- Upgrades windshaft and cartodb-redis
- Supports `!scale_denominator!` dynamic param in SQL queries
- Metrics revamp: removes and adds some metrics
- Adds poolSize configuration for mapnik
1.16.1 -- 2014-08-19
--------------------
Enhancements:
- Upgrades cartodb-redis
1.16.0 -- 2014-08-18
--------------------
New features:
- Configurable QueryTablesAPI to call directly postgresql using cartodb-psql
or to keep using a request to the SQL API
Enhancements:
- Removes mapnik dependency as it now relies on Windshaft to check mapnik version
- Upgrades dependencies:
- underscore
- lzma
- log4js
- rollbar
- windshaft
- request
1.15.0 -- 2014-08-13
--------------------
Enhancements:
- Upgrades dependencies:
- redis-mpool
- cartodb-redis
- windshaft
- Specifies name in the redis pool
- Slow pool configuration in example configurations
1.14.0 -- 2014-08-07
--------------------
Enhancements:
- SQL API requests moved to its own entity
New features:
- Affected tables and last updated time for a query are performed in a single
request to the SQL API
- Allow specifying the tile format, upgrades windshaft and grainstore
dependencies for this matter
1.13.1 -- 2014-08-04
--------------------
Enhancements:
- Profiler header sent as JSON string
1.13.0 -- 2014-07-30
--------------------
New features:
- Support for postgresql schemas
- Use public user from redis
- Support for several auth tokens
1.12.1 -- 2014-06-24
--------------------
Enhancements:
- Caches layergroup and sets X-Cache-Channel in GET requests also in named maps
1.12.0 -- 2014-06-24
--------------------
New features:
- Caches layergroup and sets X-Cache-Channel in GET requests
1.11.1 -- 2014-05-07
--------------------
Enhancements:
- Upgrade Windshaft to 0.21.0, see
http://github.com/CartoDB/Windshaft/blob/0.21.0/NEWS
1.11.0 -- 2014-04-28
--------------------
New features:
- Add support for log_filename directive
- Reopen log file on SIGHUP, for better logrotate integration
Enhancements:
- Set default PostgreSQL application name to "cartodb_tiler"
1.10.2 -- 2014-04-08
--------------------
Bug fixes:
- Fix show_style tool broken since 1.8.1
- Fix X-Cache-Channel of tiles accessed via signed token (#188)
@@ -14,7 +297,7 @@ Bug fixes:
- Do not cache non-success jsonp responses (#186)
1.10.0 -- 2014-03-20
-------------------
--------------------
New features:

71
app.js
View File

@@ -7,6 +7,11 @@
* environments: [development, production]
*/
var path = require('path'),
fs = require('fs'),
RedisPool = require('redis-mpool')
;
if ( process.argv[2] ) ENV = process.argv[2];
else if ( process.env['NODE_ENV'] ) ENV = process.env['NODE_ENV'];
@@ -21,22 +26,40 @@ if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
process.exit(1);
}
var _ = require('underscore')
, Step = require('step')
;
// set environment specific variables
global.settings = require(__dirname + '/config/settings');
global.environment = require(__dirname + '/config/environments/' + ENV);
_.extend(global.settings, global.environment);
var _ = require('underscore');
global.log4js = require('log4js')
// set environment specific variables
global.environment = require(__dirname + '/config/environments/' + ENV);
global.environment.api_hostname = require('os').hostname().split('.')[0];
global.log4js = require('log4js');
log4js_config = {
appenders: [
{ type: "console", layout: { type:'basic' } }
],
appenders: [],
replaceConsole:true
};
if (global.environment.uv_threadpool_size) {
process.env.UV_THREADPOOL_SIZE = global.environment.uv_threadpool_size;
}
if ( global.environment.log_filename ) {
var logdir = path.dirname(global.environment.log_filename);
// See cwd inlog4js.configure call below
logdir = path.resolve(__dirname, logdir);
if ( ! fs.existsSync(logdir) ) {
console.error("Log filename directory does not exist: " + logdir);
process.exit(1);
}
console.log("Logs will be written to " + global.environment.log_filename);
log4js_config.appenders.push(
{ type: "file", filename: global.environment.log_filename }
);
} else {
log4js_config.appenders.push(
{ type: "console", layout: { type:'basic' } }
);
}
if ( global.environment.rollbar ) {
log4js_config.appenders.push({
type: __dirname + "/lib/cartodb/log4js_rollbar.js",
@@ -44,16 +67,28 @@ if ( global.environment.rollbar ) {
});
}
log4js.configure(log4js_config);
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
@@ -80,6 +115,14 @@ process.on('SIGUSR2', function() {
ws.dumpCacheStats();
});
process.on('SIGHUP', function() {
global.log4js.clearAndShutdownAppenders(function() {
global.log4js.configure(log4js_config);
global.logger = log4js.getLogger();
console.log('Log files reloaded');
});
});
process.on('uncaughtException', function(err) {
logger.error('Uncaught exception: ' + err.stack);
});

View File

@@ -2,6 +2,7 @@ var config = {
environment: 'development'
,port: 8181
,host: '127.0.0.1'
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '^(.*)\\.localhost'
@@ -31,11 +32,15 @@ var config = {
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in miliseconds
// idle socket timeout, in milliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: false
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// 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: undefined
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'development_cartodb_user_<%= user_id %>'
@@ -58,9 +63,19 @@ 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
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
max_size: 500
}
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
@@ -72,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
@@ -84,13 +107,25 @@ var config = {
// Max number of connections in each pool.
// Users will be put on a queue when the limit is hit.
// Set to maxConnection to have no possible queues.
// There are currently 3 pools involved in serving
// There are currently 2 pools involved in serving
// windshaft-cartodb requests so multiply this number
// by 3 to know how many possible connections will be
// 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
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: {
protocol: 'http',
@@ -114,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

@@ -2,6 +2,7 @@ var config = {
environment: 'production'
,port: 8181
,host: '127.0.0.1'
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '^(.*)\\.cartodb\\.com$'
@@ -31,11 +32,15 @@ var config = {
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in miliseconds
// idle socket timeout, in milliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// 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'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'cartodb_user_<%= user_id %>'
@@ -51,10 +56,20 @@ var config = {
port: 6432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
row_limit: 65535,
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
max_size: 500
}
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
@@ -66,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
@@ -78,13 +101,25 @@ var config = {
// Max number of connections in each pool.
// Users will be put on a queue when the limit is hit.
// Set to maxConnection to have no possible queues.
// There are currently 3 pools involved in serving
// There are currently 2 pools involved in serving
// windshaft-cartodb requests so multiply this number
// by 3 to know how many possible connections will be
// 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
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: {
protocol: 'https',
@@ -108,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
@@ -131,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

@@ -2,6 +2,7 @@ var config = {
environment: 'production'
,port: 8181
,host: '127.0.0.1'
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '^(.*)\\.cartodb\\.com$'
@@ -31,11 +32,15 @@ var config = {
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in miliseconds
// idle socket timeout, in milliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
// 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'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'cartodb_staging_user_<%= user_id %>'
@@ -52,9 +57,19 @@ 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
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
max_size: 500
}
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
@@ -66,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
@@ -78,13 +101,25 @@ var config = {
// Max number of connections in each pool.
// Users will be put on a queue when the limit is hit.
// Set to maxConnection to have no possible queues.
// There are currently 3 pools involved in serving
// There are currently 2 pools involved in serving
// windshaft-cartodb requests so multiply this number
// by 3 to know how many possible connections will be
// 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
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: {
protocol: 'https',
@@ -108,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
@@ -131,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

@@ -2,6 +2,7 @@ var config = {
environment: 'test'
,port: 8888
,host: '127.0.0.1'
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '(.*)'
@@ -31,11 +32,15 @@ var config = {
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in miliseconds
// idle socket timeout, in milliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: false
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// 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'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'test_windshaft_cartodb_user_<%= user_id %>'
@@ -52,9 +57,19 @@ 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
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
max_size: 500
}
,mapnik_version: ''
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
@@ -66,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
@@ -78,13 +101,25 @@ var config = {
// Max number of connections in each pool.
// Users will be put on a queue when the limit is hit.
// Set to maxConnection to have no possible queues.
// There are currently 3 pools involved in serving
// There are currently 2 pools involved in serving
// windshaft-cartodb requests so multiply this number
// by 3 to know how many possible connections will be
// 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
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: {
protocol: 'http',
@@ -110,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 +0,0 @@
module.exports.oneDay = 86400000;

111
docs/Map-API-internal.md Normal file
View File

@@ -0,0 +1,111 @@
# Kind of maps
Windshaft-CartoDB supports the following types of maps:
- [Temporary maps](#temporary-maps) (created by anyone)
- [Detached maps](#detached-maps)
- [Inline maps](#inline-maps) (legacy)
- [Persistent maps](#peristent-maps) (created by CartDB user)
- [Template maps](#template-maps)
- [Table maps](#table-maps) (legacy, deprecated)
## Temporary maps
Temporary maps have no owners and are anonymous in nature.
There are two kinds of temporary maps:
- Detached maps (aka MultiLayer-API)
- Inline maps
### Detached maps
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.
Anyone can create detached maps, but users will need read access
to the data source of the map layers.
The configuration format is a [MapConfig]
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) document.
The HTTP endpoints for creating the map and using it are described [here]
(http://github.com/CartoDB/Windshaft-cartodb/wiki/MultiLayer-API)
*TODO* cleanup the referenced document
### Inline maps
Inline maps are maps that only exist for a single request,
being the request for a specific map resource (tile).
Inline maps are always bound to a table, and can only be
obtained by those having read access to the that table.
Additionally, users need to have access to any datasource
specified as part of the configuration.
Inline maps only support PNG and UTF8GRID tiles.
The configuration consist in a set of parameters, to be
specified in the query string of the tile request:
* sql - the query to run as datasource, can be an array
* style - the CartoCSS style for the datasource, can be an array
* style_version - version of the CartoCSS style, can be an array
* interactivity - only for fetching UTF8GRID,
If the style is not provided, style of the associated table is
used; if the sql is not provided, all records of the associated
table are used as the datasource; the two possibilities result
in a mix between _inline_ maps and [Table maps][].
*TODO* specify (or link) api endpoints
## Persistent maps
Persistent maps can only be created by a CartoDB user who has full
responsibility over editing and deleting them. There are two
kind of persistent maps:
- Template maps
- Table maps (legacy, deprecated)
### Templated maps
Templated maps are templated [MapConfig]
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) documents
associated with an authorization certificate.
The authorization certificate determines who can instanciate the
template and use the resulting map. Authorized users of the instanciated
maps will have the same database access privilege of the template owner.
The HTTP endpoints for creating and using templated maps are described [here]
(http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps).
*TODO* cleanup the referenced document
### Table maps
Table maps are maps associated with a table.
Configuration of such maps is limited to the CartoCSS style.
* style - the CartoCSS style for the datasource, can be an array
* style_version - version of the CartoCSS style, can be an array
You can only fetch PNG or UTF8GRID tiles from these maps.
Access method is the same as the one for [Inline maps](#inline-maps)
# Endpoints description
- **/api/maps/** (same interface than https://github.com/CartoDB/Windshaft/wiki/Multilayer-API)
- **/api/maps/named** (same interface than https://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps)
NOTE: in case Multilayer-API does not contain this info yet, the
endpoint for fetching attributes is this:
- **/api/maps/:map_id/:layer_index/attributes/:feature_id**
- would return { c: 1, d: 2 }

View File

@@ -1,111 +1,674 @@
# Kind of maps
## Maps API
Windshaft-CartoDB supports these kind of maps:
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/).
- [Temporary maps](#temporary-maps) (created by anyone)
- [Detached maps](#detached-maps)
- [Inline maps](#inline-maps) (legacy)
- [Persistent maps](#peristent-maps) (created by CartDB user)
- [Template maps](#template-maps)
- [Table maps](#table-maps) (legacy, deprecated)
You can create two types of maps with the Maps API:
## Temporary maps
- **Anonymous maps**
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 }}).
Temporary maps have no owners and are anonymous in nature.
There are two kind of temporary maps:
- **Named maps**
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.
- Detached maps (aka MultiLayer-API)
- Inline maps
## Quickstart
### Detached maps
### Anonymous maps
Detached maps are maps which are configured with a request
obtaining a temporary token and then used by referencing
the obtained token. The token expires automatically when unused.
Here is an example of how to create an anonymous map with JavaScript:
Anyone can create detached maps, but users will need read access
to the data source of the map layers.
```javascript
var mapconfig = {
"version": "1.0.1",
"layers": [{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: #FFF; }",
"sql": "select * from european_countries_e"
}
}]
}
The configuration format is a [MapConfig]
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) document.
$.ajax({
crossOrigin: true,
type: 'POST',
dataType: 'json',
contentType: 'application/json',
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'
console.log(templateUrl);
}
})
```
The HTTP endpoints for creating the map and using it are described [here]
(http://github.com/CartoDB/Windshaft-cartodb/wiki/MultiLayer-API)
### Named maps
*TODO* cleanup the referenced document
Let's create a named map using some private tables in a CartoDB account.
The following map config sets up a map of European countries that have a white fill color:
### Inline maps
```javascript
{
"version": "0.0.1",
"name": "test",
"auth": {
"method": "open"
},
"layergroup": {
"layers": [{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: #FFF; }",
"sql": "select * from european_countries_e"
}
}]
}
}
```
Inline maps are maps that only exist for a single request,
being the request for a specific map resource (tile).
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:
Inline maps are always bound to a table, and can only be
obtained by those having read access to the that table.
Additionally, users need to have access to any datasource
specified as part of the configuration.
<div class="code-title notitle code-request"></div>
```bash
curl 'https://{account}.cartodb.com/api/v1/map/named?api_key=APIKEY' -H 'Content-Type: application/json' -d @mapconfig.json
```
Inline maps only support PNG and UTF8GRID tiles.
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.
The configuration consist in a set of parameters, to be
specified in the query string of the tile request:
<div class="code-title notitle code-request"></div>
```bash
curl -X POST 'http://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
```
* sql - the query to run as datasource, can be an array
* style - the CartoCSS style for the datasource, can be an array
* style_version - version of the CartoCSS style, can be an array
* interactivity - only for fetching UTF8GRID,
The response will return JSON with properties for the `layergroupid`, `cdn_url`, and the timestamp (`last_updated`) of the last data modification.
If the style is not provided, style of the associated table is
used; if the sql is not provided, all records of the associated
table are used as the datasource; the two possibilities result
in a mix between _inline_ maps and [Table maps][].
Here is an example response:
*TODO* specify (or link) api endpoints
```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"
}
```
## Persistent maps
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:
Persistent maps can only be created by a CartoDB user who has full
responsibility over editing and deleting them. There are two
kind of persistent maps:
```bash
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
```
- Template maps
- Table maps (legacy, deprecated)
## General Concepts
### Templated maps
The following concepts are the same for every endpoint in the API except when it's noted explicitly.
Templated maps are templated [MapConfig]
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) documents
associated with an authorization certificate.
### Auth
The authorization certificate determines who can instanciate the
template and use the resulting map. Authorized users of the instanciated
maps will have the same database access privilege of the template owner.
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).
The HTTP endpoints for creating and using templated maps are described [here]
(http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps).
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`.
*TODO* cleanup the referenced document
### Errors
### Table maps
Errors are reported using standard HTTP codes and extended information encoded in JSON with this format:
Table maps are maps associated with a table.
Configuration of such maps is limited to the CartoCSS style.
```javascript
{
"errors": [
"access forbidden to table TABLE"
]
}
```
* style - the CartoCSS style for the datasource, can be an array
* style_version - version of the CartoCSS style, can be an array
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
You can only fetch PNG or UTF8GRID tiles from these maps.
### CORS support
Access method is the same as the one for [Inline maps](#inline-maps)
All the endpoints which might be accessed using a web browser add CORS headers and allow OPTIONS method.
# Endpoints description
## Anonymous Maps
- **/api/maps/** (same interface than https://github.com/CartoDB/Windshaft/wiki/Multilayer-API)
- **/api/maps/named** (same interface than https://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps)
Anonymous maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
### Instantiate
#### Definition
<div class="code-title notitle code-request"></div>
```html
POST /api/v1/map
```
#### Params
```javascript
{
"version": "1.0.1",
"layers": [{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: #FFF; }",
"sql": "select * from european_countries_e",
"interactivity": ["cartodb_id", "iso3"]
}
}]
}
```
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.19.1/doc/MapConfig-1.1.0.md).
#### Response
The response includes:
- **layergroupid**
The ID for that map, used to compose the URL for the tiles. The final URL is:
```html
http://{account}.cartodb.com/api/v1/map/:layergroupid/{z}/{x}/{y}.png
```
- **updated_at**
The ISO date of the last time the data involved in the query was updated.
- **metadata** *(optional)*
Includes information about the layers. Some layers may not have metadata.
- **cdn_url**
URLs to fetch the data using the best CDN for your zone.
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl 'http://documentation.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
```
<div class="code-title">RESPONSE</div>
```javascript
{
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
"last_updated":"1970-01-01T00:00:00.000Z"
"cdn_url": {
"http": "http://cdb.com",
"https": "https://cdb.com"
}
}
```
The tiles can be accessed using:
```bash
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
```
For UTF grid tiles:
```bash
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
```
For attributes defined in `attributes` section:
```bash
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/attributes/:feature_id
```
Which returns JSON with the attributes defined, like:
```javascript
{ c: 1, d: 2 }
```
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
The JSONP endpoint is provided in order to allow web browsers access which don't support CORS.
#### Definition
<div class="code-title notitle code-request"></div>
```bash
GET /api/v1/map?callback=method
```
#### Params
- **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 as `config`.
- **callback**
JSON callback name.
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```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>
```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.
NOTE: in case Multilayer-API does not contain this info yet, the
endpoint for fetching attributes is this:
## Named Maps
- **/api/maps/:map_id/:layer_index/attributes/:feature_id**
- would return { c: 1, d: 2 }
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:
- **auth layer**
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 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](http://docs.cartodb.com/cartodb-platform/maps-api.html#auth)).
### Create
#### Definition
<div class="code-title notitle code-request"></div>
```html
POST /api/v1/map/named
```
#### Params
- **api_key** is required
<div class="code-title">template.json</div>
```javascript
{
"version": "0.0.1",
"name": "template_name",
"auth": {
"method": "token",
"valid_tokens": [
"auth_token1",
"auth_token2"
]
},
"placeholders": {
"color": {
"type": "css_color",
"default": "red"
},
"cartodb_id": {
"type": "number",
"default": 1
}
},
"layergroup": {
"version": "1.0.1",
"layers": [
{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: <%= color %>; }",
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
}
}
]
}
}
```
##### Arguments
- **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).
- **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 [MapConfig documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.1.0.md) for more info.
#### Template Format
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 the `<%=` and `%>` strings in order to be replaced.
##### Example
```javascript
<%= my_color %>
```
The set of supported placeholders for a template will need to be explicitly defined with a specific type and default value for each.
#### Placeholder Types
The placeholder type will determine the kind of escaping for the associated value. Supported types are:
- **sql_literal** internal single-quotes will be sql-escaped
- **sql_ident** internal double-quotes will be sql-escaped
- **number** can only contain numerical representation
- **css_color** can only contain color names or hex-values
Placeholder default values will be used whenever new values are not provided as options at the time of creation on the client. They can also be used to test the template by creating a default version with now options provided.
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>
```html
curl -X POST \
-H 'Content-Type: application/json' \
-d @template.json \
'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
```
<div class="code-title">RESPONSE</div>
```javascript
{
"template_id":"name",
}
```
### Instantiate
Instantiating a map allows you to get the information needed to fetch tiles. That temporal map is an anonymous map.
#### Definition
<div class="code-title notitle code-request"></div>
```html
POST /api/v1/map/named/:template_name
```
#### Param
- **auth_token** optional, but required when `"method"` is set to `"token"`
```javascript
// params.json
{
"color": "#ff0000",
"cartodb_id": 3
}
```
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
#### Example
You can initialize a template map by passing all of the required parameters in a POST to `/api/v1/map/named/:template_name`.
Valid credentials will be needed if required by the template.
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl -X POST \
-H 'Content-Type: application/json' \
-d @params.json \
'https://documentation.cartodb.com/api/v1/map/named/@template_name?auth_token=AUTH_TOKEN'
```
<div class="code-title">Response</div>
```javascript
{
"layergroupid": "docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
"last_updated": "2013-11-14T11:20:15.000Z"
}
```
<div class="code-title">Error</div>
```javascript
{
"error": "Some error string here"
}
```
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.
### Using JSONP
There is also a special endpoint to be able to initialize a map using JSONP (for old browsers).
#### Definition
<div class="code-title notitle code-request"></div>
```bash
GET /api/v1/map/named/:template_name/jsonp
```
#### Params
- **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>
```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>
```javascript
callback({
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
"last_updated":"1970-01-01T00:00:00.000Z"
"cdn_url": {
"http": "http://cdb.com",
"https": "https://cdb.com"
}
})
```
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).
```javascript
url += "config=" + encodeURIComponent(
JSON.stringify({ color: 'red' });
```
The response is in this format:
```javascript
callback({
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
last_updated: "2014-01-27T17:41:03.021Z"
})
```
### Update
#### Definition
<div class="code-title notitle code-request"></div>
```bash
PUT /api/v1/map/named/:template_name
```
#### Params
- **api_key** is required
#### Response
Same as updating a map.
#### Other Info
Updating a named map removes all the named map instances so they need to be initialized again.
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl -X PUT \
-H 'Content-Type: application/json' \
-d @template.json \
'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
```
<div class="code-title">RESPONSE</div>
```javascript
{
"template_id": "@template_name"
}
```
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:
```javascript
{
"error": "error string here"
}
```
### Delete
Delete the specified template map from the server and disables any previously initialized versions of the map.
#### Definition
<div class="code-title notitle code-request"></div>
```bash
DELETE /api/v1/map/named/:template_name
```
#### Params
- **api_key** is required
#### Example
<div class="code-title code-request">REQUEST</div>
```bash
curl -X DELETE 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
```
<div class="code-title">RESPONSE</div>
```javascript
{
"error": "Some error string here"
}
```
On success, a 204 (No Content) response would be issued. Otherwise a 4xx response with with an error will be returned.
### Listing Available Templates
This allows you to get a list of all available templates.
#### Definition
<div class="code-title notitle code-request"></div>
```bash
GET /api/v1/map/named/
```
#### Params
- **api_key** is required
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
```
<div class="code-title with-result">RESPONSE</div>
```javascript
{
"template_ids": ["@template_name1","@template_name2"]
}
```
<div class="code-title">ERROR</div>
```javascript
{
"error": "Some error string here"
}
```
### Getting a Specific Template
This gets the definition of a template.
#### Definition
<div class="code-title notitle code-request"></div>
```bash
GET /api/v1/map/named/:template_name
```
#### Params
- **api_key** is required
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```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>
```javascript
{
"template": {...} // see template.json above
}
```
<div class="code-title">ERROR</div>
```javascript
{
"error": "Some error string here"
}
```
### 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,22 +36,19 @@ 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
{
version: '0.0.1',
version: "0.0.1",
// 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: 'template_name',
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)
valid_tokens: ['auth_token1','auth_token2'] // only (required and non empty) for 'token' method
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"]
},
// Variables not listed here are not substituted
// Variable not provided at instantiation time trigger an error
@@ -66,11 +56,11 @@ certificate that would be used to sign any instance of the template.
// Type specification is used for quoting, to avoid injections
placeholders: {
color: {
type:'css_color',
default:'red'
type:"css_color",
default:"red"
},
cartodb_id: {
type:'number',
type:"number",
default: 1
}
},
@@ -91,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
@@ -120,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 \
@@ -242,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)
@@ -274,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

44
docs/metrics.md Normal file
View File

@@ -0,0 +1,44 @@
Windshaft-cartodb metrics
=========================
See [Windshaft metrics documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/metrics.md) to understand the full picture.
The next list includes the API endpoints, each endpoint may have several inner timers, some of them are displayed within this list as subitems. Find the description for them in the Inner timers section.
## Timers
- **windshaft-cartodb.flush_cache**: time to flush the tile and sql cache
- **windshaft-cartodb.get_template**: time to retrieve an specific template
- **windshaft-cartodb.delete_template**: time to delete an specific template
- **windshaft-cartodb.get_template_list**: time to retrieve the list of owned templates
- **windshaft-cartodb.instance_template_post**: time to create a template via HTTP POST
- **windshaft-cartodb.instance_template_get**: time to create a template via HTTP GET
+ TemplateMaps_instance
+ createLayergroup
There are some endpoints that are not being tracked:
- Adding a template
- Updating a template
### Inner timers
Again, each inner timer may have several inner timers.
- **addCacheChannel**: time to add X-Cache-Channel header based on table last modifications
- **LZMA decompress**: time to decompress request params with LZMA
- **TemplateMaps_instance**: time to retrieve a map template instance, see *getTemplate* and *authorizedByCert*
- **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 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*
- **generateCacheChannel**: time to generate the headers for the cache channel based on the request, see *addCacheChannel*
- **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
- **incMapviewCount**: time to incremenent in redis the map views
- **mapStore_load**: time to retrieve from redis a map configuration
- **req2params.setup**: time to prepare the params from a request, see *req2params* in Windshaft documentation
- **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*
- **tablePrivacy_getUserDBName**: time to retrieve from redis the database for a user

View File

@@ -0,0 +1,122 @@
var sqlApi = require('../sql/sql_api'),
PSQL = require('cartodb-psql');
function QueryTablesApi() {
}
var affectedTableRegexCache = {
bbox: /!bbox!/g,
scale_denominator: /!scale_denominator!/g,
pixel_width: /!pixel_width!/g,
pixel_height: /!pixel_height!/g
};
module.exports = QueryTablesApi;
QueryTablesApi.prototype.getLastUpdatedTime = function (username, api_key, tableNames, callback) {
var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY['+
tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(',') +
'])';
// call sql api
sqlApi.query(username, api_key, sql, function(err, rows){
if (err){
var msg = err.message ? err.message : err;
callback(new Error('could not find last updated timestamp: ' + msg));
return;
}
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
var last_updated = 0;
if(rows.length !== 0) {
last_updated = rows[0].max || 0;
}
callback(null, last_updated*1000);
});
};
QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, options, sql, callback) {
var query = 'SELECT CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$)';
runQuery(username, options, query, handleAffectedTablesInQueryRows, callback);
};
function handleAffectedTablesInQueryRows(err, rows, callback) {
if (err){
var msg = err.message ? err.message : err;
callback(new Error('could not fetch source tables: ' + msg));
return;
}
var qtables = rows[0].cdb_querytables;
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
tableNames = tableNames ? tableNames.split(',') : [];
callback(null, tableNames);
}
QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, options, sql, callback) {
var query = [
'WITH querytables AS (',
'SELECT * FROM CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$) as tablenames',
')',
'SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max',
'FROM CDB_TableMetadata m',
'WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'
].join(' ');
runQuery(username, options, query, handleAffectedTablesAndLastUpdatedTimeRows, callback);
};
function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
if (err || rows.length === 0) {
var msg = err.message ? err.message : err;
callback(new Error('could not fetch affected tables and last updated time: ' + msg));
return;
}
var result = rows[0];
var tableNames = result.tablenames.split(/^\{(.*)\}$/)[1];
tableNames = tableNames ? tableNames.split(',') : [];
var lastUpdatedTime = result.max || 0;
callback(null, {
affectedTables: tableNames,
lastUpdatedTime: lastUpdatedTime * 1000
});
}
function runQuery(username, options, query, queryHandler, callback) {
if (shouldQueryPostgresDirectly()) {
var psql = new PSQL(options);
psql.query(query, function(err, resultSet) {
resultSet = resultSet || {};
var rows = resultSet.rows || [];
queryHandler(err, rows, callback);
});
} else {
sqlApi.query(username, options.api_key, query, function(err, rows) {
queryHandler(err, rows, callback);
});
}
}
function prepareSql(sql) {
return sql
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
.replace(affectedTableRegexCache.scale_denominator, '0')
.replace(affectedTableRegexCache.pixel_width, '1')
.replace(affectedTableRegexCache.pixel_height, '1')
;
}
function shouldQueryPostgresDirectly() {
return global.environment
&& global.environment.enabledFeatures
&& global.environment.enabledFeatures.cdbQueryTablesFromPostgres;
}

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,19 +1,16 @@
var _ = require('underscore')
, Step = require('step')
, Windshaft = require('windshaft')
, redisPool = new require('redis-mpool')(global.environment.redis)
// 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')
;
var CartodbWindshaft = function(serverOptions) {
var debug = global.environment.debug;
if ( ! process.env['PGAPPNAME'] )
process.env['PGAPPNAME']='cartodb_tiler';
var CartodbWindshaft = function(serverOptions) {
// Perform keyword substitution in statsd
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
if ( global.environment.statsd ) {
@@ -23,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);
@@ -38,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
//
@@ -46,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);
@@ -61,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.
@@ -130,13 +152,24 @@ 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.
*/
ws.get(serverOptions.base_url + '/infowindow', function(req, res){
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.get_infowindow');
}
ws.doCORS(res);
Step(
function(){
@@ -158,9 +191,6 @@ var CartodbWindshaft = function(serverOptions) {
* Helper to allow access to metadata to be used in embedded maps.
*/
ws.get(serverOptions.base_url + '/map_metadata', function(req, res){
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.get_map_metadata');
}
ws.doCORS(res);
Step(
function(){
@@ -201,475 +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 ) {
var report = req.profiler.toString();
res.header('X-Tiler-Profiler', report);
}
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 ) {
var report = req.profiler.toString();
res.header('X-Tiler-Profiler', report);
}
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);
if ( req.profiler ) req.profiler.done('cors');
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),
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 ) {
var report = req.profiler.toString();
res.header('X-Tiler-Profiler', report);
}
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,11 +1,9 @@
var _ = require('underscore')
, Step = require('step')
, cartoData = require('cartodb-redis')(global.environment.redis)
, Cache = require('./cache_validator')
, mapnik = require('mapnik')
, Cache = require('./cache_validator')
, QueryTablesApi = require('./api/query_tables_api')
, crypto = require('crypto')
, request = require('request')
, LZMA = require('lzma/lzma_worker.js').LZMA
, LZMA = require('lzma').LZMA
;
// This is for backward compatibility with 1.3.3
@@ -17,12 +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'
];
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 = {
@@ -46,23 +67,31 @@ module.exports = function(){
},
datasource: global.environment.postgres,
cachedir: global.environment.millstone.cache_basedir,
mapnik_version: global.environment.mapnik_version || mapnik.versions.mapnik,
mapnik_version: global.environment.mapnik_version,
mapnik_tile_format: global.environment.mapnik_tile_format || 'png',
default_layergroup_ttl: global.environment.mapConfigTTL || 7200,
gc_prob: 0.01 // @deprecated since Windshaft-1.8.0
},
mapnik: {
poolSize: rendererConfig.poolSize,
metatile: rendererConfig.metatile,
bufferSize: rendererConfig.bufferSize
},
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
@@ -72,13 +101,8 @@ module.exports = function(){
// See http://github.com/CartoDB/Windshaft-cartodb/issues/161
me.redis.unwatchOnRelease = false;
// Be nice and warn if configured mapnik version
// is != instaled mapnik version
if ( mapnik.versions.mapnik != me.grainstore.mapnik_version ) {
console.warn("WARNING: detected mapnik version ("
+ mapnik.versions.mapnik + ") != configured mapnik version ("
+ me.grainstore.mapnik_version + ")");
}
// Re-use redisPool
me.redis.pool = redisPool;
/* This whole block is about generating X-Cache-Channel { */
@@ -88,120 +112,6 @@ module.exports = function(){
// we have no SQL after layer creation.
me.channelCache = {};
// Run a query through the SQL api
me.sqlQuery = function (username, api_key, sql, callback) {
var api = global.environment.sqlapi;
// build up api string
var sqlapihostname = username;
if ( api.domain ) sqlapihostname += '.' + api.domain;
var sqlapi = api.protocol + '://';
if ( api.host && api.host != api.domain ) sqlapi += api.host;
else sqlapi += sqlapihostname;
sqlapi += ':' + api.port + '/api/' + api.version + '/sql';
var qs = { q: sql }
// add api_key if given
if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; }
// call sql api
//
// NOTE: using POST to avoid size limits:
// See http://github.com/CartoDB/Windshaft-cartodb/issues/111
//
// NOTE: uses "host" header to allow IP based specification
// of sqlapi address (and avoid a DNS lookup)
//
// NOTE: allows for keeping up to "maxConnections" concurrent
// sockets opened per SQL-API host.
// See http://nodejs.org/api/http.html#http_agent_maxsockets
//
var maxSockets = global.environment.maxConnections || 128;
var maxGetLen = api.max_get_sql_length || 2048;
var maxSQLTime = api.timeout || 100; // 1/10 of a second by default
var reqSpec = {
url:sqlapi,
json:true,
headers:{host: sqlapihostname}
// http://nodejs.org/api/http.html#http_agent_maxsockets
,pool:{maxSockets:maxSockets}
// timeout in milliseconds
,timeout:maxSQLTime
}
if ( sql.length > maxGetLen ) {
reqSpec.method = 'POST';
reqSpec.body = qs;
} else {
reqSpec.method = 'GET';
reqSpec.qs = qs;
}
request(reqSpec, function(err, res, body) {
if (err){
console.log('ERROR connecting to SQL API on ' + sqlapi + ': ' + err);
callback(err);
return;
}
if (res.statusCode != 200) {
var msg = res.body.error ? res.body.error : res.body;
callback(new Error(msg));
console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg);
return;
}
callback(null, body.rows);
});
};
//
// Invoke callback with number of milliseconds since
// last update in any of the given tables
//
me.findLastUpdated = function (username, api_key, tableNames, callback) {
var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname::name = any (\'{'
+ tableNames.join(',') + '}\')';
// call sql api
me.sqlQuery(username, api_key, sql, function(err, rows){
if (err){
var msg = err.message ? err.message : err;
callback(new Error('could not find last updated timestamp: ' + msg));
return;
}
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
var last_updated = 0;
if(rows.length !== 0) {
last_updated = rows[0].max || 0;
}
callback(null, last_updated*1000);
});
};
me.affectedTables = function (username, api_key, sql, callback) {
// Replace mapnik tokens
sql = sql.replace(RegExp('!bbox!', 'g'), 'ST_MakeEnvelope(0,0,0,0)')
.replace(RegExp('!pixel_width!', 'g'), '1')
.replace(RegExp('!pixel_height!', 'g'), '1')
;
// Pass to CDB_QueryTables
sql = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)';
// call sql api
me.sqlQuery(username, api_key, sql, function(err, rows){
if (err){
var msg = err.message ? err.message : err;
callback(new Error('could not fetch source tables: ' + msg));
return;
}
var qtables = rows[0].cdb_querytables;
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
tableNames = tableNames ? tableNames.split(',') : [];
callback(null, tableNames);
});
};
me.buildCacheChannel = function (dbName, tableNames){
return dbName + ':' + tableNames.join(',');
};
@@ -210,7 +120,7 @@ module.exports = function(){
var hash = crypto.createHash('md5');
hash.update(data);
return hash.digest('hex');
}
};
me.generateCacheChannel = function(app, req, callback){
@@ -240,7 +150,6 @@ module.exports = function(){
// See http://github.com/CartoDB/Windshaft-cartodb/issues/152
if ( ! app.mapStore ) {
throw new Error('missing channel cache for token ' + req.params.token);
return;
}
var next = this;
var mapStore = app.mapStore;
@@ -304,7 +213,14 @@ module.exports = function(){
if ( req.profiler ) req.profiler.done('getSignerMapKey');
key = data;
}
me.affectedTables(user, key, sql, this); // in addCacheChannel
queryTablesApi.getAffectedTablesInQuery(user, {
user: req.params.dbuser,
pass: req.params.dbpass,
host: req.params.dbhost,
port: req.params.dbport,
dbname: req.params.dbname,
api_key: key
}, sql, this); // in addCacheChannel
},
function finish(err, data) {
next(err,data);
@@ -396,7 +312,7 @@ module.exports = function(){
err = errors.length ? new Error(errors.join('\n')) : null;
callback(err);
}
}
};
// include in layergroup response the variables in serverMedata
// those variables are useful to send to the client information
@@ -426,33 +342,44 @@ module.exports = function(){
var key = req.params.map_key || req.params.api_key;
var cacheKey = dbName + ':' + token;
var tabNames;
Step(
function getTables() {
me.affectedTables(usr, key, sql, this); // in afterLayergroupCreate
},
function getLastupdated(err, tableNames) {
if (req.profiler) req.profiler.done('affectedTables');
if ( err ) throw err;
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
// store for caching from me.afterLayergroupCreate
me.channelCache[cacheKey] = cacheChannel;
// find last updated
if ( ! tableNames.length ) return 0; // skip for no affected tables
tabNames = tableNames;
me.findLastUpdated(usr, key, tableNames, this);
},
function(err, lastUpdated) {
if ( err ) throw err;
if (req.profiler && tabNames) req.profiler.done('findLastUpdated');
response.layergroupid = response.layergroupid + ':' + lastUpdated; // use epoch
response.last_updated = new Date(lastUpdated).toISOString();
return null;
},
function finish(err) {
done(err);
}
function getAffectedTablesAndLastUpdatedTime() {
queryTablesApi.getAffectedTablesAndLastUpdatedTime(usr, {
user: req.params.dbuser,
pass: req.params.dbpass,
host: req.params.dbhost,
port: req.params.dbport,
dbname: req.params.dbname,
api_key: key
}, sql, this);
},
function handleAffectedTablesAndLastUpdatedTime(err, result) {
if (req.profiler) req.profiler.done('queryTablesAndLastUpdated');
if ( err ) throw err;
var cacheChannel = me.buildCacheChannel(dbName, result.affectedTables);
me.channelCache[cacheKey] = cacheChannel;
if (req.res && req.method == 'GET') {
var res = req.res;
if ( req.query && req.query.cache_policy == 'persist' ) {
res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
} else {
var ttl = global.environment.varnish.layergroupTtl || 86400;
res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
}
res.header('Last-Modified', (new Date()).toUTCString());
res.header('X-Cache-Channel', cacheChannel);
}
// last update for layergroup cache buster
response.layergroupid = response.layergroupid + ':' + result.lastUpdatedTime;
response.last_updated = new Date(result.lastUpdatedTime).toISOString();
return null;
},
function finish(err) {
done(err);
}
);
};
@@ -478,7 +405,7 @@ module.exports = function(){
return;
}
return mat[1];
}
};
// Set db authentication parameters to those of the given username
//
@@ -547,21 +474,20 @@ module.exports = function(){
dbport: global.environment.postgres.port
});
Step(
function getDatabaseHost(){
cartoData.getUserDBHost(dbowner, this);
function getConnectionParams() {
cartoData.getUserDBConnectionParams(dbowner, this);
},
function getDatabase(err, data){
if(err) throw err;
if ( data ) _.extend(params, {dbhost:data});
cartoData.getUserDBName(dbowner, this);
},
function extendParams(err, data){
function extendParams(err, dbParams){
if (err) throw err;
if ( data ) _.extend(params, {dbname:data});
// we don't want null values or overwrite a non public user
if (params.dbuser != 'publicuser' || !dbParams.dbuser) {
delete dbParams.dbuser;
}
if ( dbParams ) _.extend(params, dbParams);
return null;
},
function finish(err) {
callback(err);
callback(err);
}
);
};
@@ -569,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
@@ -591,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
@@ -731,16 +662,16 @@ module.exports = function(){
//console.log("type of req.query.lzma is " + typeof(req.query.lzma));
// Decode (from base64)
var lzma = (new Buffer(req.query.lzma, 'base64').toString('binary')).split('').map(function(c) { return c.charCodeAt(0) - 128 })
var lzma = (new Buffer(req.query.lzma, 'base64').toString('binary')).split('').map(function(c) { return c.charCodeAt(0) - 128 });
// Decompress
LZMA.decompress(
lzmaWorker.decompress(
lzma,
function(result) {
if (req.profiler) req.profiler.done('LZMA decompress');
try {
delete req.query.lzma
_.extend(req.query, JSON.parse(result))
delete req.query.lzma;
_.extend(req.query, JSON.parse(result));
me.req2params(req, callback);
} catch (err) {
callback(new Error('Error parsing lzma as JSON: ' + err));
@@ -753,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
@@ -772,7 +701,7 @@ module.exports = function(){
req.params.signer = tksplit.shift();
if ( ! req.params.signer ) req.params.signer = user;
else if ( req.params.signer != user ) {
var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' + user + '"')
var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' + user + '"');
err.http_status = 403;
callback(err);
return;
@@ -820,7 +749,6 @@ module.exports = function(){
cartoData.getTableGeometryType(req.params.dbname, req.params.table, this);
},
function finishSetup(err, data){
if (req.profiler) req.profiler.done('cartoData.getTableGeometryType');
if ( err ) { callback(err, req); return; }
if (!_.isNull(data))

View File

@@ -1,401 +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) {
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' ) {
var found = cert.auth.valid_tokens.indexOf(auth);
//if ( found !== -1 ) {
//console.log("Token " + auth + " is found at position " + found + " in valid tokens " + cert.auth.valid_tokens);
// return true;
//} else return false;
return cert.auth.valid_tokens.indexOf(auth) !== -1;
}
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

@@ -0,0 +1,66 @@
var _ = require('underscore'),
request = require('request');
module.exports.query = function (username, api_key, sql, callback) {
var api = global.environment.sqlapi;
// build up api string
var sqlapihostname = username;
if ( api.domain ) sqlapihostname += '.' + api.domain;
var sqlapi = api.protocol + '://';
if ( api.host && api.host != api.domain ) sqlapi += api.host;
else sqlapi += sqlapihostname;
sqlapi += ':' + api.port + '/api/' + api.version + '/sql';
var qs = { q: sql };
// add api_key if given
if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; }
// call sql api
//
// NOTE: using POST to avoid size limits:
// See http://github.com/CartoDB/Windshaft-cartodb/issues/111
//
// NOTE: uses "host" header to allow IP based specification
// of sqlapi address (and avoid a DNS lookup)
//
// NOTE: allows for keeping up to "maxConnections" concurrent
// sockets opened per SQL-API host.
// See http://nodejs.org/api/http.html#http_agent_maxsockets
//
var maxSockets = global.environment.maxConnections || 128;
var maxGetLen = api.max_get_sql_length || 2048;
var maxSQLTime = api.timeout || 100; // 1/10 of a second by default
var reqSpec = {
url:sqlapi,
json:true,
headers:{host: sqlapihostname}
// http://nodejs.org/api/http.html#http_agent_maxsockets
,pool:{maxSockets:maxSockets}
// timeout in milliseconds
,timeout:maxSQLTime
};
if ( sql.length > maxGetLen ) {
reqSpec.method = 'POST';
reqSpec.body = qs;
} else {
reqSpec.method = 'GET';
reqSpec.qs = qs;
}
request(reqSpec, function(err, res, body) {
if (err){
console.log('ERROR connecting to SQL API on ' + sqlapi + ': ' + err);
callback(err);
return;
}
if (res.statusCode != 200) {
var msg = res.body.error ? res.body.error : res.body;
callback(new Error(msg));
console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg);
return;
}
callback(null, body.rows);
});
};

View File

@@ -1,10 +1,12 @@
var crypto = require('crypto');
var Step = require('step');
var _ = require('underscore');
var crypto = require('crypto'),
Step = require('step'),
_ = require('underscore'),
dot = require('dot');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
// Templates in this hash (keyed as <username>@<template_name>)
// are being worked on.
var user_template_locks = {};
// Class handling map templates
//
@@ -14,16 +16,16 @@ var user_template_locks = {};
// 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
@@ -38,17 +40,21 @@ function TemplateMaps(redis_pool, signed_maps, opts) {
//
// We have the following datastores:
//
// 1. User teplates: set of per-user map templates
// NOTE: each template would have an associated auth
// reference, see signed_maps.js
// 1. User templates: set of per-user map templates
// User templates (HASH:tpl_id->tpl_val)
this.key_usr_tpl = "map_tpl|<%= owner %>";
this.key_usr_tpl = dot.template("map_tpl|{{=it.owner}}");
// User template locks (HASH:tpl_id->ctime)
this.key_usr_tpl_lck = "map_tpl|<%= owner %>|locks";
this.key_usr_tpl_lck = dot.template("map_tpl|{{=it.owner}}|locks");
this.lock_ttl = this.opts['lock_ttl'] || 5000;
}
util.inherits(TemplateMaps, EventEmitter);
module.exports = TemplateMaps;
};
var o = TemplateMaps.prototype;
@@ -58,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
*
@@ -95,38 +93,7 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
);
};
// @param callback function(err, obtained)
o._obtainTemplateLock = function(owner, tpl_id, callback) {
var usr_tpl_lck_key = _.template(this.key_usr_tpl_lck, {owner:owner});
var that = this;
var gotLock = false;
Step (
function obtainLock() {
var ctime = Date.now();
that._redisCmd('HSETNX', [usr_tpl_lck_key, tpl_id, ctime], this);
},
function checkLock(err, locked) {
if ( err ) throw err;
if ( ! locked ) {
// Already locked
// TODO: unlock if expired ?
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' is locked");
}
return gotLock = true;
},
function finish(err) {
callback(err, gotLock);
}
);
};
// @param callback function(err, deleted)
o._releaseTemplateLock = function(owner, tpl_id, callback) {
var usr_tpl_lck_key = _.template(this.key_usr_tpl_lck, {owner:owner});
this._redisCmd('HDEL', [usr_tpl_lck_key, tpl_id], callback);
};
o._reValidIdentifier = /^[a-zA-Z][0-9a-zA-Z_]*$/;
var _reValidIdentifier = /^[a-zA-Z][0-9a-zA-Z_]*$/;
o._checkInvalidTemplate = function(template) {
if ( template.version != '0.0.1' ) {
return new Error("Unsupported template version " + template.version);
@@ -135,47 +102,88 @@ o._checkInvalidTemplate = function(template) {
if ( ! tplname ) {
return new Error("Missing template name");
}
if ( ! tplname.match(this._reValidIdentifier) ) {
if ( ! tplname.match(_reValidIdentifier) ) {
return new Error("Invalid characters in template name '" + tplname + "'");
}
var phold = template.placeholders;
for (var k in phold) {
if ( ! k.match(this._reValidIdentifier) ) {
return new Error("Invalid characters in placeholder name '" + k + "'");
}
if ( ! phold[k].hasOwnProperty('default') ) {
return new Error("Missing default for placeholder '" + k + "'");
}
if ( ! phold[k].hasOwnProperty('type') ) {
return new Error("Missing type for placeholder '" + k + "'");
}
};
var invalidError = isInvalidLayergroup(template.layergroup);
if (invalidError) {
return invalidError;
}
// Check certificate validity
var cert = this.getTemplateCertificate(template);
var err = this.signed_maps.checkInvalidCertificate(cert);
if ( err ) return err;
var placeholders = template.placeholders || {};
// TODO: run more checks over template format ?
var placeholderKeys = Object.keys(placeholders);
for (var i = 0, len = placeholderKeys.length; i < len; i++) {
var placeholderKey = placeholderKeys[i];
if (!placeholderKey.match(_reValidIdentifier)) {
return new Error("Invalid characters in placeholder name '" + placeholderKey + "'");
}
if ( ! placeholders[placeholderKey].hasOwnProperty('default') ) {
return new Error("Missing default for placeholder '" + placeholderKey + "'");
}
if ( ! placeholders[placeholderKey].hasOwnProperty('type') ) {
return new Error("Missing type for placeholder '" + placeholderKey + "'");
}
}
var auth = template.auth || {};
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) {
var cert = {
version: '0.0.1',
template_id: template.name,
auth: template.auth
};
return cert;
};
// Add a template
//
// NOTE: locks user+template_name or fails
@@ -189,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 = _.template(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
@@ -293,82 +257,28 @@ o.addTemplate = function(owner, template, callback) {
// @param callback function(err)
//
o.delTemplate = function(owner, tpl_id, callback) {
var usr_tpl_key = _.template(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
@@ -388,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 = _.template(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
@@ -496,8 +356,7 @@ o.updTemplate = function(owner, tpl_id, template, callback) {
// Returns a list of template identifiers
//
o.listTemplates = function(owner, callback) {
var usr_tpl_key = _.template(this.key_usr_tpl, {owner:owner});
this._redisCmd('HKEYS', [ usr_tpl_key ], callback);
this._redisCmd('HKEYS', [ this.key_usr_tpl({owner:owner}) ], callback);
};
// Get a templates
@@ -511,22 +370,43 @@ o.listTemplates = function(owner, callback) {
// Return full template definition
//
o.getTemplate = function(owner, tpl_id, callback) {
var usr_tpl_key = _.template(this.key_usr_tpl, {owner:owner});
var that = this;
Step(
function getTemplate() {
that._redisCmd('HGET', [ usr_tpl_key, tpl_id ], this);
},
function parseTemplate(err, tpl_val) {
if ( err ) throw err;
var tpl = JSON.parse(tpl_val);
// Should we strip auth_id ?
return tpl;
},
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
@@ -541,25 +421,22 @@ o.getTemplate = function(owner, tpl_id, callback) {
//
// @throws Error on malformed template or parameter
//
o._reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/;
o._reCSSColorName = /^[a-zA-Z]+$/;
o._reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
o._replaceVars = function(str, params) {
var _reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/,
_reCSSColorName = /^[a-zA-Z]+$/,
_reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
_replaceVars = function(str, params) {
//return _.template(str, params); // lazy way, possibly dangerous
// Construct regular expressions for each param
if ( ! params._re ) {
params._re = {};
for (var k in params) {
params._re[k] = RegExp("<%=\\s*" + k + "\\s*%>", "g");
}
}
for (var k in params) str = str.replace(params._re[k], params[k]);
return str;
Object.keys(params).forEach(function(k) {
str = str.replace(new RegExp("<%=\\s*" + k + "\\s*%>", "g"), params[k]);
});
return str;
};
o.instance = function(template, params) {
var all_params = {};
var phold = template.placeholders;
for (var k in phold) {
var phold = template.placeholders || {};
Object.keys(phold).forEach(function(k) {
var val = params.hasOwnProperty(k) ? params[k] : phold[k].default;
var type = phold[k].type;
// properly escape
@@ -573,7 +450,7 @@ o.instance = function(template, params) {
}
else if ( type === 'number' ) {
// check it's a number
if ( typeof(val) !== 'number' && ! val.match(this._reNumber) ) {
if ( typeof(val) !== 'number' && ! val.match(_reNumber) ) {
throw new Error("Invalid number value for template parameter '"
+ k + "': " + val);
}
@@ -581,7 +458,7 @@ o.instance = function(template, params) {
else if ( type === 'css_color' ) {
// check it only contains letters or
// starts with # and only contains hexdigits
if ( ! val.match(this._reCSSColorName) && ! val.match(this._reCSSColorVal) ) {
if ( ! val.match(_reCSSColorName) && ! val.match(_reCSSColorVal) ) {
throw new Error("Invalid css_color value for template parameter '"
+ k + "': " + val);
}
@@ -591,16 +468,23 @@ o.instance = function(template, params) {
throw new Error("Invalid placeholder type '" + type + "'");
}
all_params[k] = val;
}
});
// NOTE: we're deep-cloning the layergroup here
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
for (var i=0; i<layergroup.layers.length; ++i) {
var lyropt = layergroup.layers[i].options;
if ( lyropt.cartocss ) lyropt.cartocss = this._replaceVars(lyropt.cartocss, all_params);
if ( lyropt.sql) lyropt.sql = this._replaceVars(lyropt.sql, all_params);
if ( lyropt.cartocss ) lyropt.cartocss = _replaceVars(lyropt.cartocss, all_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;
};
@@ -611,5 +495,3 @@ o.fingerPrint = function(template) {
.digest('hex')
;
};
module.exports = TemplateMaps;

2437
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.10.2",
"version": "1.26.0",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -22,25 +22,30 @@
"Sandro Santilli <strk@vizzuality.com>"
],
"dependencies": {
"node-varnish": "http://github.com/Vizzuality/node-varnish/tarball/0.3.0",
"underscore" : "~1.3.3",
"windshaft" : "http://github.com/CartoDB/Windshaft/tarball/0.20.0",
"step": "0.0.x",
"request": "2.9.202",
"cartodb-redis": "~0.3.0",
"redis-mpool": "http://github.com/CartoDB/node-redis-mpool/tarball/0.0.4",
"mapnik": "http://github.com/Vizzuality/node-mapnik/tarball/0.7.26-cdb1",
"lzma": "~1.2.3",
"log4js": "~0.6.10",
"rollbar": "~0.3.1"
"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.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.3.0",
"lzma": "~1.3.7",
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
"rollbar": "~0.3.13"
},
"devDependencies": {
"mocha": "1.14.0",
"redis": "~0.8.3",
"strftime": "~0.6.0",
"semver": "~1.1.0"
"mocha": "~1.21.4",
"redis": "~0.8.6",
"strftime": "~0.8.2",
"semver": "~1.1.4"
},
"scripts": {
"test": "make check"
},
"engines": {
"node": ">=0.8 <0.11",
"npm": ">=1.2.1"
}
}

View File

@@ -5,6 +5,8 @@ OPT_CREATE_PGSQL=yes # create the PostgreSQL test environment
OPT_DROP_REDIS=yes # drop the redis test environment
OPT_DROP_PGSQL=yes # drop the PostgreSQL test environment
export PGAPPNAME=cartodb_tiler_tester
cd $(dirname $0)
BASEDIR=$(pwd)
cd -

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

@@ -4,7 +4,6 @@ var _ = require('underscore');
var redis = require('redis');
var querystring = require('querystring');
var semver = require('semver');
var mapnik = require('mapnik');
var Step = require('step');
var strftime = require('strftime');
var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js');
@@ -14,13 +13,20 @@ var helper = require(__dirname + '/../support/test_helper');
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 20;
var IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL = 25;
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var ServerOptions = require(__dirname + '/../../lib/cartodb/server_options');
serverOptions = ServerOptions();
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
suite('multilayer', function() {
[true, false].forEach(function(cdbQueryTablesFromPostgresEnabledValue) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue};
suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() {
var redis_client = redis.createClient(global.environment.redis.port);
var sqlapi_server;
@@ -108,12 +114,15 @@ suite('multilayer', function() {
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$'
+ layergroup.layers[0].options.sql + ';'
+ layergroup.layers[1].options.sql
+ '$windshaft$)');
var expectedQuery = [layergroup.layers[0].options.sql, ';', layergroup.layers[1].options.sql].join('');
assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$'
+ expectedQuery
+ '$windshaft$) as tablenames )'
+ ' SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max'
+ ' FROM CDB_TableMetadata m'
+ ' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])');
assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', 2,
assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL,
function(err, similarity) {
next(err);
});
@@ -148,7 +157,7 @@ suite('multilayer', function() {
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.layer0.grid.json', 2,
function(err, similarity) {
next(err);
@@ -166,7 +175,7 @@ suite('multilayer', function() {
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.layer1.grid.json', 2,
function(err, similarity) {
next(err);
@@ -226,9 +235,7 @@ suite('multilayer', function() {
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
// NOTE: another test like this is in templates.js
test("get creation requests no cache", function(done) {
test("get creation requests has cache", function(done) {
var layergroup = {
version: '1.0.0',
@@ -257,7 +264,7 @@ suite('multilayer', function() {
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
expected_token = parsedBody.layergroupid.split(':')[0];
helper.checkNoCache(res);
helper.checkCache(res);
return null;
},
function finish(err) {
@@ -279,6 +286,49 @@ suite('multilayer', function() {
);
});
test("get creation has no cache if sql is bogus", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select bogus(0,0) as the_geom_webmercator',
cartocss: '#layer { polygon-fill: red; }',
cartocss_version: '2.0.1'
} }
]
};
assert.response(server, {
url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify(layergroup)),
method: 'GET',
headers: {host: 'localhost'}
}, {}, function(res) {
assert.notEqual(res.statusCode, 200);
helper.checkNoCache(res);
done();
});
});
test("get creation has no cache if cartocss is not valid", function(done) {
var 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',
cartocss: '#layer { invalid-rule:red; }',
cartocss_version: '2.0.1'
} }
]
};
assert.response(server, {
url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify(layergroup)),
method: 'GET',
headers: {host: 'localhost'}
}, {}, function(res) {
assert.notEqual(res.statusCode, 200);
helper.checkNoCache(res);
done();
});
});
test("layergroup can hold substitution tokens", function(done) {
@@ -343,14 +393,18 @@ suite('multilayer', function() {
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$'
+ layergroup.layers[0].options.sql
.replace(RegExp('!bbox!', 'g'), 'ST_MakeEnvelope(0,0,0,0)')
.replace(RegExp('!pixel_width!', 'g'), '1')
.replace(RegExp('!pixel_height!', 'g'), '1')
+ '$windshaft$)');
var expectedQuery = layergroup.layers[0].options.sql
.replace(/!bbox!/g, 'ST_MakeEnvelope(0,0,0,0)')
.replace(/!pixel_width!/g, '1')
.replace(/!pixel_height!/g, '1');
assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$'
+ expectedQuery
+ '$windshaft$) as tablenames )'
+ ' SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max'
+ ' FROM CDB_TableMetadata m'
+ ' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])');
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2,
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, similarity) {
next(err);
});
@@ -376,14 +430,18 @@ suite('multilayer', function() {
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$'
+ layergroup.layers[0].options.sql
.replace('!bbox!', 'ST_MakeEnvelope(0,0,0,0)')
.replace('!pixel_width!', '1')
.replace('!pixel_height!', '1')
+ '$windshaft$)');
var expectedQuery = layergroup.layers[0].options.sql
.replace('!bbox!', 'ST_MakeEnvelope(0,0,0,0)')
.replace('!pixel_width!', '1')
.replace('!pixel_height!', '1');
assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$'
+ expectedQuery
+ '$windshaft$) as tablenames )'
+ ' SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max'
+ ' FROM CDB_TableMetadata m'
+ ' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])');
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2,
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, similarity) {
next(err);
});
@@ -400,7 +458,7 @@ suite('multilayer', function() {
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2,
function(err, similarity) {
next(err);
@@ -418,7 +476,7 @@ suite('multilayer', function() {
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2,
function(err, similarity) {
next(err);
@@ -694,7 +752,7 @@ suite('multilayer', function() {
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
next(err);
});
},
@@ -1012,7 +1070,7 @@ suite('multilayer', function() {
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
assert.imageEqualsFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png', 2,
assert.imageEqualsFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, similarity) {
next(err);
});
@@ -1266,6 +1324,49 @@ suite('multilayer', 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) {
@@ -1287,3 +1388,4 @@ suite('multilayer', function() {
});
});

View File

@@ -4,24 +4,31 @@ var _ = require('underscore');
var redis = require('redis');
var querystring = require('querystring');
var semver = require('semver');
var mapnik = require('mapnik');
var Step = require('step');
var http = require('http');
var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js');
var helper = require(__dirname + '/../support/test_helper');
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 20,
IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL = 0;
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
suite('server', function() {
[true, false].forEach(function(cdbQueryTablesFromPostgresEnabledValue) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue};
suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() {
var redis_client = redis.createClient(global.environment.redis.port);
var sqlapi_server;
var mapnik_version = global.environment.mapnik_version || mapnik.versions.mapnik;
var mapnik_version = global.environment.mapnik_version || server.getVersion().mapnik;
var test_database = _.template(global.environment.postgres_auth_user, {user_id:1}) + '_db';
var default_style;
if ( semver.satisfies(mapnik_version, '<2.1.0') ) {
@@ -579,7 +586,7 @@ suite('server', function() {
method: 'GET'
},{
status: 200,
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8',
headers: { 'Content-Type': 'application/json; charset=utf-8',
'X-Cache-Channel': test_database+':gadm4' }
}, function() { done(); });
});
@@ -591,7 +598,7 @@ suite('server', function() {
method: 'GET'
},{
status: 200,
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' }
headers: { 'Content-Type': 'application/json; charset=utf-8' }
}, function() { done(); });
});
@@ -603,7 +610,7 @@ suite('server', function() {
method: 'GET'
},{
status: 200,
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' }
headers: { 'Content-Type': 'application/json; charset=utf-8' }
}, function() { done(); });
});
@@ -842,7 +849,7 @@ suite('server', function() {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, similarity) {
if (err) throw err;
done();
@@ -873,7 +880,7 @@ suite('server', function() {
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body,
'./test/fixtures/test_table_15_16046_12354_styled_black.png',
2, this);
IMAGE_EQUALS_TOLERANCE_PER_MIL, this);
},
function checkImage(err, similarity) {
if (err) throw err;
@@ -889,8 +896,8 @@ suite('server', function() {
// See https://github.com/CartoDB/Windshaft-cartodb/issues/89
test("get'ing a tile with a user-specific database password", function(done){
var style = querystring.stringify({style: test_style_black_200, style_version: '2.0.0'});
var backupDBPass = global.settings.postgres_auth_pass;
global.settings.postgres_auth_pass = '<%= user_password %>';
var backupDBPass = global.environment.postgres_auth_pass;
global.environment.postgres_auth_pass = '<%= user_password %>';
Step (
function() {
var next = this;
@@ -910,14 +917,14 @@ suite('server', function() {
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body,
'./test/fixtures/test_table_15_16046_12354_styled_black.png',
2, this);
IMAGE_EQUALS_TOLERANCE_PER_MIL, this);
},
function checkImage(err, similarity) {
if (err) throw err;
return null
},
function finish(err) {
global.settings.postgres_auth_pass = backupDBPass;
global.environment.postgres_auth_pass = backupDBPass;
done(err);
}
);
@@ -934,7 +941,7 @@ suite('server', function() {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, similarity) {
if (err) throw err;
done();
@@ -971,7 +978,7 @@ suite('server', function() {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, similarity) {
next(err);
});
@@ -1011,7 +1018,7 @@ suite('server', function() {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', 0,
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL,
function(err, similarity) {
if (err) next(err);
else next();
@@ -1031,7 +1038,7 @@ suite('server', function() {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', 0,
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL,
function(err, similarity) {
if (err) next(err);
else next();
@@ -1068,7 +1075,7 @@ suite('server', function() {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, similarity) {
// NOTE: we expect them to be EQUAL here
if (err) { next(err); return; }
@@ -1105,7 +1112,7 @@ suite('server', function() {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, similarity) {
// NOTE: we expect them to be different here
if (err) next();
@@ -1386,3 +1393,4 @@ suite('server', function() {
});
});

View File

@@ -3,9 +3,9 @@ var _ = require('underscore');
var redis = require('redis');
var querystring = require('querystring');
var semver = require('semver');
var mapnik = require('mapnik');
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;
@@ -25,7 +25,11 @@ var serverOptions = ServerOptions();
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
suite('template_api', function() {
[true, false].forEach(function(cdbQueryTablesFromPostgresEnabledValue) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue};
suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() {
var redis_client = redis.createClient(global.environment.redis.port);
var sqlapi_server;
@@ -54,6 +58,29 @@ suite('template_api', function() {
}
};
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 = [];
@@ -115,15 +142,12 @@ suite('template_api', function() {
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);
@@ -454,15 +478,12 @@ suite('template_api', function() {
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);
@@ -489,7 +510,7 @@ suite('template_api', function() {
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); });
@@ -527,7 +548,7 @@ suite('template_api', function() {
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, {},
@@ -545,7 +566,7 @@ suite('template_api', function() {
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, {},
@@ -569,15 +590,12 @@ suite('template_api', function() {
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);
@@ -604,7 +622,7 @@ suite('template_api', function() {
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); });
@@ -650,7 +668,7 @@ suite('template_api', function() {
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) {
@@ -661,15 +679,12 @@ suite('template_api', function() {
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);
@@ -696,7 +711,7 @@ suite('template_api', function() {
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); });
@@ -725,7 +740,7 @@ suite('template_api', function() {
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',
@@ -1008,16 +1023,10 @@ suite('template_api', function() {
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) {
@@ -1227,16 +1236,10 @@ suite('template_api', function() {
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) {
@@ -1423,16 +1426,10 @@ suite('template_api', function() {
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) {
@@ -1593,7 +1590,8 @@ suite('template_api', function() {
if ( err ) throw err;
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) {
@@ -1609,10 +1607,9 @@ suite('template_api', function() {
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: [
@@ -1665,9 +1662,11 @@ suite('template_api', function() {
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) {
@@ -1849,6 +1848,7 @@ suite('template_api', function() {
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)
@@ -1893,6 +1893,7 @@ suite('template_api', function() {
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) {
@@ -1903,15 +1904,12 @@ suite('template_api', function() {
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);
@@ -1948,3 +1946,4 @@ suite('template_api', function() {
});
});

View File

@@ -2,7 +2,7 @@ var http = require('http');
var url = require('url');
var _ = require('underscore');
var o = function(port, cb) {
var SQLAPIEmulator = function(port, cb) {
this.queries = [];
var that = this;
@@ -37,47 +37,45 @@ var o = function(port, cb) {
}).listen(port, cb);
};
o.prototype.handleQuery = function(query, res) {
SQLAPIEmulator.prototype.handleQuery = function(query, res) {
this.queries.push(query);
if ( query.q.match('SQLAPIERROR') ) {
res.statusCode = 400;
res.write(JSON.stringify({'error':'Some error occurred'}));
} else if ( query.q.match('SQLAPINOANSWER') ) {
console.log("SQLAPIEmulator will never respond, on request");
return;
console.log("SQLAPIEmulator will never respond, on request");
return;
} else if (query.q.match('tablenames')) {
var tableNames = JSON.stringify(query);
res.write(queryResult({tablenames: '{' + tableNames + '}', max: 1234567890.123}));
} else if ( query.q.match('EPOCH.* as max') ) {
// This is the structure of the known query sent by tiler
var row = {
'max': 1234567890.123
};
res.write(JSON.stringify({rows: [ row ]}));
res.write(queryResult({max: 1234567890.123}));
} else {
if ( query.q.match('_private_') && query.api_key === undefined) {
res.statusCode = 403;
res.write(JSON.stringify({'error':'forbidden: ' + JSON.stringify(query)}));
} else {
var qs = JSON.stringify(query);
var row = {
// This is the structure of the known query sent by tiler
'cdb_querytables': '{' + qs + '}',
'max': qs
};
var out_obj = {rows: [ row ]};
var out = JSON.stringify(out_obj);
res.write(out);
res.write(queryResult({cdb_querytables: '{' + qs + '}', max: 1234567890.123}));
}
}
res.end();
};
o.prototype.close = function(cb) {
SQLAPIEmulator.prototype.close = function(cb) {
this.sqlapi_server.close(cb);
};
o.prototype.getLastRequest = function() {
SQLAPIEmulator.prototype.getLastRequest = function() {
return this.requests.pop();
};
module.exports = o;
function queryResult(row) {
return JSON.stringify({
rows: [row]
});
}
module.exports = SQLAPIEmulator;

View File

@@ -1,10 +1,11 @@
// Cribbed from the ever prolific Konstantin Kaefer
// https://github.com/mapbox/tilelive-mapnik/blob/master/test/support/assert.js
var fs = require('fs');
var http = require('http');
var path = require('path');
var exec = require('child_process').exec;
var exec = require('child_process').exec,
fs = require('fs'),
http = require('http'),
path = require('path'),
util = require('util');
var assert = module.exports = exports = require('assert');
@@ -66,35 +67,51 @@ assert.utfgridEqualsFile = function(buffer, file_b, tolerance, callback) {
callback(err);
};
//
// @param tol tolerated color distance as a percent over max channel value
// by default this is zero. For meaningful values, see
// http://www.imagemagick.org/script/command-line-options.php#metric
//
assert.imageEqualsFile = function(buffer, file_b, tol, callback) {
/**
* Takes an image data as an input and an image path and compare them using ImageMagick fuzz algorithm, if case the
* similarity is not within the tolerance limit it will callback with an error.
*
* @param buffer The image data to compare from
* @param {string} referenceImageRelativeFilePath The relative file to compare against
* @param {number} tolerance tolerated mean color distance, as a per mil (‰)
* @param {function} callback Will call to home with null in case there is no error, otherwise with the error itself
* @see FUZZY in http://www.imagemagick.org/script/command-line-options.php#metric
*/
assert.imageEqualsFile = function(buffer, referenceImageRelativeFilePath, tolerance, callback) {
if (!callback) callback = function(err) { if (err) throw err; };
file_b = path.resolve(file_b);
var file_a = '/tmp/windshaft-test-image-test.png'; // + (Math.random() * 1e16); // TODO: make predictable
var err = fs.writeFileSync(file_a, buffer, 'binary');
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath),
testImageFilePath = '/tmp/windshaft-test-image-' + (Math.random() * 1e16); // TODO: make predictable
var err = fs.writeFileSync(testImageFilePath, buffer, 'binary');
if (err) throw err;
var fuzz = tol + '%';
exec('compare -fuzz ' + fuzz + ' -metric AE "' + file_a + '" "' +
file_b + '" /dev/null', function(err, stdout, stderr) {
var imageMagickCmd = util.format(
'compare -metric fuzz "%s" "%s" /dev/null',
testImageFilePath, referenceImageFilePath
);
exec(imageMagickCmd, function(err, stdout, stderr) {
if (err) {
fs.unlinkSync(file_a);
fs.unlinkSync(testImageFilePath);
callback(err);
} else {
stderr = stderr.trim();
var similarity = parseFloat(stderr);
if ( similarity > 0 ) {
var err = new Error('Images not equal(' + similarity + '): ' +
file_a + ' ' + file_b);
err.similarity = similarity;
callback(err);
var metrics = stderr.match(/([0-9]*) \((.*)\)/);
if ( ! metrics ) {
callback(new Error("No match for " + stderr));
return;
}
var similarity = parseFloat(metrics[2]),
tolerancePerMil = (tolerance / 1000);
if (similarity > tolerancePerMil) {
err = new Error(util.format(
'Images %s and %s are not equal (got %d similarity, expected %d)',
testImageFilePath, referenceImageFilePath, similarity, tolerancePerMil)
);
err.similarity = similarity;
callback(err);
} else {
fs.unlinkSync(file_a);
callback(null);
fs.unlinkSync(testImageFilePath);
callback(null);
}
}
});
@@ -209,6 +226,8 @@ assert.response = function(server, req, res, msg){
response.on('end', function(){
if (timer) clearTimeout(timer);
check();
// Assert response body
if (res.body !== undefined) {
var eql = res.body instanceof RegExp
@@ -254,7 +273,6 @@ assert.response = function(server, req, res, msg){
// Callback
callback(response);
check();
});
});

View File

@@ -86,7 +86,8 @@ if test x"$PREPARE_REDIS" = xyes; then
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
HMSET rails:users:localhost id ${TESTUSERID} \
database_name '${TEST_DB}' \
database_name "${TEST_DB}" \
database_host localhost \
map_key 1234
SADD rails:users:localhost:map_key 1235
EOF

View File

@@ -7,18 +7,18 @@
var _ = require('underscore');
var assert = require('assert');
var LZMA = require('lzma/lzma_worker.js').LZMA;
var LZMA = require('lzma').LZMA;
var lzmaWorker = new LZMA();
// set environment specific variables
global.settings = require(__dirname + '/../../config/settings');
global.environment = require(__dirname + '/../../config/environments/test');
_.extend(global.settings, global.environment);
process.env.NODE_ENV = 'test';
// Utility function to compress & encode LZMA
function lzma_compress_to_base64(payload, mode, callback) {
LZMA.compress(payload, mode,
lzmaWorker.compress(payload, mode,
function(ints) {
ints = ints.map(function(c) { return String.fromCharCode(c + 128) }).join('')
var base64 = new Buffer(ints, 'binary').toString('base64');
@@ -39,8 +39,27 @@ function checkNoCache(res) {
}
module.exports = {
lzma_compress_to_base64: lzma_compress_to_base64,
checkNoCache: checkNoCache
/**
* Check that the response headers do not request caching
* @see checkNoCache
* @param res
*/
function checkCache(res) {
assert.ok(res.headers.hasOwnProperty('x-cache-channel'));
assert.ok(res.headers.hasOwnProperty('cache-control'));
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,23 +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: {
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);
@@ -34,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);
@@ -54,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; }
@@ -80,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; }
@@ -108,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"));
@@ -128,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"));
@@ -150,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)"));
@@ -170,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);
@@ -196,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) {
@@ -206,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() {
@@ -265,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(
@@ -325,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 = {
@@ -423,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 = [];
@@ -502,5 +521,5 @@ suite('template_maps', function() {
}
);
});
});

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

View File

@@ -0,0 +1,11 @@
{"version":"1.0.1",
"layers":[{
"type":"cartodb",
"options":{
"sql":"select 1 as id, ST_Transform(ST_SetSRID(ST_MakePoint(x/1000,x/2000),4326),3857) as the_geom_webmercator FROM generate_series(-170000,170000) x",
"cartocss":"#style{ marker-width: 12;}",
"cartocss_version":"2.1.1",
"Interactivity":"id"
}
}]
}