A provider link can now carry `iconUrl`, an https address of the site's
own logo. The confirmation dialog downloads it as soon as it opens and
shows it at the top; importing stores it as the card's custom icon. No
extra confirmation step. Any failure is silent: the card falls back to
the built-in `icon` name, or to the name's initials.
The image goes through the same path as a local upload: the backend
returns a data URI, the frontend redraws it on a canvas into a 96x96 PNG,
and the provider save validation checks it again. The address itself is
never stored or rendered, and the import command never downloads.
The download (`deeplink/icon_fetch.rs`):
- Parsing drops addresses that are not https, use an IP or a non-default
port, carry credentials or exceed 2048 characters; the rest of the link
still imports.
- Every resolved address must be public, and the connection is pinned to
them so a second DNS answer cannot point it elsewhere. Redirects are
followed by hand, up to 3, and each hop is checked again.
- No system proxy, no cookies, no Referer, 5 s overall timeout, body read
as a stream and capped at 256 KB.
- Content-Type must be PNG, JPEG, WebP or SVG and match the file's magic
bytes; bitmap dimensions are read from the header and capped at 4096.
- 198.18.0.0/15 is allowed on purpose: Clash and Surge fake-ip mode
resolve every domain into it. The trade-off is written next to
`is_public_ipv4`.
- Errors are logged without the URL, which may carry query tokens.
Closes#8117
The icon picker gets an "Upload Image" button (PNG, JPG, WebP or SVG, up
to 2 MB and 4096 px). The image is redrawn on a canvas into a 96x96 PNG
and stored as a data URI in the existing `icon` column, so it travels
with the row through cloud sync, duplication and profiles. No new column,
no files on disk, nothing keyed by provider id (ids are shared across
apps). Opaque square images fill the icon box; anything else is padded to
the size of a built-in transparent logo. Uploading only changes the form;
nothing is written until Save.
The original file never reaches the database: SVG is loaded through
<img> (no scripts, no external requests) and rasterized, and re-encoding
drops EXIF. Custom icons are only ever rendered with <img>; remote URLs
are never loaded as icons.
Validation at every way in:
- Saving a provider or a universal provider, and syncing a universal
provider to its apps, fully decodes the PNG with the `png` crate
(already in the tree via `image`) under a 1 MiB decode limit and caps
it at 64 KiB and 256x256. Other strings still pass as icon names so old
rows stay editable.
- SQL import and cloud sync bypass saving, so the staging database drops
invalid custom icons from both `providers` and the universal provider
JSON before it replaces the main one. The rest of the backup imports
normally.
- Rendering checks the length and reads the PNG signature, IHDR length
and dimensions from the header before showing an image, and falls back
to initials if it fails to decode. Browsers sniff image formats, so
without the signature check a disguised GIF could slip past the size
limit.
Also:
- Uploads still being processed are dropped once the user removes the
icon, picks a built-in one, uploads again or closes the picker.
- Aggregation mode no longer derives model key prefixes from a custom
icon; it falls back to the provider name.
- The deep-link `icon` parameter only accepts built-in icon names; other
values are dropped instead of failing the link, so links cannot put a
data URI straight into the database.
Refs #8117