diff --git a/src/serve_style.js b/src/serve_style.js index 36975ec..10ef29e 100644 --- a/src/serve_style.js +++ b/src/serve_style.js @@ -7,7 +7,7 @@ import clone from 'clone'; import express from 'express'; import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec'; -import { fixUrl, allowedOptions } from './utils.js'; +import { fixUrl, allowedOptions, readFile } from './utils.js'; const httpTester = /^https?:\/\//i; const allowedSpriteFormats = allowedOptions(['png', 'json']); @@ -101,94 +101,98 @@ export const serve_style = { * @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) => { - const { spriteID = 'default', id, format, scale } = req.params; - const sanitizedId = String(id).replace(/\n|\r/g, ''); - const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : ''; - const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, ''); - const sanitizedFormat = format - ? '.' + String(format).replace(/\n|\r/g, '') - : ''; - if (verbose) { - console.log( - `Handling sprite request for: /styles/%s/sprite/%s%s%s`, - sanitizedId, - sanitizedSpriteID, - sanitizedScale, - sanitizedFormat, - ); - } - const item = repo[id]; - const validatedFormat = allowedSpriteFormats(format); - if (!item || !validatedFormat) { - if (verbose) - console.error( - `Sprite item or format not found for: /styles/%s/sprite/%s%s%s`, + app.get( + `/:id/sprite{/:spriteID}{@:scale}{.:format}`, + async (req, res, next) => { + const { spriteID = 'default', id, format, scale } = req.params; + const sanitizedId = String(id).replace(/\n|\r/g, ''); + const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : ''; + const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, ''); + const sanitizedFormat = format + ? '.' + String(format).replace(/\n|\r/g, '') + : ''; + if (verbose) { + console.log( + `Handling sprite request for: /styles/%s/sprite/%s%s%s`, sanitizedId, sanitizedSpriteID, sanitizedScale, sanitizedFormat, ); - return res.sendStatus(404); - } - const sprite = item.spritePaths.find((sprite) => sprite.id === spriteID); - const spriteScale = allowedSpriteScales(scale); - if (!sprite || spriteScale === null) { - if (verbose) - console.error( - `Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`, - sanitizedId, - sanitizedSpriteID, - sanitizedScale, - sanitizedFormat, - ); - return res.status(400).send('Bad Sprite ID or Scale'); - } - - const modifiedSince = req.get('if-modified-since'); - const cc = req.get('cache-control'); - if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) { - if ( - new Date(item.lastModified).getTime() === - new Date(modifiedSince).getTime() - ) { - return res.sendStatus(304); } - } - - const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, ''); - const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`; - if (verbose) console.log(`Loading sprite from: %s`, filename); - - // eslint-disable-next-line security/detect-non-literal-fs-filename - fs.readFile(filename, (err, data) => { - if (err) { + const item = repo[id]; + const validatedFormat = allowedSpriteFormats(format); + if (!item || !validatedFormat) { if (verbose) + console.error( + `Sprite item or format not found for: /styles/%s/sprite/%s%s%s`, + sanitizedId, + sanitizedSpriteID, + sanitizedScale, + sanitizedFormat, + ); + return res.sendStatus(404); + } + const sprite = item.spritePaths.find( + (sprite) => sprite.id === spriteID, + ); + const spriteScale = allowedSpriteScales(scale); + if (!sprite || spriteScale === null) { + if (verbose) + console.error( + `Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`, + sanitizedId, + sanitizedSpriteID, + sanitizedScale, + sanitizedFormat, + ); + return res.status(400).send('Bad Sprite ID or Scale'); + } + + const modifiedSince = req.get('if-modified-since'); + const cc = req.get('cache-control'); + if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) { + if ( + new Date(item.lastModified).getTime() === + new Date(modifiedSince).getTime() + ) { + return res.sendStatus(304); + } + } + + const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, ''); + const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`; + if (verbose) console.log(`Loading sprite from: %s`, filename); + try { + const data = await readFile(filename); + + if (validatedFormat === 'json') { + res.header('Content-type', 'application/json'); + } else if (validatedFormat === 'png') { + res.header('Content-type', 'image/png'); + } + if (verbose) + console.log( + `Responding with sprite data for /styles/%s/sprite/%s%s%s`, + sanitizedId, + sanitizedSpriteID, + sanitizedScale, + sanitizedFormat, + ); + res.set({ 'Last-Modified': item.lastModified }); + return res.send(data); + } catch (err) { + if (verbose) { console.error( 'Sprite load error: %s, Error: %s', filename, String(err), ); + } return res.sendStatus(404); } - - if (validatedFormat === 'json') { - res.header('Content-type', 'application/json'); - } else if (validatedFormat === 'png') { - res.header('Content-type', 'image/png'); - } - if (verbose) - console.log( - `Responding with sprite data for /styles/%s/sprite/%s%s%s`, - sanitizedId, - sanitizedSpriteID, - sanitizedScale, - sanitizedFormat, - ); - res.set({ 'Last-Modified': item.lastModified }); - return res.send(data); - }); - }); + }, + ); return app; }, diff --git a/src/utils.js b/src/utils.js index a42c267..ea08895 100644 --- a/src/utils.js +++ b/src/utils.js @@ -185,6 +185,24 @@ export function fixTileJSONCenter(tileJSON) { } } +/** + * Reads a file and returns a Promise with the file data. + * @param {string} filename - Path to the file to read. + * @returns {Promise} - A Promise that resolves with the file data as a Buffer or rejects with an error. + */ +export function readFile(filename) { + return new Promise((resolve, reject) => { + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.readFile(filename, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + /** * Retrieves font data for a given font and range. * @param {object} allowedFonts - An object of allowed fonts. @@ -194,85 +212,75 @@ export function fixTileJSONCenter(tileJSON) { * @param {object} [fallbacks] - Optional fallback font list. * @returns {Promise} A promise that resolves with the font data Buffer or rejects with an error. */ -function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) { - return new Promise((resolve, reject) => { - if (!allowedFonts || (allowedFonts[name] && fallbacks)) { - const fontMatch = name?.match(/^[\w\s-]+$/); - const sanitizedName = fontMatch?.[0] || 'invalid'; - if ( - !name || - typeof name !== 'string' || - name.trim() === '' || - !fontMatch - ) { - console.error( - 'ERROR: Invalid font name: %s', - sanitizedName.replace(/\n|\r/g, ''), - ); - return reject('Invalid font name'); - } - - const rangeMatch = range?.match(/^[\d-]+$/); - const sanitizedRange = rangeMatch?.[0] || 'invalid'; - if (!/^\d+-\d+$/.test(range)) { - console.error( - 'ERROR: Invalid range: %s', - sanitizedRange.replace(/\n|\r/g, ''), - ); - return reject('Invalid range'); - } - const filename = path.join( - fontPath, - sanitizedName, - `${sanitizedRange}.pbf`, +async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) { + if (!allowedFonts || (allowedFonts[name] && fallbacks)) { + const fontMatch = name?.match(/^[\w\s-]+$/); + const sanitizedName = fontMatch?.[0] || 'invalid'; + if (!name || typeof name !== 'string' || name.trim() === '' || !fontMatch) { + console.error( + 'ERROR: Invalid font name: %s', + sanitizedName.replace(/\n|\r/g, ''), ); - if (!fallbacks) { - fallbacks = clone(allowedFonts || {}); - } - delete fallbacks[name]; - // eslint-disable-next-line security/detect-non-literal-fs-filename - fs.readFile(filename, (err, data) => { - if (err) { - console.error( - 'ERROR: Font not found: %s, Error: %s', - filename.replace(/\n|\r/g, ''), - String(err), - ); - if (fallbacks && Object.keys(fallbacks).length) { - let fallbackName; - - let fontStyle = name.split(' ').pop(); - if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) { - fontStyle = 'Regular'; - } - fallbackName = `Noto Sans ${fontStyle}`; - if (!fallbacks[fallbackName]) { - fallbackName = `Open Sans ${fontStyle}`; - if (!fallbacks[fallbackName]) { - fallbackName = Object.keys(fallbacks)[0]; - } - } - console.error( - `ERROR: Trying to use %s as a fallback for: %s`, - fallbackName, - sanitizedName, - ); - delete fallbacks[fallbackName]; - getFontPbf(null, fontPath, fallbackName, range, fallbacks).then( - resolve, - reject, - ); - } else { - reject('Font load error'); - } - } else { - resolve(data); - } - }); - } else { - reject('Font not allowed'); + throw new Error('Invalid font name'); } - }); + + const rangeMatch = range?.match(/^[\d-]+$/); + const sanitizedRange = rangeMatch?.[0] || 'invalid'; + if (!/^\d+-\d+$/.test(range)) { + console.error( + 'ERROR: Invalid range: %s', + sanitizedRange.replace(/\n|\r/g, ''), + ); + throw new Error('Invalid range'); + } + const filename = path.join( + fontPath, + sanitizedName, + `${sanitizedRange}.pbf`, + ); + + if (!fallbacks) { + fallbacks = clone(allowedFonts || {}); + } + delete fallbacks[name]; + + try { + const data = await readFile(filename); + return data; + } catch (err) { + console.error( + 'ERROR: Font not found: %s, Error: %s', + filename.replace(/\n|\r/g, ''), + String(err), + ); + if (fallbacks && Object.keys(fallbacks).length) { + let fallbackName; + + let fontStyle = name.split(' ').pop(); + if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) { + fontStyle = 'Regular'; + } + fallbackName = `Noto Sans ${fontStyle}`; + if (!fallbacks[fallbackName]) { + fallbackName = `Open Sans ${fontStyle}`; + if (!fallbacks[fallbackName]) { + fallbackName = Object.keys(fallbacks)[0]; + } + } + console.error( + `ERROR: Trying to use %s as a fallback for: %s`, + fallbackName, + sanitizedName, + ); + delete fallbacks[fallbackName]; + return getFontPbf(null, fontPath, fallbackName, range, fallbacks); + } else { + throw new Error('Font load error'); + } + } + } else { + throw new Error('Font not allowed'); + } } /** * Combines multiple font pbf buffers into one.