allowedMimeTypes : jokers de préfixe
isAllowedMedia gère désormais "video/*", "*/*" et "*" en plus des types
exacts (comparaison insensible à la casse). parseConfig valide la forme des
entrées et lève ConfigError sur un joker malformé ("video/**", "vi*deo/mp4",
"video/") au lieu de laisser passer une liste qui ne matche rien.
Schéma + doc mis à jour. 62 tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
5bff28fefe
commit
0f5bdb7080
6 changed files with 66 additions and 20 deletions
32
README.md
32
README.md
|
|
@ -165,22 +165,22 @@ Prévoir aussi l'espace disque sur le volume de la cible.
|
|||
Un seul fichier JSON (voir [`config.example.json`](config.example.json), schéma dans
|
||||
[`config.schema.json`](config.schema.json)). Chemin résolu depuis le dossier courant.
|
||||
|
||||
| Clé JSON | Défaut | Rôle |
|
||||
| ---------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `port` | `3000` | Port d'écoute |
|
||||
| `host` | `0.0.0.0` | Interface d'écoute |
|
||||
| `publicUrl` | — | **Obligatoire.** URL externe de base (redirect_uri, post-logout) |
|
||||
| `targets` | — | **Obligatoire.** Liste `[{ name, label?, dir, allowDirectories? }]` (≥ 1) ; la 1ʳᵉ est la cible par défaut |
|
||||
| `targets[].allowDirectories` | `false` | Autoriser le dépôt de dossiers (arborescence recréée sous `dir`) |
|
||||
| `maxFileSizeMb` | `512` | Taille maximale par fichier, en Mo (`10240` = 10 Go) |
|
||||
| `logLevel` | `info` | `fatal` \| `error` \| `warn` \| `info` \| `debug` \| `trace` \| `silent` |
|
||||
| `allowedMimeTypes` | _(liste média)_ | Tableau de types MIME, ou `"all"` ; absent => liste par défaut |
|
||||
| `session.key` | — | **Obligatoire.** 32 octets base64 (`openssl rand -base64 32`) |
|
||||
| `session.maxAgeHours` | `12` | Durée de vie absolue de la session |
|
||||
| `oidc.issuer` | — | **Obligatoire.** Issuer OIDC |
|
||||
| `oidc.clientId` | — | **Obligatoire.** `client_id` du client confidentiel |
|
||||
| `oidc.clientSecret` | — | **Obligatoire.** Secret du client confidentiel |
|
||||
| `oidc.scopes` | `["openid","profile","email"]` | Scopes demandés (`openid` forcé) |
|
||||
| Clé JSON | Défaut | Rôle |
|
||||
| ---------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `port` | `3000` | Port d'écoute |
|
||||
| `host` | `0.0.0.0` | Interface d'écoute |
|
||||
| `publicUrl` | — | **Obligatoire.** URL externe de base (redirect_uri, post-logout) |
|
||||
| `targets` | — | **Obligatoire.** Liste `[{ name, label?, dir, allowDirectories? }]` (≥ 1) ; la 1ʳᵉ est la cible par défaut |
|
||||
| `targets[].allowDirectories` | `false` | Autoriser le dépôt de dossiers (arborescence recréée sous `dir`) |
|
||||
| `maxFileSizeMb` | `512` | Taille maximale par fichier, en Mo (`10240` = 10 Go) |
|
||||
| `logLevel` | `info` | `fatal` \| `error` \| `warn` \| `info` \| `debug` \| `trace` \| `silent` |
|
||||
| `allowedMimeTypes` | _(liste média)_ | `"all"`, ou tableau de types exacts et/ou jokers de préfixe (`"video/*"`, `"*"`) ; absent => liste par défaut |
|
||||
| `session.key` | — | **Obligatoire.** 32 octets base64 (`openssl rand -base64 32`) |
|
||||
| `session.maxAgeHours` | `12` | Durée de vie absolue de la session |
|
||||
| `oidc.issuer` | — | **Obligatoire.** Issuer OIDC |
|
||||
| `oidc.clientId` | — | **Obligatoire.** `client_id` du client confidentiel |
|
||||
| `oidc.clientSecret` | — | **Obligatoire.** Secret du client confidentiel |
|
||||
| `oidc.scopes` | `["openid","profile","email"]` | Scopes demandés (`openid` forcé) |
|
||||
|
||||
Seule variable d'environnement : `MEDIADROP_CONFIG` (chemin du fichier, défaut `./config.json`).
|
||||
|
||||
|
|
|
|||
|
|
@ -44,10 +44,14 @@
|
|||
"default": "info"
|
||||
},
|
||||
"allowedMimeTypes": {
|
||||
"description": "Liste blanche de types MIME, ou \"all\". Absent => liste média par défaut.",
|
||||
"description": "\"all\", ou liste de types exacts et/ou jokers de préfixe (\"video/*\", \"*/*\", \"*\"). Absent => liste média par défaut.",
|
||||
"oneOf": [
|
||||
{ "const": "all" },
|
||||
{ "type": "array", "items": { "type": "string" }, "minItems": 1 }
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": "string", "pattern": "^(\\*|\\*/\\*|[^/*\\s]+/(\\*|[^/*\\s]+))$" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"session": {
|
||||
|
|
|
|||
|
|
@ -150,6 +150,15 @@ function parseMimePolicy(value: unknown): MimePolicy {
|
|||
if (list.length === 0) {
|
||||
throw new ConfigError('allowedMimeTypes ne contient aucun type MIME valide');
|
||||
}
|
||||
for (const entry of list) {
|
||||
const valid = entry === '*' || entry === '*/*' || /^[^/*\s]+\/(\*|[^/*\s]+)$/.test(entry);
|
||||
if (!valid) {
|
||||
throw new ConfigError(
|
||||
`allowedMimeTypes : entrée invalide « ${entry} » ` +
|
||||
'(formes acceptées : "type/sous-type", "type/*", "*/*", "*")',
|
||||
);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,15 +38,30 @@ const MIME_TO_EXTENSION: Readonly<Record<string, string>> = {
|
|||
'audio/mp4': '.m4a',
|
||||
};
|
||||
|
||||
/** Politique de filtrage : une liste blanche explicite, ou `'all'` pour tout accepter. */
|
||||
/**
|
||||
* Politique de filtrage : `'all'`, ou une liste d'entrées exactes (ex. `video/mp4`)
|
||||
* et/ou de jokers de préfixe (ex. `video/` suivi d'une étoile), l'étoile seule
|
||||
* autorisant tout.
|
||||
*/
|
||||
export type MimePolicy = readonly string[] | 'all';
|
||||
|
||||
function matchesEntry(mimeType: string, entry: string): boolean {
|
||||
if (entry === '*' || entry === '*/*') {
|
||||
return true;
|
||||
}
|
||||
if (entry.endsWith('/*')) {
|
||||
return mimeType.startsWith(entry.slice(0, -1));
|
||||
}
|
||||
return entry === mimeType;
|
||||
}
|
||||
|
||||
/** Indique si un type MIME est accepté selon la politique fournie. */
|
||||
export function isAllowedMedia(mimeType: string, policy: MimePolicy): boolean {
|
||||
if (policy === 'all') {
|
||||
return true;
|
||||
}
|
||||
return policy.includes(mimeType);
|
||||
const normalized = mimeType.toLowerCase();
|
||||
return policy.some((entry) => matchesEntry(normalized, entry));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -128,6 +128,15 @@ describe('parseConfig', () => {
|
|||
expect(parseConfig({ ...BASE, allowedMimeTypes: 'all' }).allowedMimeTypes).toBe('all');
|
||||
expect(() => parseConfig({ ...BASE, allowedMimeTypes: 'image/png' })).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('accepte les jokers valides, rejette les jokers malformés', () => {
|
||||
expect(
|
||||
parseConfig({ ...BASE, allowedMimeTypes: ['video/*', 'image/png', '*/*'] }).allowedMimeTypes,
|
||||
).toEqual(['video/*', 'image/png', '*/*']);
|
||||
expect(() => parseConfig({ ...BASE, allowedMimeTypes: ['video/**'] })).toThrow(ConfigError);
|
||||
expect(() => parseConfig({ ...BASE, allowedMimeTypes: ['vi*deo/mp4'] })).toThrow(ConfigError);
|
||||
expect(() => parseConfig({ ...BASE, allowedMimeTypes: ['video/'] })).toThrow(ConfigError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadConfig', () => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ describe('isAllowedMedia', () => {
|
|||
it('accepte tout avec la politique "all"', () => {
|
||||
expect(isAllowedMedia('application/x-msdownload', 'all')).toBe(true);
|
||||
});
|
||||
|
||||
it('gère les jokers de préfixe', () => {
|
||||
expect(isAllowedMedia('video/x-matroska', ['video/*'])).toBe(true);
|
||||
expect(isAllowedMedia('VIDEO/QUICKTIME', ['video/*'])).toBe(true);
|
||||
expect(isAllowedMedia('image/png', ['video/*'])).toBe(false);
|
||||
expect(isAllowedMedia('image/png', ['video/*', 'image/png'])).toBe(true);
|
||||
expect(isAllowedMedia('application/octet-stream', ['*/*'])).toBe(true);
|
||||
expect(isAllowedMedia('n-importe/quoi', ['*'])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extensionForMime', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue