diff --git a/src/serve_data.js b/src/serve_data.js index 4acda69..d533c5b 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -180,7 +180,7 @@ module.exports = function(options, repo, params, id, styles) { info.tiles = utils.getTileUrls(req, info.tiles, 'data/' + id, info.format, { 'pbf': options.pbfAlias - }); + }, options); return res.send(info); }); diff --git a/src/serve_rendered.js b/src/serve_rendered.js index aca8295..fa511e3 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -747,7 +747,7 @@ module.exports = function(options, repo, params, id, dataResolver) { app.get('/' + id + '.json', function(req, res, next) { var info = clone(tileJSON); info.tiles = utils.getTileUrls(req, info.tiles, - 'styles/' + id, info.format); + 'styles/' + id, info.format, null, options); return res.send(info); }); diff --git a/src/serve_style.js b/src/serve_style.js index cd9dfe9..81cff2d 100644 --- a/src/serve_style.js +++ b/src/serve_style.js @@ -1,25 +1,27 @@ 'use strict'; var path = require('path'), - fs = require('fs'); + fs = require('fs'), + nodeUrl = require('url'), + querystring = require('querystring'); var clone = require('clone'), - express = require('express'); + express = require('express'); -module.exports = function(options, repo, params, id, reportTiles, reportFont) { +module.exports = function (options, repo, params, id, reportTiles, reportFont) { var app = express().disable('x-powered-by'); var styleFile = path.resolve(options.paths.styles, params.style); var styleJSON = clone(require(styleFile)); - Object.keys(styleJSON.sources).forEach(function(name) { + Object.keys(styleJSON.sources).forEach(function (name) { var source = styleJSON.sources[name]; var url = source.url; if (url && url.lastIndexOf('mbtiles:', 0) === 0) { var mbtilesFile = url.substring('mbtiles://'.length); var fromData = mbtilesFile[0] == '{' && - mbtilesFile[mbtilesFile.length - 1] == '}'; + mbtilesFile[mbtilesFile.length - 1] == '}'; if (fromData) { mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2); @@ -33,7 +35,7 @@ module.exports = function(options, repo, params, id, reportTiles, reportFont) { } }); - styleJSON.layers.forEach(function(obj) { + styleJSON.layers.forEach(function (obj) { if (obj['type'] == 'symbol') { var fonts = (obj['layout'] || {})['text-font']; if (fonts && fonts.length) { @@ -45,51 +47,94 @@ module.exports = function(options, repo, params, id, reportTiles, reportFont) { } }); + var normalizeSpritePath = function (intermediatePath) { + return path.join(options.paths.sprites, + intermediatePath + .replace('{style}', path.basename(styleFile, '.json')) + .replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleFile))) + ); + } + var spritePath; var httpTester = /^(http(s)?:)?\/\//; + + // if the current style JSON has a sprite path referencing some local sprites, + // replace placeholders, use that as our sprite path for this style, and + // then update the sprite path in the styleJSON to reference the sprite url. if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) { - spritePath = path.join(options.paths.sprites, - styleJSON.sprite - .replace('{style}', path.basename(styleFile, '.json')) - .replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleFile))) - ); + spritePath = normalizeSpritePath(styleJSON.sprite); styleJSON.sprite = 'local://styles/' + id + '/sprite'; + // if there are still sprites for this style, serve them according to the config setting + } else if (params.serveSprites && typeof params.serveSprites === 'object') { + spritePath = normalizeSpritePath(params.serveSprites.file); } + if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) { styleJSON.glyphs = 'local://fonts/{fontstack}/{range}.pbf'; } repo[id] = styleJSON; - app.get('/' + id + '/style.json', function(req, res, next) { - var fixUrl = function(url, opt_nokey, opt_nostyle) { - if (!url || (typeof url !== 'string') || url.indexOf('local://') !== 0) { + var isWhitelistedUrl = function (url) { + if (!options.auth || !Array.isArray(options.auth.keyDomains) || options.auth.keyDomains.length === 0) { + return false; + } + + for (var i = 0; i < options.auth.keyDomains.length; i++) { + var keyDomain = options.auth.keyDomains[i]; + if (!keyDomain || (typeof keyDomain !== 'string') || keyDomain.length === 0) { + continue; + } + + if (url.includes(keyDomain)) { + return true; + } + } + + return false; + } + + app.get('/' + id + '/style.json', function (req, res, next) { + var fixUrl = function (url, opt_nokey, opt_nostyle) { + if (!url || (typeof url !== 'string') || (url.indexOf('local://') !== 0 && !isWhitelistedUrl(url))) { return url; } - var queryParams = []; + + var queryParams = {}; if (!opt_nostyle && global.addStyleParam) { - queryParams.push('style=' + id); + queryParams.style = id; } - if (!opt_nokey && req.query.key) { - queryParams.unshift('key=' + req.query.key); + if (!opt_nokey && req.query[options.auth.keyName]) { + queryParams[options.auth.keyName] = req.query[options.auth.keyName]; } - var query = ''; - if (queryParams.length) { - query = '?' + queryParams.join('&'); - } - return url.replace( + + if (url.indexOf('local://') === 0) { + var query = querystring.stringify(queryParams); + if (query.length) { + query = '?' + query; + } + + return url.replace( 'local://', req.protocol + '://' + req.headers.host + '/') + query; + } else { // whitelisted url. might have existing parameters + var parsedUrl = nodeUrl.parse(url); + var parsedQS = querystring.parse(parsedUrl.query); + var newParams = Object.assign(parsedQS, queryParams); + parsedUrl.search = querystring.stringify(parsedQS); + return querystring.unescape(nodeUrl.format(parsedUrl)); + } }; var styleJSON_ = clone(styleJSON); - Object.keys(styleJSON_.sources).forEach(function(name) { + Object.keys(styleJSON_.sources).forEach(function (name) { var source = styleJSON_.sources[name]; source.url = fixUrl(source.url); }); // mapbox-gl-js viewer cannot handle sprite urls with query if (styleJSON_.sprite) { - styleJSON_.sprite = fixUrl(styleJSON_.sprite, true, true); + var forceKeyParameter = options.auth.forceSpriteKey === true; + styleJSON_.sprite = fixUrl(styleJSON_.sprite, !forceKeyParameter, true); } if (styleJSON_.glyphs) { styleJSON_.glyphs = fixUrl(styleJSON_.glyphs, false, true); @@ -98,24 +143,24 @@ module.exports = function(options, repo, params, id, reportTiles, reportFont) { }); app.get('/' + id + '/sprite:scale(@[23]x)?\.:format([\\w]+)', - function(req, res, next) { - if (!spritePath) { - return res.status(404).send('File not found'); - } - var scale = req.params.scale, - format = req.params.format; - var filename = spritePath + (scale || '') + '.' + format; - return fs.readFile(filename, function(err, data) { - if (err) { - console.log('Sprite load error:', filename); + function (req, res, next) { + if (!spritePath) { return res.status(404).send('File not found'); - } else { - if (format == 'json') res.header('Content-type', 'application/json'); - if (format == 'png') res.header('Content-type', 'image/png'); - return res.send(data); } + var scale = req.params.scale, + format = req.params.format; + var filename = spritePath + (scale || '') + '.' + format; + return fs.readFile(filename, function (err, data) { + if (err) { + console.log('Sprite load error:', filename); + return res.status(404).send('File not found'); + } else { + if (format == 'json') res.header('Content-type', 'application/json'); + if (format == 'png') res.header('Content-type', 'image/png'); + return res.send(data); + } + }); }); - }); return Promise.resolve(app); }; diff --git a/src/server.js b/src/server.js index f889999..dd5babf 100644 --- a/src/server.js +++ b/src/server.js @@ -66,6 +66,12 @@ function start(opts) { } var options = config.options || {}; + + options.auth = options.auth || {}; + options.auth.keyName = options.auth.keyName || 'key'; + options.auth.keyDomains = options.auth.keyDomains || []; + options.auth.forceSpriteKey = options.auth.forceSpriteKey || false; + var paths = options.paths || {}; options.paths = paths; paths.root = path.resolve( @@ -212,7 +218,7 @@ function start(opts) { } info.tiles = utils.getTileUrls(req, info.tiles, path, info.format, { 'pbf': options.pbfAlias - }); + }, options); arr.push(info); }); return arr; @@ -299,7 +305,7 @@ function start(opts) { var tiles = utils.getTileUrls( req, style.serving_rendered.tiles, - 'styles/' + id, style.serving_rendered.format); + 'styles/' + id, style.serving_rendered.format, null, options); style.xyz_link = tiles[0]; } }); @@ -327,9 +333,9 @@ function start(opts) { '/data/' + id + '.json' + query) + '/wmts'; var tiles = utils.getTileUrls( - req, data_.tiles, 'data/' + id, data_.format, { - 'pbf': options.pbfAlias - }); + req, data_.tiles, 'data/' + id, data_.format, { + 'pbf': options.pbfAlias + }, options); data_.xyz_link = tiles[0]; } if (data_.filesize) { diff --git a/src/utils.js b/src/utils.js index 6335a40..f8a8d96 100644 --- a/src/utils.js +++ b/src/utils.js @@ -6,7 +6,7 @@ var path = require('path'), var clone = require('clone'), glyphCompose = require('glyph-pbf-composite'); -module.exports.getTileUrls = function(req, domains, path, format, aliases) { +module.exports.getTileUrls = function(req, domains, path, format, aliases, options) { if (domains) { if (domains.constructor === String && domains.length > 0) { @@ -36,8 +36,8 @@ module.exports.getTileUrls = function(req, domains, path, format, aliases) { var key = req.query.key; var queryParams = []; - if (req.query.key) { - queryParams.push('key=' + req.query.key); + if (req.query[options.auth.keyName]) { + queryParams.push(options.auth.keyName + '=' + req.query[options.auth.keyName]); } if (req.query.style) { queryParams.push('style=' + req.query.style);