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:
1 parent
62148d73cb
commit
10e86b0167
12 files changed
+50
-10
No files matched your search
@@ -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`.
|
||||
|
||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
3519d39c5a2f5a36da5db25a5fdc0660630f91604091aa1976b7b0b09a07c092 compagnon-1.0.8-jellyfin-12.1.0.zip
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"}");
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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: {
|
||||
|
||||
Reference in new issue
Block a user