From 91e0f526da83a647d1fdf4d9e13cb5f2184484cc Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:14:04 -0700 Subject: [PATCH] Introduce AssetService --- app/AssetService.main.ts | 83 ++++++++++++++++++++++++++++++++ app/main.main.ts | 2 + background.html | 24 +++++---- stylesheets/_fontfaces.scss | 22 ++++----- stylesheets/tailwind-config.css | 2 +- ts/util/avatarDataToBytes.dom.ts | 2 +- 6 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 app/AssetService.main.ts diff --git a/app/AssetService.main.ts b/app/AssetService.main.ts new file mode 100644 index 0000000000..0ca2182c8b --- /dev/null +++ b/app/AssetService.main.ts @@ -0,0 +1,83 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { createReadStream } from 'node:fs'; +import { join } from 'node:path'; +import { protocol } from 'electron'; + +import type { OptionalResourceService } from './OptionalResourceService.main.ts'; +import { getAppRootDir } from '../ts/util/appRootDir.main.ts'; +import { toWebStream } from '../ts/util/toWebStream.node.ts'; + +const LOCAL_ASSETS = new Set([ + 'fonts/signal-symbols/SignalSymbolsVariable.woff2', + 'fonts/stories/Hatsuishi-Regular.woff2', + 'fonts/stories/EBGaramond-Regular.ttf', + 'fonts/stories/Parisienne-Regular.ttf', + 'fonts/stories/BarlowCondensed-Medium.ttf', + 'fonts/inter-v3.19/Inter-Medium.woff2', + 'fonts/inter-v3.19/Inter-Regular.woff2', + 'fonts/inter-v3.19/Inter-SemiBold.woff2', + 'fonts/inter-v3.19/Inter-BoldItalic.woff2', + 'fonts/inter-v3.19/Inter-Bold.woff2', + 'fonts/inter-v3.19/Inter-Italic.woff2', + 'fonts/inter-v3.19/Inter-SemiBoldItalic.woff2', +]); + +// pathname to optional resource name +const OPTIONAL_ASSETS = new Map([]); + +export class AssetService { + readonly #resourceService: OptionalResourceService; + + private constructor(resourceService: OptionalResourceService) { + this.#resourceService = resourceService; + + protocol.handle('asset', async req => { + const url = new URL(req.url); + + return this.#fetch(url.pathname); + }); + } + + public static create(resourceService: OptionalResourceService): AssetService { + return new AssetService(resourceService); + } + + async #fetch(pathname: string): Promise { + if (!pathname.startsWith('/')) { + return new Response('invalid pathname', { status: 400 }); + } + + const path = pathname.slice(1); + + if (LOCAL_ASSETS.has(path)) { + const stream = createReadStream( + join(getAppRootDir(), ...path.split('/')) + ); + return new Response(toWebStream(stream), { + status: 200, + headers: { + 'cache-control': 'public, max-age=2592000, immutable', + }, + }); + } + + const optional = OPTIONAL_ASSETS.get(path); + if (optional == null) { + return new Response('asset not found', { status: 404 }); + } + + const asset = await this.#resourceService.getData(optional); + if (!asset) { + return new Response('optional asset not found', { status: 404 }); + } + + return new Response(asset, { + status: 200, + headers: { + 'cache-control': 'public, max-age=2592000, immutable', + }, + }); + } +} diff --git a/app/main.main.ts b/app/main.main.ts index 3be19f604e..9091950cc4 100644 --- a/app/main.main.ts +++ b/app/main.main.ts @@ -84,6 +84,7 @@ import { import { SystemTraySettingCache } from './SystemTraySettingCache.node.ts'; import { OptionalResourceService } from './OptionalResourceService.main.ts'; import { EmojiService } from './EmojiService.main.ts'; +import { AssetService } from './AssetService.main.ts'; import { SystemTraySetting, shouldMinimizeToSystemTray, @@ -2082,6 +2083,7 @@ app.on('ready', async () => { join(userDataPath, 'optionalResources') ); await EmojiService.create(resourceService); + AssetService.create(resourceService); if (!resolvedTranslationsLocale) { preferredSystemLocales = resolveCanonicalLocales( diff --git a/background.html b/background.html index 90442ef412..1126f07fa4 100644 --- a/background.html +++ b/background.html @@ -17,7 +17,7 @@ content="default-src 'none'; child-src 'self'; connect-src 'self' https: wss: attachment:; - font-src 'self'; + font-src 'self' asset:; form-action 'self'; frame-src 'none'; img-src 'self' blob: data: emoji: attachment:; @@ -29,56 +29,56 @@ Signal + { const font = new window.FontFace( 'Inter', - 'url("fonts/inter-v3.19/Inter-Regular.woff2")' + 'url("asset:///fonts/inter-v3.19/Inter-Regular.woff2")' ); await font.load();