Commit Graph
2 Commits
Author SHA1 Message Date
Jason bee47fdf48 feat(deeplink): let provider links bring their own icon via iconUrl
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
2026-10-12 23:26:43 +08:00
Jason 21203803ed feat(providers): upload a custom provider icon
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
2026-10-12 23:26:43 +08:00