Skip to content

Latest commit

 

History

History
258 lines (194 loc) · 16.7 KB

plugins.md

File metadata and controls

258 lines (194 loc) · 16.7 KB

Plugins are written in JavaScript and run in a sandbox. This page covers installing them and what they can reach. Writing plugins covers making one.

What plugins are

A plugin can draw a panel on your dashboard, run a job on the hub, or both. They get fine-grained permissions to an internal API, so you can add features without waiting on a release.

The Spotify plugin's panel beside two monitors, with the cover art behind the dashboard

A panel joins the dashboard's layout, so it drags, pins and hides with the monitors. A plugin can also draw on every monitor tile or add its own heading to every monitor's settings.

The shipped plugins

Four plugins are in the catalogue, and their source is in plugins/.

Plugin What it does Asks for
Picture in picture Puts a button on every monitor that floats its camera above your other windows state:read, camera:view
Alert sounds Sounds a horn, a bell or an alarm when a defect is caught, on the monitors you pick state:read, sound
Progress reports Sends how far a print has got and how many defects it has seen, as often as you ask while the monitor is watching state:read, alert:send
Spotify Puts the current cover behind the dashboard, with the track and the transport in a panel net, oauth, background

Installing a plugin

The Plugins tab in Settings, with one plugin installed, three more in the catalogue, a box for a repository and a button to import a zip

The Plugins tab in Settings lists what you have installed and what the catalogue offers.

From How
The catalogue Open a plugin's page for its screenshots, README and the permissions it will ask for, then install from there
A GitHub repository Paste owner/repo, or owner/repo/path@branch for one inside a larger repo. A full https://github.com/owner/repo URL works too, and so does @tag or @sha in place of a branch
A file Import a .zip of the plugin's folder, up to 12 MB, holding one plugin.json and the files beside it

An install or update from a repository fails with GitHub's status if a plugin file can't be read, and only a 404 means the plugin has no such file.

The Spotify plugin's page in the store, with its screenshot, its README and the permissions it will ask for

Every installed plugin has the same page, opened from its card. A README is shown as Markdown only, so any HTML in it beyond text, links, images, tables and code is dropped.

The catalogue is filtered by where your hub runs, and an install from a repository or a zip is refused if the plugin names other platforms.

Verified and third party

Verified means the manifest and every file hash to what the catalogue pins at a commit. With the default catalogue these are the ones I have reviewed, and a hub whose catalogue_url points elsewhere checks against that file instead. Anything else is third party, so read it first. Both run under the same restrictions.

A zip that verifies is recorded as an install from the catalogue, so its page shows the README, icon and screenshots at the pinned commit and not the ones in the zip.

When you enable one, PrintGuard reads its code and shows in the same dialog where the code and the manifest disagree.

It says Meaning
Asks for a permission but never uses it The manifest is wider than the code needs
Uses a permission without asking. PrintGuard will refuse it The sandbox refuses it anyway, so this is early notice
Uses something, so its reach cannot be read from the code It builds a command, an address or a channel as it runs

It says what it found, it does not pass a verdict. A plugin that builds a URL as it runs is not a bad plugin, and the check that stops anything is the one at the sandbox edge.

Enabling a plugin

A plugin arrives switched off. Enable lists what it asks for, what each permission allows and the author's reason for it. It is all or nothing. Disabling keeps what you accepted.

The list names the addresses a network permission covers, where a sign-in happens and where its tokens come from, and the other plugins it calls, so you see its whole reach before it runs. An update that asks for more brings up the same dialog.

Updates

A repository install pins the commit it resolved to. Update re-resolves the branch it was installed from, or the default branch if it had none, and re-checks the hashes.

An update that asks for more stands the plugin down until you accept the wider list. More means a permission, an address, another plugin it calls, a channel it answers other plugins on, a sign-in scope or a different sign-in address. A different sign-in address also signs the plugin out.

Installed over Grants, stored data and credentials
The same repository and path Carry across
A zip over the zip it replaces Stored data and credentials carry across when it asks for nothing more, and the permissions are asked again
Anywhere else Start from scratch

