tileserver-gl-light 5.4.1-pre.0 → 5.5.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -2
- package/docs/config.rst +158 -2
- package/docs/usage.rst +66 -3
- package/package.json +7 -6
- package/public/resources/maplibre-gl.js +4 -4
- package/public/resources/maplibre-gl.js.map +1 -1
- package/src/main.js +89 -14
- package/src/mbtiles_wrapper.js +5 -6
- package/src/pmtiles_adapter.js +413 -60
- package/src/render.js +48 -13
- package/src/serve_data.js +28 -9
- package/src/serve_rendered.js +78 -27
- package/src/serve_style.js +13 -8
- package/src/server.js +115 -25
- package/src/utils.js +79 -11
package/src/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getTileUrls,
|
|
22
22
|
getPublicUrl,
|
|
23
23
|
isValidHttpUrl,
|
|
24
|
+
isValidRemoteUrl,
|
|
24
25
|
} from './utils.js';
|
|
25
26
|
|
|
26
27
|
import { fileURLToPath } from 'url';
|
|
@@ -59,7 +60,8 @@ async function start(opts) {
|
|
|
59
60
|
app.use(
|
|
60
61
|
morgan(logFormat, {
|
|
61
62
|
stream: opts.logFile
|
|
62
|
-
? fs
|
|
63
|
+
? // eslint-disable-next-line security/detect-non-literal-fs-filename -- logFile is from CLI/config, admin-controlled
|
|
64
|
+
fs.createWriteStream(opts.logFile, { flags: 'a' })
|
|
63
65
|
: process.stdout,
|
|
64
66
|
skip: (req, res) =>
|
|
65
67
|
opts.silent && (res.statusCode === 200 || res.statusCode === 304),
|
|
@@ -72,6 +74,7 @@ async function start(opts) {
|
|
|
72
74
|
if (opts.configPath) {
|
|
73
75
|
configPath = path.resolve(opts.configPath);
|
|
74
76
|
try {
|
|
77
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- configPath is from CLI argument, expected behavior
|
|
75
78
|
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
76
79
|
} catch (e) {
|
|
77
80
|
console.log('ERROR: Config file not found or invalid!');
|
|
@@ -106,8 +109,11 @@ async function start(opts) {
|
|
|
106
109
|
const startupPromises = [];
|
|
107
110
|
|
|
108
111
|
for (const type of Object.keys(paths)) {
|
|
112
|
+
// eslint-disable-next-line security/detect-object-injection -- type is from Object.keys of paths config
|
|
113
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename, security/detect-object-injection -- paths[type] constructed from validated config paths
|
|
109
114
|
if (!fs.existsSync(paths[type])) {
|
|
110
115
|
console.error(
|
|
116
|
+
// eslint-disable-next-line security/detect-object-injection -- type is from Object.keys of paths config
|
|
111
117
|
`The specified path for "${type}" does not exist (${paths[type]}).`,
|
|
112
118
|
);
|
|
113
119
|
process.exit(1);
|
|
@@ -122,6 +128,7 @@ async function start(opts) {
|
|
|
122
128
|
*/
|
|
123
129
|
async function getFiles(directory) {
|
|
124
130
|
// Fetch all entries of the directory and attach type information
|
|
131
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- directory is constructed from validated config paths
|
|
125
132
|
const dirEntries = await fs.promises.readdir(directory, {
|
|
126
133
|
withFileTypes: true,
|
|
127
134
|
});
|
|
@@ -150,10 +157,13 @@ async function start(opts) {
|
|
|
150
157
|
|
|
151
158
|
if (options.dataDecorator) {
|
|
152
159
|
try {
|
|
160
|
+
// eslint-disable-next-line security/detect-non-literal-require -- dataDecorator path is from config file, admin-controlled
|
|
153
161
|
options.dataDecoratorFunc = require(
|
|
154
162
|
path.resolve(paths.root, options.dataDecorator),
|
|
155
163
|
);
|
|
156
|
-
} catch (e) {
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// Silently fail if dataDecorator cannot be loaded
|
|
166
|
+
}
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
const data = clone(config.data || {});
|
|
@@ -178,13 +188,14 @@ async function start(opts) {
|
|
|
178
188
|
* @param {object} item - The style configuration object.
|
|
179
189
|
* @param {boolean} allowMoreData - Whether to allow adding more data sources.
|
|
180
190
|
* @param {boolean} reportFonts - Whether to report fonts.
|
|
181
|
-
* @returns {Promise<
|
|
191
|
+
* @returns {Promise<boolean>} - Returns true if successful, false otherwise.
|
|
182
192
|
*/
|
|
183
193
|
async function addStyle(id, item, allowMoreData, reportFonts) {
|
|
184
194
|
let success = true;
|
|
185
195
|
|
|
186
196
|
let styleJSON;
|
|
187
197
|
try {
|
|
198
|
+
// Style files should only be HTTP/HTTPS, not S3
|
|
188
199
|
if (isValidHttpUrl(item.style)) {
|
|
189
200
|
const res = await fetch(item.style);
|
|
190
201
|
if (!res.ok) {
|
|
@@ -193,6 +204,7 @@ async function start(opts) {
|
|
|
193
204
|
styleJSON = await res.json();
|
|
194
205
|
} else {
|
|
195
206
|
const styleFile = path.resolve(options.paths.styles, item.style);
|
|
207
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- styleFile constructed from config base path and style name
|
|
196
208
|
const styleFileData = await fs.promises.readFile(styleFile);
|
|
197
209
|
styleJSON = JSON.parse(styleFileData);
|
|
198
210
|
}
|
|
@@ -212,11 +224,14 @@ async function start(opts) {
|
|
|
212
224
|
(styleSourceId, protocol) => {
|
|
213
225
|
let dataItemId;
|
|
214
226
|
for (const id of Object.keys(data)) {
|
|
227
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
215
228
|
if (id === styleSourceId) {
|
|
216
229
|
// Style id was found in data ids, return that id
|
|
217
230
|
dataItemId = id;
|
|
218
231
|
} else {
|
|
232
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
219
233
|
const fileType = Object.keys(data[id])[0];
|
|
234
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config, fileType is from Object.keys
|
|
220
235
|
if (data[id][fileType] === styleSourceId) {
|
|
221
236
|
// Style id was found in data filename, return the id that filename belong to
|
|
222
237
|
dataItemId = id;
|
|
@@ -236,12 +251,15 @@ async function start(opts) {
|
|
|
236
251
|
let id =
|
|
237
252
|
styleSourceId.substr(0, styleSourceId.lastIndexOf('.')) ||
|
|
238
253
|
styleSourceId;
|
|
239
|
-
|
|
254
|
+
// PMTiles can be remote URLs (HTTP or S3), generate unique ID for remote sources
|
|
255
|
+
if (isValidRemoteUrl(styleSourceId)) {
|
|
240
256
|
id =
|
|
241
257
|
fnv1a(styleSourceId) + '_' + id.replace(/^.*\/(.*)$/, '$1');
|
|
242
258
|
}
|
|
259
|
+
// eslint-disable-next-line security/detect-object-injection -- id is being checked for existence before modification
|
|
243
260
|
while (data[id]) id += '_'; //if the data source id already exists, add a "_" untill it doesn't
|
|
244
261
|
//Add the new data source to the data array.
|
|
262
|
+
// eslint-disable-next-line security/detect-object-injection -- id is constructed above to be unique
|
|
245
263
|
data[id] = {
|
|
246
264
|
[protocol]: styleSourceId,
|
|
247
265
|
};
|
|
@@ -252,6 +270,7 @@ async function start(opts) {
|
|
|
252
270
|
},
|
|
253
271
|
(font) => {
|
|
254
272
|
if (reportFonts) {
|
|
273
|
+
// eslint-disable-next-line security/detect-object-injection -- font is font name from style
|
|
255
274
|
serving.fonts[font] = true;
|
|
256
275
|
}
|
|
257
276
|
},
|
|
@@ -271,8 +290,12 @@ async function start(opts) {
|
|
|
271
290
|
let resolvedFileType;
|
|
272
291
|
let resolvedInputFile;
|
|
273
292
|
let resolvedSparse = false;
|
|
293
|
+
let resolvedS3Profile;
|
|
294
|
+
let resolvedRequestPayer;
|
|
295
|
+
let resolvedS3Region;
|
|
274
296
|
|
|
275
297
|
for (const id of Object.keys(data)) {
|
|
298
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
276
299
|
const sourceData = data[id];
|
|
277
300
|
let currentFileType;
|
|
278
301
|
let currentInputFileValue;
|
|
@@ -295,13 +318,28 @@ async function start(opts) {
|
|
|
295
318
|
resolvedFileType = currentFileType;
|
|
296
319
|
resolvedInputFile = currentInputFileValue;
|
|
297
320
|
|
|
298
|
-
// Get sparse
|
|
299
|
-
// Default to false if 'sparse' key doesn't exist or is falsy in a boolean context
|
|
321
|
+
// Get sparse if present
|
|
300
322
|
if (sourceData.hasOwnProperty('sparse')) {
|
|
301
|
-
resolvedSparse = !!sourceData.sparse;
|
|
323
|
+
resolvedSparse = !!sourceData.sparse;
|
|
302
324
|
} else {
|
|
303
|
-
resolvedSparse = false;
|
|
325
|
+
resolvedSparse = false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Get s3Profile if present
|
|
329
|
+
if (sourceData.hasOwnProperty('s3Profile')) {
|
|
330
|
+
resolvedS3Profile = sourceData.s3Profile;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Get requestPayer if present
|
|
334
|
+
if (sourceData.hasOwnProperty('requestPayer')) {
|
|
335
|
+
resolvedRequestPayer = !!sourceData.requestPayer;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Get s3Region if present
|
|
339
|
+
if (sourceData.hasOwnProperty('s3Region')) {
|
|
340
|
+
resolvedS3Region = sourceData.s3Region;
|
|
304
341
|
}
|
|
342
|
+
|
|
305
343
|
break; // Found our match, exit the outer loop
|
|
306
344
|
}
|
|
307
345
|
}
|
|
@@ -316,17 +354,23 @@ async function start(opts) {
|
|
|
316
354
|
inputFile: undefined,
|
|
317
355
|
fileType: undefined,
|
|
318
356
|
sparse: false,
|
|
357
|
+
s3Profile: undefined,
|
|
358
|
+
requestPayer: false,
|
|
359
|
+
s3Region: undefined,
|
|
319
360
|
};
|
|
320
361
|
}
|
|
321
362
|
|
|
322
|
-
|
|
363
|
+
// PMTiles supports remote URLs (HTTP and S3), skip path resolution for those
|
|
364
|
+
if (!isValidRemoteUrl(resolvedInputFile)) {
|
|
323
365
|
// Ensure options.paths and options.paths[resolvedFileType] exist before trying to use them
|
|
324
366
|
if (
|
|
325
367
|
options &&
|
|
326
368
|
options.paths &&
|
|
369
|
+
// eslint-disable-next-line security/detect-object-injection -- resolvedFileType is either 'pmtiles' or 'mbtiles'
|
|
327
370
|
options.paths[resolvedFileType]
|
|
328
371
|
) {
|
|
329
372
|
resolvedInputFile = path.resolve(
|
|
373
|
+
// eslint-disable-next-line security/detect-object-injection -- resolvedFileType is either 'pmtiles' or 'mbtiles'
|
|
330
374
|
options.paths[resolvedFileType],
|
|
331
375
|
resolvedInputFile,
|
|
332
376
|
);
|
|
@@ -341,6 +385,9 @@ async function start(opts) {
|
|
|
341
385
|
inputFile: resolvedInputFile,
|
|
342
386
|
fileType: resolvedFileType,
|
|
343
387
|
sparse: resolvedSparse,
|
|
388
|
+
s3Profile: resolvedS3Profile,
|
|
389
|
+
requestPayer: resolvedRequestPayer,
|
|
390
|
+
s3Region: resolvedS3Region,
|
|
344
391
|
};
|
|
345
392
|
},
|
|
346
393
|
),
|
|
@@ -353,6 +400,7 @@ async function start(opts) {
|
|
|
353
400
|
}
|
|
354
401
|
|
|
355
402
|
for (const id of Object.keys(config.styles || {})) {
|
|
403
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of config.styles
|
|
356
404
|
const item = config.styles[id];
|
|
357
405
|
if (!item.style || item.style.length === 0) {
|
|
358
406
|
console.log(`Missing "style" property for ${id}`);
|
|
@@ -366,7 +414,9 @@ async function start(opts) {
|
|
|
366
414
|
}),
|
|
367
415
|
);
|
|
368
416
|
for (const id of Object.keys(data)) {
|
|
417
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
369
418
|
const item = data[id];
|
|
419
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
370
420
|
const fileType = Object.keys(data[id])[0];
|
|
371
421
|
if (!fileType || !(fileType === 'pmtiles' || fileType === 'mbtiles')) {
|
|
372
422
|
console.log(
|
|
@@ -377,6 +427,7 @@ async function start(opts) {
|
|
|
377
427
|
startupPromises.push(serve_data.add(options, serving.data, item, id, opts));
|
|
378
428
|
}
|
|
379
429
|
if (options.serveAllStyles) {
|
|
430
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- options.paths.styles is from validated config
|
|
380
431
|
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
|
|
381
432
|
if (err) {
|
|
382
433
|
return;
|
|
@@ -419,6 +470,7 @@ async function start(opts) {
|
|
|
419
470
|
* Handles requests for a list of available styles.
|
|
420
471
|
* @param {object} req - Express request object.
|
|
421
472
|
* @param {object} res - Express response object.
|
|
473
|
+
* @param {object} next - Express next middleware function.
|
|
422
474
|
* @param {string} [req.query.key] - Optional API key.
|
|
423
475
|
* @returns {void}
|
|
424
476
|
*/
|
|
@@ -428,6 +480,7 @@ async function start(opts) {
|
|
|
428
480
|
? `?key=${encodeURIComponent(req.query.key)}`
|
|
429
481
|
: '';
|
|
430
482
|
for (const id of Object.keys(serving.styles)) {
|
|
483
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
|
|
431
484
|
const styleJSON = serving.styles[id].styleJSON;
|
|
432
485
|
result.push({
|
|
433
486
|
version: styleJSON.version,
|
|
@@ -451,7 +504,9 @@ async function start(opts) {
|
|
|
451
504
|
* @returns {Array} - An array of TileJSON objects.
|
|
452
505
|
*/
|
|
453
506
|
function addTileJSONs(arr, req, type, tileSize) {
|
|
507
|
+
// eslint-disable-next-line security/detect-object-injection -- type is 'rendered' or 'data', validated by caller
|
|
454
508
|
for (const id of Object.keys(serving[type])) {
|
|
509
|
+
// eslint-disable-next-line security/detect-object-injection -- type is 'rendered' or 'data', id is from Object.keys
|
|
455
510
|
const info = clone(serving[type][id].tileJSON);
|
|
456
511
|
let path = '';
|
|
457
512
|
if (type === 'rendered') {
|
|
@@ -479,6 +534,7 @@ async function start(opts) {
|
|
|
479
534
|
* Handles requests for a rendered tilejson endpoint.
|
|
480
535
|
* @param {object} req - Express request object.
|
|
481
536
|
* @param {object} res - Express response object.
|
|
537
|
+
* @param {object} next - Express next middleware function.
|
|
482
538
|
* @param {string} req.params.tileSize - Optional tile size parameter.
|
|
483
539
|
* @returns {void}
|
|
484
540
|
*/
|
|
@@ -501,6 +557,7 @@ async function start(opts) {
|
|
|
501
557
|
* Handles requests for a combined rendered and data tilejson endpoint.
|
|
502
558
|
* @param {object} req - Express request object.
|
|
503
559
|
* @param {object} res - Express response object.
|
|
560
|
+
* @param {object} next - Express next middleware function.
|
|
504
561
|
* @param {string} req.params.tileSize - Optional tile size parameter.
|
|
505
562
|
* @returns {void}
|
|
506
563
|
*/
|
|
@@ -526,8 +583,8 @@ async function start(opts) {
|
|
|
526
583
|
* Serves a Handlebars template.
|
|
527
584
|
* @param {string} urlPath - The URL path to serve the template at
|
|
528
585
|
* @param {string} template - The name of the template file
|
|
529
|
-
* @param {
|
|
530
|
-
*
|
|
586
|
+
* @param {(req: object) => object|null} dataGetter - A function to get data to be passed to the template.
|
|
587
|
+
* @returns {void}
|
|
531
588
|
*/
|
|
532
589
|
function serveTemplate(urlPath, template, dataGetter) {
|
|
533
590
|
let templateFile = `${templates}/${template}.tmpl`;
|
|
@@ -542,6 +599,7 @@ async function start(opts) {
|
|
|
542
599
|
}
|
|
543
600
|
}
|
|
544
601
|
try {
|
|
602
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- templateFile is from internal templates or config-specified path
|
|
545
603
|
const content = fs.readFileSync(templateFile, 'utf-8');
|
|
546
604
|
const compiled = handlebars.compile(content.toString());
|
|
547
605
|
app.get(urlPath, (req, res, next) => {
|
|
@@ -581,15 +639,17 @@ async function start(opts) {
|
|
|
581
639
|
/**
|
|
582
640
|
* Handles requests for the index page, providing a list of available styles and data.
|
|
583
641
|
* @param {object} req - Express request object.
|
|
584
|
-
* @
|
|
585
|
-
* @returns {void}
|
|
642
|
+
* @returns {object|null} Template data object or null
|
|
586
643
|
*/
|
|
587
644
|
serveTemplate('/', 'index', (req) => {
|
|
588
645
|
let styles = {};
|
|
589
646
|
for (const id of Object.keys(serving.styles || {})) {
|
|
590
647
|
let style = {
|
|
648
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
|
|
591
649
|
...serving.styles[id],
|
|
650
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
|
|
592
651
|
serving_data: serving.styles[id],
|
|
652
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
|
|
593
653
|
serving_rendered: serving.rendered[id],
|
|
594
654
|
};
|
|
595
655
|
|
|
@@ -618,12 +678,15 @@ async function start(opts) {
|
|
|
618
678
|
)[0];
|
|
619
679
|
}
|
|
620
680
|
|
|
681
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.styles
|
|
621
682
|
styles[id] = style;
|
|
622
683
|
}
|
|
623
684
|
let datas = {};
|
|
624
685
|
for (const id of Object.keys(serving.data || {})) {
|
|
686
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.data
|
|
625
687
|
let data = Object.assign({}, serving.data[id]);
|
|
626
688
|
|
|
689
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.data
|
|
627
690
|
const { tileJSON } = serving.data[id];
|
|
628
691
|
const { center } = tileJSON;
|
|
629
692
|
|
|
@@ -682,6 +745,7 @@ async function start(opts) {
|
|
|
682
745
|
}
|
|
683
746
|
data.formatted_filesize = `${size.toFixed(2)} ${suffix}`;
|
|
684
747
|
}
|
|
748
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of serving.data
|
|
685
749
|
datas[id] = data;
|
|
686
750
|
}
|
|
687
751
|
return {
|
|
@@ -693,12 +757,11 @@ async function start(opts) {
|
|
|
693
757
|
/**
|
|
694
758
|
* Handles requests for a map viewer template for a specific style.
|
|
695
759
|
* @param {object} req - Express request object.
|
|
696
|
-
* @
|
|
697
|
-
* @param {string} req.params.id - ID of the style.
|
|
698
|
-
* @returns {void}
|
|
760
|
+
* @returns {object|null} Template data object or null
|
|
699
761
|
*/
|
|
700
762
|
serveTemplate('/styles/:id/', 'viewer', (req) => {
|
|
701
763
|
const { id } = req.params;
|
|
764
|
+
// eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
|
|
702
765
|
const style = clone(((serving.styles || {})[id] || {}).styleJSON);
|
|
703
766
|
|
|
704
767
|
if (!style) {
|
|
@@ -707,8 +770,11 @@ async function start(opts) {
|
|
|
707
770
|
return {
|
|
708
771
|
...style,
|
|
709
772
|
id,
|
|
773
|
+
// eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
|
|
710
774
|
name: (serving.styles[id] || serving.rendered[id]).name,
|
|
775
|
+
// eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
|
|
711
776
|
serving_data: serving.styles[id],
|
|
777
|
+
// eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
|
|
712
778
|
serving_rendered: serving.rendered[id],
|
|
713
779
|
};
|
|
714
780
|
});
|
|
@@ -716,12 +782,11 @@ async function start(opts) {
|
|
|
716
782
|
/**
|
|
717
783
|
* Handles requests for a Web Map Tile Service (WMTS) XML template.
|
|
718
784
|
* @param {object} req - Express request object.
|
|
719
|
-
* @
|
|
720
|
-
* @param {string} req.params.id - ID of the style.
|
|
721
|
-
* @returns {void}
|
|
785
|
+
* @returns {object|null} Template data object or null
|
|
722
786
|
*/
|
|
723
787
|
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
|
|
724
788
|
const { id } = req.params;
|
|
789
|
+
// eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
|
|
725
790
|
const wmts = clone((serving.styles || {})[id]);
|
|
726
791
|
|
|
727
792
|
if (!wmts) {
|
|
@@ -746,6 +811,7 @@ async function start(opts) {
|
|
|
746
811
|
return {
|
|
747
812
|
...wmts,
|
|
748
813
|
id,
|
|
814
|
+
// eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
|
|
749
815
|
name: (serving.styles[id] || serving.rendered[id]).name,
|
|
750
816
|
baseUrl,
|
|
751
817
|
};
|
|
@@ -754,13 +820,11 @@ async function start(opts) {
|
|
|
754
820
|
/**
|
|
755
821
|
* Handles requests for a data view template for a specific data source.
|
|
756
822
|
* @param {object} req - Express request object.
|
|
757
|
-
* @
|
|
758
|
-
* @param {string} req.params.id - ID of the data source.
|
|
759
|
-
* @param {string} [req.params.view] - Optional view type.
|
|
760
|
-
* @returns {void}
|
|
823
|
+
* @returns {object|null} Template data object or null
|
|
761
824
|
*/
|
|
762
825
|
serveTemplate('/data{/:view}/:id/', 'data', (req) => {
|
|
763
826
|
const { id, view } = req.params;
|
|
827
|
+
// eslint-disable-next-line security/detect-object-injection -- id is route parameter from URL
|
|
764
828
|
const data = serving.data[id];
|
|
765
829
|
|
|
766
830
|
if (!data) {
|
|
@@ -806,14 +870,40 @@ async function start(opts) {
|
|
|
806
870
|
process.env.PORT || opts.port,
|
|
807
871
|
process.env.BIND || opts.bind,
|
|
808
872
|
function () {
|
|
809
|
-
|
|
873
|
+
const addressInfo = this.address();
|
|
874
|
+
|
|
875
|
+
if (!addressInfo) {
|
|
876
|
+
console.error('Failed to bind to port');
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
let address = addressInfo.address;
|
|
810
881
|
if (address.indexOf('::') === 0) {
|
|
811
882
|
address = `[${address}]`; // literal IPv6 address
|
|
812
883
|
}
|
|
813
|
-
console.log(`Listening at http://${address}:${
|
|
884
|
+
console.log(`Listening at http://${address}:${addressInfo.port}/`);
|
|
814
885
|
},
|
|
815
886
|
);
|
|
816
887
|
|
|
888
|
+
// Handle server errors
|
|
889
|
+
server.on('error', (err) => {
|
|
890
|
+
const port = process.env.PORT || opts.port;
|
|
891
|
+
if (err.code === 'EADDRINUSE') {
|
|
892
|
+
console.error(`ERROR: Port ${port} is already in use.`);
|
|
893
|
+
console.error(`Please choose a different port with -p or --port option.`);
|
|
894
|
+
process.exit(1);
|
|
895
|
+
} else if (err.code === 'EACCES') {
|
|
896
|
+
console.error(`ERROR: Permission denied to bind to port ${port}.`);
|
|
897
|
+
console.error(
|
|
898
|
+
`Try using a port number above 1024 or run with appropriate permissions.`,
|
|
899
|
+
);
|
|
900
|
+
process.exit(1);
|
|
901
|
+
} else {
|
|
902
|
+
console.error('Server error:', err.message);
|
|
903
|
+
process.exit(1);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
817
907
|
// add server.shutdown() to gracefully stop serving
|
|
818
908
|
enableShutdown(server);
|
|
819
909
|
|
package/src/utils.js
CHANGED
|
@@ -9,18 +9,23 @@ import { existsP } from './promises.js';
|
|
|
9
9
|
import { getPMtilesTile } from './pmtiles_adapter.js';
|
|
10
10
|
|
|
11
11
|
export const allowedSpriteFormats = allowedOptions(['png', 'json']);
|
|
12
|
-
|
|
13
12
|
export const allowedTileSizes = allowedOptions(['256', '512']);
|
|
13
|
+
export const httpTester = /^https?:\/\//i;
|
|
14
|
+
export const s3Tester = /^s3:\/\//i; // Plain AWS S3 format
|
|
15
|
+
export const s3HttpTester = /^s3\+https?:\/\//i; // S3-compatible with custom endpoint
|
|
16
|
+
export const pmtilesTester = /^pmtiles:\/\//i;
|
|
17
|
+
export const mbtilesTester = /^mbtiles:\/\//i;
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Restrict user input to an allowed set of options.
|
|
17
21
|
* @param {string[]} opts - An array of allowed option strings.
|
|
18
22
|
* @param {object} [config] - Optional configuration object.
|
|
19
23
|
* @param {string} [config.defaultValue] - The default value to return if input doesn't match.
|
|
20
|
-
* @returns {
|
|
24
|
+
* @returns {(value: string) => string} - A function that takes a value and returns it if valid or a default.
|
|
21
25
|
*/
|
|
22
26
|
export function allowedOptions(opts, { defaultValue } = {}) {
|
|
23
27
|
const values = Object.fromEntries(opts.map((key) => [key, key]));
|
|
28
|
+
// eslint-disable-next-line security/detect-object-injection -- value is checked against allowed opts keys
|
|
24
29
|
return (value) => values[value] || defaultValue;
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -35,7 +40,7 @@ export function allowedScales(scale, maxScale = 9) {
|
|
|
35
40
|
return 1;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
43
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- maxScale is a number parameter, not user input
|
|
39
44
|
const regex = new RegExp(`^[2-${maxScale}]x$`);
|
|
40
45
|
if (!regex.test(scale)) {
|
|
41
46
|
return null;
|
|
@@ -156,6 +161,7 @@ export function getTileUrls(
|
|
|
156
161
|
const hostParts = urlObject.host.split('.');
|
|
157
162
|
const relativeSubdomainsUsable =
|
|
158
163
|
hostParts.length > 1 &&
|
|
164
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- Simple IPv4 validation, no nested quantifiers
|
|
159
165
|
!/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(urlObject.host);
|
|
160
166
|
const newDomains = [];
|
|
161
167
|
for (const domain of domains) {
|
|
@@ -184,7 +190,9 @@ export function getTileUrls(
|
|
|
184
190
|
}
|
|
185
191
|
const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
|
186
192
|
|
|
193
|
+
// eslint-disable-next-line security/detect-object-injection -- format is validated format string from tileJSON
|
|
187
194
|
if (aliases && aliases[format]) {
|
|
195
|
+
// eslint-disable-next-line security/detect-object-injection -- format is validated format string from tileJSON
|
|
188
196
|
format = aliases[format];
|
|
189
197
|
}
|
|
190
198
|
|
|
@@ -245,7 +253,7 @@ export function fixTileJSONCenter(tileJSON) {
|
|
|
245
253
|
export function readFile(filename) {
|
|
246
254
|
return new Promise((resolve, reject) => {
|
|
247
255
|
const sanitizedFilename = path.normalize(filename); // Normalize path, remove ..
|
|
248
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
256
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- filename is normalized and validated by caller
|
|
249
257
|
fs.readFile(String(sanitizedFilename), (err, data) => {
|
|
250
258
|
if (err) {
|
|
251
259
|
reject(err);
|
|
@@ -266,6 +274,7 @@ export function readFile(filename) {
|
|
|
266
274
|
* @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error.
|
|
267
275
|
*/
|
|
268
276
|
async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
|
277
|
+
// eslint-disable-next-line security/detect-object-injection -- name is validated font name from sanitizedName check
|
|
269
278
|
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
|
270
279
|
const fontMatch = name?.match(/^[\p{L}\p{N} \-\.~!*'()@&=+,#$\[\]]+$/u);
|
|
271
280
|
const sanitizedName = fontMatch?.[0] || 'invalid';
|
|
@@ -295,6 +304,7 @@ async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
|
|
295
304
|
if (!fallbacks) {
|
|
296
305
|
fallbacks = clone(allowedFonts || {});
|
|
297
306
|
}
|
|
307
|
+
// eslint-disable-next-line security/detect-object-injection -- name is validated font name
|
|
298
308
|
delete fallbacks[name];
|
|
299
309
|
|
|
300
310
|
try {
|
|
@@ -314,8 +324,10 @@ async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
|
|
314
324
|
fontStyle = 'Regular';
|
|
315
325
|
}
|
|
316
326
|
fallbackName = `Noto Sans ${fontStyle}`;
|
|
327
|
+
// eslint-disable-next-line security/detect-object-injection -- fallbackName is constructed from validated font style
|
|
317
328
|
if (!fallbacks[fallbackName]) {
|
|
318
329
|
fallbackName = `Open Sans ${fontStyle}`;
|
|
330
|
+
// eslint-disable-next-line security/detect-object-injection -- fallbackName is constructed from validated font style
|
|
319
331
|
if (!fallbacks[fallbackName]) {
|
|
320
332
|
fallbackName = Object.keys(fallbacks)[0];
|
|
321
333
|
}
|
|
@@ -325,6 +337,7 @@ async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
|
|
325
337
|
fallbackName,
|
|
326
338
|
sanitizedName,
|
|
327
339
|
);
|
|
340
|
+
// eslint-disable-next-line security/detect-object-injection -- fallbackName is constructed from validated font style
|
|
328
341
|
delete fallbacks[fallbackName];
|
|
329
342
|
return getFontPbf(null, fontPath, fallbackName, range, fallbacks);
|
|
330
343
|
} else {
|
|
@@ -377,13 +390,16 @@ export async function getFontsPbf(
|
|
|
377
390
|
export async function listFonts(fontPath) {
|
|
378
391
|
const existingFonts = {};
|
|
379
392
|
|
|
393
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- fontPath is from validated config
|
|
380
394
|
const files = await fsPromises.readdir(fontPath);
|
|
381
395
|
for (const file of files) {
|
|
396
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- file is from readdir of validated fontPath
|
|
382
397
|
const stats = await fsPromises.stat(path.join(fontPath, file));
|
|
383
398
|
if (
|
|
384
399
|
stats.isDirectory() &&
|
|
385
400
|
(await existsP(path.join(fontPath, file, '0-255.pbf')))
|
|
386
401
|
) {
|
|
402
|
+
// eslint-disable-next-line security/detect-object-injection -- file is from readdir, used as font name key
|
|
387
403
|
existingFonts[path.basename(file)] = true;
|
|
388
404
|
}
|
|
389
405
|
}
|
|
@@ -392,20 +408,72 @@ export async function listFonts(fontPath) {
|
|
|
392
408
|
}
|
|
393
409
|
|
|
394
410
|
/**
|
|
395
|
-
* Checks if a string is a valid HTTP
|
|
396
|
-
* @param {string} string - The string to
|
|
397
|
-
* @returns {boolean} True if the string is a valid HTTP/HTTPS URL
|
|
411
|
+
* Checks if a string is a valid HTTP/HTTPS URL.
|
|
412
|
+
* @param {string} string - The string to check.
|
|
413
|
+
* @returns {boolean} - True if the string is a valid HTTP/HTTPS URL.
|
|
398
414
|
*/
|
|
399
415
|
export function isValidHttpUrl(string) {
|
|
400
|
-
|
|
416
|
+
try {
|
|
417
|
+
return httpTester.test(string);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Checks if a string is a valid S3 URL.
|
|
425
|
+
* @param {string} string - The string to check.
|
|
426
|
+
* @returns {boolean} - True if the string is a valid S3 URL.
|
|
427
|
+
*/
|
|
428
|
+
export function isS3Url(string) {
|
|
429
|
+
try {
|
|
430
|
+
return s3Tester.test(string) || s3HttpTester.test(string);
|
|
431
|
+
} catch (e) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Checks if a string is a valid remote URL (HTTP, HTTPS, or S3).
|
|
438
|
+
* @param {string} string - The string to check.
|
|
439
|
+
* @returns {boolean} - True if the string is a valid remote URL.
|
|
440
|
+
*/
|
|
441
|
+
export function isValidRemoteUrl(string) {
|
|
442
|
+
try {
|
|
443
|
+
return (
|
|
444
|
+
httpTester.test(string) ||
|
|
445
|
+
s3Tester.test(string) ||
|
|
446
|
+
s3HttpTester.test(string)
|
|
447
|
+
);
|
|
448
|
+
} catch (e) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
401
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Checks if a string uses the pmtiles:// protocol.
|
|
455
|
+
* @param {string} string - The string to check.
|
|
456
|
+
* @returns {boolean} - True if the string uses pmtiles:// protocol.
|
|
457
|
+
*/
|
|
458
|
+
export function isPMTilesProtocol(string) {
|
|
402
459
|
try {
|
|
403
|
-
|
|
404
|
-
} catch (
|
|
460
|
+
return pmtilesTester.test(string);
|
|
461
|
+
} catch (e) {
|
|
405
462
|
return false;
|
|
406
463
|
}
|
|
464
|
+
}
|
|
407
465
|
|
|
408
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Checks if a string uses the mbtiles:// protocol.
|
|
468
|
+
* @param {string} string - The string to check.
|
|
469
|
+
* @returns {boolean} - True if the string uses mbtiles:// protocol.
|
|
470
|
+
*/
|
|
471
|
+
export function isMBTilesProtocol(string) {
|
|
472
|
+
try {
|
|
473
|
+
return mbtilesTester.test(string);
|
|
474
|
+
} catch (e) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
409
477
|
}
|
|
410
478
|
|
|
411
479
|
/**
|