polotno-node 3.14.0 → 3.15.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/README.md +37 -7
- package/dist/assets/{index-BZiOShMo.js → index-DO5siyU6.js} +137 -137
- package/dist/assets/{index.es-C6iz1CEp.js → index.es-NZjPkaXm.js} +1 -1
- package/dist/assets/{jspdf.es.min-DcBWbXDt.js → jspdf.es.min-Cjjh_5Ab.js} +3 -3
- package/dist/assets/{lib-Blxs_aC-.js → lib-IzftT-th.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/errors.d.ts +5 -0
- package/lib/font-cache.d.ts +1 -0
- package/lib/font-cache.js +23 -3
- package/lib/instance.d.ts +6 -1
- package/lib/instance.js +191 -125
- package/lib/ssrf-guard.d.ts +15 -13
- package/lib/ssrf-guard.js +17 -129
- package/lib/types.d.ts +29 -18
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -80,8 +80,10 @@ const instance = await createInstance({
|
|
|
80
80
|
// for full Chrome. Ignored if you pass `browser`.
|
|
81
81
|
headless: true,
|
|
82
82
|
|
|
83
|
-
// useFontCache - cache Google Fonts responses (in memory + on disk in
|
|
84
|
-
// OS temp folder, shared across processes)
|
|
83
|
+
// useFontCache - cache Google Fonts responses (in memory + on disk in a
|
|
84
|
+
// private folder in the OS temp folder, shared across this user's processes)
|
|
85
|
+
// and serve them to every page. Only https responses from
|
|
86
|
+
// fonts.googleapis.com and fonts.gstatic.com are cached.
|
|
85
87
|
// Every page starts with an empty HTTP cache, so without it each render
|
|
86
88
|
// re-downloads the same fonts and bursts of parallel renders may get
|
|
87
89
|
// throttled by Google Fonts (fonts time out and text falls back to a
|
|
@@ -93,6 +95,9 @@ const instance = await createInstance({
|
|
|
93
95
|
// - Modify headers like User-Agent to access protected image resources
|
|
94
96
|
// - Add authentication tokens or credentials to requests
|
|
95
97
|
// - Log or monitor network traffic
|
|
98
|
+
// It runs after polotno-node's own handling: local files and cached fonts
|
|
99
|
+
// are answered before it, and requests the URL policy blocks never reach it
|
|
100
|
+
// (see "Restricted hosts" and "Local files").
|
|
96
101
|
requestInterceptor: (request) => {
|
|
97
102
|
const targetUrl = request.url();
|
|
98
103
|
if (/\.(png|jpe?g)(\?|$)/i.test(targetUrl)) {
|
|
@@ -321,7 +326,7 @@ Native audio processing and finalization can also take time without intermediate
|
|
|
321
326
|
Custom clients with older video exporters can report fewer stages.
|
|
322
327
|
|
|
323
328
|
The inactivity timer runs in Node, so it can report a blocked renderer without another CDP request.
|
|
324
|
-
It cannot fire while Node itself is blocked,
|
|
329
|
+
It cannot fire while Node itself is blocked, for example during base64 conversion.
|
|
325
330
|
An independent parent-process monitor is necessary to detect that condition during the block.
|
|
326
331
|
Existing `protocolTimeout` and navigation timeouts still apply.
|
|
327
332
|
|
|
@@ -360,7 +365,7 @@ By default, when exporting video and **only** when using the default local clien
|
|
|
360
365
|
- Download all **remote** video elements (`type: 'video'`) and `json.audios` sources into a temporary folder
|
|
361
366
|
- Deduplicate downloads (same URL is downloaded once)
|
|
362
367
|
- Probe downloaded remote videos with `ffprobe`
|
|
363
|
-
- Conditionally normalize incompatible downloaded videos into MP4 with H.264 video, `yuv420p`, and AAC audio for better Chromium render compatibility
|
|
368
|
+
- Conditionally normalize incompatible downloaded videos into MP4 with H.264 video, `yuv420p`, and AAC audio for better Chromium render compatibility. `ffprobe` and `ffmpeg` run as child processes and do not block Node. Each run is stopped at a time limit: a probe after 30 seconds (the video is then used as it is), and a conversion after 10 minutes (the export then fails).
|
|
364
369
|
- Rewrite `src` to a URL that `polotno-node` answers from the temporary folder
|
|
365
370
|
|
|
366
371
|
For best compatibility, use MP4 with H.264 (`yuv420p`) video and AAC audio.
|
|
@@ -381,17 +386,42 @@ A `file:` source anywhere in a design — an image, an SVG, a video, an audio tr
|
|
|
381
386
|
|
|
382
387
|
Because the bytes come from Node, a browser running somewhere else (`browser: await puppeteer.connect(...)`, browserless) can render a design that names files on this machine.
|
|
383
388
|
|
|
384
|
-
|
|
389
|
+
Local files are served before, and without, your `requestInterceptor`: it never sees these reads, so it cannot block them. If you render designs from users you do not trust, turn local files off:
|
|
385
390
|
|
|
386
|
-
|
|
391
|
+
```js
|
|
392
|
+
await instance.jsonToImageBase64(json, { allowLocalFiles: false });
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
With `allowLocalFiles: false`, a design that names any `file:` source is rejected with a `DESIGN_INVALID` error before rendering. Each `details.errors` entry names the field. The remote media that a video export downloads for itself is still served.
|
|
396
|
+
|
|
397
|
+
Whatever `allowLocalFiles` is set to, the render page cannot read files directly: a `file:` request from the page outside the client's own folder is refused. A `file:` URL that a script inside `instance.run()` loads itself (not through the design) is refused too: the script gets a `403` whose text explains why, and the refusal is logged with `console.warn`. Put the `file:` URL in the design instead, or read the file in Node and pass its contents in.
|
|
398
|
+
|
|
399
|
+
### Restricted hosts
|
|
400
|
+
|
|
401
|
+
Every http(s) request the render makes is checked first: each request the render page sends (images, SVGs, fonts, media, and each redirect hop), and the server-side media downloads of a video export. Cloud-metadata addresses (e.g. `169.254.169.254`) are blocked by default. Normal URLs, `localhost`, and private hosts still work. A blocked asset fails like any asset that does not load (for example `IMAGE_FAILED`, unless `skipImageError` is set). The check runs before your `requestInterceptor`. To restrict further, in any export method:
|
|
387
402
|
|
|
388
403
|
```js
|
|
389
|
-
await instance.
|
|
404
|
+
await instance.jsonToImageBase64(json, {
|
|
390
405
|
blockPrivateNetwork: true, // also block loopback / private IPs
|
|
391
406
|
fetchGuard: (url) => isAllowed(url), // custom allow rule (checked per redirect hop)
|
|
392
407
|
});
|
|
393
408
|
```
|
|
394
409
|
|
|
410
|
+
A `fetchGuard` replaces the built-in checks for the design's URLs. The client's own services skip both options, so a strict guard cannot cost the render its license or its fonts: the Polotno license check (`https://api.polotno.com`) and Google Fonts (`https://fonts.googleapis.com`, `https://fonts.gstatic.com`).
|
|
411
|
+
|
|
412
|
+
Each blocked request is logged with `console.warn`, naming the URL and the rule that refused it.
|
|
413
|
+
|
|
414
|
+
The guard is also published for your own fetches of design URLs:
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
import { assertUrlAllowed, guardedFetch } from 'polotno-node/ssrf-guard';
|
|
418
|
+
|
|
419
|
+
await assertUrlAllowed(url, { blockPrivateNetwork: true }); // throws FETCH_FAILED
|
|
420
|
+
const res = await guardedFetch(url, { blockPrivateNetwork: true }); // checks every redirect hop
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Hostnames are resolved and every address is checked. A hostile DNS server can still answer differently to the browser (DNS rebinding); to close that, also restrict egress at the network level.
|
|
424
|
+
|
|
395
425
|
### `attrs.assetLoadTimeout`
|
|
396
426
|
|
|
397
427
|
You can add `assetLoadTimeout` attribute to `attrs` object. It will be used to set timeout for loading assets. By default it is 30000ms.
|