fix: permit dictionary services in companion CSP (1.0.8)

The companion served a connect-src self policy while its vocabulary card fetched Wiktionary and MyMemory directly. Allow only those two origins and cover the real installed browser policy in tests.
This commit is contained in:
nessar committed 2026-09-23 20:06:10 +02:00
1 parent 62148d73cb
commit 10e86b0167
12 files changed
+50 -10

No files matched your search

+3 -3
View File
@@ -86,11 +86,11 @@ navigateur.
## Installation manuelle (sans compilation)
Version publiée : **1.0.7**. Version compatible : **Jellyfin Server 12.1.0**, Linux
Version publiée : **1.0.8**. Version compatible : **Jellyfin Server 12.1.0**, Linux
x64. Les autres versions ne sont pas déclarées compatibles.
1. Télécharger [le paquet 1.0.7](dist/compagnon-1.0.7-jellyfin-12.1.0.zip)
et sa [somme SHA-256](dist/compagnon-1.0.7-jellyfin-12.1.0.zip.sha256).
1. Télécharger [le paquet 1.0.8](dist/compagnon-1.0.8-jellyfin-12.1.0.zip)
et sa [somme SHA-256](dist/compagnon-1.0.8-jellyfin-12.1.0.zip.sha256).
2. Arrêter Jellyfin. Extraire le dossier `Compagnon` de l'archive dans le dossier
`plugins` de ses données. Avec l'image Docker officielle et son volume
`/config`, le résultat est `/config/plugins/Compagnon/Compagnon.dll`.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
3519d39c5a2f5a36da5db25a5fdc0660630f91604091aa1976b7b0b09a07c092 compagnon-1.0.8-jellyfin-12.1.0.zip
+8
View File
@@ -7,6 +7,14 @@
"owner": "nessar",
"category": "General",
"versions": [
{
"version": "1.0.8",
"changelog": "Correction de la fiche de vocabulaire : la politique de sécurité autorise les requêtes au Wiktionnaire et à MyMemory.",
"targetAbi": "12.1.0.0",
"sourceUrl": "https://gitlab.nessar.fr/nessar/subtitle-compagnion/-/releases/v1.0.8/downloads/compagnon-1.0.8-jellyfin-12.1.0.zip",
"checksum": "323e96043e0aba508a1e4730a079c218",
"timestamp": "2026-09-23T18:05:33Z"
},
{
"version": "1.0.7",
"changelog": "Recherche d'expressions de plusieurs mots dans la fiche de vocabulaire, avec sens du Wiktionnaire ou traduction proposée par MyMemory.",
+1 -1
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from zipfile import ZipFile, ZIP_DEFLATED
import hashlib
archive = Path('dist/compagnon-1.0.7-jellyfin-12.1.0.zip')
archive = Path('dist/compagnon-1.0.8-jellyfin-12.1.0.zip')
archive.parent.mkdir(exist_ok=True)
with ZipFile(archive, 'w', ZIP_DEFLATED) as package:
package.write('src/Compagnon/bin/Release/net10.0/Compagnon.dll', 'Compagnon/Compagnon.dll')
+1 -1
View File
@@ -4,7 +4,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.0.7</Version>
<Version>1.0.8</Version>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
+1 -1
View File
@@ -53,7 +53,7 @@ public sealed class CompagnonController : ControllerBase
};
if (contentType is null) return NotFound();
Response.Headers.ContentSecurityPolicy = "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'";
Response.Headers.ContentSecurityPolicy = "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self' https://fr.wiktionary.org https://api.mymemory.translated.net; base-uri 'none'; frame-ancestors 'none'; form-action 'self'";
Response.Headers.XContentTypeOptions = "nosniff";
Response.Headers.CacheControl = "no-store";
var stream = typeof(Plugin).Assembly.GetManifestResourceStream($"Compagnon.Web.{asset ?? "index.html"}");
+8 -1
View File
@@ -1,9 +1,13 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFile } from 'node:fs/promises';
import { chromium, expect } from '@playwright/test';
import { showSettings } from './helpers.mjs';
test('English words and selected expressions open a French card without sending the subtitle or Jellyfin token', async () => {
const controller = await readFile('src/Compagnon/CompagnonController.cs', 'utf8');
const csp = /Response\.Headers\.ContentSecurityPolicy = "([^"]+)"/.exec(controller)?.[1];
assert.ok(csp, 'the browser test must use the plugin CSP');
const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 375, height: 812 } });
@@ -31,7 +35,10 @@ test('English words and selected expressions open a French card without sending
await page.route('http://compagnon.test/**', async route => {
const path = new URL(route.request().url()).pathname;
if (path.startsWith('/Compagnon/') && !path.includes('/Subtitles/')) {
return route.fulfill({ path: `src/Compagnon/Web/${path.split('/').at(-1) || 'index.html'}` });
return route.fulfill({
path: `src/Compagnon/Web/${path.split('/').at(-1) || 'index.html'}`,
headers: { 'Content-Security-Policy': csp },
});
}
const json = path.endsWith('/Sessions') ? [{ Id: 'session', UserId: 'alice', NowPlayingItem: { Id: 'film' }, PlayState: { MediaSourceId: 'source', SubtitleStreamIndex: 4, PositionTicks: 15, IsPaused: true } }]
: path.endsWith('/PlaybackInfo') ? { MediaSources: [{ Id: 'source', MediaStreams: [
+24
View File
@@ -98,7 +98,31 @@ test('installed plugin serves the companion without administrator authentication
const response = await fetch(`${base}/Compagnon/`);
assert.equal(response.status, 200);
assert.match(response.headers.get('content-type'), /text\/html/);
assert.match(response.headers.get('content-security-policy'), /connect-src 'self' https:\/\/fr\.wiktionary\.org https:\/\/api\.mymemory\.translated\.net/);
assert.match(await response.text(), /Compagnon de sous-titres/);
const browser = await chromium.launch();
try {
const page = await browser.newPage();
const urls = [
'https://fr.wiktionary.org/w/api.php?action=parse&page=appliances&prop=text&format=json&origin=*',
'https://api.mymemory.translated.net/get?q=household%20appliances&langpair=en%7Cfr',
];
for (const url of urls) {
await page.route(url, route => route.fulfill({
json: { ok: true },
headers: { 'Access-Control-Allow-Origin': '*' },
}));
}
await page.goto(`${base}/Compagnon/`);
const results = await page.evaluate(async requestedUrls => Promise.all(requestedUrls.map(async url => {
const response = await fetch(url, { credentials: 'omit', referrerPolicy: 'no-referrer' });
return response.ok;
})), urls);
assert.deepEqual(results, [true, true]);
} finally {
await browser.close();
}
});
test('real playback is followed without controlling it or following another account', async () => {
+1 -1
View File
@@ -41,7 +41,7 @@ for format in srt vtt ass; do
done
node tests/setup.mjs
mkdir -p .test-state/package
python3 -m zipfile -e dist/compagnon-1.0.7-jellyfin-12.1.0.zip .test-state/package
python3 -m zipfile -e dist/compagnon-1.0.8-jellyfin-12.1.0.zip .test-state/package
docker cp .test-state/package/Compagnon "$container:/config/plugins/Compagnon"
docker restart "$container" >/dev/null
export JELLYFIN_URL="http://$(docker port "$container" 8096/tcp)"
+1 -1
View File
@@ -1,4 +1,4 @@
export const version = '1.0.7';
export const version = '1.0.8';
export class ApiError extends Error {
status: number;
+1 -1
View File
@@ -21,7 +21,7 @@ export default defineConfig({
build: {
outDir: fileURLToPath(new URL('../src/Compagnon/Web', import.meta.url)),
emptyOutDir: true,
// The controller's CSP allows 'self' only: never emit an inline script.
// The controller's script-src allows 'self' only: never emit an inline script.
modulePreload: { polyfill: false },
rollupOptions: {
output: {