Compare commits

..

36 Commits
1.8.2 ... 1.8.5

Author SHA1 Message Date
Sandro Santilli
c314819ade Release 1.8.5 2014-03-10 17:36:21 +01:00
Sandro Santilli
7eb26a7326 Upgrade windshaft to 0.19.3, fixing crash on dns error
Closes #180
2014-03-05 18:16:42 +01:00
Sandro Santilli
0afc9c154b Cleanly catch exceptions from sendResponse
Closes #178
2014-03-04 18:04:58 +01:00
Sandro Santilli
97e00fb47d Do not send duplicated stats on template instanciation
Closes #179
2014-03-04 17:51:50 +01:00
Sandro Santilli
dbae0eeb31 It is "cacheDns", not "dnsCache"
See https://github.com/sivy/node-statsd/issues/38
2014-03-04 17:37:19 +01:00
Sandro Santilli
bd9a21b805 Add "dnsCache" statsd setting in the example configs 2014-03-04 16:52:16 +01:00
Sandro Santilli
033f8df500 Include API docs, moved from wiki
Closes #164
2014-03-04 15:39:21 +01:00
Sandro Santilli
ffda103d61 Do not UNWATCH on every redis client release
Closes #161
2014-03-04 15:36:08 +01:00
Sandro Santilli
ecc9ea1226 Use 403 for forbidden, not 401
Includes upgrade of windshaft to 0.19.3
Includes upgrade of redis-mpool to 0.0.4
2014-03-04 15:32:31 +01:00
Sandro Santilli
93345a19b2 Do not log an error on GET /
Closes #177
2014-03-04 14:26:41 +01:00
Sandro Santilli
1741a20575 Do not cache map creation responses
Closes #176
CDB-1908 #resolve
CDB-1901 #resolve

