openpencil/vite/font-proxy.ts

95 lines
3.6 KiB
TypeScript
Raw Normal View History

2026-08-26 22:42:41 +00:00
import type { Plugin } from 'vite'
2026-09-05 08:25:33 +00:00
// Dev-only same-origin proxy for CORS-hostile online-font endpoints.
2026-08-26 22:42:41 +00:00
//
// unifont's `google` provider reads `https://fonts.google.com/metadata/fonts` to
2026-09-05 08:25:33 +00:00
// build its family catalog and `https://fonts.googleapis.com/css2` to resolve
// font faces. The metadata endpoint never sends `Access-Control-Allow-Origin`
2026-08-26 22:42:41 +00:00
// and sets `cross-origin-resource-policy: same-site`, so a browser app served on
// its own origin (any origin that is not Google) cannot read it directly — the
// provider fails to initialize with "Could not initialize provider `google`".
2026-09-05 08:25:33 +00:00
// The css2 endpoint's OPTIONS preflight carries no CORS headers either, so some
// browsers (notably Firefox) block the font-discovery fetch.
2026-08-26 22:42:41 +00:00
//
2026-09-05 08:25:33 +00:00
// The browser relays such requests here instead (see `createBrowserFontFetch()` in
2026-08-26 22:42:41 +00:00
// src/app/editor/fonts), which keeps the request same-origin and forwards it
// server-side to Google. This is a W4C fork delta; upstream openpencil does not
2026-09-05 08:25:33 +00:00
// ship it because Tauri goes through the Rust command that is not CORS-bound.
2026-08-26 22:42:41 +00:00
export const FONT_PROXY_PREFIX = '/__font-proxy'
// Only these hosts are CORS-hostile from the browser; everything else
// (notably fonts.gstatic.com glyph files) already sends CORS headers and is
// fetched directly by the browser.
2026-09-05 08:25:33 +00:00
// - fonts.google.com — metadata never sends CORS headers.
// - fonts.googleapis.com — css2 font-discovery OPTIONS preflight returns no
// CORS headers, so Firefox blocks the discovery fetch unless it is proxied.
const PROXIED_HOSTS = new Set(['fonts.google.com', 'fonts.googleapis.com'])
const FONT_PROXY_USER_AGENT =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36'
2026-08-26 22:42:41 +00:00
export function createDevFontProxyPlugin(): Plugin {
return {
name: 'open-pencil-dev-font-proxy',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
let pathname: string
try {
pathname = new URL(req.url ?? '/', 'http://dev').pathname
} catch {
next()
return
}
if (pathname !== FONT_PROXY_PREFIX) {
next()
return
}
const target = new URL(req.url ?? '/', 'http://dev').searchParams.get('url')
if (!target) {
res.statusCode = 400
res.end('missing url')
return
}
2026-09-05 08:25:33 +00:00
// The browser forwards the original `user-agent` here because it cannot
// send one cross-origin; unifont uses it to pick the glyph format.
const ua = new URL(req.url ?? '/', 'http://dev').searchParams.get('ua')
2026-08-26 22:42:41 +00:00
let targetURL: URL
try {
targetURL = new URL(decodeURIComponent(target))
} catch {
res.statusCode = 400
res.end('invalid url')
return
}
if (targetURL.protocol !== 'https:' || !PROXIED_HOSTS.has(targetURL.host)) {
res.statusCode = 403
res.end('proxied host denied')
return
}
try {
const upstream = await fetch(targetURL, {
headers: {
2026-09-05 08:25:33 +00:00
'user-agent': ua ?? FONT_PROXY_USER_AGENT,
2026-08-26 22:42:41 +00:00
accept: 'application/json, text/plain, */*',
},
redirect: 'follow',
})
const body = Buffer.from(await upstream.arrayBuffer())
res.statusCode = upstream.status
const type = upstream.headers.get('content-type')
if (type) res.setHeader('content-type', type)
res.setHeader('access-control-allow-origin', '*')
res.end(body)
} catch (e) {
res.statusCode = 502
res.end(String(e))
}
})
},
}
}