openvibers/openvibe.media Commits
Objects: an SVG file is taken as given and served as a download (#231)
Changes
@@@ -3,9 +3,9 @@** 1. The declared type must suit the kind: vod -> video/* or audio/*, clip -> video/*, thumbnail /* screenshot / avatar -> image/* (not SVG). file and asset take any well-formed type.-- * 2. Bytes that /o/:id serves inline (image/*, video/*, audio/*, application/pdf) must look like what-- * their type says: the first bytes are checked against the container signatures below at-- * complete. A kind with a rule is always checked.++ * 2. Bytes that /o/:id serves inline (image/* except SVG, video/*, audio/*, application/pdf) must look like++ * what their type says: the first bytes are checked against the container signatures below at++ * complete. A kind with a rule is always checked. An SVG is served as an attachment (routes.js INLINE).* Anything else is served as an attachment with nosniff, so its declared type is taken as given.*/'use strict';@@@ -63,9 +63,13 @@ function sniff(head) {return null;}--/** Is `type` served inline (so its bytes must match it)? */++/**++ * Is `type` served inline (so its bytes must match it)? An SVG is served as an attachment on this origin (it is++ * never inline, INLINE in routes.js), so its text bytes are taken as given, like any other downloaded file.++ */function servedInline(type) {-- return /^(image\/|video\/|audio\/)/.test(type || '') || type === 'application/pdf';++ const t = String(type || '').toLowerCase();++ return /^(image\/(?!svg)|video\/|audio\/)/.test(t) || t === 'application/pdf';}/** null when the bytes fit the declared type (or need no check), else the reason. */@@@ -173,6 +173,23 @@ const crypto = require('crypto');r = await call('GET', `${privSigned.replace(/sig=[^&]+/, 'sig=AAAA')}&w=96&fmt=webp`, { bearer: null });assert.strictEqual(r.status, 404, 'a bad signature gets no variant either');await db.run('DELETE FROM media_objects WHERE id = ?', [privId]); // the listing checks below count the objects++ // An SVG is a file like any other (Actor's charts): its text bytes are taken as given, and it is served as a++ // download on this origin, never inline. An image kind that must be raster still refuses it.++ const svgBody = Buffer.from('<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><circle cx="5" cy="5" r="4"/></svg>');++ r = await call('POST', O, { body: { kind: 'file', visibility: 'private', mime_type: 'image/svg+xml', filename: 'chart.svg', size_bytes: svgBody.length } });++ assert.strictEqual(r.status, 201, JSON.stringify(r.body));++ const svgId = r.body.id;++ r = await call('PUT', `${O}/${svgId}/content`, { raw: svgBody, headers: { 'content-type': 'image/svg+xml' } });++ assert.strictEqual(r.status, 200, JSON.stringify(r.body));++ r = await call('POST', `${O}/${svgId}/complete`);++ assert.deepStrictEqual([r.status, r.body.lifecycle_status], [200, 'ready'], JSON.stringify(r.body));++ r = await call('GET', `${O}/${svgId}/download`);++ const sv = await fetch(local(r.body.url));++ assert.deepStrictEqual([sv.status, sv.headers.get('content-type'), /^attachment/.test(sv.headers.get('content-disposition') || ''), sv.headers.get('x-content-type-options')],++ [200, 'image/svg+xml', true, 'nosniff'], 'the SVG downloads, never inline');++ r = await call('POST', O, { body: { kind: 'avatar', visibility: 'public', mime_type: 'image/svg+xml' } });++ assert.ok(r.status >= 400, 'an avatar still may not be an SVG: ' + r.status);++ await db.run('DELETE FROM media_objects WHERE id = ?', [svgId]);console.log('✅ image variants: a public image in its asked size, the original otherwise; a private one only signed');// ── Service tokens ──
@@ -3,9 +3,9 @@
@@ -3,9 +3,9 @@
*
*
* 1. The declared type must suit the kind: vod -> video/* or audio/*, clip -> video/*, thumbnail /
* 1. The declared type must suit the kind: vod -> video/* or audio/*, clip -> video/*, thumbnail /
* screenshot / avatar -> image/* (not SVG). file and asset take any well-formed type.
* screenshot / avatar -> image/* (not SVG). file and asset take any well-formed type.
- * 2. Bytes that /o/:id serves inline (image/*, video/*, audio/*, application/pdf) must look like what
- * their type says: the first bytes are checked against the container signatures below at
- * complete. A kind with a rule is always checked.
+ * 2. Bytes that /o/:id serves inline (image/* except SVG, video/*, audio/*, application/pdf) must look like
+ * what their type says: the first bytes are checked against the container signatures below at
+ * complete. A kind with a rule is always checked. An SVG is served as an attachment (routes.js INLINE).
* Anything else is served as an attachment with nosniff, so its declared type is taken as given.
* Anything else is served as an attachment with nosniff, so its declared type is taken as given.
*/
*/
'use strict';
'use strict';
@@ -63,9 +63,13 @@ function sniff(head) {
@@ -63,9 +63,13 @@ function sniff(head) {
return null;
return null;
}
}
-/** Is `type` served inline (so its bytes must match it)? */
+/**
+ * Is `type` served inline (so its bytes must match it)? An SVG is served as an attachment on this origin (it is
+ * never inline, INLINE in routes.js), so its text bytes are taken as given, like any other downloaded file.
+ */
function servedInline(type) {
function servedInline(type) {
- return /^(image\/|video\/|audio\/)/.test(type || '') || type === 'application/pdf';
+ const t = String(type || '').toLowerCase();
+ return /^(image\/(?!svg)|video\/|audio\/)/.test(t) || t === 'application/pdf';
}
}
/** null when the bytes fit the declared type (or need no check), else the reason. */
/** null when the bytes fit the declared type (or need no check), else the reason. */
@@ -173,6 +173,23 @@ const crypto = require('crypto');
@@ -173,6 +173,23 @@ const crypto = require('crypto');
r = await call('GET', `${privSigned.replace(/sig=[^&]+/, 'sig=AAAA')}&w=96&fmt=webp`, { bearer: null });
r = await call('GET', `${privSigned.replace(/sig=[^&]+/, 'sig=AAAA')}&w=96&fmt=webp`, { bearer: null });
assert.strictEqual(r.status, 404, 'a bad signature gets no variant either');
assert.strictEqual(r.status, 404, 'a bad signature gets no variant either');
await db.run('DELETE FROM media_objects WHERE id = ?', [privId]); // the listing checks below count the objects
await db.run('DELETE FROM media_objects WHERE id = ?', [privId]); // the listing checks below count the objects
+ // An SVG is a file like any other (Actor's charts): its text bytes are taken as given, and it is served as a
+ // download on this origin, never inline. An image kind that must be raster still refuses it.
+ const svgBody = Buffer.from('<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><circle cx="5" cy="5" r="4"/></svg>');
+ r = await call('POST', O, { body: { kind: 'file', visibility: 'private', mime_type: 'image/svg+xml', filename: 'chart.svg', size_bytes: svgBody.length } });
+ assert.strictEqual(r.status, 201, JSON.stringify(r.body));
+ const svgId = r.body.id;
+ r = await call('PUT', `${O}/${svgId}/content`, { raw: svgBody, headers: { 'content-type': 'image/svg+xml' } });
+ assert.strictEqual(r.status, 200, JSON.stringify(r.body));
+ r = await call('POST', `${O}/${svgId}/complete`);
+ assert.deepStrictEqual([r.status, r.body.lifecycle_status], [200, 'ready'], JSON.stringify(r.body));
+ r = await call('GET', `${O}/${svgId}/download`);
+ const sv = await fetch(local(r.body.url));
+ assert.deepStrictEqual([sv.status, sv.headers.get('content-type'), /^attachment/.test(sv.headers.get('content-disposition') || ''), sv.headers.get('x-content-type-options')],
+ [200, 'image/svg+xml', true, 'nosniff'], 'the SVG downloads, never inline');
+ r = await call('POST', O, { body: { kind: 'avatar', visibility: 'public', mime_type: 'image/svg+xml' } });
+ assert.ok(r.status >= 400, 'an avatar still may not be an SVG: ' + r.status);
+ await db.run('DELETE FROM media_objects WHERE id = ?', [svgId]);
console.log('✅ image variants: a public image in its asked size, the original otherwise; a private one only signed');
console.log('✅ image variants: a public image in its asked size, the original otherwise; a private one only signed');
// ── Service tokens ──
// ── Service tokens ──