95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
import type { Plugin } from 'vite'
|
|
|
|
// Dev-only same-origin proxy for CORS-hostile online-font endpoints.
|
|
//
|
|
// unifont's `google` provider reads `https://fonts.google.com/metadata/fonts` to
|
|
// build its family catalog and `https://fonts.googleapis.com/css2` to resolve
|
|
// font faces. The metadata endpoint never sends `Access-Control-Allow-Origin`
|
|
// 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`".
|
|
// The css2 endpoint's OPTIONS preflight carries no CORS headers either, so some
|
|
// browsers (notably Firefox) block the font-discovery fetch.
|
|
//
|
|
// The browser relays such requests here instead (see `createBrowserFontFetch()` in
|
|
// 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
|
|
// ship it because Tauri goes through the Rust command that is not CORS-bound.
|
|
|
|
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.
|
|
// - 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'
|
|
|
|
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
|
|
}
|
|
// 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')
|
|
|
|
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: {
|
|
'user-agent': ua ?? FONT_PROXY_USER_AGENT,
|
|
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))
|
|
}
|
|
})
|
|
},
|
|
}
|
|
}
|