Authorize direct file invitations within paired rooms and parties
This commit is contained in:
39
frontend/direct-protocol.js
Normal file
39
frontend/direct-protocol.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/* Shared bounded control protocol: media bytes belong exclusively on DataChannel. */
|
||||
(function (root) {
|
||||
'use strict';
|
||||
const CHUNK = 65536;
|
||||
const text = (v, n) => typeof v === 'string' && v.length <= n && !/[\u0000-\u001f]/.test(v);
|
||||
const id = (v) => text(v, 128) && /^[A-Za-z0-9_-]+$/.test(v);
|
||||
function file(v) {
|
||||
if (!v || !id(v.id) || !/^[a-f0-9]{64}$/.test(v.cid) || !Number.isSafeInteger(v.size) || v.size <= 0 || v.size > 4 * 1024 ** 3) return null;
|
||||
if (!text(v.title, 300) || !text(v.channel || '', 200)) return null;
|
||||
return { id: v.id, cid: v.cid, size: v.size, title: v.title, channel: v.channel || '', duration: Number.isFinite(v.duration) && v.duration >= 0 ? v.duration : 0 };
|
||||
}
|
||||
function parse(value) {
|
||||
if (typeof value === 'string') { if (value.length > 65536) return null; try { value = JSON.parse(value); } catch { return null; } }
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value) || value.type !== 'direct') return null;
|
||||
const m = { type: 'direct', action: value.action };
|
||||
if (value.action === 'invite') {
|
||||
const f = file(value.file); if (!id(value.to) || !f) return null;
|
||||
return { ...m, to: value.to, file: f };
|
||||
}
|
||||
if (!['accept', 'decline', 'signal', 'complete'].includes(value.action) || !id(value.token)) return null;
|
||||
m.token = value.token;
|
||||
if (value.action === 'signal') {
|
||||
const d = value.data;
|
||||
if (!d || typeof d !== 'object') return null;
|
||||
if (['offer', 'answer'].includes(d.kind) && typeof d.sdp === 'string' && d.sdp.length <= 60000) m.data = { kind: d.kind, sdp: d.sdp };
|
||||
else if (d.kind === 'ice' && d.candidate && text(d.candidate.candidate, 2048)) {
|
||||
const c = d.candidate;
|
||||
if (!(c.sdpMid == null || text(c.sdpMid, 64)) || !(c.sdpMLineIndex == null || Number.isInteger(c.sdpMLineIndex) && c.sdpMLineIndex >= 0 && c.sdpMLineIndex < 100)) return null;
|
||||
m.data = { kind: 'ice', candidate: { candidate: c.candidate, sdpMid: c.sdpMid ?? null, sdpMLineIndex: c.sdpMLineIndex ?? null } };
|
||||
} else return null;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
function offset(v, size) { if (!Number.isSafeInteger(v) || v < 0 || v > size) throw new Error('Invalid resume offset'); return v; }
|
||||
function nextChunk(pos, size) { offset(pos, size); return { start: pos, end: Math.min(size, pos + CHUNK) }; }
|
||||
const api = { CHUNK, file, parse, offset, nextChunk };
|
||||
if (typeof module !== 'undefined') module.exports = api;
|
||||
root.DirectProtocol = api;
|
||||
}(typeof globalThis !== 'undefined' ? globalThis : this));
|
||||
18
frontend/direct-protocol.test.js
Normal file
18
frontend/direct-protocol.test.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const p = require('./direct-protocol.js');
|
||||
test('direct protocol strips unexpected payload and validates file claims', () => {
|
||||
const m = p.parse({ type: 'direct', action: 'invite', to: 'peer', file: { id: 'video', title: 'Song', cid: 'a'.repeat(64), size: 10, bytes: 'secret' }, bytes: 'not relayed' });
|
||||
assert.equal(m.file.size, 10); assert.equal(m.bytes, undefined); assert.equal(m.file.bytes, undefined);
|
||||
assert.equal(p.parse({ ...m, file: { ...m.file, size: Infinity } }), null);
|
||||
assert.equal(p.parse(new Uint8Array(4)), null);
|
||||
});
|
||||
test('chunks retain exact resume offset and never exceed file end', () => {
|
||||
assert.deepEqual(p.nextChunk(17, 100000), { start: 17, end: 65553 });
|
||||
assert.deepEqual(p.nextChunk(99999, 100000), { start: 99999, end: 100000 });
|
||||
for (const n of [-1, 100001, 0.5, NaN]) assert.throws(() => p.offset(n, 100000));
|
||||
});
|
||||
test('signalling only admits bounded SDP or ICE, never byte frames', () => {
|
||||
assert.equal(p.parse({ type: 'direct', action: 'signal', token: 'abc', data: { kind: 'bytes', data: [1, 2] } }), null);
|
||||
assert.equal(p.parse({ type: 'direct', action: 'signal', token: 'abc', data: { kind: 'offer', sdp: 'x'.repeat(60001) } }), null);
|
||||
});
|
||||
Reference in New Issue
Block a user