Includes testcase
2014-03-04 10:46:15 +01:00
Sandro Santilli
30eb939dc7 Fix error message on missing requested signature
We don't really distinguish between missing or non-authorizing
signature. And that's fine. See #170
2014-03-03 18:14:17 +01:00
Sandro Santilli
40a254922a Raise 403 forbidden on missing requested signature
Closes #170
Includes testcase
2014-03-03 18:06:39 +01:00
Sandro Santilli
7bc5bab432 Properly prefix statsd labels for all endpoints
CDB-1861 #resolve
Will be 100% complete with update of Windshaft to 0.19.3+
2014-03-03 16:24:20 +01:00
Sandro Santilli
6034f49f40 Prepare for 1.8.5 2014-03-03 11:45:23 +01:00
Sandro Santilli
087eff4734 Release 1.8.4 2014-03-03 11:26:16 +01:00
Sandro Santilli
ed5b045a15 Allow using NODE_ENV env variable to determine app configuration
Default to "development" environment.
Forward NODE_ENV variable to childrens (for example, to hush
millstone).
2014-02-28 16:22:24 +01:00
Sandro Santilli
c1a3cbc28c Hush millstone during testsuite 2014-02-28 16:14:44 +01:00
Sandro Santilli
bddc65a504 Forbid instanciating templates of foreign users
Closes #173
Includes testcase
2014-02-28 16:05:46 +01:00
Sandro Santilli
ddd2628c19 Fix database connection settings on template instanciation
Closes #174
Enhances testsuite to ensure test.js settings are read
2014-02-28 15:56:31 +01:00
Sandro Santilli
cf0c33a85d Oops, previous commit closed #172, not #173
Closes #172
Reopens #173
2014-02-28 13:25:28 +01:00
Sandro Santilli
f46dc90035 Forbid using map signatures of foreign users
Closes #173
Includes testcase
2014-02-28 13:24:38 +01:00
Sandro Santilli
73276b1003 Upgrade windshaft to 0.19.2
Fixes obscure "ECONNREFUSED" error message (closes #171)
Change some http status responses to be more appropriate to the case
2014-02-28 10:54:18 +01:00
Sandro Santilli
16e67387c9 Tell npm to use known registrars
See http://blog.npmjs.org/post/78085451721/npms-self-signed-certificate-is-no-more
2014-02-28 10:50:45 +01:00
Sandro Santilli
ca1b31bd9c Add example MapConfig using a torque layer 2014-02-27 17:20:23 +01:00
Sandro Santilli
55f333c0b7 Call userByReq() only once in req2params 2014-02-27 16:40:59 +01:00
Sandro Santilli
f24e4f8a0a Really skip CDB_TableMetadata lookup for sql affected by no tables
Closes #169
2014-02-27 15:34:09 +01:00
Sandro Santilli
eec9933fb8 Accept a slightly different error message on timeout
Node 0.10 uses ESOCKETTIMEDOUT while 0.8 uses ETIMEDOUT
See http://travis-ci.org/CartoDB/Windshaft-cartodb/builds/19722727
2014-02-27 13:37:44 +01:00
Sandro Santilli
238e8f39f2 Fix ticket referenc ein NEWS entry of 1.8.3 2014-02-27 12:46:56 +01:00
Sandro Santilli
919bcb6888 Prepare for 1.8.4 2014-02-27 12:46:43 +01:00
Sandro Santilli
50ebb25205 Release 1.8.3 2014-02-27 12:45:02 +01:00
Sandro Santilli
625642ca33 Oops, previous commit closed #168, not #16
Closes #168
2014-02-27 12:43:15 +01:00
Sandro Santilli
36632c762e Do not query CDB_TableMetadata for queries affected by no tables
Closes #16
2014-02-27 12:32:34 +01:00
Sandro Santilli
f284362988 Reduce sql-api communication timeout, and allow overriding it
Introduces new sqlapi.timeout directive, defaults to 100 ms
Includes testcase.
Closes #167
2014-02-27 10:33:32 +01:00
Sandro Santilli
cf01f01bc9 Upgrades windshaft to 0.19.1 with many performance improvements
Among others:

- Improve speed of instanciating a map
- Give meaningful error on attempts to use map tokens with
  attribute service

Closes #156 -- CDB-1796 #resolve
Closes #147
Closes #159
Closes #165
2014-02-26 17:26:17 +01:00
Sandro Santilli
5d0c71d292 Prepare for 1.8.3 2014-02-25 11:10:56 +01:00
20 changed files with 897 additions and 139 deletions

View File

@@ -4,6 +4,9 @@ before_install:
- sudo apt-get install -q libmapnik-dev
- createdb template_postgis
- psql -c "CREATE EXTENSION postgis" template_postgis
# Tell npm to use known registrars:
# see http://blog.npmjs.org/post/78085451721/npms-self-signed-certificate-is-no-more
- npm config set ca ""
env:
- NPROCS=1 JOBS=1

51
NEWS.md
View File

@@ -1,3 +1,54 @@
1.8.5 -- 2014-03-10
-------------------
Enhancements:
- Set statsd prefix for all endpoints
- Respond with a permission denied on attempt to access map tiles waiving
signature of someone who had not left any (#170)
- Do not log an error on GET / (#177)
- Do not UNWATCH on every redis client release (#161)
- Include API docs (#164)
- Add "cacheDns" statsd setting in the example configs
- Do not send duplicated stats on template instanciation
- Do not die on dns resolution errors (#178, #180)
Bug fixes:
- Do not cache map creation responses (#176)
1.8.4 -- 2014-03-03
-------------------
Enhancements:
- Really skip CDB_TableMetadata lookup for sql affected by no tables (#169)
- Upgrade windshaft to 0.19.2, see node_modules/windshaft/NEWS
- Clarify obscure "ECONNREFUSED" error message (#171)
- Change some http status responses to be more appropriate to the case
- Forbid using map signatures of foreign users (#172)
- Forbid instanciating templates of foreign users (#173)
- Allow passing environment configuration name via NODE_ENV to app.js
- Print environment configuration name on app start
Bug fixes:
- Fix database connection settings on template instanciation (#174)
1.8.3 -- 2014-02-27
-------------------
Enhancements:
- Upgrades windshaft to 0.19.1 with many performance improvements,
See node_modules/windshaft/NEWS
- Improve speed of instanciating a map (#147, #159, #165)
- Give meaningful error on attempts to use map tokens
with attribute service (#156)
- Reduce sql-api communication timeout, and allow overriding (#167)
[ new sqlapi.timeout directive, defaults to 100 ms ]
- Do not query CDB_TableMetadata for queries affected by no tables (#168)
1.8.2 -- 2014-02-25
-------------------

13
app.js
View File

@@ -8,11 +8,16 @@
*/
if ( process.argv[2] ) ENV = process.argv[2];
else if ( process.env['NODE_ENV'] ) ENV = process.env['NODE_ENV'];
else ENV = 'development';
process.env['NODE_ENV'] = ENV;
// sanity check
var ENV = process.argv[2]
if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
console.error("\nnode app.js [environment]");
console.error("environments: [development, production, staging]\n");
console.error("environments: development, production, staging\n");
process.exit(1);
}
@@ -49,7 +54,9 @@ ws.maxConnections = global.environment.maxConnections || 128;
ws.listen(global.environment.port, global.environment.host);
ws.on('listening', function() {
console.log("Windshaft tileserver started on " + global.environment.host + ':' + global.environment.port);
console.log("Windshaft tileserver started on "
+ global.environment.host + ':' + global.environment.port
+ " (" + ENV + ")");
});
// DEPRECATED, use SIGUSR2

View File

@@ -64,7 +64,8 @@ var config = {
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'dev.'
prefix: 'dev.',
cacheDns: true
// support all allowed node-statsd options
}
,renderer: {
@@ -106,7 +107,10 @@ var config = {
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048
max_get_sql_length: 2048,
// Maximum time to wait for a response,
// in milliseconds. Defaults to 100.
timeout: 100
}
,varnish: {
host: 'localhost',

View File

@@ -59,6 +59,7 @@ var config = {
host: 'localhost',
port: 8125,
prefix: ':host.', // could be hostname, better not containing dots
cacheDns: true
// support all allowed node-statsd options
}
,renderer: {
@@ -100,7 +101,10 @@ var config = {
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048
max_get_sql_length: 2048,
// Maximum time to wait for a response,
// in milliseconds. Defaults to 100.
timeout: 100
}
,varnish: {
host: 'localhost',

View File

@@ -58,7 +58,8 @@ var config = {
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'stage.:host.'
prefix: 'stage.:host.',
cacheDns: true
// support all allowed node-statsd options
}
,renderer: {
@@ -100,7 +101,10 @@ var config = {
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048
max_get_sql_length: 2048,
// Maximum time to wait for a response,
// in milliseconds. Defaults to 100.
timeout: 100
}
,varnish: {
host: 'localhost',

View File

@@ -58,7 +58,8 @@ var config = {
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'test.:host.'
prefix: 'test.:host.',
cacheDns: true
// support all allowed node-statsd options
}
,renderer: {
@@ -102,7 +103,10 @@ var config = {
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048
max_get_sql_length: 2048,
// Maximum time to wait for a response,
// in milliseconds. Defaults to 100.
timeout: 100
}
,varnish: {
host: '',

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

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

15
docs/MultiLayer-API.md Normal file
View File

@@ -0,0 +1,15 @@
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/Vizzuality/Windshaft/wiki/Multilayer-API) in a few ways.
## Last modification timestamps
It encodes a timestamp of 'last modification time' into the map token (token:EPOCH) returned to the client.
It accepts tokens with encoded timestamp from the client considering the token suffix as a cache_buster value.
Clients don't need to be aware of the extension but rather use the API as they would use the base one.
The only difference will be that the _same_ layergroup configuration may result in different tokens if source data was modified between the mapview requests.
Also Windshaft-CartoDB adds a ``last_update`` field with ISO format (2013-11-30T12:23:10).
## Stats tag
Windshaft-CartoDB adds support for a ``stat_tag`` element in the multilayer configuration to help [stats](Redis-stats-format) gathering.

292
docs/Template-maps.md Normal file
View File

@@ -0,0 +1,292 @@
Template maps are layergroup configurations that rather than being
fully defined contain variables that can be set to produce a different
layergroup configurations (instantiation).
Template maps are persistent, can only be created and deleted by the
CartoDB user showing a valid API_KEY.
Instantiating a signed template map would result in a [signed
map](https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps)
instance that would be signed with the same signature as the template.
Deleting a signed template results in deletion of all signatures created
as a result of instantiation.
# Template format
A templated layergroup would allow using placeholders
in the "cartocss" and "sql" elements in the "option"
field of any "layer" of a layergroup configuration
(see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification).
Valid placeholder names start with a letter and can only
contain letters, numbers or underscores. They have to be
written between ``<%= `` and `` %>`` strings in order to be
replaced. Example: ``<%= my_color %>``.
The set of supported placeholders for a template will need to be
explicitly defined specifying type and default value for each.
**placeholder types**
Placeholder type will determine the kind of escaping for the
associated value. Supported types are:
* sql_literal (internal single-quotes will be sql-escaped)
* sql_ident (internal double-quotes will be sql-escaped)
* number (can only contain numerical representation)
* css_color (can only contain color names or hex-values)
* ... (add more as need arises)
Placeholder default value will be used when not provided at
instantiation time and could be used to test validity of the
template by creating a default instance.
Additionally you'll be able to embed an authorization
certificate that would be used to sign any instance of the template.
```js
// template.json
{
version: '0.0.1',
// there can be at most 1 template with the same name for any user
// valid names start with a letter and only contains letter, numbers
// or underscores
name: 'template_name',
// embedded authorization certificate
auth: {
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps
method: 'token', // or "open" (the default if no "method" is given)
valid_tokens: ['auth_token1','auth_token2'] // only (required and non empty) for 'token' method
},
// Variables not listed here are not substituted
// Variable not provided at instantiation time trigger an error
// A default is required for optional variables
// Type specification is used for quoting, to avoid injections
placeholders: {
color: {
type:'css_color',
default:'red'
},
cartodb_id: {
type:'number',
default: 1
}
},
layergroup: {
// see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification
"version": "1.0.1",
"layers": [{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: <%= color %>; }",
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
}
}]
}
}
```
# Creating a templated map
You can create a signed template map with a single call (for simplicity).
You'd use a POST sending JSON data:
```sh
curl -X POST \
-H 'Content-Type: application/json' \
-d @template.json \
'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
```
The response would be like this:
```js
{
"template_id":"@template_name"
}
```
If a template with the same name exists in the user storage,
a 400 response is generated.
Errors are in this form:
```js
{
"error":"Some error string here"
}
```
# Updating an existing template
Update of a template map implies removal all signatures from previous
map instances.
You can update a signed template map with a PUT:
```sh
curl -X PUT \
-H 'Content-Type: application/json' \
-d @template.json \
'https://docs.cartodb.com/tiles/template/:template_name?api_key=APIKEY'
```
A template with the same name will be updated, if any.
The response would be like this:
```js
{
"template_id":"@template_name"
}
```
If a template with the same name does NOT exist,
a 400 HTTP response is generated with an error in this format:
```js
{
"error":"Some error string here"
}
```
# Listing available templates
You can get a list of available templates with a GET to ``/template``.
A valid api_key is required.
```sh
curl -X GET 'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
```
The response would be like this:
```js
{
"template_ids": ["@template_name1","@template_name2"]
}
```
Or, on error:
```js
{
"error":"Some error string here"
}
```
# Getting a specific template
You can get the definition of a template with a
GET to ``/template/:template_name``.
A valid api_key is required.
Example:
```sh
curl -X GET 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
```
The response would be like this:
```js
{
"template": {...} // see template.json above
}
```
Or, on error:
```js
{
"error":"Some error string here"
}
```
# Instantiating a template map
You can instantiate a template map passing all required parameters with
a POST to ``/template/:template_name``.
Valid credentials will be needed, if required by the template.
```js
// params.js
{
color: '#ff0000',
cartodb_id: 3
}
```
```sh
curl -X POST \
-H 'Content-Type: application/json' \
-d @params.js \
'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
```
The response would be like this:
```js
{
"layergroupid":"docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
"last_updated":"2013-11-14T11:20:15.000Z"
}
```
or, on error:
```js
{
"error":"Some error string here"
}
```
You can then use the ``layergroupid`` for fetching tiles and grids as you do
normally ( see https://github.com/CartoDB/Windshaft/wiki/Multilayer-API).
But you'll still have to show the ``auth_token``, if required by the template
(see https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps)
Instances of a signed template map will be signed with the same signature
certificate associated with the template. Such certificate would contain
a reference to the template identifier, so that it can be revoked every
time the template is updated or deleted.
### using JSONP
There is also a special endpoint to be able to instanciate using JSONP (for old browsers)
```
curl 'https://docs.cartodb.com/tiles/template/@template_name/jsonp?auth_token=AUTH_TOKEN&callback=function_name&config=template_params_json'
```
it takes the ``callback`` function (required), ``auth_token`` in case the template needs auth and ``config`` which is the variabñes for the template (in case it has variables). For example config may be created (using javascript)
```
url += "config=" + encodeURIComponent(
JSON.stringify({ color: 'red' });
```
the response it's in this format:
```
jQuery17205720721024554223_1390996319118(
{
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
last_updated: "2014-01-27T17:41:03.021Z"
}
)
```
# Deleting a template map
Deletion of a template map will imply removal all instance signatures
You can delete a templated map with a DELETE to ``/template/:template_name``:
```sh
curl -X DELETE 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
```
On success, a 204 (No Content) response would be issued.
Otherwise a 4xx response with this format:
```js
{
"error":"Some error string here"
}
```

View File

@@ -40,6 +40,12 @@ var CartodbWindshaft = function(serverOptions) {
callback(err, req);
}
// 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)';
serverOptions.signedMaps = new SignedMaps(redisPool);
var templateMapsOpts = {
max_user_templates: global.environment.maxUserTemplates
@@ -58,6 +64,14 @@ var CartodbWindshaft = function(serverOptions) {
}
var ws_sendResponse = ws.sendResponse;
// GET routes for which we don't want to request any caching.
// POST/PUT/DELETE requests are never cached anyway.
var noCacheGETRoutes = [
'/',
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
serverOptions.base_url_mapconfig,
template_baseurl + '/:template_id/jsonp'
];
ws.sendResponse = function(res, args) {
var that = this;
var thatArgs = arguments;
@@ -85,10 +99,20 @@ var CartodbWindshaft = function(serverOptions) {
// unsuccessful responses
return false;
}
if ( _.contains(noCacheGETRoutes, req.route.path) ) {
//console.log("Skipping cache channel in route:\n" + req.route.path);
return false;
}
//console.log("Adding cache channel to route\n" + req.route.path + " not matching any in:\n" + mapCreateRoutes.join("\n"));
serverOptions.addCacheChannel(that, req, this);
},
function sendResponse(err, added) {
if ( err ) console.log(err + err.stack);
ws_sendResponse.apply(that, thatArgs);
return null;
},
function finish(err) {
if ( err ) console.log(err + err.stack);
}
);
};
@@ -97,6 +121,9 @@ var CartodbWindshaft = function(serverOptions) {
* Helper to allow access to the layer to be used in the maps infowindow popup.
*/
ws.get(serverOptions.base_url + '/infowindow', function(req, res){
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.get_infowindow');
}
ws.doCORS(res);
Step(
function(){
@@ -118,6 +145,9 @@ var CartodbWindshaft = function(serverOptions) {
* Helper to allow access to metadata to be used in embedded maps.
*/
ws.get(serverOptions.base_url + '/map_metadata', function(req, res){
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.get_map_metadata');
}
ws.doCORS(res);
Step(
function(){
@@ -139,6 +169,9 @@ var CartodbWindshaft = function(serverOptions) {
* TODO: Move?
*/
ws.del(serverOptions.base_url + '/flush_cache', function(req, res){
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.flush_cache');
}
ws.doCORS(res);
Step(
function flushCache(){
@@ -161,12 +194,6 @@ var CartodbWindshaft = function(serverOptions) {
return serverOptions.userByReq(req);
}
// 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) {
ws.doCORS(res);
@@ -181,7 +208,7 @@ var CartodbWindshaft = function(serverOptions) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated user can create templated maps");
err.http_status = 401;
err.http_status = 403;
throw err;
}
var next = this;
@@ -230,7 +257,7 @@ var CartodbWindshaft = function(serverOptions) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated user can list templated maps");
err.http_status = 401;
err.http_status = 403;
throw err;
}
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
@@ -273,6 +300,9 @@ var CartodbWindshaft = function(serverOptions) {
// Get a specific template
ws.get(template_baseurl + '/:template_id', function(req, res) {
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.get_template');
}
ws.doCORS(res);
var that = this;
var response = {};
@@ -287,7 +317,7 @@ var CartodbWindshaft = function(serverOptions) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated users can get template maps");
err.http_status = 401;
err.http_status = 403;
throw err;
}
tpl_id = req.params.template_id.split('@');
@@ -330,7 +360,10 @@ var CartodbWindshaft = function(serverOptions) {
});
// Delete a specific template
ws.delete(template_baseurl + '/:template_id', function(req, res) {
ws.del(template_baseurl + '/:template_id', function(req, res) {
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.delete_template');
}
ws.doCORS(res);
var that = this;
var response = {};
@@ -345,7 +378,7 @@ var CartodbWindshaft = function(serverOptions) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated users can delete template maps");
err.http_status = 401;
err.http_status = 403;
throw err;
}
tpl_id = req.params.template_id.split('@');
@@ -381,6 +414,9 @@ var CartodbWindshaft = function(serverOptions) {
// Get a list of owned templates
ws.get(template_baseurl, function(req, res) {
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.get_template_list');
}
ws.doCORS(res);
var that = this;
var response = {};
@@ -393,7 +429,7 @@ var CartodbWindshaft = function(serverOptions) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated user can list templated maps");
err.http_status = 401;
err.http_status = 403;
throw err;
}
templateMaps.listTemplates(cdbuser, this);
@@ -454,7 +490,14 @@ var CartodbWindshaft = function(serverOptions) {
// Format of template_id: [<template_owner>]@<template_id>
var tpl_id = req.params.template_id.split('@');
if ( tpl_id.length > 1 ) {
if ( tpl_id[0] ) cdbuser = tpl_id[0];
if ( tpl_id[0] && tpl_id[0] != cdbuser ) {
var err = new Error('Cannot instanciate map of user "'
+ tpl_id[0] + '" on database of user "'
+ cdbuser + '"')
err.http_status = 403;
callback(err);
return;
}
tpl_id = tpl_id[1];
}
var auth_token = req.query.auth_token;
@@ -478,12 +521,12 @@ var CartodbWindshaft = function(serverOptions) {
authorized = signedMaps.authorizedByCert(cert, auth_token);
} catch (err) {
// we catch to add http_status
err.http_status = 401;
err.http_status = 403;
throw err;
}
if ( ! authorized ) {
err = new Error('Unauthorized template instanciation');
err.http_status = 401;
err.http_status = 403;
throw err;
}
/*if ( (! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json') && req.query.callback === undefined) {
@@ -564,9 +607,6 @@ var CartodbWindshaft = function(serverOptions) {
} 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) {

View File

@@ -67,6 +67,10 @@ module.exports = function(){
log_format: global.environment.log_format,
useProfiler: global.environment.useProfiler
};
// Do not send unwatch on release
// See http://github.com/CartoDB/Windshaft-cartodb/issues/161
me.redis.unwatchOnRelease = false;
// Be nice and warn if configured mapnik version
// is != instaled mapnik version
@@ -116,13 +120,15 @@ module.exports = function(){
//
var maxSockets = global.environment.maxConnections || 128;
var maxGetLen = api.max_get_sql_length || 2048;
var maxSQLTime = api.timeout || 100; // 1/10 of a second by default
var reqSpec = {
url:sqlapi,
json:true,
headers:{host: sqlapihostname}
// http://nodejs.org/api/http.html#http_agent_maxsockets
,pool:{maxSockets:maxSockets}
//,timeout:100
// timeout in milliseconds
,timeout:maxSQLTime
}
if ( sql.length > maxGetLen ) {
reqSpec.method = 'POST';
@@ -191,7 +197,7 @@ module.exports = function(){
}
var qtables = rows[0].cdb_querytables;
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
tableNames = tableNames.split(',');
tableNames = tableNames ? tableNames.split(',') : [];
callback(null, tableNames);
});
};
@@ -389,23 +395,34 @@ module.exports = function(){
var key = req.params.map_key || req.params.api_key;
var cacheKey = dbName + ':' + token;
var tabNames;
me.affectedTables(usr, key, sql, function(err, tableNames) {
Step(
function getTables() {
me.affectedTables(usr, key, sql, this);
},
function getLastupdated(err, tableNames) {
if (req.profiler) req.profiler.done('affectedTables');
if ( err ) { done(err); return; }
if ( err ) throw err;
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
// 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');
if ( err ) { done(err); return; }
response.layergroupid = response.layergroupid + ':' + lastUpdated; // use epoch
response.last_updated = new Date(lastUpdated).toISOString(); // TODO: use ISO format
done(null);
});
});
if ( ! tableNames.length ) return 0; // skip for no affected tables
tabNames = tableNames;
me.findLastUpdated(usr, key, tableNames, this);
},
function(err, lastUpdated) {
if ( err ) throw err;
if (req.profiler && tabNames) req.profiler.done('findLastUpdated');
response.layergroupid = response.layergroupid + ':' + lastUpdated; // use epoch
response.last_updated = new Date(lastUpdated).toISOString();
return null;
},
function finish(err) {
done(err);
}
);
};
/* X-Cache-Channel generation } */
@@ -490,6 +507,14 @@ module.exports = function(){
// @param callback function(err)
//
me.setDBConn = function(dbowner, params, callback) {
// Add default database connection parameters
// if none given
_.defaults(params, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
Step(
function getDatabaseHost(){
cartoData.getUserDBHost(dbowner, this);
@@ -619,12 +644,21 @@ module.exports = function(){
}
if ( ! signed_by ) {
// request not authorized by signer,
// continue to check table privacy,
// if table was given
// request not authorized by signer.
// if table was given, continue to check table privacy
if ( req.params.table ) return null;
// otherwise return no authorization
callback(err, null);
// if no signer name was given, let dbparams and
// PostgreSQL do the rest.
//
if ( ! req.params.signer ) {
callback(null, true); // authorized so far
return;
}
// if signer name was given, return no authorization
callback(null, false);
return;
}
@@ -646,7 +680,7 @@ module.exports = function(){
},
function(err, privacy){
if (req.profiler) req.profiler.done('getTablePrivacy');
callback(err, privacy);
callback(err, privacy !== "0");
}
);
};
@@ -694,6 +728,8 @@ module.exports = function(){
_.each(bad_query, function(key){ delete req.query[key]; });
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
var user = me.userByReq(req);
if ( req.params.token ) {
//console.log("Request parameters include token " + req.params.token);
var tksplit = req.params.token.split(':');
@@ -702,7 +738,13 @@ module.exports = function(){
tksplit = req.params.token.split('@');
if ( tksplit.length > 1 ) {
req.params.signer = tksplit.shift();
if ( ! req.params.signer ) req.params.signer = this.userByReq(req);
if ( ! req.params.signer ) req.params.signer = user;
else if ( req.params.signer != user ) {
var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' + user + '"')
err.http_status = 403;
callback(err);
return;
}
if ( tksplit.length > 1 ) {
var template_hash = tksplit.shift(); // unused
}
@@ -721,19 +763,21 @@ module.exports = function(){
if (req.profiler) req.profiler.done('req2params.setup');
var user = me.userByReq(req);
Step(
function getPrivacy(){
me.authorize(req, this);
},
function gatekeep(err, data){
function gatekeep(err, authorized){
if (req.profiler) req.profiler.done('authorize');
if(err) throw err;
if(data === "0") throw new Error("Sorry, you are unauthorized (permission denied)");
return data;
if(!authorized) {
err = new Error("Sorry, you are unauthorized (permission denied)");
err.http_status = 403;
throw err;
}
return null;
},
function getDatabase(err, data){
function getDatabase(err){
if(err) throw err;
that.setDBConn(user, req.params, this);
},

40
npm-shrinkwrap.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "windshaft-cartodb",
"version": "1.8.2",
"version": "1.8.5",
"dependencies": {
"node-varnish": {
"version": "0.2.0",
@@ -10,11 +10,11 @@
"version": "1.3.3"
},
"windshaft": {
"version": "0.19.0",
"from": "http://github.com/CartoDB/Windshaft/tarball/0.19.0-rc1",
"version": "0.19.4",
"from": "http://github.com/CartoDB/Windshaft/tarball/0.19.4",
"dependencies": {
"grainstore": {
"version": "0.18.0",
"version": "0.18.1",
"dependencies": {
"mapnik-reference": {
"version": "5.0.7"
@@ -191,7 +191,7 @@
}
},
"readable-stream": {
"version": "1.0.25-1",
"version": "1.0.26-2",
"dependencies": {
"string_decoder": {
"version": "0.10.25-1"
@@ -220,15 +220,10 @@
}
},
"rc": {
"version": "0.3.3",
"version": "0.3.4",
"dependencies": {
"optimist": {
"version": "0.3.7",
"dependencies": {
"wordwrap": {
"version": "0.0.2"
}
}
"minimist": {
"version": "0.0.8"
},
"deep-extend": {
"version": "0.2.8"
@@ -258,7 +253,7 @@
"version": "0.0.2"
},
"minimist": {
"version": "0.0.7"
"version": "0.0.8"
}
}
}
@@ -308,7 +303,7 @@
}
},
"tilelive-mapnik": {
"version": "0.6.5",
"version": "0.6.7",
"dependencies": {
"eio": {
"version": "0.2.2"
@@ -349,7 +344,7 @@
"version": "0.0.2"
},
"minimist": {
"version": "0.0.7"
"version": "0.0.8"
}
}
}
@@ -392,7 +387,8 @@
"version": "0.3.0"
},
"redis-mpool": {
"version": "0.0.3",
"version": "0.0.4",
"from": "http://github.com/CartoDB/node-redis-mpool/tarball/0.0.4",
"dependencies": {
"generic-pool": {
"version": "2.0.4"
@@ -420,7 +416,7 @@
"version": "0.1.15"
},
"readable-stream": {
"version": "1.0.25-1",
"version": "1.0.26",
"dependencies": {
"string_decoder": {
"version": "0.10.25-1"
@@ -429,15 +425,15 @@
}
}
},
"semver": {
"version": "1.1.4"
},
"redis": {
"version": "0.8.6"
},
"strftime": {
"version": "0.6.2"
},
"semver": {
"version": "1.1.4"
},
"mocha": {
"version": "1.14.0",
"dependencies": {
@@ -482,7 +478,7 @@
}
},
"graceful-fs": {
"version": "2.0.1"
"version": "2.0.2"
},
"inherits": {
"version": "2.0.1"

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "1.8.2",
"version": "1.8.5",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -24,11 +24,11 @@
"dependencies": {
"node-varnish": "http://github.com/Vizzuality/node-varnish/tarball/v0.2.0",
"underscore" : "~1.3.3",
"windshaft" : "http://github.com/CartoDB/Windshaft/tarball/0.19.0",
"windshaft" : "http://github.com/CartoDB/Windshaft/tarball/0.19.4",
"step": "0.0.x",
"request": "2.9.202",
"cartodb-redis": "~0.3.0",
"redis-mpool": "~0.0.2",
"redis-mpool": "http://github.com/CartoDB/node-redis-mpool/tarball/0.0.4",
"mapnik": "~0.7.22",
"lzma": "~1.2.3",
"log4js": "~0.6.10"

View File

@@ -10,7 +10,7 @@ var strftime = require('strftime');
var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js');
var redis_stats_db = 5;
require(__dirname + '/../support/test_helper');
var helper = require(__dirname + '/../support/test_helper');
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
@@ -20,14 +20,6 @@ serverOptions = ServerOptions();
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
// Check that the response headers do not request caching
// Throws on failure
function checkNoCache(res) {
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
assert.ok(!res.headers.hasOwnProperty('cache-control')); // is this correct ?
assert.ok(!res.headers.hasOwnProperty('last-modified')); // is this correct ?
}
suite('multilayer', function() {
var redis_client = redis.createClient(global.environment.redis.port);
@@ -127,6 +119,24 @@ suite('multilayer', function() {
});
});
},
// See https://github.com/CartoDB/Windshaft-cartodb/issues/170
function do_get_tile_nosignature(err)
{
if ( err ) throw err;
var next = this;
assert.response(server, {
url: '/tiles/layergroup/localhost@' + expected_token + ':cb0/0/0/0.png',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}, {}, function(res) {
assert.equal(res.statusCode, 403, res.statusCode + ':' + res.body);
var parsed = JSON.parse(res.body);
var msg = parsed.error; // TODO: should it be "errors" ?
assert.ok(msg.match(/permission denied/i), msg);
next(err);
});
},
function do_get_grid_layer0(err)
{
if ( err ) throw err;
@@ -182,6 +192,59 @@ suite('multilayer', function() {
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
// NOTE: another test like this is in templates.js
test("get creation requests no cache", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator from test_table limit 2',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.0.1'
} }
]
};
var expected_token;
Step(
function do_create_get()
{
var next = this;
assert.response(server, {
url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify(layergroup)),
method: 'GET',
headers: {host: 'localhost'}
}, {}, function(res, err) { next(err, res); });
},
function do_check_create(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
expected_token = parsedBody.layergroupid.split(':')[0];
helper.checkNoCache(res);
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));
else done(null);
});
});
}
);
});
test("layergroup can hold substitution tokens", function(done) {
@@ -493,11 +556,11 @@ suite('multilayer', function() {
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 400, res.body);
assert.equal(res.statusCode, 404, res.statusCode + ": " + res.body);
var parsed = JSON.parse(res.body);
var msg = parsed.errors[0];
assert.ok(msg.match(/bogus.*exist/), msg);
checkNoCache(res);
helper.checkNoCache(res);
done();
});
});
@@ -611,7 +674,7 @@ suite('multilayer', function() {
headers: {host: 'localhost' },
encoding: 'binary'
}, {}, function(res) {
assert.equal(res.statusCode, 401);
assert.equal(res.statusCode, 403);
var re = RegExp('permission denied');
assert.ok(res.body.match(re), 'No "permission denied" error: ' + res.body);
next(err);
@@ -627,7 +690,7 @@ suite('multilayer', function() {
headers: {host: 'localhost' },
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 401);
assert.equal(res.statusCode, 403);
var re = RegExp('permission denied');
assert.ok(res.body.match(re), 'No "permission denied" error: ' + res.body);
next(err);
@@ -643,7 +706,7 @@ suite('multilayer', function() {
headers: {host: 'localhost' },
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 401);
assert.equal(res.statusCode, 403);
var re = RegExp('permission denied');
assert.ok(res.body.match(re), 'No "permission denied" error: ' + res.body);
next(err);
@@ -1128,6 +1191,47 @@ suite('multilayer', function() {
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/167
test("lack of response from sql-api will result in a timeout", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: "select *, 'SQLAPINOANSWER' from test_table",
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.1.0'
} }
]
};
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.ok(msg, /could not fetch source tables/, msg);
return null;
},
function finish(err) {
done(err);
}
);
});
suiteTeardown(function(done) {

View File

@@ -134,8 +134,7 @@ suite('server', function() {
method: 'GET'
},{
}, function(res) {
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 400, res.body);
assert.equal(res.statusCode, 403, res.statusCode + ':' + res.body);
assert.deepEqual(JSON.parse(res.body),
{error: 'Sorry, you are unauthorized (permission denied)'});
assert.ok(!res.headers.hasOwnProperty('cache-control'));
@@ -152,7 +151,7 @@ suite('server', function() {
method: 'GET'
},{
}, function(res) {
// FIXME: should be 401 Unauthorized
// FIXME: should be 403 Forbidden or 404 User Not Found
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
assert.deepEqual(JSON.parse(res.body),
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
@@ -320,7 +319,7 @@ suite('server', function() {
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
data: querystring.stringify({style: 'Map { background-color:#aaa; }'})
},{}, function(res) {
// FIXME: should be 401 Unauthorized
// FIXME: should be 403 Forbidden
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
assert.ok(res.body.indexOf('map state cannot be changed by unauthenticated request') != -1, res.body);
@@ -418,8 +417,8 @@ suite('server', function() {
method: 'DELETE',
headers: {host: 'localhost'},
},{}, function(res) {
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 500, res.body);
// FIXME: should be 403 Forbidden
assert.equal(res.statusCode, 400, res.body);
assert.ok(res.body.indexOf('map state cannot be changed by unauthenticated request') != -1, res.body);
// check that the style wasn't really deleted !
assert.response(server, {
@@ -532,7 +531,7 @@ suite('server', function() {
url: '/tiles/test_table_private_1/infowindow',
method: 'GET'
},{}, function(res) {
// FIXME: should be 401 Unauthorized
// FIXME: should be 403 Forbidden
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
done();
});
@@ -547,7 +546,7 @@ suite('server', function() {
method: 'GET'
},{
}, function(res) {
// FIXME: should be 401 Unauthorized
// FIXME: should be 403 Forbidden
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
assert.deepEqual(JSON.parse(res.body),
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
@@ -615,8 +614,8 @@ suite('server', function() {
url: '/tiles/test_table_private_1/6/31/24.grid.json',
method: 'GET'
},{}, function(res) {
// 401 Unauthorized
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
// 403 Forbidden
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
done();
});
});
@@ -630,7 +629,7 @@ suite('server', function() {
method: 'GET'
},{
}, function(res) {
// FIXME: should be 401 Unauthorized
// FIXME: should be 403 Forbidden
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
assert.deepEqual(JSON.parse(res.body),
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
@@ -764,8 +763,8 @@ suite('server', function() {
method: 'GET'
},{
}, function(res) {
// 401 Unauthorized
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
// 403 Forbidden
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
done();
});
});
@@ -781,7 +780,7 @@ suite('server', function() {
method: 'GET'
},{
}, function(res) {
// FIXME: should be 401 Unauthorized
// FIXME: should be 403 Forbidden
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
assert.deepEqual(JSON.parse(res.body),
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
@@ -805,8 +804,8 @@ suite('server', function() {
method: 'GET'
},{
}, function(res) {
// 401 Unauthorized
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
// 403 Forbidden
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
// Failed in 1.6.0 of https://github.com/CartoDB/Windshaft-cartodb/issues/107
assert.ok(!res.headers.hasOwnProperty('cache-control'),
"Unexpected Cache-Control: " + res.headers['cache-control']);
@@ -1279,7 +1278,7 @@ suite('server', function() {
url: '/tiles/test_table_private_1/map_metadata',
method: 'GET'
},{}, function(res) {
// FIXME: should be 401 instead
// FIXME: should be 403 instead
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
assert.ok(!res.headers.hasOwnProperty('cache-control'));
done();

View File

@@ -1,5 +1,4 @@
var assert = require('../support/assert');
var tests = module.exports = {};
var _ = require('underscore');
var redis = require('redis');
var querystring = require('querystring');
@@ -10,7 +9,13 @@ var strftime = require('strftime');
var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js');
var redis_stats_db = 5;
require(__dirname + '/../support/test_helper');
// Pollute the PG environment to make sure
// configuration settings are always enforced
// See https://github.com/CartoDB/Windshaft-cartodb/issues/174
process.env['PGPORT'] = '666';
process.env['PGHOST'] = 'fake';
var helper = require(__dirname + '/../support/test_helper');
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
@@ -69,7 +74,7 @@ suite('template_api', function() {
function postTemplate(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 401);
assert.equal(res.statusCode, 403);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('error'), res.body);
err = parsed.error;
@@ -364,7 +369,7 @@ suite('template_api', function() {
function litsTemplates(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('error'),
'Missing error from response: ' + res.body);
@@ -577,7 +582,7 @@ suite('template_api', function() {
function getTemplate(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 401, res.statusCode + ": " + res.body);
assert.equal(res.statusCode, 403, res.statusCode + ": " + res.body);
var parsedBody = JSON.parse(res.body);
assert.ok(parsedBody.hasOwnProperty('error'), res.body);
assert.ok(parsedBody.error.match(/only.*authenticated.*user/i),
@@ -686,7 +691,7 @@ suite('template_api', function() {
function deleteTemplate(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 401, res.statusCode + ": " + res.body);
assert.equal(res.statusCode, 403, res.statusCode + ": " + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('error'),
"Missing 'error' from response body: " + res.body);
@@ -811,10 +816,10 @@ suite('template_api', function() {
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateAuth(err, res)
{
// See https://github.com/CartoDB/Windshaft-cartodb/issues/173
function instanciateForeignDB(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
assert.equal(res.statusCode, 403,
'Unexpected success instanciating template with no auth: '
+ res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
@@ -822,6 +827,25 @@ suite('template_api', function() {
"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: 'foreign', '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, 403, 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(/cannot instanciate/i),
'Unexpected error for forbidden instance : ' + parsed.error);
var post_request = {
url: '/tiles/template/' + tpl_id + '?auth_token=valid2',
method: 'POST',
@@ -858,14 +882,14 @@ suite('template_api', function() {
},
function fetchTileAuth(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
assert.equal(res.statusCode, 403,
'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);
+ '(expected /permission denied/): ' + parsed.error);
var get_request = {
url: '/tiles/layergroup/' + layergroupid + '/0/0/0.png?auth_token=valid1',
method: 'GET',
@@ -884,6 +908,33 @@ suite('template_api', function() {
assert.equal(res.headers['content-type'], "image/png");
return null;
},
// See https://github.com/CartoDB/Windshaft-cartodb/issues/172
function fetchTileForeignSignature(err, res) {
if ( err ) throw err;
var foreignsigned = layergroupid.replace(/[^@]*@/, 'foreign@');
var get_request = {
url: '/tiles/layergroup/' + foreignsigned + '/0/0/0.png?auth_token=valid1',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}
var next = this;
assert.response(server, get_request, {},
function(res) { next(null, res); });
},
function checkForeignSignerError(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 403,
'Unexpected error for authorized instance: '
+ 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(/cannot use/i),
'Unexpected error for unauthorized instance '
+ '(expected /cannot use/): ' + parsed.error);
return null;
},
function deleteTemplate(err)
{
if ( err ) throw err;
@@ -912,7 +963,7 @@ suite('template_api', function() {
},
function checkTileDeleted(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
assert.equal(res.statusCode, 403,
'Unexpected statusCode fetch tile after signature revokal: '
+ res.statusCode + ':' + res.body);
var parsed = JSON.parse(res.body);
@@ -1008,7 +1059,7 @@ suite('template_api', function() {
function instanciateAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 401,
assert.equal(res.statusCode, 403,
'Unexpected success instanciating template with no auth: '
+ res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
@@ -1052,7 +1103,7 @@ suite('template_api', function() {
},
function fetchTileAuth(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
assert.equal(res.statusCode, 403,
'Fetching tile with no auth: ' + res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('error'),
@@ -1106,7 +1157,7 @@ suite('template_api', function() {
},
function checkTileDeleted(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
assert.equal(res.statusCode, 403,
'Unexpected statusCode fetch tile after signature revokal: '
+ res.statusCode + ':' + res.body);
var parsed = JSON.parse(res.body);
@@ -1204,7 +1255,7 @@ suite('template_api', function() {
function instanciateAuth(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 401,
assert.equal(res.statusCode, 403,
'Unexpected success instanciating template with no auth: '
+ res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
@@ -1248,7 +1299,7 @@ suite('template_api', function() {
},
function fetchAttributeAuth(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
assert.equal(res.statusCode, 403,
'Fetching tile with no auth: ' + res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('error'),
@@ -1302,7 +1353,7 @@ suite('template_api', function() {
},
function checkTileDeleted(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 401,
assert.equal(res.statusCode, 403,
'Unexpected statusCode fetch tile after signature revokal: '
+ res.statusCode + ':' + res.body);
var parsed = JSON.parse(res.body);
@@ -1393,6 +1444,7 @@ suite('template_api', function() {
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(template_params)
}
helper.checkNoCache(res);
var next = this;
assert.response(server, post_request, {},
function(res) { next(null, res); });
@@ -1464,13 +1516,16 @@ suite('template_api', function() {
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateAuth(err, res)
function checkInstanciation(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Unexpected success instanciating template with no auth: '
+ res.statusCode + ': ' + res.body);
done();
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
helper.checkNoCache(res);
return null;
},
function finish(err) {
done(err);
}
);
});
@@ -1535,12 +1590,12 @@ suite('template_api', function() {
assert.response(server, post_request, {},
function(res) { next(null, res); });
},
function instanciateAuth(err, res)
function checkInstanciation(err, res)
{
if ( err ) throw err;
assert.equal(res.statusCode, 200,
'Unexpected success instanciating template with no auth: '
+ res.statusCode + ': ' + res.body);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
helper.checkNoCache(res);
return null;
},
function finish(err) {

View File

@@ -42,6 +42,9 @@ o.prototype.handleQuery = function(query, res) {
if ( query.q.match('SQLAPIERROR') ) {
res.statusCode = 400;
res.write(JSON.stringify({'error':'Some error occurred'}));
} else if ( query.q.match('SQLAPINOANSWER') ) {
console.log("SQLAPIEmulator will never respond, on request");
return;
} else if ( query.q.match('EPOCH.* as max') ) {
// This is the structure of the known query sent by tiler
var row = {

View File

@@ -6,12 +6,14 @@
*/
var _ = require('underscore');
var assert = require('assert');
var LZMA = require('lzma/lzma_worker.js').LZMA;
// set environment specific variables
global.settings = require(__dirname + '/../../config/settings');
global.environment = require(__dirname + '/../../config/environments/test');
_.extend(global.settings, global.environment);
process.env.NODE_ENV = 'test';
// Utility function to compress & encode LZMA
@@ -28,7 +30,17 @@ function lzma_compress_to_base64(payload, mode, callback) {
);
}
module.exports = {
lzma_compress_to_base64: lzma_compress_to_base64
// Check that the response headers do not request caching
// Throws on failure
function checkNoCache(res) {
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
assert.ok(!res.headers.hasOwnProperty('cache-control')); // is this correct ?
assert.ok(!res.headers.hasOwnProperty('last-modified')); // is this correct ?
}
module.exports = {
lzma_compress_to_base64: lzma_compress_to_base64,
checkNoCache: checkNoCache
}

View File

@@ -0,0 +1,10 @@
{"version":"1.0.1",
"layers":[{
"type":"torque",
"options":{
"sql":"select 1 as id, ST_SetSRID(ST_MakePoint(0,0),3857) as the_geom_webmercator",
"cartocss":"Map{ -torque-time-attribute:'id'; -torque-aggregation-function:'count(id)'; -torque-frame-count:2; -torque-resolution:2}",
"cartocss_version": "2.1.1"
}
}]
}