pmtiles-swarm 0.92.0 → 0.94.0
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 +39 -0
- package/docs/configuration.md +11 -2
- package/package.json +1 -1
- package/src/api.js +26 -2
- package/src/config.js +74 -0
- package/src/index.js +22 -5
- package/src/web/index.html +69 -22
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.94.0
|
|
11
|
+
### 🐞 Bug fixes
|
|
12
|
+
- **Saving settings rewrote a proxy list nobody had touched.** A trusted-proxy list may be stored as
|
|
13
|
+
a string — `"loopback, 10.0.0.0/8"` is what the documentation shows — and the box rendered that as
|
|
14
|
+
one line while reading it back as an array. The two never compared equal, so the field was sent on
|
|
15
|
+
every Save whether or not anybody had looked at it, and before the previous release that rewrite
|
|
16
|
+
was the thing that stopped the node from starting. Opening the settings page and pressing Save was
|
|
17
|
+
enough to do it.
|
|
18
|
+
|
|
19
|
+
The box now shows one entry per line whichever way the config wrote them, and records what a save
|
|
20
|
+
will read back rather than what the config holds — so an untouched field is untouched.
|
|
21
|
+
|
|
22
|
+
## 0.93.0
|
|
23
|
+
### 🐞 Bug fixes
|
|
24
|
+
- **A trusted-proxy list typed with commas stopped the node from starting.** The settings field
|
|
25
|
+
split what was typed on newlines only, so one line reading `172.16.1.2, 172.16.1.3` was saved as
|
|
26
|
+
an array holding both addresses in one string. Express splits a comma list when it is handed a
|
|
27
|
+
bare string and never inside an array, so proxy-addr was given `172.16.1.2, 172.16.1.3` as a
|
|
28
|
+
single address and threw -- while the app was being built, before the listener binds. The node
|
|
29
|
+
would not start, could not be reached, and could not be corrected from the console that had
|
|
30
|
+
written the value; on the node that found this it was 155 restarts.
|
|
31
|
+
|
|
32
|
+
Three things were wrong and all three are fixed. The field now splits on commas as well as
|
|
33
|
+
newlines. Every shape the setting can be written in -- a string, an array, commas, spaces,
|
|
34
|
+
newlines -- is flattened to what Express wants. And an entry that is not an address is ignored
|
|
35
|
+
and logged rather than thrown: this setting is not worth a node that will not boot, and trusting
|
|
36
|
+
nobody is the safe end of being wrong about it.
|
|
37
|
+
|
|
38
|
+
- **A failed restore took the whole node down, console included.** Handing the library back to the
|
|
39
|
+
engine at startup already tolerates a failure per archive; the call coming apart as a whole was
|
|
40
|
+
unguarded, and it happens before the listener binds — so under `Restart=always` the result is a
|
|
41
|
+
crash loop with no console to look at and no way to see why. It is now reported and the node
|
|
42
|
+
starts anyway, where every archive shows as **not loaded** until it is fixed.
|
|
43
|
+
|
|
44
|
+
- **Nothing tested that the node starts at all.** Every other test builds the pieces `src/index.js`
|
|
45
|
+
wires together and never runs the wiring, so an import cycle or a step that throws before the
|
|
46
|
+
listener binds was a failure only a real start could find. There is now a boot test that runs the
|
|
47
|
+
entry point the way the service does and asks it for a page.
|
|
48
|
+
|
|
10
49
|
## 0.92.0
|
|
11
50
|
### 🐞 Bug fixes
|
|
12
51
|
- **A stack's TileJSON published the addresses its sources are read from.** A URL source is named by
|
package/docs/configuration.md
CHANGED
|
@@ -82,8 +82,17 @@ Takes effect on the next request; no restart. See
|
|
|
82
82
|
### `trustProxy`
|
|
83
83
|
|
|
84
84
|
Takes anything Express accepts: `true`, a hop count, or a subnet list such as
|
|
85
|
-
`"loopback, 10.0.0.0/8"`.
|
|
86
|
-
|
|
85
|
+
`"loopback, 10.0.0.0/8"`. A list may be written as one string or as an array,
|
|
86
|
+
and either may separate its entries with commas, spaces or newlines — all four
|
|
87
|
+
shapes mean the same thing here. Off by default, because trusting these headers
|
|
88
|
+
from an untrusted client lets it claim any protocol or address it likes.
|
|
89
|
+
|
|
90
|
+
An entry that is not an address, a subnet, or one of `loopback`, `linklocal`
|
|
91
|
+
and `uniquelocal` is **ignored and logged**, and a value that cannot be read at
|
|
92
|
+
all leaves the node trusting nobody. Deliberately not fatal: Express compiles
|
|
93
|
+
this the moment it is set, which is before the listener binds, so a value it
|
|
94
|
+
refuses used to be a node that would not start, could not be reached, and could
|
|
95
|
+
not be corrected from the console that wrote it.
|
|
87
96
|
|
|
88
97
|
Set it when a proxy terminates TLS, or the TileJSON will advertise `http://` tile
|
|
89
98
|
URLs that browsers block as mixed content. Setting `publicUrl` instead sidesteps
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.0",
|
|
4
4
|
"description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/api.js
CHANGED
|
@@ -22,7 +22,12 @@ import {
|
|
|
22
22
|
import { mutableMagnet, trackersFromMagnet } from './mutable.js';
|
|
23
23
|
import { guessKind } from './library.js';
|
|
24
24
|
import { QBittorrentEngine } from './engines/qbittorrent.js';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
RESTART_REQUIRED,
|
|
27
|
+
redactConfig,
|
|
28
|
+
saveConfig,
|
|
29
|
+
trustProxyFor,
|
|
30
|
+
} from './config.js';
|
|
26
31
|
import { freeSpace, listLocations } from './locations.js';
|
|
27
32
|
import { restart, restartMode } from './restart.js';
|
|
28
33
|
import { parseFeed, renderFeed } from './feed.js';
|
|
@@ -260,7 +265,26 @@ export function createApp({
|
|
|
260
265
|
// the TileJSON advertises http:// tile URLs. A browser that loaded the map
|
|
261
266
|
// over https then blocks every one of them as mixed content, which looks
|
|
262
267
|
// like an empty map rather than like a configuration mistake.
|
|
263
|
-
|
|
268
|
+
//
|
|
269
|
+
// Normalised first, and then guarded anyway. Express compiles this value
|
|
270
|
+
// the moment it is set, which is before the listener binds -- so a value it
|
|
271
|
+
// cannot compile is not a setting that fails to apply, it is a node that
|
|
272
|
+
// will not start, cannot be reached, and cannot be corrected from the
|
|
273
|
+
// console that wrote it. Nothing about which addresses to trust is worth
|
|
274
|
+
// that: an unparseable one is reported and the node comes up trusting
|
|
275
|
+
// nobody, which is the safe end of being wrong.
|
|
276
|
+
const trustProxy = trustProxyFor(config.trustProxy);
|
|
277
|
+
if (trustProxy !== false) {
|
|
278
|
+
try {
|
|
279
|
+
app.set('trust proxy', trustProxy);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(
|
|
282
|
+
`[config] trustProxy could not be applied (${error.message}). ` +
|
|
283
|
+
'X-Forwarded-* headers are being ignored; the node is starting ' +
|
|
284
|
+
'anyway so this can be corrected from Settings.',
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
264
288
|
app.use(express.json({ limit: '1mb' }));
|
|
265
289
|
|
|
266
290
|
// Tiles, TileJSON and the feed stay public — serving them is the point.
|
package/src/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import net from 'node:net';
|
|
2
3
|
import { hashPassword } from './auth.js';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
|
|
@@ -10,6 +11,79 @@ import path from 'node:path';
|
|
|
10
11
|
* Every setting is documented in docs/configuration.md. Comments here say only
|
|
11
12
|
* what a value is; why it is what it is belongs in the document.
|
|
12
13
|
*/
|
|
14
|
+
/** What proxy-addr accepts as a name for a group of addresses. */
|
|
15
|
+
const TRUST_KEYWORDS = new Set(['loopback', 'linklocal', 'uniquelocal']);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether one entry of a trust list is something proxy-addr can compile.
|
|
19
|
+
* @param {string} entry - One address, subnet or keyword.
|
|
20
|
+
* @returns {boolean} - True if it is usable.
|
|
21
|
+
*/
|
|
22
|
+
function isTrustEntry(entry) {
|
|
23
|
+
if (TRUST_KEYWORDS.has(entry)) return true;
|
|
24
|
+
const [address, mask, ...rest] = entry.split('/');
|
|
25
|
+
if (rest.length > 0) return false;
|
|
26
|
+
if (!net.isIP(address)) return false;
|
|
27
|
+
if (mask === undefined) return true;
|
|
28
|
+
if (net.isIP(mask)) return true;
|
|
29
|
+
const bits = Number(mask);
|
|
30
|
+
if (!Number.isInteger(bits) || bits < 0) return false;
|
|
31
|
+
return bits <= (net.isIP(address) === 6 ? 128 : 32);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What to hand Express as `trust proxy`, from what the config says.
|
|
36
|
+
*
|
|
37
|
+
* Express takes four different things here and means something different by
|
|
38
|
+
* each, and it splits a comma-separated list only when it is handed a bare
|
|
39
|
+
* string -- never inside an array. So `["10.0.0.1, 10.0.0.2"]`, which is what
|
|
40
|
+
* a settings field split on newlines alone produces from one line with a
|
|
41
|
+
* comma in it, reaches proxy-addr as a single address, and proxy-addr throws.
|
|
42
|
+
*
|
|
43
|
+
* That throw happened while the app was being built, before the listener
|
|
44
|
+
* bound: a node that could not start, could not be reached, and could not be
|
|
45
|
+
* corrected from the console that wrote the value. So every shape is
|
|
46
|
+
* flattened here, and anything left that is not an address is dropped with a
|
|
47
|
+
* warning rather than carried into Express.
|
|
48
|
+
* @param {boolean|number|string|string[]} value - `config.trustProxy`.
|
|
49
|
+
* @returns {boolean|number|string[]} - Something Express can compile.
|
|
50
|
+
*/
|
|
51
|
+
export function trustProxyFor(value) {
|
|
52
|
+
if (
|
|
53
|
+
value === true ||
|
|
54
|
+
value === false ||
|
|
55
|
+
value === undefined ||
|
|
56
|
+
value === null
|
|
57
|
+
) {
|
|
58
|
+
return value === true;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : false;
|
|
61
|
+
|
|
62
|
+
const entries = (Array.isArray(value) ? value : [value])
|
|
63
|
+
.flatMap((one) => String(one).split(/[\s,]+/))
|
|
64
|
+
.map((one) => one.trim())
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
if (!entries.length) return false;
|
|
67
|
+
|
|
68
|
+
// A lone number is a hop count, and a lone `true` trusts everybody. Both
|
|
69
|
+
// are things somebody may type into a field that mostly takes addresses.
|
|
70
|
+
if (entries.length === 1) {
|
|
71
|
+
if (entries[0] === 'true') return true;
|
|
72
|
+
if (entries[0] === 'false') return false;
|
|
73
|
+
if (!Number.isNaN(Number(entries[0]))) return Number(entries[0]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const usable = entries.filter((entry) => isTrustEntry(entry));
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (usable.includes(entry)) continue;
|
|
79
|
+
console.warn(
|
|
80
|
+
`[config] trustProxy: ignoring "${entry}", which is not an address, ` +
|
|
81
|
+
'a subnet, or one of loopback, linklocal, uniquelocal.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return usable.length ? usable : false;
|
|
85
|
+
}
|
|
86
|
+
|
|
13
87
|
const DEFAULTS = {
|
|
14
88
|
port: 8090,
|
|
15
89
|
host: '0.0.0.0',
|
package/src/index.js
CHANGED
|
@@ -339,11 +339,28 @@ PMTILES_SWARM_PUBLIC_URL
|
|
|
339
339
|
|
|
340
340
|
const catalogued = catalog.list().length;
|
|
341
341
|
if (catalogued > 0) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
342
|
+
// Reported, never fatal. Restore already tolerates a failure per archive;
|
|
343
|
+
// what this catches is the whole call coming apart -- and the node it
|
|
344
|
+
// takes down with it is the one that could have said so. Under
|
|
345
|
+
// `Restart=always` that is a crash loop with no console to look at, which
|
|
346
|
+
// is a worse failure than a library that is not being seeded: the console
|
|
347
|
+
// marks an archive the engine has no record of as `not loaded`, so a node
|
|
348
|
+
// that comes up says exactly what went wrong here.
|
|
349
|
+
try {
|
|
350
|
+
const { restored, failed } = await library.restore();
|
|
351
|
+
console.log(
|
|
352
|
+
`[restore] ${restored} of ${catalogued} archives handed back to the engine` +
|
|
353
|
+
(failed > 0 ? ` (${failed} could not be)` : ''),
|
|
354
|
+
);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error(
|
|
357
|
+
`[restore] could not hand the library back to the engine: ` +
|
|
358
|
+
`${error.stack ?? error.message}
|
|
359
|
+
` +
|
|
360
|
+
'[restore] the node is starting anyway; every archive will show as ' +
|
|
361
|
+
'not loaded until this is fixed and it is restarted.',
|
|
362
|
+
);
|
|
363
|
+
}
|
|
347
364
|
}
|
|
348
365
|
|
|
349
366
|
// And again if the engine loses its backing process and starts another. A
|
package/src/web/index.html
CHANGED
|
@@ -5579,11 +5579,15 @@ Every piece is hashed against the ` +
|
|
|
5579
5579
|
type: 'addresses',
|
|
5580
5580
|
placeholder: 'off — or one address or subnet per line',
|
|
5581
5581
|
help:
|
|
5582
|
-
'
|
|
5583
|
-
'
|
|
5584
|
-
'
|
|
5585
|
-
'
|
|
5586
|
-
'
|
|
5582
|
+
'An address or a subnet — <code>172.16.1.49</code>, ' +
|
|
5583
|
+
'<code>172.16.1.0/24</code>, <code>loopback</code> — one ' +
|
|
5584
|
+
'per line, or separated by commas. A lone number is read ' +
|
|
5585
|
+
'as a hop count instead, and a lone <code>true</code> ' +
|
|
5586
|
+
'trusts any caller at all, which is worth avoiding: this ' +
|
|
5587
|
+
'header is what decides which address the node believes a ' +
|
|
5588
|
+
'request came from. Anything that is not an address is ' +
|
|
5589
|
+
'ignored and said so in the log, rather than stopping the ' +
|
|
5590
|
+
'node from starting.',
|
|
5587
5591
|
},
|
|
5588
5592
|
{
|
|
5589
5593
|
key: 'publicIndex',
|
|
@@ -6307,7 +6311,14 @@ Every piece is hashed against the ` +
|
|
|
6307
6311
|
field.type === 'secret'
|
|
6308
6312
|
? `<input type="password" autocomplete="new-password" data-setting="${field.key}"${locked} data-type="secret" data-initial="""" value="" placeholder="${escapeHtml(field.placeholder ?? 'unchanged')}" />`
|
|
6309
6313
|
: field.type === 'addresses'
|
|
6310
|
-
?
|
|
6314
|
+
? // Its own initial, because this is the one field whose
|
|
6315
|
+
// box holds a different shape from the config. A
|
|
6316
|
+
// stored string never compares equal to the array
|
|
6317
|
+
// the box parses to, so sharing `initial` meant
|
|
6318
|
+
// every Save rewrote a setting nobody had touched
|
|
6319
|
+
// -- which is how a value that worked became one
|
|
6320
|
+
// the node would not start with.
|
|
6321
|
+
`<textarea data-setting="${field.key}"${locked} data-type="addresses" data-initial="${escapeHtml(JSON.stringify(parseAddresses(addressText(value))))}" rows="3" placeholder="${escapeHtml(field.placeholder ?? '')}">${escapeHtml(addressText(value))}</textarea>`
|
|
6311
6322
|
: field.type === 'list'
|
|
6312
6323
|
? `<textarea data-setting="${field.key}"${locked} data-type="list"${initial} rows="${Math.min(8, Math.max(3, (Array.isArray(value) ? value.length : 0) + 1))}" placeholder="${escapeHtml(field.placeholder ?? '')}">${escapeHtml((Array.isArray(value) ? value : []).join('\n'))}</textarea>`
|
|
6313
6324
|
: field.type === 'boolean'
|
|
@@ -6337,6 +6348,56 @@ Every piece is hashed against the ` +
|
|
|
6337
6348
|
return panel;
|
|
6338
6349
|
}
|
|
6339
6350
|
|
|
6351
|
+
/**
|
|
6352
|
+
* The entries an address field holds, however the config wrote them.
|
|
6353
|
+
*
|
|
6354
|
+
* A list may be stored as an array or as one comma-separated string --
|
|
6355
|
+
* Express accepts both, and the documented example is a string. Shown
|
|
6356
|
+
* one per line either way, so that what a save reads back is the list
|
|
6357
|
+
* that was rendered rather than a different shape of the same thing.
|
|
6358
|
+
* @param {null|boolean|number|string|string[]} value - What the config holds.
|
|
6359
|
+
* @returns {string} - The textarea's contents.
|
|
6360
|
+
*/
|
|
6361
|
+
const addressText = (value) => {
|
|
6362
|
+
if (value === undefined || value === null) return '';
|
|
6363
|
+
const entries = Array.isArray(value) ? value : [String(value)];
|
|
6364
|
+
return entries
|
|
6365
|
+
.flatMap((entry) => String(entry).split(/[\n,]/))
|
|
6366
|
+
.map((entry) => entry.trim())
|
|
6367
|
+
.filter(Boolean)
|
|
6368
|
+
.join('\n');
|
|
6369
|
+
};
|
|
6370
|
+
|
|
6371
|
+
/**
|
|
6372
|
+
* What an address field means by what it holds.
|
|
6373
|
+
*
|
|
6374
|
+
* Express takes four things here and means something different by each:
|
|
6375
|
+
* `true` trusts any caller, a number is a hop count, and a string or an
|
|
6376
|
+
* array of them names the proxies. What was typed decides which.
|
|
6377
|
+
*
|
|
6378
|
+
* Commas separate as well as newlines. Express splits a comma list only
|
|
6379
|
+
* when it is handed a bare string and never inside an array, so one line
|
|
6380
|
+
* reading `10.0.0.1, 10.0.0.2` was saved as a one-element array holding
|
|
6381
|
+
* both -- which is not an address, and which Express rejects while the
|
|
6382
|
+
* app is being built, before the listener binds. The node then would not
|
|
6383
|
+
* start, and could not be corrected from the console that wrote it.
|
|
6384
|
+
* @param {string} text - What the box holds.
|
|
6385
|
+
* @returns {null|boolean|number|string[]} - What to save.
|
|
6386
|
+
*/
|
|
6387
|
+
const parseAddresses = (text) => {
|
|
6388
|
+
const lines = String(text)
|
|
6389
|
+
.split(/[\n,]/)
|
|
6390
|
+
.map((line) => line.trim())
|
|
6391
|
+
.filter(Boolean);
|
|
6392
|
+
if (!lines.length) return null;
|
|
6393
|
+
if (lines.length === 1) {
|
|
6394
|
+
if (lines[0] === 'true') return true;
|
|
6395
|
+
if (lines[0] === 'false') return false;
|
|
6396
|
+
if (!Number.isNaN(Number(lines[0]))) return Number(lines[0]);
|
|
6397
|
+
}
|
|
6398
|
+
return lines;
|
|
6399
|
+
};
|
|
6400
|
+
|
|
6340
6401
|
/**
|
|
6341
6402
|
* Collects every schema control into an update, grouped by top-level key.
|
|
6342
6403
|
*
|
|
@@ -6368,22 +6429,8 @@ Every piece is hashed against the ` +
|
|
|
6368
6429
|
continue;
|
|
6369
6430
|
}
|
|
6370
6431
|
if (type === 'boolean') value = element.checked;
|
|
6371
|
-
else if (type === 'addresses')
|
|
6372
|
-
|
|
6373
|
-
// each: `true` trusts any caller, a number is a hop count, and a
|
|
6374
|
-
// string or an array of them names the proxies. One per line, and
|
|
6375
|
-
// what was typed decides which of the four it is.
|
|
6376
|
-
const lines = String(element.value)
|
|
6377
|
-
.split('\n')
|
|
6378
|
-
.map((line) => line.trim())
|
|
6379
|
-
.filter(Boolean);
|
|
6380
|
-
if (!lines.length) value = null;
|
|
6381
|
-
else if (lines.length === 1 && (lines[0] === 'true' || lines[0] === 'false')) {
|
|
6382
|
-
value = lines[0] === 'true';
|
|
6383
|
-
} else if (lines.length === 1 && !Number.isNaN(Number(lines[0]))) {
|
|
6384
|
-
value = Number(lines[0]);
|
|
6385
|
-
} else value = lines;
|
|
6386
|
-
} else if (type === 'list') {
|
|
6432
|
+
else if (type === 'addresses') value = parseAddresses(element.value);
|
|
6433
|
+
else if (type === 'list') {
|
|
6387
6434
|
// One per line, which is how a person reads a list of trackers —
|
|
6388
6435
|
// and empty means an empty list rather than "unset", because a
|
|
6389
6436
|
// node with no trackers is a real thing to want and JSON would
|