A plugin saved before 2.6.0 that lists an address on your own network, or signs in at one, without Reach your own network stays installed with its data and credentials, off, and the hub warns at start that it now needs that permission. Accept it on the plugin's page to turn it back on. One whose sign-in is plain http is removed at start, since that sign-in can't be read any more, and the warning names it.

Permissions

Permission Lets the plugin
state:read Read monitor names, scores and alerts, and camera and printer status, and hear each score, alert, warning, printer update and error as it happens
camera:view Put a live feed in its own panel
sound Sound a short alert through the speakers, and play audio or video in its own panel
monitor:control Enable, disable and retune any monitor
printer:control Pause, resume and cancel prints
notify Raise a message in the dashboard
alert:send Send through whichever of your ntfy, Pushover, Telegram, Discord and desktop notification channels are set up
net Reach the addresses its manifest lists
net:local Reach addresses on this machine and the network around it, which covers private addresses, localhost, names ending .local, .lan, .home, .home.arpa, .internal or .localhost, a wildcard over one of those such as *.local or over the end of an address such as *.168.1.50, and an IPv4 address written inside an IPv6 one
monitor:manage Add monitors and delete them
camera:control Rename any camera and change its brightness, contrast, sharpness, crop, rotation and detection rate
camera:manage Register cameras and delete them, and scan for ones not yet registered
camera:frames Take a still of any camera and read the picture itself
history:read Read a monitor's score history and past alerts
printer:manage Connect, edit, test and delete printers, setting their credentials, refresh their webcams, and read which integrations exist
settings Change alert channels, theme and the rest of Settings, send a test alert, and read which notifiers exist
tokens Mint and revoke API tokens
oauth Sign you in to a service and use the result
link:provide Answer other plugins on the channels it offers
link:consume Ask the plugins and channels it names, and hear them
background Put a picture behind the dashboard, which the Glass theme shows through its see-through panels
routes Answer requests under /plugins/<id>/, reading each request's headers
gate See and refuse every other request to the hub. A yes is reused for 10 seconds for a request with the same method, path, query and headers it is shown

Every permission a manifest asks for carries a line saying why, in the plugin author's own words, and one without a reason will not install. That line sits beside PrintGuard's own description of the permission when you are asked to accept it.

A plugin can start a scan (camera:manage), a printer test (printer:manage) or a test alert (settings), but none of their answers reaches a plugin, so it never sees the result.

Storing its own data needs no permission. The store is capped at 16 KB and saved with your PrintGuard state.

What a plugin can and cannot do

A plugin gets the state its permissions allow and hands back what to draw and a list of things to do. PrintGuard does them, checking each against your permissions first.

flowchart LR
    engine["PrintGuard"] -- "the state its permissions allow" --> plugin["Plugin in its sandbox"]
    plugin -- "what to draw, things to do" --> check{"Granted?"}
    check -- "yes" --> engine
    check -- "no" --> refused["Refused"]
Loading

A plugin has up to three files, and each runs in a sandbox.

File Runs in
plugin.js A hidden iframe in the dashboard, with an opaque origin and default-src 'none'
panel.html A visible iframe with the same origin rules, where its own markup, styles and inline scripts are allowed
worker.js QuickJS compiled to WebAssembly on the hub, under wasmtime

The dashboard lets a frame load only from the hub and removes one that loads anything a second time, so a plugin that sends its frame elsewhere is stopped with "sandbox navigated away".

