diff --git a/src/serve_data.js b/src/serve_data.js index a4379ff..685f18d 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -46,7 +46,9 @@ export const serve_data = { */ app.get('/:id/:z/:x/:y.:format', async (req, res) => { if (verbose) { - console.log(req.params); + console.log( + `Handling tile request for: /data/${req.params.id}/${req.params.z}/${req.params.x}/${req.params.y}.${req.params.format}`, + ); } const item = repo[req.params.id]; if (!item) { @@ -156,6 +158,11 @@ export const serve_data = { */ app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => { try { + if (verbose) { + console.log( + `Handling elevation request for: /data/${req.params.id}/elevation/${req.params.z}/${req.params.x}/${req.params.y}`, + ); + } const item = repo?.[req.params.id]; if (!item) return res.sendStatus(404); if (!item.source) return res.status(404).send('Missing source'); @@ -292,13 +299,18 @@ export const serve_data = { }); /** - * Handles requests for metadata for the tiles. + * Handles requests for tilejson for the data tiles. * @param {object} req - Express request object. * @param {object} res - Express response object. * @param {string} req.params.id - ID of the data source. * @returns {Promise} */ app.get('/:id.json', (req, res) => { + if (verbose) { + console.log( + `Handling tilejson request for: /data/${req.params.id}.json`, + ); + } const item = repo[req.params.id]; if (!item) { return res.sendStatus(404); diff --git a/src/serve_font.js b/src/serve_font.js index 0319e90..020d6d5 100644 --- a/src/serve_font.js +++ b/src/serve_font.js @@ -23,7 +23,6 @@ export async function serve_font(options, allowedFonts, programOpts) { /** * Handles requests for a font file. - * * @param {object} req - Express request object. * @param {object} res - Express response object. * @param {string} req.params.fontstack - Name of the font stack. @@ -32,7 +31,9 @@ export async function serve_font(options, allowedFonts, programOpts) { */ app.get('/fonts/:fontstack/:range.pbf', async (req, res) => { if (verbose) { - console.log(req.params); + console.log( + `Handling font request for: /fonts/${req.params.fontstack}/${req.params.range}.pbf`, + ); } const fontstack = decodeURI(req.params.fontstack); const range = req.params.range; @@ -60,12 +61,14 @@ export async function serve_font(options, allowedFonts, programOpts) { /** * Handles requests for a list of all available fonts. - * * @param {object} req - Express request object. * @param {object} res - Express response object. * @returns {void} */ app.get('/fonts.json', (req, res) => { + if (verbose) { + console.log('Handling list font request for /fonts.json'); + } res.header('Content-type', 'application/json'); return res.send( Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(), diff --git a/src/serve_rendered.js b/src/serve_rendered.js index 3413ca9..3a45639 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -34,6 +34,7 @@ import { isValidHttpUrl, fixTileJSONCenter, fetchTileData, + allowedOptions, } from './utils.js'; import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js'; import { renderOverlay, renderWatermark, renderAttribution } from './render.js'; @@ -635,6 +636,7 @@ const respondImage = async ( * @param {object} res - Express response object. * @param {Function} next - Express next middleware function. * @param {number} maxScaleFactor - The maximum scale factor allowed. + * @param defailtTileSize * @returns {Promise} */ async function handleTileRequest( @@ -644,6 +646,7 @@ async function handleTileRequest( res, next, maxScaleFactor, + defailtTileSize, ) { const { id, @@ -658,6 +661,7 @@ async function handleTileRequest( if (!item) { return res.sendStatus(404); } + console.log(req.params); const modifiedSince = req.get('if-modified-since'); const cc = req.get('cache-control'); @@ -670,7 +674,19 @@ async function handleTileRequest( const x = parseFloat(xParam) | 0; const y = parseFloat(yParam) | 0; const scale = parseScale(scaleParam, maxScaleFactor); - const parsedTileSize = parseInt(tileSize, 10) || 256; + + let parsedTileSize = defailtTileSize; + if (tileSize) { + const allowedTileSizes = allowedOptions(['256', '512'], { + defaultValue: null, + }); + parsedTileSize = allowedTileSizes(tileSize); + + if (parsedTileSize == null) { + return res.status(400).send('Invalid Tile Size'); + } + } + if ( scale == null || z < 0 || @@ -680,7 +696,7 @@ async function handleTileRequest( x >= Math.pow(2, z) || y >= Math.pow(2, z) ) { - return res.status(404).send('Out of bounds'); + return res.status(400).send('Out of bounds'); } const tileCenter = mercator.ll( @@ -722,14 +738,43 @@ async function handleStaticRequest( id, p2: raw, p3: staticType, - p4: width, - p5: height, + p4: widthAndHeight, scale: scaleParam, format, } = req.params; + console.log(req.params); const item = repo[id]; - const parsedWidth = parseInt(width) || 512; - const parsedHeight = parseInt(height) || 512; + + let parsedWidth = null; + let parsedHeight = null; + if (widthAndHeight) { + const sizeMatch = widthAndHeight.match(/^(\d+)x(\d+)$/); + if (sizeMatch) { + const width = parseInt(sizeMatch[1], 10); + const height = parseInt(sizeMatch[2], 10); + + if ( + isNaN(width) || + isNaN(height) || + width !== parseFloat(sizeMatch[1]) || + height !== parseFloat(sizeMatch[2]) + ) { + return res + .status(400) + .send('Invalid width or height provided in size parameter'); + } + parsedWidth = width; + parsedHeight = height; + } else { + return res + .status(400) + .send('Invalid width or height provided in size parameter'); + } + } else { + return res + .status(400) + .send('Invalid width or height provided in size parameter'); + } const scale = parseScale(scaleParam, maxScaleFactor); let isRaw = raw === 'raw'; @@ -740,11 +785,12 @@ async function handleStaticRequest( const staticTypeMatch = staticType.match(staticTypeRegex); if (staticTypeMatch.groups.lon) { // Center Based Static Image - const z = parseFloat(staticTypeMatch.groups.zoom) || 0; - let x = parseFloat(staticTypeMatch.groups.lon) || 0; - let y = parseFloat(staticTypeMatch.groups.lat) || 0; - const bearing = parseFloat(staticTypeMatch.groups.bearing) || 0; - const pitch = parseInt(staticTypeMatch.groups.pitch) || 0; + const z = staticTypeMatch.groups.zoom; + let x = staticTypeMatch.groups.lon; + let y = staticTypeMatch.groups.lat; + const bearing = staticTypeMatch.groups.bearing; + const pitch = staticTypeMatch.groups.pitch; + if (z < 0) { return res.status(404).send('Invalid zoom'); } @@ -764,13 +810,13 @@ async function handleStaticRequest( // prettier-ignore const overlay = await renderOverlay( - z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query, - ); + z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query, + ); // prettier-ignore return await respondImage( - options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static', - ); + options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static', + ); } else if (staticTypeMatch.groups.minx) { // Area Based Static Image const bbox = [ @@ -802,15 +848,16 @@ async function handleStaticRequest( const pitch = 0; const paths = extractPathsFromQuery(req.query, transformer); const markers = extractMarkersFromQuery(req.query, options, transformer); + // prettier-ignore const overlay = await renderOverlay( - z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query, - ); + z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query, + ); // prettier-ignore return await respondImage( - options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static', - ); + options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static', + ); } else if (staticTypeMatch.groups.auto) { // Area Static Image const bearing = 0; @@ -863,13 +910,13 @@ async function handleStaticRequest( // prettier-ignore const overlay = await renderOverlay( - z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query, - ); + z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query, + ); // prettier-ignore return await respondImage( - options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static', - ); + options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static', + ); } else { return res.sendStatus(404); } @@ -887,7 +934,7 @@ export const serve_rendered = { * @returns {Promise} A promise that resolves to the Express app. */ init: async function (options, repo, programOpts) { - const { verbose } = programOpts; + const { verbose, tileSize: defailtTileSize = 256 } = programOpts; maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9); const app = express().disable('x-powered-by'); @@ -896,7 +943,7 @@ export const serve_rendered = { * @param {object} req - Express request object. * @param {object} res - Express response object. * @param {string} req.params.id - The id of the style. - * @param {string} req.params.p1 - The tile size or static parameter, if available + * @param {string} [req.params.p1] - The tile size or static parameter, if available. * @param {string} req.params.p2 - The z, static, or raw parameter. * @param {string} req.params.p3 - The x or staticType parameter. * @param {string} req.params.p4 - The y or width parameter. @@ -906,14 +953,24 @@ export const serve_rendered = { * @returns {Promise} */ app.get( - `/:id{/:p1}/:p2/:p3/:p4{x:p5}{@:scale}{.:format}`, + `/:id{/:p1}/:p2/:p3/:p4{@:scale}{.:format}`, async (req, res, next) => { try { + const { p1, p2, id, p3, p4, p5, scale, format } = req.params; + const requestType = + (!p1 && p2 === 'static') || (p1 === 'static' && p2 === 'raw') + ? 'static' + : 'tile'; + if (verbose) { - console.log(req.params); + console.log( + `Handling rendered ${requestType} request for: /styles/${id}${p1 ? '/' + p1 : ''}/${p2}/${p3}/${p4}${p5 ? 'x' + p5 : ''}${ + scale ? '@' + scale : '' + }.${format}`, + ); } - const { p1, p2 } = req.params; - if ((!p1 && p2 === 'static') || (p1 === 'static' && p2 === 'raw')) { + + if (requestType === 'static') { // Route to static if p2 is static if (options.serveStaticMaps !== false) { return handleStaticRequest( @@ -923,6 +980,7 @@ export const serve_rendered = { res, next, maxScaleFactor, + defailtTileSize, ); } return res.sendStatus(404); @@ -935,6 +993,7 @@ export const serve_rendered = { res, next, maxScaleFactor, + defailtTileSize, ); } catch (e) { console.log(e); @@ -944,7 +1003,7 @@ export const serve_rendered = { ); /** - * Handles requests for tile json endpoint. + * Handles requests for rendered tilejson endpoint. * @param {object} req - Express request object. * @param {object} res - Express response object. * @param {string} req.params.id - The id of the tilejson @@ -957,6 +1016,11 @@ export const serve_rendered = { return res.sendStatus(404); } const tileSize = parseInt(req.params.tileSize, 10) || undefined; + if (verbose) { + console.log( + `Handling rendered tilejson request for: /styles/${tileSize ? tileSize + '/' : ''}${req.params.id}.json`, + ); + } const info = clone(item.tileJSON); info.tiles = getTileUrls( req, diff --git a/src/serve_style.js b/src/serve_style.js index b5f59aa..80f42e6 100644 --- a/src/serve_style.js +++ b/src/serve_style.js @@ -35,59 +35,95 @@ export const serve_style = { init: function (options, repo, programOpts) { const { verbose } = programOpts; const app = express().disable('x-powered-by'); - + /** + * Handles requests for style.json files. + * @param {express.Request} req - Express request object. + * @param {express.Response} res - Express response object. + * @param {express.NextFunction} next - Express next function. + * @param {string} req.params.id - ID of the style. + * @returns {Promise} + */ app.get('/:id/style.json', (req, res, next) => { - const item = repo[req.params.id]; - if (!item) { - return res.sendStatus(404); + const { id } = req.params; + if (verbose) { + console.log(`Handling style request for: /styles/${id}/style.json`); } - const styleJSON_ = clone(item.styleJSON); - for (const name of Object.keys(styleJSON_.sources)) { - const source = styleJSON_.sources[name]; - source.url = fixUrl(req, source.url, item.publicUrl); - if (typeof source.data == 'string') { - source.data = fixUrl(req, source.data, item.publicUrl); + try { + const item = repo[id]; + if (!item) { + return res.sendStatus(404); } - } - // mapbox-gl-js viewer cannot handle sprite urls with query - if (styleJSON_.sprite) { - if (Array.isArray(styleJSON_.sprite)) { - styleJSON_.sprite.forEach((spriteItem) => { - spriteItem.url = fixUrl(req, spriteItem.url, item.publicUrl); - }); - } else { - styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl); + const styleJSON_ = clone(item.styleJSON); + for (const name of Object.keys(styleJSON_.sources)) { + const source = styleJSON_.sources[name]; + source.url = fixUrl(req, source.url, item.publicUrl); + if (typeof source.data == 'string') { + source.data = fixUrl(req, source.data, item.publicUrl); + } } + if (styleJSON_.sprite) { + if (Array.isArray(styleJSON_.sprite)) { + styleJSON_.sprite.forEach((spriteItem) => { + spriteItem.url = fixUrl(req, spriteItem.url, item.publicUrl); + }); + } else { + styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl); + } + } + if (styleJSON_.glyphs) { + styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl); + } + return res.send(styleJSON_); + } catch (e) { + next(e); } - if (styleJSON_.glyphs) { - styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl); - } - return res.send(styleJSON_); }); + /** + * Handles GET requests for sprite images and JSON files. + * @param {express.Request} req - Express request object. + * @param {express.Response} res - Express response object. + * @param {express.NextFunction} next - Express next function. + * @param {string} req.params.id - ID of the sprite. + * @param {string} [req.params.spriteID='default'] - ID of the specific sprite image, defaults to 'default'. + * @param {string} [req.params.scale] - Scale of the sprite image, defaults to ''. + * @param {string} req.params.format - Format of the sprite file, 'png' or 'json'. + * @returns {Promise} + */ app.get(`/:id/sprite{/:spriteID}{@:scale}{.:format}`, (req, res, next) => { - if (verbose) { - console.log(req.params); - } - const { spriteID = 'default', id, format } = req.params; - const spriteScale = allowedSpriteScales(req.params.scale); + const { spriteID = 'default', id, format, scale } = req.params; + const spriteScale = allowedSpriteScales(scale); + if (verbose) { + console.log( + `Handling sprite request for: /${id}/sprite/${spriteID}${scale}.${format}`, + ); + } const item = repo[id]; if (!item || !allowedSpriteFormats(format)) { + if (verbose) + console.error( + `Sprite item or format not found for: /${id}/sprite/${spriteID}${scale}.${format}`, + ); return res.sendStatus(404); } - const sprite = item.spritePaths.find((sprite) => sprite.id === spriteID); if (!sprite) { + if (verbose) + console.error( + `Sprite not found for: /${id}/sprite/${spriteID}${scale}.${format}`, + ); return res.status(400).send('Bad Sprite ID or Scale'); } const filename = `${sprite.path}${spriteScale}.${format}`; + if (verbose) console.log(`Loading sprite from: ${filename}`); // eslint-disable-next-line security/detect-non-literal-fs-filename fs.readFile(filename, (err, data) => { if (err) { - console.error('Sprite load error: %s, Error: %s', filename, err); + if (verbose) + console.error('Sprite load error: %s, Error: %s', filename, err); return res.sendStatus(404); } @@ -96,6 +132,10 @@ export const serve_style = { } else if (format === 'png') { res.header('Content-type', 'image/png'); } + if (verbose) + console.log( + `Responding with sprite data for /${id}/sprite/${spriteID}${scale}.${format}`, + ); return res.send(data); }); }); diff --git a/test/static.js b/test/static.js index 32bd80c..e6183bf 100644 --- a/test/static.js +++ b/test/static.js @@ -135,7 +135,7 @@ describe('Static endpoints', function () { testStatic(prefix, '0,0,1,1/1x1', 'gif', 400); - testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 404); + testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 400); }); }); diff --git a/test/tiles_rendered.js b/test/tiles_rendered.js index 6f7f438..61996d9 100644 --- a/test/tiles_rendered.js +++ b/test/tiles_rendered.js @@ -60,16 +60,16 @@ describe('Raster tiles', function () { describe('invalid requests return 4xx', function () { testTile('non_existent', 256, 0, 0, 0, 'png', 404); - testTile(prefix, 256, -1, 0, 0, 'png', 404); - testTile(prefix, 256, 25, 0, 0, 'png', 404); - testTile(prefix, 256, 0, 1, 0, 'png', 404); - testTile(prefix, 256, 0, 0, 1, 'png', 404); + testTile(prefix, 256, -1, 0, 0, 'png', 400); + testTile(prefix, 256, 25, 0, 0, 'png', 400); + testTile(prefix, 256, 0, 1, 0, 'png', 400); + testTile(prefix, 256, 0, 0, 1, 'png', 400); testTile(prefix, 256, 0, 0, 0, 'gif', 400); testTile(prefix, 256, 0, 0, 0, 'pbf', 400); - testTile(prefix, 256, 0, 0, 0, 'png', 404, 1); - testTile(prefix, 256, 0, 0, 0, 'png', 404, 5); + testTile(prefix, 256, 0, 0, 0, 'png', 400, 1); + testTile(prefix, 256, 0, 0, 0, 'png', 400, 5); - testTile(prefix, 300, 0, 0, 0, 'png', 404); + testTile(prefix, 300, 0, 0, 0, 'png', 400); }); });