Changeset 74c5abc in Klonkt for src/server.js


Ignore:
Timestamp:
06/28/2026 04:46:38 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
99989f9
Parents:
f79a471
Message:

feat(images): downscale remote (fediverse) avatars via a signed proxy

Remote avatars live on other servers, so the browser shrank full-res line-art to ~44px
(jagged). Fetch them once (SSRF-safe via safeFetch), downscale identically (lanczos -> WebP),
cache, serve. HMAC-signed proxy URLs → not an open resizer.

  • services/ActivityPubService.js — export safeFetch
  • services/ThumbnailService.js — getRemoteThumbnail + signed imgProxyUrl/verifyImg; +128px size
  • server.js — GET /img/a/:w signed proxy route
  • middleware/render.js — avatar(url,w) helper (local thumb / remote proxy)
  • views: news.ejs, fedi-node.ejs, following.ejs avatars -> avatar()
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/server.js

    rf79a471 r74c5abc  
    2020import { SqliteSessionStore } from './services/SqliteSessionStore.js';
    2121import { ensurePrimarySite } from './services/ensurePrimarySite.js';
    22 import { getThumbnail, THUMB_SIZES } from './services/ThumbnailService.js';
     22import { getThumbnail, getRemoteThumbnail, verifyImg, THUMB_SIZES } from './services/ThumbnailService.js';
    2323import { resolveSite, loadAudioTracks, loadTheme } from './middleware/site.js';
    2424import { isViewer } from './middleware/auth.js';
     
    234234});
    235235
     236// Signed remote-image proxy: downscale a REMOTE avatar/image (SSRF-safe via safeFetch)
     237// to a cached WebP, so line-art fediverse avatars don't render jagged. Only HMAC-signed
     238// URLs (produced by the avatar() view helper) are accepted — not an open resizer.
     239app.get('/img/a/:w', async (req, res) => {
     240  const w = parseInt(req.params.w, 10);
     241  const url = typeof req.query.u === 'string' ? req.query.u : '';
     242  const sig = typeof req.query.s === 'string' ? req.query.s : '';
     243  if (!THUMB_SIZES.has(w) || !verifyImg(url, w, sig)) return res.status(400).end();
     244  let file = null;
     245  try { file = await getRemoteThumbnail(url, w); } catch { /* fall through to original */ }
     246  if (!file) return res.redirect(302, url); // fetch/downscale failed → let the browser load the remote original
     247  res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
     248  res.setHeader('Cache-Control', isDev ? 'no-cache' : 'public, max-age=604800');
     249  res.type('webp');
     250  res.sendFile(file);
     251});
     252
    236253app.use('/media', express.static(process.env.MEDIA_PATH || './storage/media', {
    237254  // Public media (post covers, avatars) must be cross-origin embeddable by other
Note: See TracChangeset for help on using the changeset viewer.