Attack What stops it
Take your credentials somewhere Neither sandbox has sockets. The browser files' policy is connect-src 'none' and lets no script load from an address, WebRTC is removed from their frames, and the hub file has no WASI network and no filesystem. The only request out is one through PrintGuard, to addresses the plugin declared, apart from the three permissions listed under the table. A redirect is never followed, by a request or by a WebSocket, and without Reach your own network a name is resolved once and only connected to if every address it resolves to is a public one. What a browser still allows is below
Read your credentials at all State is cut down to the fields a permission names. Printer configuration, notifier settings, MQTT credentials and API tokens are in no permission. The exceptions are routes and gate, which see the cookie and authorisation headers of the requests they answer
Read your camera frames A camera in a plugin's panel is a placeholder PrintGuard fills with its own player, and the video never enters the sandbox. Reading the picture itself is camera:frames, which is its own thing to agree to, and a plugin's own pages are refused the live stream
Hang or exhaust the hub The worker runs each call against a memory cap and a CPU budget, and a call that waits more than 5 seconds to start is dropped. A plugin that fails, or answers with anything but its data and a list of effects, is disabled and reported
Read the dashboard through the hub's own API Every request, sign-in and socket made for a plugin carries an X-PrintGuard-Plugin header the plugin can't set or remove, and the hub answers 403 to anything that arrives with it, at any address, name or proxy. The video server beside it needs a login only the hub holds
Open the hub by breaking its own gate A plugin holding gate that fails refuses every request until you enable it again, reinstall it or remove it. So does a running one the hub switches off because an update asks for more than you accepted, until you accept it or remove it, and one whose saved record can't be read at start, until you install it again or restart the hub
Do something it was not granted Every command maps to a permission, checked at the sandbox edge before it goes anywhere
Pretend to be PrintGuard A plugin.js has no styling and no markup of its own, and PrintGuard draws what it describes with its own components. A panel.html does draw itself, inside a panel carrying the plugin's name. A plugin's own pages are served into a sandboxed origin that is not the dashboard's
Change after review The manifest and every source file are pinned by SHA-256 at a commit

settings, printer:manage and camera:manage let a plugin name an address that PrintGuard then contacts, by testing an alert channel, adding a printer or a camera, or repointing the catalogue. They are the exception to the declared addresses, so each is highlighted when you are asked to accept it.

What a browser still allows

The two frames and a plugin's own pages are held by the browser's own rules, and those rules leave three things open.

Still possible What it carries
A frame sends itself to another address on your hub One request, to the hub. The dashboard removes the frame as soon as the page loads
A frame asks Safari to connect ahead to a host, with <link rel="preconnect"> No request and no body. The hostname is the plugin's to choose, so a few bytes can leave in the lookup
A page the plugin serves under /plugins/<id>/ sends your browser to another site, or opens a WebRTC connection Whatever the plugin's worker put in that page, in the address it goes to. The page loads nothing from another host and makes no request of its own, but it is a tab like any other, and no policy stops a tab leaving. routes is the permission that allows it

The frame and page rules are tested in Chromium and in WebKit, which is Safari's engine. Firefox is not in the test run.

Credentials

A plugin can set a credential and never read one back. Printer passwords, notifier keys and API tokens go in and do not come out. A plugin with printer:manage or settings can test a saved printer or alert channel with its saved key, at the address it was saved with.

Its own credentials work the same way. A plugin that needs a key shows a field for it on its page in the Plugins tab, once it's enabled. Paste the value there and PrintGuard holds it and fills it in as the plugin's requests leave.

Be clear on what that buys. The value never enters the sandbox, the plugin's stored data, the state the dashboard reads or a bug report. It does not stop a plugin you granted the network from sending a secret to an address it declared. Those addresses are in front of you before you enable it, the code check holds them against what it calls, and a listed plugin has been reviewed. That is the control.

A plugin that signs you in to a service, such as Spotify, needs an app of your own with that service. No plugin carries one, since a shared app is what providers hand out quota and terms against.

  1. Open the plugin's page and follow Create one to the service's developer page.
  2. Register an app there, giving it the redirect URI the page shows.
  3. Paste the app's client id into the page.
  4. Press Connect and sign in.

The redirect URI is the address you opened PrintGuard at with /oauth/callback on the end, written as 127.0.0.1 since providers stopped accepting localhost. PrintGuard runs the sign-in with PKCE, so there is no client secret to paste. Disconnect forgets the sign-in and keeps the client id.

Switching plugins off at boot

A plugin holding Authorise every request can lock you out. One that fails locks everyone out, since a hub with a broken gate refuses every request. So does one an update switched off until you accept what it now asks for.

To start the hub with plugins off, add PRINTGUARD_PLUGINS=off to its environment, then remove the plugin or enable it again.

Deployment has the compose snippet and what the routes and gate permissions mean for an exposed hub.

Writing a plugin

A plugin is a folder with a manifest and up to three source files, with no build step. Writing plugins starts from a working one and covers the API, the limits and publishing to the catalogue.