Compare commits

..

114 Commits
1.6.2 ... 1.8.2

Author SHA1 Message Date
Sandro Santilli
b3d3269d3d Release 1.8.2 2014-02-25 10:52:55 +01:00
Sandro Santilli
a13c1f61af Do not log an error for a legit request requiring no X-Cache-Channel 2014-02-24 17:34:00 +01:00
Sandro Santilli
4064b8f254 Add test for lack of X-Cache-Channel in response to root request 2014-02-24 16:24:01 +01:00
Sandro Santilli
5c466c51a8 Revert order of hostname components for statsd.prefix 2014-02-21 17:25:10 +01:00
Sandro Santilli
36628ce78e Also enable the profiler in the example test config
This is again for #157 without closing it
2014-02-21 17:06:29 +01:00
Sandro Santilli
d2d7bba357 Add statsd prefix in test example config
Still doesn't add automated testing (#157) but makes manual
testing easier.
2014-02-21 16:57:02 +01:00
Sandro Santilli
8e68716d16 Give more info on failure 2014-02-21 16:56:50 +01:00
Sandro Santilli
6824c09916 Change example test user and database names
This is to avoid a clash with cartodb test databases
2014-02-20 18:03:43 +01:00
Sandro Santilli
09ea924eb2 Allow using GET with sql-api for queries shorter than configured len
Introduces new sqlapi.max_get_sql_length directive, defaults to 2048.
Closes #155
Includes testcases.
2014-02-20 10:17:48 +01:00
Sandro Santilli
c8a042abdd Expand "addCacheChannel" stats 2014-02-19 18:10:33 +01:00
Sandro Santilli
019540e622 Set example statsd prefix with :host placeholder 2014-02-19 16:16:39 +01:00
Sandro Santilli
9a5243ade3 Fix munin plugin after log format changes
Closes #154
2014-02-19 15:38:14 +01:00
Sandro Santilli
b4fc8ec4a5 Allow using ":host" as part of statsd.prefix
It'll be replaced with hostname.
Closes #153
2014-02-19 15:31:12 +01:00
Sandro Santilli
30a2d85e92 Prepare for 1.8.2 2014-02-19 15:26:43 +01:00
Sandro Santilli
98603594b1 Release 1.8.1 2014-02-19 12:24:43 +01:00
Sandro Santilli
7410d98d56 Require windsahft 0.19.0 final 2014-02-19 11:25:42 +01:00
Sandro Santilli
1f552a9e24 Do not duplicate date in logs (already injected by logger) 2014-02-19 11:16:37 +01:00
Sandro Santilli
6c6f3d02f6 Always generate X-Cache-Channel for token-based tile responses
Closes #152
2014-02-19 10:09:54 +01:00
Sandro Santilli
36a135f02b Refactor addCacheChannel using Step 2014-02-19 07:19:41 +01:00
Sandro Santilli
1c3734fde7 Make server_option a callable function, to reduce globals
Updates acceptance test for #152 to not mess wit internals
2014-02-19 06:45:29 +01:00
Sandro Santilli
3c09be64ce Add pending test for X-Cache-Channel on tiler restart (#152) 2014-02-18 18:33:00 +01:00
Sandro Santilli
719346a472 Use log4js logger
Closes #138.
The logger will be automatically used by Windshaft on upgrade
to 0.18.1, see https://github.com/CartoDB/Windshaft/issues/140
2014-02-18 15:12:08 +01:00
Sandro Santilli
69693acea0 Add statsd prefix in example configs 2014-02-18 10:38:15 +01:00
Sandro Santilli
3873fdf5db Prepare for 1.8.1 2014-02-18 10:38:01 +01:00
Sandro Santilli
c3a05e5041 Set 1.8.0 release date 2014-02-18 10:09:17 +01:00
Sandro Santilli
4c0ab92771 Default to installed mapnik_version during testing
See https://github.com/CartoDB/Windshaft/issues/117
2014-02-18 09:55:00 +01:00
Sandro Santilli
c14378ca5d Avoid checking for table privacy when not using table maps
See #147
2014-02-17 18:20:18 +01:00
Sandro Santilli
26b9c8123d Set maxSocket to allow more than 5 concurrent connections to sql-api 2014-02-17 18:03:11 +01:00
Sandro Santilli
5a504ac1dc Require Windshaft-0.18.2 for statsd-profiler integration 2014-02-17 16:56:44 +01:00
Sandro Santilli
8401dcf6d7 Use /api/v1/map route for unified map api in the examples
Closes #146
2014-02-17 16:29:23 +01:00
Sandro Santilli
1f2e4edd35 Comments cleanup 2014-02-17 11:10:08 +01:00
Sandro Santilli
212eec2ca6 Pass profiler back to windshaft on createLayergroup 2014-02-17 08:50:12 +01:00
Sandro Santilli
935826ed1a Integrate statsd in template instanciation endpoint
NOTE: stats are only enabled using windshaft 0.18.2+
2014-02-15 08:23:43 +01:00
Sandro Santilli
8f3c6c3c87 Add profiler calls in template instanciation endpoint 2014-02-15 08:06:57 +01:00
Sandro Santilli
cd3f8dcf89 Only print layergroupid on template instanciation 2014-02-14 17:33:20 +01:00
Sandro Santilli
9ff192366a Add example mapconfig 2014-02-14 17:24:18 +01:00
Sandro Santilli
63401ca3df Use a single redis client in SignedMap.isAuthorized 2014-02-14 17:07:52 +01:00
Sandro Santilli
8e323a6c07 Remove more commas (see previous commit) 2014-02-14 16:08:32 +01:00
xavijam
d50c6c6dc3 close #144 - removed unnecessary comma 2014-02-14 13:12:52 +01:00
Sandro Santilli
def474c611 Skip getting geometry type if request has no table 2014-02-14 12:26:34 +01:00
Sandro Santilli
c1b2d16119 rename tablePrivacy_getUserDBName profile label 2014-02-14 11:47:43 +01:00
Sandro Santilli
678d653ee9 Allow configuring TTL of mapConfigs via "mapConfigTTL" 2014-02-13 15:44:54 +01:00
Sandro Santilli
4a6af108b4 Fix use of maxUserTemplate configuration variable 2014-02-13 15:01:58 +01:00
Sandro Santilli
e4cd37647e Allow limiting number of allowed user templates
Adds maxUserTemplates directive.
Closes #136
2014-02-13 14:55:31 +01:00
Sandro Santilli
4254f56093 Fix output from list_template to be more readable 2014-02-13 12:56:44 +01:00
Sandro Santilli
f7cef9dcd8 Fix bogus reference in SignedMaps when globals.environment is not set 2014-02-13 10:57:41 +01:00
Sandro Santilli
1c69eb1ae4 Add example template to use with the commandline tools 2014-02-13 10:31:28 +01:00
Sandro Santilli
b673cb2a1f Add more detailed profile info about the "authorize" step
Closes #142
2014-02-13 10:25:28 +01:00
Sandro Santilli
e88e49001a Do not retrive user's api key if no api key was provided
Reduces redis interaction, see #142
2014-02-13 10:16:11 +01:00
Sandro Santilli
6a599ccb5d Add script to list templates 2014-02-13 09:19:19 +01:00
Sandro Santilli
a90bf2e87b Require windshaft 0.18.1 for improve garbage collection 2014-02-13 08:54:47 +01:00
Sandro Santilli
115b1a5267 Add reference to node-0.10 ticket 2014-02-13 08:46:16 +01:00
Sandro Santilli
84e346057e Have travis also build with node-0.10
Closes #141
2014-02-13 08:15:43 +01:00
Sandro Santilli
333de67ed5 Require Windshaft 0.18.0 2014-02-12 22:51:52 +01:00
Sandro Santilli
e4dd215808 Tested with node-0.10.25, works fine 2014-02-12 22:45:00 +01:00
javi
c214e269e9 added statsd to npm-shrinkwrap.json 2014-02-12 16:48:48 +01:00
Sandro Santilli
466eac18a7 Recommend setting "from" for other packages too 2014-02-12 16:23:52 +01:00
Sandro Santilli
6e5d8b2d30 Loosen node-varnish dep 2014-02-12 16:23:12 +01:00
Sandro Santilli
bf45bbea56 Do not send multiple equal commands to Varnish on connect
Closes #135
Also accept varnish "secret" in config
2014-02-12 16:14:27 +01:00
Sandro Santilli
cdbcc7dc18 Put statsd config in all example configs 2014-02-12 16:01:50 +01:00
Sandro Santilli
66e57606d2 Retarget to 1.8.0 for the statsd addition 2014-02-12 16:00:15 +01:00
Sandro Santilli
c7f3bb5722 Target 1.7.2 2014-02-12 15:54:21 +01:00
Sandro Santilli
0e2f921b7e Add flush_cache script. Closes #140 2014-02-12 15:54:21 +01:00
javi
c421ea6bfc added basic statsd cnfiguration options in sample file #139 2014-02-12 15:33:41 +01:00
javi
01feeae6f4 include state configuration for windshaft fixes #139 2014-02-12 15:27:42 +01:00
Sandro Santilli
1ff52fcd00 Add windshaft.from tweak for npm-shrinkwrap.json 2014-02-12 10:51:47 +01:00
Sandro Santilli
6db25c3b6a Release 1.7.1 2014-02-12 10:42:51 +01:00
Sandro Santilli
88deded0fe Workaround npm registry bug by downloading windshaft from github
See the problem here:
https://travis-ci.org/CartoDB/Windshaft-cartodb/builds/18663972
2014-02-11 19:33:05 +01:00
Sandro Santilli
fc0f2b5952 Require windshaft 0.17.2 for further reducing log noise
Closes #137
2014-02-11 17:31:21 +01:00
Sandro Santilli
e211e944e5 Set target version to 1.7.1 2014-02-11 16:37:22 +01:00
Sandro Santilli
a948038ff4 Disable debug logging unless "debug" config param evaluates to true
Closes #137
2014-02-11 16:34:43 +01:00
Sandro Santilli
c70d192987 Release 1.7.0 2014-02-11 15:19:36 +01:00
Sandro Santilli
3fc8630634 Require newer windshaft, regenerated shrinkwrap 2014-02-11 15:19:13 +01:00
Sandro Santilli
8c013ed2d1 Rename Step function in setDBConn 2014-02-11 13:42:44 +01:00
Sandro Santilli
7a749631e8 Fix profiler labels 2014-02-11 13:40:17 +01:00
Sandro Santilli
e3a5f398e4 Add test for instace token changing on template change 2014-02-10 15:48:35 +01:00
Sandro Santilli
747f4803ba Include hash of template in the maptoken returned from instanciation
Doing so basically removes the need to include the template identifier
in the surrogate keys of the responses for resources fetched via
the instance whenever template is updated. See #105
2014-02-10 15:30:35 +01:00
Sandro Santilli
24709e8341 Add acceptance test for use of attributes service from template
Closes #120
2014-02-10 12:31:36 +01:00
Sandro Santilli
53861ad327 Populate test private table 2014-02-10 12:31:00 +01:00
Sandro Santilli
399bed34ad Do not try to replace template variables in undefined elements
See #133
2014-02-10 11:26:21 +01:00
Sandro Santilli
6b41fef96c Fix sendError calls to receive the full Error instance 2014-02-10 11:11:35 +01:00
Sandro Santilli
031e2a2e0c Add test for missing cartocss from mapnik layer on layergroup post
See #133
2014-02-10 11:05:02 +01:00
Sandro Santilli
9b4787c4b7 Reword in bug fixes NEWS entries 2014-02-07 18:10:45 +01:00
Sandro Santilli
fe6e915c0d Always set database access parameters from req2params
Fixes privileged database access from unauthorized users while
fetching torque tiles or feature attributes (unreleased feature).
Closes #132.

Includes testcase, which closes #119
2014-02-07 18:08:41 +01:00
javi
b5d67ec6c0 updated news for #130 2014-02-06 17:46:39 +01:00
javi
f5e0d06e2f fixed when default value in a template attribute is a number and type = number checking fails fixed #130 2014-02-06 17:45:48 +01:00
javi
78f69d5236 template variables with spaces are not replaced correctly fixed #129 2014-02-06 17:33:26 +01:00
Sandro Santilli
ab7d603171 Drop unified.js acceptance test.
Does not really make sense now that endpoints are configurable.
See #126 and #127
2014-02-06 12:58:10 +01:00
Sandro Santilli
b4936ffafa Do not allow creating template with auth='token' and no valid tokens
Closes #128
Includes acceptance test for both creation and update
2014-02-06 12:24:14 +01:00
Sandro Santilli
752e9ec655 Add checkInvalidCertificate method for SignedMap class
Includes unit test
2014-02-06 12:05:01 +01:00
Sandro Santilli
9018e39762 Make endpoints configurable
Closes #127
Uses /api/v1/maps* in the production and staging example configs,
keeps /maps* for development and test (they are examples...)
2014-02-05 15:14:47 +01:00
Sandro Santilli
a964ed5fe6 Implement Unified Map API
Closes #126
2014-02-04 19:04:59 +01:00
Sandro Santilli
b862904506 Be explicit about the map output srid configuration 2014-02-04 16:26:26 +01:00
javi
7197cc2d62 added stack to response in development mode 2014-02-04 14:58:21 +01:00
Sandro Santilli
b01570924d Add support for torque tiles and attributes fetching
Retargets self to 1.7.0
Upgrades Windshaft to 0.16.0

Closes #118 -- CDB-1525 #resolve
Closes #112 -- CDB-1329 #resolve
2014-02-04 13:30:59 +01:00
Sandro Santilli
db478579c5 Fix example development configuration to avoid use of empty sqlapi.domain
This is because as of CartoDB-SQL-API-1.8.2 the "user_from_host"
default configuration for "development" environment is:

  '^(.*)\\.localhost'

Which would not match a domain-less hostname

Closes #117 for real now.
2014-01-30 16:56:23 +01:00
Sandro Santilli
978ea9cd04 Fix sqlapi request header to be "Host", not "Hostname"
Closes (better) #117 -- automated test included
2014-01-30 16:46:26 +01:00
Sandro Santilli
ca4f3d2025 Re-introduce sqlapi.host directive, allowing DNS lookups drop
For backward compatibility, sqlapi.host is only used if domain
is also defined and has a different value (empty string allowed).

Closes #117
2014-01-30 16:12:37 +01:00
Sandro Santilli
c0020fd75a Release 1.6.3 2014-01-30 12:44:25 +01:00
Sandro Santilli
add4255bdc Update windshaft to 0.15.1, fixing maxzoom in layergroup
Regenerates shrinkwrap, which includes other minor updates
in dependency modules.
2014-01-30 12:42:11 +01:00
Sandro Santilli
1f0faba71c Stop processing XML on renderer creation
Not needed anymore since 1.6.1 introduced on-demand XML generation.
2014-01-30 11:14:52 +01:00
Sandro Santilli
e3f2658d53 Port show_style to node (really needed now) 2014-01-29 16:01:27 +01:00
Sandro Santilli
f7cdb5f0b7 Typo 2014-01-29 15:14:47 +01:00
Sandro Santilli
d32278b227 Rename template instanciation function 2014-01-29 14:30:27 +01:00
Sandro Santilli
76acc5af99 Indent and other minor tweaks 2014-01-29 13:34:22 +01:00
javi
5755e382fb Merge branch 'master' of github.com:Vizzuality/Windshaft-cartodb 2014-01-29 13:12:40 +01:00
javi
95c450fe99 update NEWS for #116 2014-01-29 13:12:19 +01:00
javi
ad0b2ffc8e added support for template instanciation with jsonp closes #116 2014-01-29 13:11:37 +01:00
Sandro Santilli
1b1b6b975e Add test for malformed CartoCSS error (#115)
The test is disabled for it's failing, it isn't yet decided if
the regression has to be fixed or not.
2014-01-29 10:40:35 +01:00
Sandro Santilli
67e4e7e99b Set api_key to signer's when instanciating a template map
Closes #114
2014-01-28 12:37:41 +01:00
javi
ac31c69c80 added spec to test instanciation of open templated maps without api_key 2014-01-28 12:12:33 +01:00
javi
92ca447c06 fixed #91 2014-01-28 12:05:01 +01:00
javi
bdea9f10fc fixed sqlemu to return forbidden when table name contains "private" in its name 2014-01-28 12:04:10 +01:00
Sandro Santilli
dc3d36e0a5 Prepare for 1.6.3 2014-01-23 12:27:39 +01:00
30 changed files with 2490 additions and 407 deletions

View File

@@ -11,6 +11,7 @@ env:
language: node_js
node_js:
- "0.8"
- "0.10"
notifications:
irc:

View File

@@ -4,8 +4,10 @@
4. Run npm install
5. Test (make check or npm test), fix if broken before proceeding
6. Run npm shrinkwrap
7. Commit package.json, npm-shrinwrap.json, NEWS
8. Tag Major.Minor.Patch
9. Announce
10. Stub NEWS/package for next version
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

88
NEWS.md
View File

@@ -1,3 +1,91 @@
1.8.2 -- 2014-02-25
-------------------
Enhancements:
* Allow using ":host" as part of statsd.prefix (#153)
* Expand "addCacheChannel" stats
* Allow using GET with sql-api for queries shorter than configured len (#155)
[ new sqlapi.max_get_sql_length directive, defaults to 2048 ]
* Do not log an error for a legit request requiring no X-Cache-Channel
Bug fixes:
* Fix munin plugin after log format changes (#154)
1.8.1 -- 2014-02-19
-------------------
Enhancements:
* Use log4js logger (#138)
Bug fixes:
* Always generate X-Cache-Channel for token-based tile responses (#152)
1.8.0 -- 2014-02-18
-------------------
Enhancements:
* Add script to flush caches (#140)
* Add script to list templates
* Add statsd support (#139)
* Add support for specifying a varnish password
* Avoid sending multiple varnish invalidation at once (#135)
* Tested with node-0.10 (#141)
* Use single redis pooler for torque and grainstore
* Reduce cost of garbage collection for localized resources
* Allow limiting number of templates for each user (#136)
* Allow configuring TTL of mapConfigs via "mapConfigTTL"
1.7.1 -- 2014-02-11
-------------------
Enhancements:
* Disable debug logging unless "debug" config param evaluates to true (#137)
* Require windshaft 0.17.2 for further reducing log noise (#137)
1.7.0 -- 2014-02-11
-------------------
New features:
* Add support for torque tiles (#112)
* Add attributes service (#118)
* Implement Unified Map API (#126)
* Make endpoints configurable (#127)
Enhancements:
* Allow specifying fixed sqlapi host address (#117)
* Include template hash in template instance response, to keep caches
of different instances separated (#105)
Bug fixes:
* Allow space padding in template variables usage (#129)
* Allow passing numbers as values for numeric template variables (#130)
1.6.3 -- 2014-01-30
-------------------
Bug fixes:
* layergroup accept both map_key and api_key (#91)
* Fix public instanciation of signed template accessing private data (#114)
* Fix show_style in presence of complex styles
* Fix use of maxzoom in layergroup config (via windshaft-0.15.1)
Enhancements:
* Add support for instanciating a template map with JSONP (#116)
* Stop processing XML on renderer creation, not needed anymore since 1.6.1
introduced on-demand XML generation.
1.6.2 -- 2014-01-23
-------------------

12
app.js
View File

@@ -24,10 +24,19 @@ global.settings = require(__dirname + '/config/settings');
global.environment = require(__dirname + '/config/environments/' + ENV);
_.extend(global.settings, global.environment);
global.log4js = require('log4js')
log4js.configure({
appenders: [
{ type: "console", layout: { type:'basic' } }
],
replaceConsole:true
});
// 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 serverOptions = require('./lib/cartodb/server_options')();
ws = CartodbWindshaft(serverOptions);
@@ -52,3 +61,4 @@ process.on('SIGUSR1', function() {
process.on('SIGUSR2', function() {
ws.dumpCacheStats();
});

View File

@@ -5,14 +5,37 @@ var config = {
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '^(.*)\\.localhost'
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "maps" is the the new API,
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
// Base url for the Inline Maps and Table Maps API
,base_url_legacy: '/tiles/:table'
// Maximum number of connections for one process
// 128 is a good value with a limit of 1024 open file descriptors
,maxConnections:128
// Maximum number of templates per user. Unlimited by default.
,maxUserTemplates:1024
// Seconds since "last creation" before a detached
// or template instance map expires. Or: how long do you want
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in miliseconds
,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])'
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'development_cartodb_user_<%= user_id %>'
@@ -38,6 +61,12 @@ var config = {
max_size: 500
}
,mapnik_version: undefined
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'dev.'
// support all allowed node-statsd options
}
,renderer: {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
@@ -64,13 +93,25 @@ var config = {
}
,sqlapi: {
protocol: 'http',
domain: 'localhost.lan',
// If "host" is given, it will be used
// to connect to the SQL-API without a
// DNS lookup
host: '127.0.0.1',
port: 8080,
version: 'v1'
// The "domain" part will be appended to
// the cartodb username and passed to
// SQL-API requests in the Host HTTP header
domain: 'localhost.lan',
version: 'v1',
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048
}
,varnish: {
host: 'localhost',
port: 6082,
secret: 'xxx',
ttl: 86400
}
// If useProfiler is true every response will be served with an

View File

@@ -5,14 +5,37 @@ var config = {
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '^(.*)\\.cartodb\\.com$'
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "maps" is the the new API,
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
// Base url for the Inline Maps and Table Maps API
,base_url_legacy: '/tiles/:table'
// Maximum number of connections for one process
// 128 is a good value with a limit of 1024 open file descriptors
,maxConnections:128
// Maximum number of templates per user. Unlimited by default.
,maxUserTemplates:1024
// Seconds since "last creation" before a detached
// or template instance map expires. Or: how long do you want
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in miliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'cartodb_user_<%= user_id %>'
@@ -32,6 +55,12 @@ var config = {
max_size: 500
}
,mapnik_version: undefined
,statsd: {
host: 'localhost',
port: 8125,
prefix: ':host.', // could be hostname, better not containing dots
// support all allowed node-statsd options
}
,renderer: {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
@@ -58,13 +87,25 @@ var config = {
}
,sqlapi: {
protocol: 'https',
domain: 'cartodb.com',
// If "host" is given, it will be used
// to connect to the SQL-API without a
// DNS lookup
//host: '127.0.0.1',
port: 8080,
version: 'v2'
// The "domain" part will be appended to
// the cartodb username and passed to
// SQL-API requests in the Host HTTP header
domain: 'cartodb.com',
version: 'v2',
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048
}
,varnish: {
host: 'localhost',
port: 6082,
secret: 'xxx',
ttl: 86400
}
// If useProfiler is true every response will be served with an

View File

@@ -5,14 +5,37 @@ var config = {
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '^(.*)\\.cartodb\\.com$'
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/maps/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/maps/named|/tiles/template)'
// Base url for the Detached Maps API
// "/api/v1/maps" is the the new API,
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/maps|/tiles/layergroup)'
// Base url for the Inline Maps and Table Maps API
,base_url_legacy: '/tiles/:table'
// Maximum number of connections for one process
// 128 is a good value with a limit of 1024 open file descriptors
,maxConnections:128
// Maximum number of templates per user. Unlimited by default.
,maxUserTemplates:1024
// Seconds since "last creation" before a detached
// or template instance map expires. Or: how long do you want
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in miliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'cartodb_staging_user_<%= user_id %>'
@@ -32,6 +55,12 @@ var config = {
max_size: 500
}
,mapnik_version: undefined
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'stage.:host.'
// support all allowed node-statsd options
}
,renderer: {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
@@ -58,13 +87,25 @@ var config = {
}
,sqlapi: {
protocol: 'https',
domain: 'cartodb.com',
// If "host" is given, it will be used
// to connect to the SQL-API without a
// DNS lookup
//host: '127.0.0.1',
port: 8080,
version: 'v2'
// The "domain" part will be appended to
// the cartodb username and passed to
// SQL-API requests in the Host HTTP header
domain: 'cartodb.com',
version: 'v2',
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048
}
,varnish: {
host: 'localhost',
port: 6082,
secret: 'xxx',
ttl: 86400
}
// If useProfiler is true every response will be served with an

View File

@@ -5,9 +5,32 @@ var config = {
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '(.*)'
// Base URLs for the APIs
//
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "maps" is the the new API,
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
// Base url for the Inline Maps and Table Maps API
,base_url_legacy: '/tiles/:table'
// Maximum number of connections for one process
// 128 is a good value with a limit of 1024 open file descriptors
,maxConnections:128
// Maximum number of templates per user. Unlimited by default.
,maxUserTemplates:1024
// Seconds since "last creation" before a detached
// or template instance map expires. Or: how long do you want
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in miliseconds
,socket_timeout: 600000
,enable_cors: true
@@ -15,10 +38,10 @@ var config = {
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'test_cartodb_user_<%= user_id %>'
,postgres_auth_user: 'test_windshaft_cartodb_user_<%= user_id %>'
// Templated database password for authorized user
// Supported labels: 'user_id', 'user_password' (both read from redis)
,postgres_auth_pass: 'test_cartodb_user_<%= user_id %>_pass'
,postgres_auth_pass: 'test_windshaft_cartodb_user_<%= user_id %>_pass'
,postgres: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
@@ -31,7 +54,13 @@ var config = {
simplify_geometries: true,
max_size: 500
}
,mapnik_version: '2.0.2'
,mapnik_version: ''
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'test.:host.'
// support all allowed node-statsd options
}
,renderer: {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
@@ -58,21 +87,33 @@ var config = {
}
,sqlapi: {
protocol: 'http',
domain: '',
// If "host" is given, it will be used
// to connect to the SQL-API without a
// DNS lookup
host: '127.0.0.1',
port: 1080,
// The "domain" part will be appended to
// the cartodb username and passed to
// SQL-API requests in the Host HTTP header
domain: 'donot_look_this_up',
// This port will be used by "make check" for testing purposes
// It must be available
port: 1080,
version: 'v1'
version: 'v1',
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048
}
,varnish: {
host: '',
port: null,
secret: 'xxx',
ttl: 86400
}
// 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:false
,useProfiler:true
};
module.exports = config;

View File

@@ -2,16 +2,21 @@ var _ = require('underscore'),
Varnish = require('node-varnish'),
varnish_queue = null;
function init(host, port) {
varnish_queue = new Varnish.VarnishQueue(host, port);
function init(host, port, secret) {
varnish_queue = new Varnish.VarnishQueue(host, port, secret);
varnish_queue.on('error', function(e) {
console.log("[CACHE VALIDATOR ERROR] " + e);
});
}
function invalidate_db(dbname, table) {
var cmd = 'purge obj.http.X-Cache-Channel ~ "^' + dbname +
':(.*'+ table +'.*)|(table)$"';
try{
varnish_queue.run_cmd('purge obj.http.X-Cache-Channel ~ "^' + dbname + ':(.*'+ table +'.*)|(table)$"');
console.log('[SUCCESS FLUSHING CACHE]');
varnish_queue.run_cmd(cmd, false);
} catch (e) {
console.log("[ERROR FLUSHING CACHE] Is enable_cache set to true? Failed for: " + 'purge obj.http.X-Cache-Channel ~ "^' + dbname + ':(.*'+ table +'.*)|(table)$"');
console.log("[CACHE VALIDATOR ERROR] could not queue command " +
cmd + " -- " + e);
}
}

View File

@@ -7,13 +7,25 @@ var _ = require('underscore')
, cartoData = require('cartodb-redis')(global.environment.redis)
, SignedMaps = require('./signed_maps.js')
, TemplateMaps = require('./template_maps.js')
, Cache = require('./cache_validator');
, Cache = require('./cache_validator')
, os = require('os')
;
var CartodbWindshaft = function(serverOptions) {
var debug = global.environment.debug;
// Perform keyword substitution in statsd
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
if ( global.environment.statsd ) {
if ( global.environment.statsd.prefix ) {
var host_token = os.hostname().split('.').reverse().join('.');
global.environment.statsd.prefix = global.environment.statsd.prefix.replace(/:host/, host_token);
}
}
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);
Cache.init(serverOptions.varnish_host, serverOptions.varnish_port, serverOptions.varnish_secret);
serverOptions.afterStateChange = function(req, data, callback) {
Cache.invalidate_db(req.params.dbname, req.params.table);
callback(null, data);
@@ -22,14 +34,17 @@ var CartodbWindshaft = function(serverOptions) {
serverOptions.beforeStateChange = function(req, callback) {
var err = null;
if ( ! req.params.hasOwnProperty('dbuser') ) {
if ( ! req.params.hasOwnProperty('_authorizedByApiKey') ) {
err = new Error("map state cannot be changed by unauthenticated request!");
}
callback(err, req);
}
serverOptions.signedMaps = new SignedMaps(redisPool);
var templateMaps = new TemplateMaps(redisPool, serverOptions.signedMaps);
var templateMapsOpts = {
max_user_templates: global.environment.maxUserTemplates
}
var templateMaps = new TemplateMaps(redisPool, serverOptions.signedMaps, templateMapsOpts);
// boot
var ws = new Windshaft.Server(serverOptions);
@@ -42,20 +57,40 @@ var CartodbWindshaft = function(serverOptions) {
return version;
}
// Override sendError to drop added cache headers (if any)
// See http://github.com/CartoDB/Windshaft-cartodb/issues/107
var ws_sendError = ws.sendError;
ws.sendError = function(res) {
// NOTE: the "res" object will have no _headers when
// faked by Windshaft, see
// http://github.com/CartoDB/Windshaft-cartodb/issues/109
//
if ( res._headers ) {
delete res._headers['cache-control'];
delete res._headers['last-modified'];
delete res._headers['x-cache-channel'];
}
ws_sendError.apply(this, arguments);
var ws_sendResponse = ws.sendResponse;
ws.sendResponse = function(res, args) {
var that = this;
var thatArgs = arguments;
var statusCode;
if ( args.length > 2 ) statusCode = args[2];
else {
statusCode = args[1] || 200;
}
var req = res.req;
Step (
function addCacheChannel() {
if ( ! req ) {
// having no associated request can happen when
// using fake response objects for testing layergroup
// creation
return false;
}
if ( ! req.params ) {
// service requests (/version, /)
// have no need for an X-Cache-Channel
return false;
}
if ( statusCode != 200 ) {
// We do not want to cache
// unsuccessful responses
return false;
}
serverOptions.addCacheChannel(that, req, this);
},
function sendResponse(err, added) {
ws_sendResponse.apply(that, thatArgs);
}
);
};
/**
@@ -69,10 +104,10 @@ var CartodbWindshaft = function(serverOptions) {
},
function(err, data){
if (err){
ws.sendError(res, {error: err.message}, 500, 'GET INFOWINDOW');
//res.send({error: err.message}, 500);
ws.sendError(res, {error: err.message}, 500, 'GET INFOWINDOW', err);
//ws.sendResponse(res, [{error: err.message}, 500]);
} else {
res.send({infowindow: data}, 200);
ws.sendResponse(res, [{infowindow: data}, 200]);
}
}
);
@@ -90,10 +125,10 @@ var CartodbWindshaft = function(serverOptions) {
},
function(err, data){
if (err){
ws.sendError(res, {error: err.message}, 500, 'GET MAP_METADATA');
//res.send(err.message, 500);
ws.sendError(res, {error: err.message}, 500, 'GET MAP_METADATA', err);
//ws.sendResponse(res, [err.message, 500]);
} else {
res.send({map_metadata: data}, 200);
ws.sendResponse(res, [{map_metadata: data}, 200]);
}
}
);
@@ -111,10 +146,10 @@ var CartodbWindshaft = function(serverOptions) {
},
function sendResponse(err, data){
if (err){
ws.sendError(res, {error: err.message}, 500, 'DELETE CACHE');
//res.send(500);
ws.sendError(res, {error: err.message}, 500, 'DELETE CACHE', err);
//ws.sendResponse(res, [500]);
} else {
res.send({status: 'ok'}, 200);
ws.sendResponse(res, [{status: 'ok'}, 200]);
}
}
);
@@ -126,7 +161,11 @@ var CartodbWindshaft = function(serverOptions) {
return serverOptions.userByReq(req);
}
var template_baseurl = serverOptions.base_url_notable + '/template';
// This is for Templated maps
//
// "named" is the official, "template" is for backward compatibility up to 1.6.x
//
var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)';
// Add a template
ws.post(template_baseurl, function(req, res) {
@@ -157,15 +196,19 @@ var CartodbWindshaft = function(serverOptions) {
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;
}
ws.sendError(res, response, statusCode, 'POST TEMPLATE', err.message);
ws.sendError(res, response, statusCode, 'POST TEMPLATE', err);
} else {
res.send(response, 200);
ws.sendResponse(res, [response, 200]);
}
}
);
@@ -210,15 +253,19 @@ var CartodbWindshaft = function(serverOptions) {
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.message);
ws.sendError(res, response, statusCode, 'PUT TEMPLATE', err);
} else {
res.send(response, 200);
ws.sendResponse(res, [response, 200]);
}
}
);
@@ -274,9 +321,9 @@ var CartodbWindshaft = function(serverOptions) {
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
}
ws.sendError(res, response, statusCode, 'GET TEMPLATE', err.message);
ws.sendError(res, response, statusCode, 'GET TEMPLATE', err);
} else {
res.send(response, 200);
ws.sendResponse(res, [response, 200]);
}
}
);
@@ -324,9 +371,9 @@ var CartodbWindshaft = function(serverOptions) {
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
}
ws.sendError(res, response, statusCode, 'DELETE TEMPLATE', err.message);
ws.sendError(res, response, statusCode, 'DELETE TEMPLATE', err);
} else {
res.send('', 204);
ws.sendResponse(res, ['', 204]);
}
}
);
@@ -364,9 +411,9 @@ var CartodbWindshaft = function(serverOptions) {
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
}
ws.sendError(res, response, statusCode, 'GET TEMPLATE LIST', err.message);
ws.sendError(res, response, statusCode, 'GET TEMPLATE LIST', err);
} else {
res.send(response, statusCode);
ws.sendResponse(res, [response, statusCode]);
}
}
);
@@ -393,8 +440,9 @@ var CartodbWindshaft = function(serverOptions) {
});
// Instantiate a template
ws.post(template_baseurl + '/:template_id', function(req, res) {
function instanciateTemplate(req, res, template_params, callback) {
ws.doCORS(res);
if ( req.profiler ) req.profiler.done('cors');
var that = this;
var response = {};
var template;
@@ -415,6 +463,7 @@ var CartodbWindshaft = function(serverOptions) {
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");
@@ -437,22 +486,36 @@ var CartodbWindshaft = function(serverOptions) {
err.http_status = 401;
throw err;
}
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
/*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;
}*/
//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) };
fakereq = { query: {}, params: {}, headers: _.clone(req.headers),
profiler: req.profiler
};
ws.setDBParams(cdbuser, fakereq.params, this);
},
function createLayergroup(err) {
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;
@@ -470,23 +533,82 @@ var CartodbWindshaft = function(serverOptions) {
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!
response.layergroupid = cdbuser + '@' + response.layergroupid;
var tplhash = templateMaps.fingerPrint(template).substring(0,8);
if ( req.profiler ) req.profiler.done('fingerPrint');
response.layergroupid = cdbuser + '@' + tplhash + '@' + response.layergroupid;
return response;
},
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, 'POST INSTANCE TEMPLATE', err.message);
} else {
res.send(response, 200);
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]);
}
if ( req.profiler && req.profiler.statsd_client) {
req.profiler.sendStats();
}
}
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);
}
);
});

View File

@@ -10,7 +10,11 @@ var _ = require('underscore')
// This is for backward compatibility with 1.3.3
if ( _.isUndefined(global.environment.sqlapi.domain) ) {
global.environment.sqlapi.domain = global.environment.sqlapi.host;
// Only use "host" as "domain" if it contains alphanumeric characters
var host = global.environment.sqlapi.host;
if ( host && host.match(/[a-zA-Z]/) ) {
global.environment.sqlapi.domain = host;
}
}
module.exports = function(){
@@ -22,19 +26,35 @@ module.exports = function(){
});
var me = {
base_url: '/tiles/:table',
base_url_notable: '/tiles',
// This is for inline maps and table maps
base_url: global.environment.base_url_legacy || '/tiles/:table',
/// @deprecated with Windshaft-0.17.0
///base_url_notable: '/tiles',
// This is for Detached maps
//
// "maps" is the official, while
// "tiles/layergroup" is for backward compatibility up to 1.6.x
//
base_url_mapconfig: global.environment.base_url_detached || '(?:/maps|/tiles/layergroup)',
grainstore: {
map: {
// TODO: allow to specify in configuration
srid: 3857
},
datasource: global.environment.postgres,
cachedir: global.environment.millstone.cache_basedir,
mapnik_version: global.environment.mapnik_version || mapnik.versions.mapnik,
default_layergroup_ttl: 7200, // seconds (default is 300)
gc_prob: 0.01 // default is 0.01 TODO: make configurable via env config
default_layergroup_ttl: global.environment.mapConfigTTL || 7200,
gc_prob: 0.01 // @deprecated since Windshaft-1.8.0
},
mapnik: {
metatile: rendererConfig.metatile,
bufferSize: rendererConfig.bufferSize
},
statsd: global.environment.statsd,
renderCache: {
ttl: rendererConfig.cache_ttl
},
@@ -42,6 +62,7 @@ module.exports = function(){
enable_cors: global.environment.enable_cors,
varnish_host: global.environment.varnish.host,
varnish_port: global.environment.varnish.port,
varnish_secret: global.environment.varnish.secret,
cache_enabled: global.environment.cache_enabled,
log_format: global.environment.log_format,
useProfiler: global.environment.useProfiler
@@ -68,9 +89,13 @@ module.exports = function(){
var api = global.environment.sqlapi;
// build up api string
var sqlapi = api.protocol + '://' + username;
if ( api.domain ) sqlapi += '.' + api.domain;
sqlapi += ':' + api.port + '/api/' + api.version + '/sql'
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 }
@@ -80,13 +105,33 @@ module.exports = function(){
// call sql api
//
// NOTE: using POST to avoid size limits:
// Seehttp://github.com/CartoDB/Windshaft-cartodb/issues/111
// See http://github.com/CartoDB/Windshaft-cartodb/issues/111
//
// TODO: use "host" header to allow IP based specification
// NOTE: uses "host" header to allow IP based specification
// of sqlapi address (and avoid a DNS lookup)
//
request.post({url:sqlapi, body:qs, json:true},
function(err, res, body){
// 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 reqSpec = {
url:sqlapi,
json:true,
headers:{host: sqlapihostname}
// http://nodejs.org/api/http.html#http_agent_maxsockets
,pool:{maxSockets:maxSockets}
//,timeout:100
}
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);
@@ -161,53 +206,101 @@ module.exports = function(){
return hash.digest('hex');
}
me.generateCacheChannel = function(req, callback){
// use key to call sql api with sql request if present, else
// just return dbname and table name base key
var dbName = req.params.dbname;
me.generateCacheChannel = function(app, req, callback){
// Build channelCache key
var dbName = req.params.dbname;
var cacheKey = [ dbName ];
if ( req.params.token ) cacheKey.push(req.params.token);
else if ( req.params.sql ) cacheKey.push( me.generateMD5(req.params.sql) );
cacheKey = cacheKey.join(':');
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
callback(null, me.channelCache[cacheKey]);
return;
}
else if ( req.params.token ) {
// cached cache channel for token-based access should be constructed
// at cache creation time
callback(new Error('missing channel cache for token ' + req.params.token));
return;
}
var that = this;
if ( ! req.params.sql && ! req.params.token ) {
var cacheChannel = me.buildCacheChannel(dbName, [req.params.table]);
// not worth caching this
callback(null, cacheChannel);
return;
}
Step (
function checkCached() {
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
callback(null, me.channelCache[cacheKey]);
return;
}
return null;
},
function extractSQL(err) {
if ( err ) throw err;
if ( ! req.params.sql ) {
callback(new Error("this request doesn't need an X-Cache-Channel generated"));
return;
}
if ( req.params.token ) {
// TODO: cached cache channel for token-based access should
// be constructed at renderer cache creation time
// 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;
Step(
function loadFromStore() {
mapStore.load(req.params.token, this);
},
function getSQL(err, mapConfig) {
if (req.profiler) req.profiler.done('mapStore_load');
if ( err ) throw err;
var sql = [];
_.each(mapConfig.obj().layers, function(lyr) {
sql.push(lyr.options.sql);
});
sql = sql.join(';');
return sql;
},
function finish(err, sql) {
next(err, sql);
}
);
return;
}
var dbName = req.params.dbname;
var username = this.userByReq(req);
if ( ! req.params.sql ) {
return null; // no sql
}
// strip out windshaft/mapnik inserted sql if present
var sql = req.params.sql.match(/^\((.*)\)\sas\scdbq$/);
sql = (sql != null) ? sql[1] : req.params.sql;
// We have sql, and no token...
me.affectedTables(username, req.params.map_key, sql, function(err, tableNames) {
if ( err ) { callback(err); return; }
// strip out windshaft/mapnik inserted sql if present
var sql = req.params.sql.match(/^\((.*)\)\sas\scdbq$/);
sql = (sql != null) ? sql[1] : req.params.sql;
return sql;
},
function findAffectedTables(err, sql) {
if ( err ) throw err;
if ( ! sql ) {
if ( ! req.params.table ) {
throw new Error("this request doesn't need an X-Cache-Channel generated");
}
return [req.params.table];
}
var username = that.userByReq(req);
me.affectedTables(username, req.params.map_key, sql, this);
},
function buildCacheChannel(err, tableNames) {
if ( err ) throw err;
if (req.profiler && ! req.params.table ) {
req.profiler.done('affectedTables');
}
var dbName = req.params.dbname;
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
me.channelCache[cacheKey] = cacheChannel; // store for caching
callback(null, cacheChannel);
});
// store for caching from me.generateCacheChannel
// (not worth when table was specified in params)
if ( ! req.params.table ) {
me.channelCache[cacheKey] = cacheChannel;
}
return cacheChannel;
},
function finish(err, cacheChannel) {
callback(err, cacheChannel);
}
);
};
// Set the cache chanel info to invalidate the cache on the frontend server
@@ -219,9 +312,10 @@ module.exports = function(){
// @param cb function(err, channel) will be called when ready.
// the channel parameter will be null if nothing was added
//
me.addCacheChannel = function(req, cb) {
me.addCacheChannel = function(app, req, cb) {
// skip non-GET requests, or requests for which there's no response
if ( req.method != 'GET' || ! req.res ) { cb(null, null); return; }
if (req.profiler) req.profiler.start('addCacheChannel');
var res = req.res;
var cache_policy = req.query.cache_policy;
if ( req.params.token ) cache_policy = 'persist';
@@ -243,7 +337,9 @@ module.exports = function(){
}
res.header('Last-Modified', lastUpdated.toUTCString());
me.generateCacheChannel(req, function(err, channel){
me.generateCacheChannel(app, req, function(err, channel){
if (req.profiler) req.profiler.done('generateCacheChannel');
if (req.profiler) req.profiler.end();
if ( ! err ) {
res.header('X-Cache-Channel', channel);
cb(null, channel);
@@ -290,7 +386,7 @@ module.exports = function(){
var dbName = req.params.dbname;
var usr = this.userByReq(req);
var key = req.params.map_key;
var key = req.params.map_key || req.params.api_key;
var cacheKey = dbName + ':' + token;
@@ -299,7 +395,8 @@ module.exports = function(){
if ( err ) { done(err); return; }
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
me.channelCache[cacheKey] = cacheChannel; // store for caching
// store for caching from me.afterLayergroupCreate
me.channelCache[cacheKey] = cacheChannel;
// find last updated
me.findLastUpdated(usr, key, tableNames, function(err, lastUpdated) {
if (req.profiler) req.profiler.done('findLastUpdated');
@@ -402,7 +499,7 @@ module.exports = function(){
if ( data ) _.extend(params, {dbhost:data});
cartoData.getUserDBName(dbowner, this);
},
function getGeometryType(err, data){
function extendParams(err, data){
if (err) throw err;
if ( data ) _.extend(params, {dbname:data});
return null;
@@ -437,7 +534,7 @@ module.exports = function(){
var layergroup_id = req.params.token;
var auth_token = req.params.auth_token;
console.log("Checking authorization from signer " + signer + " for resource " + layergroup_id + " with auth_token " + auth_token);
//console.log("Checking authorization from signer " + signer + " for resource " + layergroup_id + " with auth_token " + auth_token);
me.signedMaps.isAuthorized(signer, layergroup_id, auth_token,
function(err, authorized) {
@@ -449,9 +546,20 @@ console.log("Checking authorization from signer " + signer + " for resource " +
//
// @param req express request object
// @param callback function(err, authorized)
// NOTE: authorized is expected to be 0 or 1 (integer)
//
me.authorizedByAPIKey = function(req, callback)
{
var givenKey = req.query.api_key || req.query.map_key;
if ( ! givenKey && req.body ) {
// check also in request body
givenKey = req.body.api_key || req.body.map_key;
}
if ( ! givenKey ) {
callback(null, 0); // no api key, no authorization...
return;
}
//console.log("given ApiKey: " + givenKey);
var user = me.userByReq(req);
Step(
function (){
@@ -459,16 +567,7 @@ console.log("Checking authorization from signer " + signer + " for resource " +
},
function checkApiKey(err, val){
if (err) throw err;
var valid = 0;
if ( val ) {
if ( val == req.query.map_key ) valid = 1;
else if ( val == req.query.api_key ) valid = 1;
// check also in request body
else if ( req.body && req.body.map_key && val == req.body.map_key ) valid = 1;
else if ( req.body && req.body.api_key && val == req.body.api_key ) valid = 1;
}
return valid;
return ( val && givenKey == val ) ? 1 : 0;
},
function finish(err, authorized) {
callback(err, authorized);
@@ -491,6 +590,7 @@ console.log("Checking authorization from signer " + signer + " for resource " +
that.authorizedByAPIKey(req, this);
},
function checkApiKey(err, authorized){
if (req.profiler) req.profiler.done('authorizedByAPIKey');
if (err) throw err;
// if not authorized by api_key, continue
@@ -501,6 +601,8 @@ console.log("Checking authorization from signer " + signer + " for resource " +
return;
}
_.extend(req.params, { _authorizedByApiKey: true });
// authorized by api key, login as the given username and stop
that.setDBAuth(user, req.params, function(err) {
callback(err, true); // authorized (or error)
@@ -508,15 +610,27 @@ console.log("Checking authorization from signer " + signer + " for resource " +
},
function checkSignAuthorized(err, signed_by){
if (err) throw err;
if (req.profiler) {
if ( req.params._authorizedByApiKey ) {
req.profiler.done('setDBAuth');
} else {
req.profiler.done('authorizedBySigner');
}
}
if ( ! signed_by ) {
// request not authorized by signer, continue
// to check map privacy
return null;
// request not authorized by signer,
// continue to check table privacy,
// if table was given
if ( req.params.table ) return null;
// otherwise return no authorization
callback(err, null);
return;
}
// Authorized by "signed_by" !
that.setDBAuth(signed_by, req.params, function(err) {
if (req.profiler) req.profiler.done('setDBAuth');
callback(err, true); // authorized (or error)
});
},
@@ -527,9 +641,11 @@ console.log("Checking authorization from signer " + signer + " for resource " +
},
function getPrivacy(err, dbname){
if (err) throw err;
if (req.profiler) req.profiler.done('tablePrivacy_getUserDBName');
cartoData.getTablePrivacy(dbname, req.params.table, this);
},
function(err, privacy){
if (req.profiler) req.profiler.done('getTablePrivacy');
callback(err, privacy);
}
);
@@ -585,9 +701,12 @@ console.log("Checking authorization from signer " + signer + " for resource " +
if ( tksplit.length > 1 ) req.params.cache_buster= tksplit[1];
tksplit = req.params.token.split('@');
if ( tksplit.length > 1 ) {
req.params.signer = this.userByReq(req);
if ( tksplit[0] ) req.params.signer = tksplit[0];
req.params.token = tksplit[1];
req.params.signer = tksplit.shift();
if ( ! req.params.signer ) req.params.signer = this.userByReq(req);
if ( tksplit.length > 1 ) {
var template_hash = tksplit.shift(); // unused
}
req.params.token = tksplit.shift();
//console.log("Request for token " + req.params.token + " with signature from " + req.params.signer);
}
}
@@ -598,26 +717,6 @@ console.log("Checking authorization from signer " + signer + " for resource " +
// for cartodb, ensure interactivity is cartodb_id or user specified
req.params.interactivity = req.params.interactivity || 'cartodb_id';
req.params.processXML = function(req, xml, callback) {
// Replace dbuser
var dbuser = req.params.dbuser || global.environment.postgres.user;
if ( ! me.rx_dbuser ) me.rx_dbuser = /(<Parameter name="user"><!\[CDATA\[)[^\]]*(]]><\/Parameter>)/g;
xml = xml.replace(me.rx_dbuser, "$1" + dbuser + "$2");
// Replace dbpass
var dbpass = req.params.dbpassword || global.environment.postgres.password;
if ( ! me.rx_dbpass ) me.rx_dbpass = /(<Parameter name="password"><!\[CDATA\[)[^\]]*(]]><\/Parameter>)/g;
xml = xml.replace(me.rx_dbpass, "$1" + dbpass + "$2");
// Replace or set dbhost
var dbhost = req.params.dbhost || global.environment.postgres.host;
if ( ! me.rx_dbhost ) me.rx_dbhost = /(<Parameter name="host"><!\[CDATA\[)[^\]]*(]]><\/Parameter>)/g;
xml = xml.replace(me.rx_dbhost, "$1" + dbhost + "$2");
callback(null, xml);
}
var that = this;
if (req.profiler) req.profiler.done('req2params.setup');
@@ -639,21 +738,28 @@ console.log("Checking authorization from signer " + signer + " for resource " +
that.setDBConn(user, req.params, this);
},
function getGeometryType(err){
if (req.profiler) req.profiler.done('cartoData.getDatabase');
if (req.profiler) req.profiler.done('setDBConn');
if (err) throw err;
if ( ! req.params.table ) return null;
cartoData.getTableGeometryType(req.params.dbname, req.params.table, this);
},
function finishSetup(err, data){
if (req.profiler) req.profiler.done('cartoData.getGeometryType');
if (req.profiler) req.profiler.done('cartoData.getTableGeometryType');
if ( err ) { callback(err, req); return; }
if (!_.isNull(data))
_.extend(req.params, {geom_type: data});
that.addCacheChannel(req, function(err) {
if (req.profiler) req.profiler.done('addCacheChannel');
callback(err, req);
// Add default database connection parameters
// if none given
_.defaults(req.params, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
callback(null, req);
}
);
};
@@ -743,4 +849,4 @@ console.log("Checking authorization from signer " + signer + " for resource " +
};
return me;
}();
};

View File

@@ -2,6 +2,7 @@ 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
//
@@ -85,28 +86,59 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
);
};
o._getAuthMethod = function(auth) {
return auth.method || 'open';
};
//--------------- PUBLIC API -------------------------------------
// Check if the given certificate authorizes waiver of "auth"
o.authorizedByCert = function(cert, auth) {
/// 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" ) {
throw new Error("Unsupported certificate version " + cert.version);
return new Error("Unsupported certificate version " + cert.version);
}
if ( ! cert.auth ) {
throw new Error("No certificate authorization");
console.log("Cert is : "); console.dir(cert);
return new Error("No certificate authorization");
}
if ( ! cert.auth.method ) {
throw new Error("No certificate authorization method");
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 ( cert.auth.method === 'open' ) return true;
if ( method === 'open' ) return true;
// Token based authentication requires valid token
if ( cert.auth.method === '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);
@@ -130,18 +162,30 @@ o.authorizedByCert = function(cert, auth) {
//
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 = [];
console.log("Check auth from signer '" + signer + "' on map '" + map_id + "' with auth '" + auth + "'");
if ( debug ) {
console.log("Check auth from signer '" + signer + "' on map '" + map_id + "' with auth '" + auth + "'");
}
Step(
function getMapSignatures() {
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});
that._redisCmd('SMEMBERS', [ map_sig_key ], this);
redisClient.SMEMBERS(map_sig_key, this);
//that._redisCmd('SMEMBERS', [ map_sig_key ], this);
},
function getCertificates(err, crt_lst) {
if ( err ) throw err;
console.log("Map '" + map_id + "' is signed by " + crt_lst.length + " certificates of user '" + signer + "': " + crt_lst);
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.
@@ -150,7 +194,8 @@ console.log("Check auth from signer '" + signer + "' on map '" + map_id + "' wit
return crt_lst;
}
var map_crt_key = _.template(that.key_map_crt, {signer:signer});
that._redisCmd('HMGET', [ map_crt_key ].concat(crt_lst), this);
//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;
@@ -170,8 +215,10 @@ console.log("Check auth from signer '" + signer + "' on map '" + map_id + "' wit
continue;
}
if ( authorized ) {
console.log("Access to map '" + map_id + "' authorized by cert '"
+ certificate_id_list[i] + "' of user '" + signer + "'");
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
}
@@ -185,6 +232,7 @@ console.log("Check auth from signer '" + signer + "' on map '" + map_id + "' wit
+ " missing certificates: "
+ missing_certificates + " (TODO: give cleanup instructions)");
}
if ( redisClient ) that.redis_pool.release(db, redisClient);
callback(err, authorized);
}
);
@@ -248,8 +296,10 @@ o.delCertificate = function(signer, crt_id, callback) {
function delMapSignaturesReference(err, map_id_list) {
if ( err ) throw err;
signed_map_list = map_id_list;
console.log("Certificate '" + crt_id + "' from user '" + signer
+ "' was used to sign " + signed_map_list.length + " maps");
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) {
@@ -266,16 +316,18 @@ o.delCertificate = function(signer, crt_id, callback) {
},
function reportTransaction(err, rets) {
if ( err ) throw err;
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] + "'");
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;
@@ -302,7 +354,9 @@ o.signMap = function(signer, map_id, crt_id, callback) {
Step(
function addMapSignature() {
var map_sig_key = _.template(that.key_map_sig, {signer:signer, map_id:map_id});
console.log("Adding " + crt_id + " to " + map_sig_key);
if ( debug ) {
console.log("Adding " + crt_id + " to " + map_sig_key);
}
that._redisCmd('SADD', [ map_sig_key, crt_id ], this);
},
function addCertificateUsage(err) {

View File

@@ -16,10 +16,15 @@ var user_template_locks = {};
//
// @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) {
function TemplateMaps(redis_pool, signed_maps, opts) {
this.redis_pool = redis_pool;
this.signed_maps = signed_maps;
this.opts = opts || {};
// Database containing templates
// TODO: allow configuring ?
@@ -49,6 +54,10 @@ var o = TemplateMaps.prototype;
//--------------- PRIVATE METHODS --------------------------------
o._userTemplateLimit = function() {
return this.opts['max_user_templates'] || 0;
};
o._acquireRedis = function(callback) {
this.redis_pool.acquire(this.db_signatures, callback);
};
@@ -143,6 +152,11 @@ o._checkInvalidTemplate = function(template) {
}
};
// Check certificate validity
var cert = this.getTemplateCertificate(template);
var err = this.signed_maps.checkInvalidCertificate(cert);
if ( err ) return err;
// TODO: run more checks over template format ?
};
@@ -184,6 +198,7 @@ o.addTemplate = function(owner, template, callback) {
// 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
@@ -197,9 +212,18 @@ o.addTemplate = function(owner, template, callback) {
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() {
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) {
@@ -233,10 +257,7 @@ o.addTemplate = function(owner, template, callback) {
// TODO: how to recover this ?!
}
if ( ! gotLock ) {
if ( err ) throw err;
return null;
}
if ( err && ! gotLock ) throw err;
// release the lock
var next = this;
@@ -529,7 +550,7 @@ o._replaceVars = function(str, params) {
if ( ! params._re ) {
params._re = {};
for (var k in params) {
params._re[k] = RegExp("<%= " + k + " %>", "g");
params._re[k] = RegExp("<%=\\s*" + k + "\\s*%>", "g");
}
}
for (var k in params) str = str.replace(params._re[k], params[k]);
@@ -552,7 +573,7 @@ o.instance = function(template, params) {
}
else if ( type === 'number' ) {
// check it's a number
if ( ! val.match(this._reNumber) ) {
if ( typeof(val) !== 'number' && ! val.match(this._reNumber) ) {
throw new Error("Invalid number value for template parameter '"
+ k + "': " + val);
}
@@ -576,11 +597,19 @@ o.instance = function(template, params) {
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
for (var i=0; i<layergroup.layers.length; ++i) {
var lyropt = layergroup.layers[i].options;
lyropt.cartocss = this._replaceVars(lyropt.cartocss, all_params);
lyropt.sql = this._replaceVars(lyropt.sql, all_params);
if ( lyropt.cartocss ) lyropt.cartocss = this._replaceVars(lyropt.cartocss, all_params);
if ( lyropt.sql) lyropt.sql = this._replaceVars(lyropt.sql, all_params);
// Anything else ?
}
return layergroup;
};
// Return a fingerPrint of the object
o.fingerPrint = function(template) {
return crypto.createHash('md5')
.update(JSON.stringify(template))
.digest('hex')
;
};
module.exports = TemplateMaps;

151
npm-shrinkwrap.json generated
View File

@@ -1,52 +1,26 @@
{
"name": "windshaft-cartodb",
"version": "1.6.2",
"version": "1.8.2",
"dependencies": {
"node-varnish": {
"version": "0.1.1"
"version": "0.2.0",
"from": "http://github.com/Vizzuality/node-varnish/tarball/v0.2.0"
},
"underscore": {
"version": "1.3.3"
},
"windshaft": {
"version": "0.15.0",
"version": "0.19.0",
"from": "http://github.com/CartoDB/Windshaft/tarball/0.19.0-rc1",
"dependencies": {
"grainstore": {
"version": "0.16.0",
"version": "0.18.0",
"dependencies": {
"carto": {
"version": "0.9.5-cdb2",
"from": "http://github.com/CartoDB/carto/tarball/0.9.5-cdb2",
"dependencies": {
"underscore": {
"version": "1.4.4"
},
"xml2js": {
"version": "0.2.8",
"dependencies": {
"sax": {
"version": "0.5.8"
}
}
},
"optimist": {
"version": "0.6.0",
"dependencies": {
"wordwrap": {
"version": "0.0.2"
},
"minimist": {
"version": "0.0.5"
}
}
}
}
},
"mapnik-reference": {
"version": "5.0.7"
},
"millstone": {
"version": "0.6.8",
"version": "0.6.11",
"dependencies": {
"underscore": {
"version": "1.5.2"
@@ -61,7 +35,7 @@
"version": "5.0.0"
},
"forever-agent": {
"version": "0.5.0"
"version": "0.5.2"
},
"tunnel-agent": {
"version": "0.3.0"
@@ -121,14 +95,14 @@
}
},
"async": {
"version": "0.2.9"
"version": "0.2.10"
}
}
}
}
},
"srs": {
"version": "0.3.8"
"version": "0.3.10"
},
"zipfile": {
"version": "0.4.3"
@@ -137,7 +111,7 @@
"version": "2.2.0",
"dependencies": {
"node-pre-gyp": {
"version": "0.2.5",
"version": "0.2.6",
"dependencies": {
"nopt": {
"version": "2.1.2",
@@ -171,7 +145,7 @@
"version": "0.1.25",
"dependencies": {
"graceful-fs": {
"version": "2.0.1"
"version": "2.0.2"
}
}
}
@@ -193,7 +167,7 @@
"version": "0.1.25",
"dependencies": {
"graceful-fs": {
"version": "2.0.1"
"version": "2.0.2"
},
"inherits": {
"version": "2.0.1"
@@ -217,7 +191,12 @@
}
},
"readable-stream": {
"version": "1.0.24"
"version": "1.0.25-1",
"dependencies": {
"string_decoder": {
"version": "0.10.25-1"
}
}
},
"graceful-fs": {
"version": "1.2.3"
@@ -225,7 +204,7 @@
}
},
"aws-sdk": {
"version": "2.0.0-rc8",
"version": "2.0.0-rc9",
"dependencies": {
"xml2js": {
"version": "0.2.4",
@@ -241,7 +220,7 @@
}
},
"rc": {
"version": "0.3.2",
"version": "0.3.3",
"dependencies": {
"optimist": {
"version": "0.3.7",
@@ -252,7 +231,7 @@
}
},
"deep-extend": {
"version": "0.2.6"
"version": "0.2.8"
},
"ini": {
"version": "1.1.0"
@@ -260,7 +239,7 @@
}
},
"rimraf": {
"version": "2.2.5"
"version": "2.2.6"
}
}
}
@@ -273,13 +252,13 @@
"version": "0.3.5"
},
"optimist": {
"version": "0.6.0",
"version": "0.6.1",
"dependencies": {
"wordwrap": {
"version": "0.0.2"
},
"minimist": {
"version": "0.0.5"
"version": "0.0.7"
}
}
}
@@ -344,6 +323,62 @@
},
"lru-cache": {
"version": "2.3.1"
},
"carto": {
"version": "0.9.5-cdb2",
"from": "http://github.com/CartoDB/carto/tarball/0.9.5-cdb2",
"dependencies": {
"underscore": {
"version": "1.4.4"
},
"mapnik-reference": {
"version": "5.0.7"
},
"xml2js": {
"version": "0.2.8",
"dependencies": {
"sax": {
"version": "0.5.8"
}
}
},
"optimist": {
"version": "0.6.1",
"dependencies": {
"wordwrap": {
"version": "0.0.2"
},
"minimist": {
"version": "0.0.7"
}
}
}
}
},
"underscore.string": {
"version": "1.1.6",
"dependencies": {
"underscore": {
"version": "1.1.7"
}
}
},
"pg": {
"version": "2.6.2",
"dependencies": {
"generic-pool": {
"version": "2.0.3"
},
"buffer-writer": {
"version": "1.0.0"
}
}
},
"torque.js": {
"version": "2.2.00"
},
"node-statsd": {
"version": "0.0.7"
}
}
},
@@ -373,20 +408,36 @@
}
},
"mapnik": {
"version": "0.7.25"
"version": "0.7.26"
},
"lzma": {
"version": "1.2.3"
},
"log4js": {
"version": "0.6.10",
"dependencies": {
"async": {
"version": "0.1.15"
},
"readable-stream": {
"version": "1.0.25-1",
"dependencies": {
"string_decoder": {
"version": "0.10.25-1"
}
}
}
}
},
"redis": {
"version": "0.8.6"
},
"strftime": {
"version": "0.6.2"
},
"semver": {
"version": "1.1.4"
},
"redis": {
"version": "0.8.6"
},
"mocha": {
"version": "1.14.0",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "1.6.2",
"version": "1.8.2",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -22,15 +22,16 @@
"Sandro Santilli <strk@vizzuality.com>"
],
"dependencies": {
"node-varnish": "0.1.1",
"node-varnish": "http://github.com/Vizzuality/node-varnish/tarball/v0.2.0",
"underscore" : "~1.3.3",
"windshaft" : "~0.15.0",
"windshaft" : "http://github.com/CartoDB/Windshaft/tarball/0.19.0",
"step": "0.0.x",
"request": "2.9.202",
"cartodb-redis": "~0.3.0",
"redis-mpool": "~0.0.2",
"mapnik": "~0.7.22",
"lzma": "~1.2.3"
"lzma": "~1.2.3",
"log4js": "~0.6.10"
},
"devDependencies": {
"mocha": "1.14.0",

View File

@@ -15,7 +15,8 @@ require(__dirname + '/../support/test_helper');
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var ServerOptions = require(__dirname + '/../../lib/cartodb/server_options');
serverOptions = ServerOptions();
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
@@ -34,6 +35,10 @@ suite('multilayer', function() {
var expected_last_updated_epoch = 1234567890123; // this is hard-coded into SQLAPIEmu
var expected_last_updated = new Date(expected_last_updated_epoch).toISOString();
var test_user = _.template(global.environment.postgres_auth_user, {user_id:1});
var test_pubuser = global.environment.postgres.user;
var test_database = test_user + '_db';
suiteSetup(function(done){
sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done);
});
@@ -58,7 +63,7 @@ suite('multilayer', function() {
]
};
var expected_token = "e34dd7e235138a062f8ba7ad051aa3a7";
var expected_token; // = "e34dd7e235138a062f8ba7ad051aa3a7";
Step(
function do_post()
{
@@ -83,7 +88,7 @@ suite('multilayer', function() {
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid;
else expected_token = parsedBody.layergroupid.split(':')[0];
next(null, res);
});
},
@@ -107,7 +112,7 @@ suite('multilayer', function() {
// Check X-Cache-Channel
cc = res.headers['x-cache-channel'];
assert.ok(cc);
var dbname = 'test_cartodb_user_1_db'
var dbname = test_database;
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
@@ -164,7 +169,7 @@ suite('multilayer', function() {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_style|test_cartodb_user_1_db|~" + expected_token, function(err, matches) {
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
@@ -193,7 +198,7 @@ suite('multilayer', function() {
]
};
var expected_token = "6d8e4ad5458e2d25cf0eef38e38717a6";
var expected_token; // = "6d8e4ad5458e2d25cf0eef38e38717a6";
Step(
function do_post()
{
@@ -217,7 +222,7 @@ suite('multilayer', function() {
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid;
else expected_token = parsedBody.layergroupid.split(':')[0];
next(null, res);
});
},
@@ -237,7 +242,7 @@ suite('multilayer', function() {
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc);
var dbname = 'test_cartodb_user_1_db'
var dbname = test_database;
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
@@ -270,7 +275,7 @@ suite('multilayer', function() {
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc);
var dbname = 'test_cartodb_user_1_db'
var dbname = test_database;
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
@@ -329,7 +334,7 @@ suite('multilayer', function() {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_style|test_cartodb_user_1_db|~" + expected_token, function(err, matches) {
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
@@ -425,7 +430,7 @@ suite('multilayer', function() {
var next = this;
// trip epoch
expected_token = expected_token.split(':')[0];
redis_client.keys("map_style|test_cartodb_user_1_db|~" + expected_token, function(err, matches) {
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
redis_client.del(matches, next);
});
},
@@ -517,7 +522,7 @@ suite('multilayer', function() {
]
};
var expected_token = "b4ed64d93a411a59f330ab3d798e4009";
var expected_token; // = "b4ed64d93a411a59f330ab3d798e4009";
Step(
function do_post()
{
@@ -542,7 +547,7 @@ suite('multilayer', function() {
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid;
else expected_token = parsedBody.layergroupid.split(':')[0];
next(null, res);
});
},
@@ -562,7 +567,7 @@ suite('multilayer', function() {
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc);
var dbname = 'test_cartodb_user_1_db'
var dbname = test_database;
assert.equal(cc.substring(0, dbname.length), dbname);
next(err);
});
@@ -650,7 +655,7 @@ suite('multilayer', function() {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_style|test_cartodb_user_1_db|~" + expected_token, function(err, matches) {
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
@@ -663,6 +668,123 @@ suite('multilayer', function() {
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/152
test("x-cache-channel still works for GETs after tiler restart", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select * from test_table where cartodb_id=1',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.1.0',
interactivity: 'cartodb_id'
} }
]
};
var expected_token; // = "b4ed64d93a411a59f330ab3d798e4009";
Step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/tiles/layergroup?map_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
var expectedBody = { layergroupid: expected_token };
// check last modified
var qTables = JSON.stringify({
'q': 'SELECT CDB_QueryTables($windshaft$'
+ layergroup.layers[0].options.sql
+ '$windshaft$)'
});
assert.equal(parsedBody.last_updated, expected_last_updated);
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid.split(':')[0];
return null;
},
function do_get0(err)
{
if ( err ) throw err;
var next = this;
assert.response(server, {
url: '/tiles/layergroup/' + expected_token + ':cb0/0/0/0.png?map_key=1234',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function do_check0(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc, "Missing X-Cache-Channel");
var dbname = test_database;
assert.equal(cc.substring(0, dbname.length), dbname);
return null;
},
function do_restart_server(err, res) {
if ( err ) throw err;
// hack simulating restart...
serverOptions = ServerOptions();
server = new CartodbWindshaft(serverOptions);
return null;
},
function do_get1(err)
{
if ( err ) throw err;
var next = this;
assert.response(server, {
url: '/tiles/layergroup/' + expected_token + ':cb0/0/0/0.png?map_key=1234',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function do_check1(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc, "Missing X-Cache-Channel on restart");
var dbname = test_database;
assert.equal(cc.substring(0, dbname.length), dbname);
return null;
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors.join(',')));
else done(null);
});
});
}
);
});
// https://github.com/cartodb/Windshaft-cartodb/issues/81
test("invalid text-name in CartoCSS", function(done) {
@@ -805,7 +927,7 @@ suite('multilayer', function() {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_style|test_cartodb_user_1_db|~" + expected_token, function(err, matches) {
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
@@ -880,7 +1002,7 @@ suite('multilayer', function() {
if ( err ) errors.push(err.message);
if ( ! expected_token ) return null;
var next = this;
redis_client.keys("map_style|test_cartodb_user_1_db|~" + expected_token, function(err, matches) {
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
@@ -941,13 +1063,15 @@ suite('multilayer', function() {
var parsedBody = JSON.parse(res.body);
var token_components = parsedBody.layergroupid.split(':');
expected_token = token_components[0];
var last_request = sqlapi_server.getLastRequest();
assert.equal(last_request.method, 'POST');
return null;
},
function cleanup(err) {
if ( err ) errors.push(err.message);
if ( err ) errors.push('' + err);
if ( ! expected_token ) return null;
var next = this;
redis_client.keys("map_style|test_cartodb_user_1_db|~" + expected_token, function(err, matches) {
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
@@ -964,6 +1088,46 @@ suite('multilayer', function() {
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/133
test("MapConfig with mapnik layer and no cartocss", 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',
interactivity: 'cartodb_id'
} }
]
};
Step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/tiles/layergroup',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, 'Missing "errors" in response: ' + JSON.stringify(parsed));
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.equal(msg, 'Missing cartocss for layer 0 options');
return null;
},
function finish(err) {
done(err);
}
);
});
suiteTeardown(function(done) {

View File

@@ -12,7 +12,7 @@ var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js');
var helper = require(__dirname + '/../support/test_helper');
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
@@ -22,7 +22,7 @@ suite('server', function() {
var sqlapi_server;
var mapnik_version = global.environment.mapnik_version || mapnik.versions.mapnik;
var test_database = 'test_cartodb_user_1_db';
var test_database = _.template(global.environment.postgres_auth_user, {user_id:1}) + '_db';
var default_style;
if ( semver.satisfies(mapnik_version, '<2.1.0') ) {
// 2.0.0 default
@@ -53,12 +53,25 @@ suite('server', function() {
// TODO: I guess this should be a 404 instead...
test("get call to server returns 200", function(done){
assert.response(server, {
url: '/',
method: 'GET'
},{
status: 200
}, function() { done(); });
Step(
function doGet() {
var next = this;
assert.response(server, {
url: '/',
method: 'GET'
},{}, function(res, err) { next(err,res); });
},
function doCheck(err, res) {
if ( err ) throw err;
assert.ok(res.statusCode, 200);
var cc = res.headers['x-cache-channel'];
assert.ok(!cc);
return null;
},
function finish(err) {
done(err);
}
);
});
/////////////////////////////////////////////////////////////////////////////////
@@ -175,7 +188,8 @@ suite('server', function() {
},
function setupRedisBase(err, matches) {
if ( err ) throw err;
assert.equal(matches.length, 0);
assert.equal(matches.length, 0,
'Unexpected redis keys at test start: ' + matches.join("\n"));
redis_client.set(base_key,
JSON.stringify({ style: style }),
this);
@@ -1111,8 +1125,8 @@ suite('server', function() {
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
var cc = res.headers['x-cache-channel'];
assert(cc);
var dbname = 'test_cartodb_user_1_db'
assert(cc, 'Missing X-Cache-Channel');
var dbname = test_database;
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
@@ -1126,6 +1140,38 @@ suite('server', function() {
);
});
test("passes hostname header to sqlapi", function(done){
var qo = {
sql: "SELECT * from gadm4",
map_key: 1234
};
var sqlapi;
Step(
function sendRequest(err) {
var next = this;
assert.response(server, {
headers: {host: 'localhost'},
url: '/tiles/gadm4/6/31/24.png?' + querystring.stringify(qo),
method: 'GET'
},{}, function(res) { next(null, res); });
},
function checkResponse(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var last_request = sqlapi_server.getLastRequest();
assert.ok(last_request);
var host = last_request.headers['host'];
assert.ok(host);
assert.equal(last_request.method, 'GET');
assert.equal(host, 'localhost.donot_look_this_up');
return null;
},
function finish(err) {
done(err);
}
);
});
test("requests to skip cache on sqlapi error", function(done){
var qo = {
sql: "SELECT g.cartodb_id, g.codineprov, t.the_geom_webmercator "
@@ -1176,6 +1222,20 @@ suite('server', function() {
});
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/115
test.skip("get'ing tile with not-strictly-valid style", function(done) {
var style = querystring.stringify({style: '#test_table{line-color:black}}', style_version: '2.0.0'});
assert.response(server, {
headers: {host: 'localhost'},
url: '/tiles/test_table/0/0/0.png?' + style, // madrid
method: 'GET',
encoding: 'binary'
},{}, function(res){
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
done();
});
});
/////////////////////////////////////////////////////////////////////////////////
//
// DELETE CACHE

View File

@@ -15,7 +15,7 @@ require(__dirname + '/../support/test_helper');
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
@@ -131,6 +131,153 @@ suite('template_api', function() {
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/128
test("cannot create template with auth='token' and no valid tokens", function(done) {
var tpl_id;
Step(
function postTemplate1()
{
// clone the valid one, and give it another name
var broken_template = JSON.parse(JSON.stringify(template_acceptance1));
broken_template.name = 'broken1';
// Set auth='token' and specify no tokens
broken_template.auth.method = 'token';
delete broken_template.auth.tokens;
var post_request_1 = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(broken_template)
}
var next = this;
assert.response(server, post_request_1, {},
function(res) { next(null, res); });
},
function checkFailure1(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 400, res.body);
var parsedBody = JSON.parse(res.body);
assert.ok(parsedBody.hasOwnProperty('error'), res.body);
var re = RegExp(/invalid.*authentication.*missing/i);
assert.ok(parsedBody.error.match(re),
'Error for invalid authentication does not match ' + re + ': ' + parsedBody.error);
return null;
},
function postTemplate2(err)
{
if ( err ) throw err;
// clone the valid one and rename it
var broken_template = JSON.parse(JSON.stringify(template_acceptance1));
broken_template.name = 'broken1';
// Set auth='token' and specify no tokens
broken_template.auth.method = 'token';
broken_template.auth.tokens = [];
var post_request_1 = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(broken_template)
}
var next = this;
assert.response(server, post_request_1, {},
function(res) { next(null, res); });
},
function checkFailure2(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 400, res.body);
var parsedBody = JSON.parse(res.body);
assert.ok(parsedBody.hasOwnProperty('error'), res.body);
var re = RegExp(/invalid.*authentication.*missing/i);
assert.ok(parsedBody.error.match(re),
'Error for invalid authentication does not match ' + re + ': ' + parsedBody.error);
return null;
},
function postTemplateValid(err)
{
if ( err ) throw err;
// clone the valid one and rename it
var broken_template = JSON.parse(JSON.stringify(template_acceptance1));
broken_template.name = 'broken1';
var post_request_1 = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(broken_template)
}
var next = this;
assert.response(server, post_request_1, {},
function(res) { next(null, res); });
},
function putTemplateInvalid(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('template_id'),
"Missing 'template_id' from response body: " + res.body);
tpl_id = parsed.template_id;
// clone the valid one and rename it
var broken_template = JSON.parse(JSON.stringify(template_acceptance1));
broken_template.name = 'broken1';
// Set auth='token' and specify no tokens
broken_template.auth.method = 'token';
broken_template.auth.tokens = [];
var put_request_1 = {
url: '/tiles/template/' + tpl_id + '/?api_key=1234',
method: 'PUT',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(broken_template)
}
var next = this;
assert.response(server, put_request_1, {},
function(res) { next(null, res); });
},
function deleteTemplate(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 400, res.statusCode + ": " + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('error'),
"Missing 'error' from response body: " + res.body);
var re = RegExp(/invalid.*authentication.*missing/i);
assert.ok(parsed.error.match(re),
'Error for invalid authentication on PUT does not match ' +
re + ': ' + parsed.error);
var del_request = {
url: '/tiles/template/' + tpl_id + '?api_key=1234',
method: 'DELETE',
headers: {host: 'localhost'}
}
var next = this;
assert.response(server, del_request, {},
function(res, err) { next(err, res); });
},
function checkDelete(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 204, res.statusCode + ': ' + res.body);
assert.ok(!res.body, 'Unexpected body in DELETE /template response');
return null;
},
function finish(err) {
var errors = [];
if ( err ) errors.push(err);
redis_client.keys("map_*|localhost", function(err, keys) {
if ( err ) errors.push(err.message);
var todrop = _.map(keys, function(m) {
if ( m.match(/^map_(tpl|crt)|/) )
return m;
});
if ( todrop.length )
errors.push(new Error("Unexpected keys in redis: " + todrop));
if ( errors.length ) done(new Error(errors.join(',')));
else done();
});
}
);
});
test("instance endpoint should return CORS headers", function(done){
Step(function postTemplate1(err, res) {
var next = this;
@@ -803,6 +950,605 @@ suite('template_api', function() {
);
});
test("can instanciate a template with torque layer by id", function(done) {
// This map fetches data from a private table
var template = {
version: '0.0.1',
name: 'acceptance1',
auth: { method: 'token', valid_tokens: ['valid1','valid2'] },
layergroup: {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "select * from test_table_private_1 LIMIT 0",
cartocss: "Map { -torque-frame-count:1; -torque-resolution:1; -torque-aggregation-function:'count(*)'; -torque-time-attribute:'updated_at'; }"
} }
]
}
};
var template_params = {};
var errors = [];
var expected_failure = false;
var tpl_id;
var layergroupid;
Step(
function postTemplate(err, res)
{
var next = this;
var post_request = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template)
}
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateNoAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('template_id'),
"Missing 'template_id' from response body: " + res.body);
tpl_id = parsed.template_id;
var post_request = {
url: '/tiles/template/' + tpl_id,
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_params)
}
var next = this;
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 401,
'Unexpected success instanciating template with no auth: '
+ 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(/unauthorized/i),
'Unexpected error for unauthorized instance : ' + parsed.error);
var post_request = {
url: '/tiles/template/' + tpl_id + '?auth_token=valid2',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_params)
}
var next = this;
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function fetchTileNoAuth(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Instantiating template: ' + res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('layergroupid'),
"Missing 'layergroupid' from response body: " + res.body);
layergroupid = parsed.layergroupid;
assert.ok(layergroupid.match(/^localhost@/),
"Returned layergroupid does not start with signer name: "
+ layergroupid);
assert.ok(parsed.hasOwnProperty('last_updated'),
"Missing 'last_updated' from response body: " + res.body);
// TODO: check value of last_updated ?
var get_request = {
url: '/tiles/layergroup/' + layergroupid + ':cb0/0/0/0/0.json.torque',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}
var next = this;
assert.response(server, get_request, {},
function(res) { next(null, res); });
},
function fetchTileAuth(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
'Fetching tile with no auth: ' + 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 instance '
+ '(expected /permission denied): ' + parsed.error);
var get_request = {
url: '/tiles/layergroup/' + layergroupid + ':cb1/0/0/0/0.json.torque?auth_token=valid1',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}
var next = this;
assert.response(server, get_request, {},
function(res) { next(null, res); });
},
function checkTile(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Unexpected error for authorized instance: '
+ res.statusCode + ' -- ' + res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
return null;
},
function deleteTemplate(err)
{
if ( err ) throw err;
var del_request = {
url: '/tiles/template/' + tpl_id + '?api_key=1234',
method: 'DELETE',
headers: {host: 'localhost'}
}
var next = this;
assert.response(server, del_request, {},
function(res) { next(null, res); });
},
function fetchTileDeleted(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 204,
'Deleting template: ' + res.statusCode + ':' + res.body);
var get_request = {
url: '/tiles/layergroup/' + layergroupid + ':cb2/0/0/0/0.json.torque?auth_token=valid1',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}
var next = this;
assert.response(server, get_request, {},
function(res) { next(null, res); });
},
function checkTileDeleted(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
'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);
return null;
},
function finish(err) {
if ( err ) errors.push(err);
redis_client.keys("map_*|localhost", function(err, keys) {
if ( err ) errors.push(err.message);
var todrop = _.map(keys, function(m) {
if ( m.match(/^map_(tpl|crt)|/) )
return m;
});
if ( todrop.length ) {
errors.push(new Error("Unexpected keys in redis: " + todrop));
redis_client.del(todrop, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) {
done(new Error(errors));
}
else done(null);
});
} else {
if ( errors.length ) {
done(new Error(errors));
}
else done(null);
}
});
}
);
});
test("can instanciate a template with attribute service by id", function(done) {
// This map fetches data from a private table
var template = {
version: '0.0.1',
name: 'acceptance1',
auth: { method: 'token', valid_tokens: ['valid1','valid2'] },
layergroup: {
version: '1.1.0',
layers: [
{ options: {
sql: "select * from test_table_private_1 where cartodb_id in ( 5,6 )",
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
cartocss_version: '2.0.2',
attributes: { id:'cartodb_id', columns: ['name', 'address'] }
} }
]
}
};
var template_params = {};
var errors = [];
var expected_failure = false;
var tpl_id;
var layergroupid;
Step(
function postTemplate(err, res)
{
var next = this;
var post_request = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template)
}
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateNoAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('template_id'),
"Missing 'template_id' from response body: " + res.body);
tpl_id = parsed.template_id;
var post_request = {
url: '/tiles/template/' + tpl_id,
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_params)
}
var next = this;
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 401,
'Unexpected success instanciating template with no auth: '
+ 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(/unauthorized/i),
'Unexpected error for unauthorized instance : ' + parsed.error);
var post_request = {
url: '/tiles/template/' + tpl_id + '?auth_token=valid2',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_params)
}
var next = this;
assert.response(server, post_request, {},
function(res, err) { next(null, res); });
},
function fetchAttributeNoAuth(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Instantiating template: ' + res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('layergroupid'),
"Missing 'layergroupid' from response body: " + res.body);
layergroupid = parsed.layergroupid;
assert.ok(layergroupid.match(/^localhost@/),
"Returned layergroupid does not start with signer name: "
+ layergroupid);
assert.ok(parsed.hasOwnProperty('last_updated'),
"Missing 'last_updated' from response body: " + res.body);
// TODO: check value of last_updated ?
var get_request = {
url: '/tiles/layergroup/' + layergroupid + ':cb0/0/attributes/5',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}
var next = this;
assert.response(server, get_request, {},
function(res) { next(null, res); });
},
function fetchAttributeAuth(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
'Fetching tile with no auth: ' + 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 getAttributes '
+ '(expected /permission denied/): ' + parsed.error);
var get_request = {
url: '/tiles/layergroup/' + layergroupid + ':cb1/0/attributes/5?auth_token=valid2',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}
var next = this;
assert.response(server, get_request, {},
function(res) { next(null, res); });
},
function checkAttribute(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Unexpected error for authorized getAttributes: '
+ res.statusCode + ' -- ' + res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
return null;
},
function deleteTemplate(err)
{
if ( err ) throw err;
var del_request = {
url: '/tiles/template/' + tpl_id + '?api_key=1234',
method: 'DELETE',
headers: {host: 'localhost'}
}
var next = this;
assert.response(server, del_request, {},
function(res) { next(null, res); });
},
function fetchAttrDeleted(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 204,
'Deleting template: ' + res.statusCode + ':' + res.body);
var get_request = {
url: '/tiles/layergroup/' + layergroupid + ':cb2/0/attributes/5?auth_token=valid2',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}
var next = this;
assert.response(server, get_request, {},
function(res) { next(null, res); });
},
function checkTileDeleted(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
'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);
return null;
},
function finish(err) {
if ( err ) errors.push(err);
redis_client.keys("map_*|localhost", function(err, keys) {
if ( err ) errors.push(err.message);
var todrop = _.map(keys, function(m) {
if ( m.match(/^map_(tpl|crt)|/) )
return m;
});
if ( todrop.length ) {
errors.push(new Error("Unexpected keys in redis: " + todrop));
redis_client.del(todrop, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) {
done(new Error(errors));
}
else done(null);
});
} else {
if ( errors.length ) {
done(new Error(errors));
}
else done(null);
}
});
}
);
});
test("can instanciate a template by id with open auth", function(done) {
// This map fetches data from a private table
var template_acceptance_open = {
version: '0.0.1',
name: 'acceptance_open',
auth: { method: 'open' },
layergroup: {
version: '1.0.0',
layers: [
{ options: {
sql: "select * from test_table_private_1 LIMIT 0",
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
cartocss_version: '2.0.2',
interactivity: 'cartodb_id'
} }
]
}
};
var template_params = {};
var errors = [];
var expected_failure = false;
var tpl_id;
var layergroupid;
Step(
function postTemplate(err, res)
{
var next = this;
var post_request = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_acceptance_open)
}
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateNoAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('template_id'),
"Missing 'template_id' from response body: " + res.body);
tpl_id = parsed.template_id;
var post_request = {
url: '/tiles/template/' + tpl_id,
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_params)
}
var next = this;
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Unexpected success instanciating template with no auth: '
+ res.statusCode + ': ' + res.body);
done();
}
);
});
test("can instanciate a template using jsonp", function(done) {
// This map fetches data from a private table
var template_acceptance_open = {
version: '0.0.1',
name: 'acceptance_open_jsonp',
auth: { method: 'open' },
layergroup: {
version: '1.0.0',
layers: [
{ options: {
sql: "select * from test_table_private_1 LIMIT 0",
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
cartocss_version: '2.0.2',
interactivity: 'cartodb_id'
} }
]
}
};
var template_params = {};
var errors = [];
var expected_failure = false;
var tpl_id;
var layergroupid;
Step(
function postTemplate(err, res)
{
var next = this;
var post_request = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_acceptance_open)
}
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateNoAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('template_id'),
"Missing 'template_id' from response body: " + res.body);
tpl_id = parsed.template_id;
var post_request = {
url: '/tiles/template/' + tpl_id + "/jsonp?callback=test",
method: 'GET',
headers: {host: 'localhost' }
}
var next = this;
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Unexpected success instanciating template with no auth: '
+ res.statusCode + ': ' + res.body);
done();
}
);
});
test("can instanciate a template using jsonp with params", function(done) {
// This map fetches data from a private table
var template_acceptance_open = {
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: [
{ options: {
sql: "select * from test_table_private_1 LIMIT 0",
cartocss: '#layer { marker-fill: <%= color %>; marker-allow-overlap:true; }',
cartocss_version: '2.0.2',
interactivity: 'cartodb_id'
} }
]
}
};
var template_params = {};
var errors = [];
var expected_failure = false;
var tpl_id;
var layergroupid;
Step(
function postTemplate(err, res)
{
var next = this;
var post_request = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_acceptance_open)
}
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateNoAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('template_id'),
"Missing 'template_id' from response body: " + res.body);
tpl_id = parsed.template_id;
var post_request = {
url: '/tiles/template/' + tpl_id + "/jsonp?callback=test%config=" + JSON.stringify('{color:blue}'),
method: 'GET',
headers: {host: 'localhost' }
}
var next = this;
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Unexpected success instanciating template with no auth: '
+ res.statusCode + ': ' + res.body);
return null;
},
function finish(err) {
done(err);
}
);
});
test("template instantiation raises mapviews counter", function(done) {
var layergroup = {
stat_tag: 'random_tag',
@@ -911,6 +1657,147 @@ suite('template_api', function() {
);
});
test("instance map token changes with templates certificate changes", function(done) {
// This map fetches data from a private table
var template_acceptance2 = {
version: '0.0.1',
name: 'acceptance2',
auth: { method: 'token', valid_tokens: ['valid1','valid2'] },
layergroup: {
version: '1.0.0',
layers: [
{ options: {
sql: "select * from test_table_private_1 LIMIT 0",
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
cartocss_version: '2.0.2',
interactivity: 'cartodb_id'
} }
]
}
};
var template_params = {};
var errors = [];
var expected_failure = false;
var tpl_id;
var layergroupid;
Step(
function postTemplate(err, res)
{
var next = this;
var post_request = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_acceptance2)
}
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instance1(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('template_id'),
"Missing 'template_id' from response body: " + res.body);
tpl_id = parsed.template_id;
var post_request = {
url: '/tiles/template/' + tpl_id + '?auth_token=valid2',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_params)
}
var next = this;
assert.response(server, post_request, {},
function(res, err) { next(err, res); });
},
function checkInstance1(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Instantiating template: ' + res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('layergroupid'),
"Missing 'layergroupid' from response body: " + res.body);
layergroupid = parsed.layergroupid;
return null;
},
function updateTemplate(err, res)
{
if ( err ) throw err;
// clone the valid one and rename it
var changedTemplate = JSON.parse(JSON.stringify(template_acceptance2));
changedTemplate.auth.method = 'open';
var post_request = {
url: '/tiles/template/' + tpl_id + '/?api_key=1234',
method: 'PUT',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(changedTemplate)
}
var next = this;
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instance2(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('template_id'),
"Missing 'template_id' from response body: " + res.body);
assert.equal(tpl_id, parsed.template_id);
var post_request = {
url: '/tiles/template/' + tpl_id + '?auth_token=valid2',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_params)
}
var next = this;
assert.response(server, post_request, {},
function(res, err) { next(err, res); });
},
function checkInstance2(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Instantiating template: ' + res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('layergroupid'),
"Missing 'layergroupid' from response body: " + res.body);
assert.ok(layergroupid != parsed.layergroupid);
return null;
},
function finish(err) {
if ( err ) errors.push(err);
redis_client.keys("map_*|localhost", function(err, keys) {
if ( err ) errors.push(err.message);
var todrop = _.map(keys, function(m) {
if ( m.match(/^map_(tpl|crt)|/) )
return m;
});
if ( todrop.length != 2 ) {
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);
if ( errors.length ) {
done(new Error(errors));
}
else done(null);
});
});
}
);
});
suiteTeardown(function(done) {
// This test will add map_style records, like

View File

@@ -1,14 +1,19 @@
var http = require('http');
var url = require('url');
var _ = require('underscore');
var o = function(port, cb) {
this.queries = [];
var that = this;
this.requests = [];
this.sqlapi_server = http.createServer(function(req,res) {
//console.log("server got request with method " + req.method);
var query;
that.requests.push(req);
if ( req.method == 'GET' ) {
query = url.parse(req.url, true).query;
that.handleQuery(query, res);
@@ -22,7 +27,6 @@ var o = function(port, cb) {
req.on('end', function() {
//console.log("Data is: "); console.dir(data);
query = JSON.parse(data);
//console.log("Parsed is: "); console.dir(query);
//console.log("handleQuery is " + that.handleQuery);
that.handleQuery(query, res);
});
@@ -45,15 +49,20 @@ o.prototype.handleQuery = function(query, res) {
};
res.write(JSON.stringify({rows: [ row ]}));
} 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);
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.end();
};
@@ -63,5 +72,9 @@ o.prototype.close = function(cb) {
this.sqlapi_server.close(cb);
};
o.prototype.getLastRequest = function() {
return this.requests.pop();
};
module.exports = o;

View File

@@ -174,5 +174,6 @@ CREATE TABLE test_table_private_1 (
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
);
INSERT INTO test_table_private_1 SELECT * from test_table;
GRANT ALL ON TABLE test_table_private_1 TO :TESTUSER;

View File

@@ -7,7 +7,12 @@ var assert = require('assert')
suite('req2params', function() {
// configure redis pool instance to use in tests
var opts = require('../../../lib/cartodb/server_options');
var opts = require('../../../lib/cartodb/server_options')();
var test_user = _.template(global.environment.postgres_auth_user, {user_id:1});
var test_pubuser = global.environment.postgres.user;
var test_database = test_user + '_db';
test('can be found in server_options', function(){
assert.ok(_.isFunction(opts.req2params));
@@ -20,8 +25,8 @@ suite('req2params', function() {
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
assert.equal(req.params.dbname, 'test_cartodb_user_1_db', 'could forge dbname: '+ req.params.dbname);
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')');
assert.equal(req.params.dbname, test_database, 'could forge dbname: '+ req.params.dbname);
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
done();
});
});
@@ -34,10 +39,8 @@ suite('req2params', function() {
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
// database_name for user "localhost" (see test/support/prepare_db.sh)
assert.equal(req.params.dbname, 'test_cartodb_user_1_db');
// unauthenticated request gets no dbuser
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')');
assert.equal(req.params.dbname, test_database);
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
done();
});
});
@@ -50,14 +53,12 @@ suite('req2params', function() {
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
// database_name for user "localhost" (see test/support/prepare_db.sh)
assert.equal(req.params.dbname, 'test_cartodb_user_1_db');
// id for user "localhost" (see test/support/prepare_db.sh)
assert.equal(req.params.dbuser, 'test_cartodb_user_1');
assert.equal(req.params.dbname, test_database);
assert.equal(req.params.dbuser, test_user);
opts.req2params({headers: { host:'localhost' }, query: {map_key: '1235'} }, function(err, req) {
// wrong key resets params to no user
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')');
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
done();
});
});

View File

@@ -81,5 +81,29 @@ suite('signed_maps', function() {
);
});
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

@@ -147,6 +147,28 @@ 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);
assert.ok(tmap);
var tpl = { version:'0.0.1',
name: "invalid_auth1", placeholders: { },
auth: { method: 'token' }, layergroup: {} };
tmap.addTemplate('me', tpl, function(err) {
if ( ! err ) {
done(new Error("Unexpected success with invalid token auth (undefined tokens)"));
}
else if ( ! err.message.match(/invalid 'token' authentication/i) ) {
done(new Error("Unexpected error message with invalid token auth (undefined tokens): "
+ err));
}
else {
done();
}
});
});
test('add, get and delete a valid template', function(done) {
var tmap = new TemplateMaps(redis_pool, signed_maps);
assert.ok(tmap);
@@ -271,6 +293,7 @@ suite('template_maps', function() {
assert.ok(err);
tpl.name = 'first';
tpl.auth.method = 'token';
tpl.auth.valid_tokens = [ 'tok1' ];
tmap.updTemplate(owner, tpl_id, tpl, this);
},
function updateTemplateWithInvalid(err) {
@@ -314,17 +337,18 @@ suite('template_maps', function() {
color: { type: "css_color", default: "#a0fF9A" },
name: { type: "sql_literal", default: "test" },
zoom: { type: "number", default: "0" },
test_number: { type: "number", default: 23 },
},
layergroup: {
version: '1.0.0',
global_cartocss_version: '2.0.2',
layers: [
{ options: {
sql: "select '<%= name %>' || id, g from t",
cartocss: '#layer { marker-fill:<%= fill %>; }'
sql: "select '<%=name %>' || id, g from t",
cartocss: '#layer { marker-fill:<%= fill %>; marker-width: <%=test_number %>; }'
} },
{ options: {
sql: "select fun('<%= name %>') g from x",
sql: "select fun('<%= name%>') g from x",
cartocss: '#layer { line-color:<%= color %>; marker-fill:<%= color %>; }'
} },
{ options: {
@@ -339,7 +363,7 @@ suite('template_maps', function() {
var lyr = inst.layers[0].options;
assert.equal(lyr.sql, "select 'test' || id, g from t");
assert.equal(lyr.cartocss, '#layer { marker-fill:red; }');
assert.equal(lyr.cartocss, '#layer { marker-fill:red; marker-width: 23; }');
lyr = inst.layers[1].options;
assert.equal(lyr.sql, "select fun('test') g from x");
@@ -349,7 +373,7 @@ suite('template_maps', function() {
lyr = inst.layers[0].options;
assert.equal(lyr.sql, "select 'it''s dangerous' || id, g from t");
assert.equal(lyr.cartocss, '#layer { marker-fill:red; }');
assert.equal(lyr.cartocss, '#layer { marker-fill:red; marker-width: 23; }');
lyr = inst.layers[1].options;
assert.equal(lyr.sql, "select fun('it''s dangerous') g from x");
@@ -396,5 +420,87 @@ suite('template_maps', function() {
catch (e) { err = e; }
assert.ok(!err);
});
// 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, {
max_user_templates: 2
});
assert.ok(tmap);
var tpl = { version:'0.0.1', auth: {}, layergroup: {} };
var expectErr = false;
var idMe = [];
var idYou = [];
Step(
function oneForMe() {
tpl.name = 'oneForMe';
tmap.addTemplate('me', tpl, this);
},
function twoForMe(err, id) {
if ( err ) throw err;
assert.ok(id);
idMe.push(id);
tpl.name = 'twoForMe';
tmap.addTemplate('me', tpl, this);
},
function threeForMe(err, id) {
if ( err ) throw err;
assert.ok(id);
idMe.push(id);
tpl.name = 'threeForMe';
expectErr = true;
tmap.addTemplate('me', tpl, this);
},
function errForMe(err, id) {
if ( err && ! expectErr ) throw err;
expectErr = false;
assert.ok(err);
assert.ok(err.message.match(/limit.*template/), err);
return null;
},
function delOneMe(err) {
if ( err ) throw err;
tmap.delTemplate('me', idMe.shift(), this);
},
function threeForMeRetry(err) {
if ( err ) throw err;
tpl.name = 'threeForMe';
tmap.addTemplate('me', tpl, this);
},
function oneForYou(err, id) {
if ( err ) throw err;
assert.ok(id);
idMe.push(id);
tpl.name = 'oneForYou';
tmap.addTemplate('you', tpl, this);
},
function twoForYou(err, id) {
if ( err ) throw err;
assert.ok(id);
idYou.push(id);
tpl.name = 'twoForYou';
tmap.addTemplate('you', tpl, this);
},
function threeForYou(err, id) {
if ( err ) throw err;
assert.ok(id);
idYou.push(id);
tpl.name = 'threeForYou';
expectErr = true;
tmap.addTemplate('you', tpl, this);
},
function errForYou(err, id) {
if ( err && ! expectErr ) throw err;
expectErr = false;
assert.ok(err);
assert.ok(err.message.match(/limit.*template/), err);
return null;
},
function finish(err) {
// TODO: delete all templates
done(err);
}
);
});
});

View File

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

View File

@@ -0,0 +1,17 @@
{
"version":"0.0.1",
"name":"simple",
"placeholders":{},
"auth":{ "method":"open" },
"layergroup":{
"version":"1.0.1",
"layers":[{
"type":"cartodb",
"options":{
"sql":"select ST_SetSRID(ST_MakePoint(0,0),3857) as the_geom_webmercator",
"cartocss":"#s{ marker-width: 12;}",
"cartocss_version":"2.1.1"
}
}]
}
}

59
tools/flush_cache Executable file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env node
var path = require('path');
var request = require('request');
function usage(me, exitcode) {
console.log("Usage: " + me + " [--env <environment>] <username> <tablename>");
process.exit(exitcode);
}
var node_path = process.argv.shift();
var script_path = process.argv.shift();
var basedir = path.dirname(script_path);
var me = path.basename(script_path);
var ENV = 'development.js';
var username, table;
var arg;
while ( arg = process.argv.shift() ) {
if ( arg == '--env' ) {
ENV = process.argv.shift();
}
else if ( ! username ) {
username = arg;
}
else if ( ! table ) {
table = arg;
}
else {
console.warn("Unused parameter " + arg);
}
}
if ( ! table ) {
usage(me, 1);
}
global.environment = require('../config/environments/' + ENV);
// _after_ setting global.environment
var serverOptions = require('../lib/cartodb/server_options');
var host = global.environment.host;
var port = global.environment.port;
var re = ''+serverOptions.re_userFromHost;
var hostname = re.replace(/^\/\^/, '')
.replace(/\/$$/, '')
.replace(/\\/g,'')
.replace(/\([^)]*\)/,username)
;
//console.log("re: " + re);
//console.log("hostname: " + hostname);
var url = 'http://' + host + ':' + port + '/tiles/' + table + '/flush_cache';
request.del({ url: url, headers: { host: hostname } },
function(err, res, body) {
if ( err ) throw err;
console.log(res.body);
});

View File

@@ -49,5 +49,5 @@ if test x${verbose} = xyes; then
echo "${res}"
fi
tok=`echo "$res" | sed 's/.*"template_id":"\([^"]*\)".*/\1/'`
tok=`echo "$res" | sed 's/.*"layergroupid":"\([^"]*\)".*/\1/'`
echo $tok

45
tools/list_templates Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/sh
verbose=no
tiler_url=http://dev.localhost.lan:8181/tiles/template
apikey=${CDB_APIKEY}
while test -n "$1"; do
if test "$1" = "-v"; then
verbose=yes
elif test "$1" = "-k"; then
shift
apikey="$1"
elif test "$1" = "-u"; then
shift
tiler_url="$1"
elif test "$1" = "-h" -o "$1" = "-?"; then
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>]" >&2
echo "Default <tiler_url> is ${tiler_url}" >&2
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
exit 0
else
echo "Unused parameter $1" >&2
fi
shift
done
cmd="curl -X GET -sk ${tiler_url}?api_key=${apikey}"
if test x${verbose} = xyes; then
cmd="${cmd} -v"
fi
res=`${cmd}`
if test $? -gt 0; then
echo "curl command failed: ${cmd}"
fi
if test x${verbose} = xyes; then
echo "${res}"
fi
node <<EOF
var parsed = JSON.parse('$res');
console.dir(parsed);
EOF

View File

@@ -68,7 +68,7 @@ for pid in ${pids}; do
log=$(grep "${pid}" "${tmpreport}" | grep -w 1w | awk '{print $9}')
if test -e "${log}"; then
kill -USR2 "${pid}"
cnt=$(tac ${log} | sed -n -e '/ItemKey/p;/^RenderCache/q' | wc -l)
cnt=$(tac ${log} | sed -n -e '/ItemKey/p;/ RenderCache /q' | wc -l)
if test $cnt -gt $maxcache; then maxcache=$cnt; fi
else
# report the error...

View File

@@ -1,85 +1,147 @@
#!/bin/sh
#!/usr/bin/env node
# TODO: port to node, if you really need it
ENV='development';
BASEDIR=`cd $(dirname $0)/../; pwd`
if test -z "$1"; then
echo "Usage: $0 [--env <environment>] <username> [<tablename>|~<token>]" >&2
echo " environment defaults to 'development'"
exit 1
fi
username=""
token=""
while test -n "$1"; do
if test "$1" = "--env"; then
shift; ENV="$1"; shift
elif test -z "$username"; then
username="$1"; shift
elif test -z "$token"; then
token="$1"; shift
else
echo "Unused option $1" >&2
shift
fi
done
echo "Using environment '${ENV}'"
CONFIG="${BASEDIR}/config/environments/${ENV}.js"
REDIS_PORT=`node -e "console.log(require('${CONFIG}').redis.port)"`
if test $? -ne 0; then
exit 1
fi
var path = require('path');
var redis = require('redis');
var Step = require('step');
dbname=`redis-cli -p ${REDIS_PORT} -n 5 hget "rails:users:${username}" "database_name"`
if test $? -ne 0; then
exit 1
fi
if test -z "${dbname}"; then
echo "Username ${username} unknown by redis on port ${REDIS_PORT} (try CARTODB/script/restore_redis?)" >&2
exit 1
fi
echo "Database name for user ${username}: ${dbname}" # only if verbose?
if test -n "$token"; then
rec=`redis-cli get "map_style|${dbname}|${token}"`
if test -z "${rec}"; then
echo "${token}: no such map style known by redis on port ${REDIS_PORT}" >&2
exit 1
fi
#echo "${rec}"
escrec=`echo "${rec}" | sed -e 's/\\\\/\\\\\\\\/g'`
#echo "${escrec}"
node <<EOF
var x=JSON.parse('${escrec}');
console.log('style: ' + x.style);
console.log('version: ' + x.version);
global.environment = require('${CONFIG}');
var serverOptions = require('${BASEDIR}/lib/cartodb/server_options'); // _after_ setting global.environment
var grainstore = require('${BASEDIR}/node_modules/windshaft/node_modules/grainstore/lib/grainstore');
var mml_store = new grainstore.MMLStore(serverOptions.redis, serverOptions.grainstore);
var builderconfig = {dbname:'${dbname}'};
if ( '${token}'.match(/^~/) ) {
builderconfig.token = '${token}'.substring(1);
} else {
builderconfig.table = '${token}';
function usage(me, exitcode) {
console.log("Usage: " + me + " [--env <environment>] <username> [<tablename>|~<token>]");
process.exit(exitcode);
}
var mml_builder = mml_store.mml_builder(builderconfig,
function(err, payload) {
var node_path = process.argv.shift();
var script_path = process.argv.shift();
var basedir = path.dirname(script_path);
var me = path.basename(script_path);
var ENV = 'development.js';
var username, token;
var arg;
while ( arg = process.argv.shift() ) {
if ( arg == '--env' ) {
ENV = process.argv.shift();
}
else if ( ! username ) {
username = arg;
}
else if ( ! token ) {
token = arg;
}
else {
console.warn("Unused parameter " + arg);
}
}
if ( ! username ) usage(me, 1);
console.log("Using environment " + ENV);
global.environment = require('../config/environments/' + ENV);
var serverOptions = require('../lib/cartodb/server_options'); // _after_ setting global.environment
var client;
var dbname;
Step(
function getClient() {
client = redis.createClient(serverOptions.redis.port, serverOptions.redis.host);
client.on('connect', this);
},
function getUserMeta(err) {
if ( err ) throw err;
client.select(5);
client.hgetall('rails:users:' + username, this);
},
function readDB(err, data) {
if ( err ) throw err;
if ( ! data )
throw new Error('Username ' + username + ' unknown by redis on port '
+ serverOptions.redis.port + ' (try CARTODB/script/restore_redis?)');
//console.log("Data:"); console.dir(data);
dbname = data['database_name'];
console.log("Database name for user " + username + ": " + dbname);
client.select(0);
return null;
},
function showTokens(err) {
if ( err ) throw err;
if ( token ) return null;
var next = this;
Step(
function getTokens() {
client.keys('map_style|' + dbname + '|*', this);
},
function showTokens(err, data) {
if (err) throw err;
if ( data ) console.log(data.join('\n'));
return null;
},
function showTokensFinish(err) {
next(err);
}
);
},
function showStyle(err) {
if ( err ) throw err;
if ( ! token ) return null;
var next = this;
Step(
function getStyle() {
client.get('map_style|' + dbname + '|' + token, this);
},
function showStyle(err, data) {
if ( err ) throw err;
mml_builder.toXML(function(err, xml) {
if ( err ) throw err;
console.log('- XML - ');
console.log(xml);
});
});
EOF
#echo "${rec}" | sed -e 's/\\n/\n/g' -e 's/\\//g'
else
redis-cli keys "map_style|${dbname}|*"
fi
if ( ! data ) {
throw new Error(token + ': no such map style known by redis on port '
+ serverOptions.redis.port);
}
//console.log("data: " + data);
var x=JSON.parse(data);
printMapnikStyle(x, this);
},
function showStyleFinish(err) {
next(err);
}
);
},
function finish(err) {
if ( err ) {
console.error(err.message)
process.exit(1);
}
process.exit(0);
}
);
function printMapnikStyle(x, callback) {
console.log('style: ' + x.style);
console.log('version: ' + x.version);
var grainstore = require(basedir + '/../node_modules/windshaft/node_modules/grainstore/lib/grainstore');
var mml_store = new grainstore.MMLStore(serverOptions.redis, serverOptions.grainstore);
var builderconfig = {dbname:dbname};
if ( token.match(/^~/) ) {
builderconfig.token = token.substring(1);
} else {
builderconfig.table = token;
}
var mml_builder;
Step(
function getBuilder() {
mml_builder = mml_store.mml_builder(builderconfig, this);
},
function getXML(err, builder) {
if ( err ) throw err;
mml_builder.toXML(this);
},
function showXML(err, xml) {
if ( err ) throw err;
console.log('- XML - ');
console.log(xml);
return null;
},
function finish(err) {
callback(err);
}
);
}