diff --git a/lib/cartodb/controllers/map.js b/lib/cartodb/controllers/map.js index 9aa3b34f..417cf266 100644 --- a/lib/cartodb/controllers/map.js +++ b/lib/cartodb/controllers/map.js @@ -1,11 +1,11 @@ var _ = require('underscore'); -var dot = require('dot'); -dot.templateSettings.strip = false; var assert = require('assert'); var step = require('step'); var windshaft = require('windshaft'); var QueryTables = require('cartodb-query-tables'); +var ResourceLocator = require('../models/resource-locator'); + var util = require('util'); var BaseController = require('./base'); @@ -46,20 +46,7 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata this.layergroupAffectedTables = layergroupAffectedTables; this.mapConfigAdapter = mapConfigAdapter; - - this.resourcesUrlTemplates = null; - if (global.environment.resources_url_templates) { - var templates = global.environment.resources_url_templates; - - if (templates.http) { - this.resourcesUrlTemplates = this.resourcesUrlTemplates || {}; - this.resourcesUrlTemplates.http = dot.template(templates.http + '/{{=it.resource}}'); - } - if (templates.https) { - this.resourcesUrlTemplates = this.resourcesUrlTemplates || {}; - this.resourcesUrlTemplates.https = dot.template(templates.https + '/{{=it.resource}}'); - } - } + this.resourceLocator = new ResourceLocator(global.environment); } util.inherits(MapController, BaseController); @@ -399,7 +386,7 @@ MapController.prototype.addAnalysesMetadata = function(username, layergroup, ana var nodeResource = layergroup.layergroupid + '/analysis/node/' + node.id(); var nodeRepr = { status: node.getStatus(), - url: this.getUrls(username, nodeResource) + url: this.resourceLocator.getUrls(username, nodeResource) }; if (includeQuery) { nodeRepr.query = node.getQuery(); @@ -429,7 +416,7 @@ MapController.prototype.addDataviewsUrls = function(username, layergroup, mapCon Object.keys(dataviews).forEach(function(dataviewName) { var resource = layergroup.layergroupid + '/dataview/' + dataviewName; layergroup.metadata.dataviews[dataviewName] = { - url: this.getUrls(username, resource) + url: this.resourceLocator.getUrls(username, resource) }; }.bind(this)); }; @@ -444,7 +431,7 @@ MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName; layer.widgets[widgetName] = { type: mapConfigLayer.options.widgets[widgetName].type, - url: this.getUrls(username, resource) + url: this.resourceLocator.getUrls(username, resource) }; }.bind(this)); } @@ -452,46 +439,3 @@ MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig }.bind(this)); } }; - -MapController.prototype.getUrls = function(username, resource) { - if (this.resourcesUrlTemplates) { - return this.getUrlsFromTemplate(username, resource); - } - var cdnUrl = global.environment.serverMetadata && global.environment.serverMetadata.cdn_url; - if (cdnUrl) { - return { - http: 'http://' + cdnUrl.http + '/' + username + '/api/v1/map/' + resource, - https: 'https://' + cdnUrl.https + '/' + username + '/api/v1/map/' + resource - }; - } else { - var port = global.environment.port; - return { - http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource - }; - } -}; - -MapController.prototype.getUrlsFromTemplate = function(username, resource) { - var urls = {}; - var cdnUrl = global.environment.serverMetadata && global.environment.serverMetadata.cdn_url || {}; - - if (this.resourcesUrlTemplates.http) { - urls.http = this.resourcesUrlTemplates.http({ - cdn_url: cdnUrl.http, - user: username, - port: global.environment.port, - resource: resource - }); - } - - if (this.resourcesUrlTemplates.https) { - urls.https = this.resourcesUrlTemplates.https({ - cdn_url: cdnUrl.https, - user: username, - port: global.environment.port, - resource: resource - }); - } - - return urls; -}; diff --git a/lib/cartodb/models/resource-locator.js b/lib/cartodb/models/resource-locator.js new file mode 100644 index 00000000..190cbe85 --- /dev/null +++ b/lib/cartodb/models/resource-locator.js @@ -0,0 +1,99 @@ +var crypto = require('crypto'); + +var dot = require('dot'); +dot.templateSettings.strip = false; + +function ResourceLocator(environment) { + this.environment = environment; + + this.resourcesUrlTemplates = null; + if (this.environment.resources_url_templates) { + var templates = environment.resources_url_templates; + + if (templates.http) { + this.resourcesUrlTemplates = this.resourcesUrlTemplates || {}; + this.resourcesUrlTemplates.http = dot.template(templates.http + '/{{=it.resource}}'); + } + if (templates.https) { + this.resourcesUrlTemplates = this.resourcesUrlTemplates || {}; + this.resourcesUrlTemplates.https = dot.template(templates.https + '/{{=it.resource}}'); + } + } +} + +module.exports = ResourceLocator; + +ResourceLocator.prototype.getUrls = function(username, resource) { + if (this.resourcesUrlTemplates) { + return this.getUrlsFromTemplate(username, resource); + } + var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource); + if (cdnDomain) { + return { + http: 'http://' + cdnDomain.http + '/' + username + '/api/v1/map/' + resource, + https: 'https://' + cdnDomain.https + '/' + username + '/api/v1/map/' + resource + }; + } else { + var port = this.environment.port; + return { + http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource + }; + } +}; + + +ResourceLocator.prototype.getUrlsFromTemplate = function(username, resource) { + var urls = {}; + var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource) || {}; + + if (this.resourcesUrlTemplates.http) { + urls.http = this.resourcesUrlTemplates.http({ + cdn_url: cdnDomain.http, + user: username, + port: this.environment.port, + resource: resource + }); + } + + if (this.resourcesUrlTemplates.https) { + urls.https = this.resourcesUrlTemplates.https({ + cdn_url: cdnDomain.https, + user: username, + port: this.environment.port, + resource: resource + }); + } + + return urls; +}; + +function getCdnDomain(serverMetadata, resource) { + if (serverMetadata && serverMetadata.cdn_url) { + var cdnUrl = serverMetadata.cdn_url; + var http = cdnUrl.http; + var https = cdnUrl.https; + if (cdnUrl.templates) { + var templates = cdnUrl.templates; + var httpUrlTemplate = templates.http.url; + var httpsUrlTemplate = templates.https.url; + http = httpUrlTemplate + .replace(/^(http[s]*:\/\/)/, '') + .replace('{s}', subdomain(templates.http.subdomains, resource)); + https = httpsUrlTemplate + .replace(/^(http[s]*:\/\/)/, '') + .replace('{s}', subdomain(templates.https.subdomains, resource)); + } + return { + http: http, + https: https, + }; + } + return null; +} + +function subdomain(subdomains, resource) { + var resourceHash = crypto.createHash('md5').update(resource, 'binary').digest('hex'); + var index = parseInt(resourceHash, 16) % subdomains.length; + return subdomains[index]; +} +module.exports.subdomain = subdomain; \ No newline at end of file diff --git a/test/unit/cartodb/model/resource-locator.test.js b/test/unit/cartodb/model/resource-locator.test.js new file mode 100644 index 00000000..2f4c5b88 --- /dev/null +++ b/test/unit/cartodb/model/resource-locator.test.js @@ -0,0 +1,132 @@ +require('../../../support/test_helper'); + +var assert = require('../../../support/assert'); +var ResourceLocator = require('../../../../lib/cartodb/models/resource-locator'); + +describe('ResourceLocator.getUrls', function() { + var USERNAME = 'username'; + var RESOURCE = 'wadus'; + var HTTP_SUBDOMAINS = ['1', '2', '3', '4']; + var HTTPS_SUBDOMAINS = ['a', 'b', 'c', 'd']; + + it('should return default urls when no serverMetadata is in environment', function() { + var resourceLocator = new ResourceLocator({}); + var urls = resourceLocator.getUrls(USERNAME, RESOURCE); + assert.ok(urls); + }); + + var BASIC_ENVIRONMENT = { + serverMetadata: { + cdn_url: { + http: 'cdn.carto.com', + https: 'cdn.ssl.carto.com' + } + } + }; + it('should return default urls when basic http and https domains are provided', function() { + var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT); + var urls = resourceLocator.getUrls(USERNAME, RESOURCE); + assert.ok(urls); + + assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')); + assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')); + }); + + var RESOURCE_TEMPLATES_ENVIRONMENT = { + serverMetadata: { + cdn_url: { + http: 'cdn.carto.com', + https: 'cdn.ssl.carto.com' + } + }, + resources_url_templates: { + http: 'http://{{=it.user}}.localhost.lan/api/v1/map', + https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map' + } + }; + it('resources_url_templates should take precedence over http and https domains', function() { + var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT); + var urls = resourceLocator.getUrls(USERNAME, RESOURCE); + assert.ok(urls); + + assert.equal(urls.http, ['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/')); + assert.equal(urls.https, ['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/')); + }); + + var CDN_TEMPLATES_ENVIRONMENT = { + serverMetadata: { + cdn_url: { + http: 'cdn.carto.com', + https: 'cdn.ssl.carto.com', + templates: { + http: { + url: "http://{s}.cdn.carto.com", + subdomains: HTTP_SUBDOMAINS + }, + https: { + url: "https://cdn_{s}.ssl.cdn.carto.com", + subdomains: HTTPS_SUBDOMAINS + } + } + } + } + }; + it('cdn_url templates should take precedence over http and https domains', function() { + var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT); + var urls = resourceLocator.getUrls(USERNAME, RESOURCE); + assert.ok(urls); + + var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE); + var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE); + + assert.equal( + urls.http, + ['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/') + ); + assert.equal( + urls.https, + ['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/') + ); + }); + + var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = { + serverMetadata: { + cdn_url: { + http: 'cdn.carto.com', + https: 'cdn.ssl.carto.com', + templates: { + http: { + url: "http://{s}.cdn.carto.com", + subdomains: HTTP_SUBDOMAINS + }, + https: { + url: "https://cdn_{s}.ssl.cdn.carto.com", + subdomains: HTTPS_SUBDOMAINS + } + } + } + }, + resources_url_templates: { + http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map', + https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map' + } + }; + it('should mix cdn_url templates and resources_url_templates', function() { + var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT); + var urls = resourceLocator.getUrls(USERNAME, RESOURCE); + assert.ok(urls); + + var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE); + var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE); + + assert.equal( + urls.http, + ['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/') + ); + assert.equal( + urls.https, + ['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/') + ); + }); + +});