Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class Endpoint extends EventEmitter {
try {
const res = await this._request('licensing.generate-token', { callId });
this._sessionToken1 = res?.token || '';
this._validatedToken2 = '';
return { body: this._sessionToken1 ? `+OK ${this._sessionToken1}` : '-ERR not licensed' };
} catch (err) {
return { body: `-ERR ${err.message}` };
Expand Down Expand Up @@ -514,8 +515,15 @@ class Endpoint extends EventEmitter {
* feature-server drops the call — its media-timeout path already handles it. */
async _validateSessionToken2(token2) {
if (!this._sessionToken1 || !token2) return;
/* token-2 is a one-time proof minted at call setup, and the feature-server
* sets it again on every later provisional and on the final response, still
* carrying its original timestamp. Re-validating that same token after the
* 40s window fails as 'expired' on any call that rings that long, tearing
* the endpoint down before answer. Check each token once. */
if (token2 === this._validatedToken2) return;
try {
await this._request('licensing.validate-token-2', { token1: this._sessionToken1, token2 });
this._validatedToken2 = token2;
} catch (err) {
if (this.connected) {
this.connected = false;
Expand Down
60 changes: 60 additions & 0 deletions test/licensing-conduit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,63 @@ test('token-2 with no token-1 (unlicensed) is a no-op — no validate call, no t
assert.strictEqual(destroyed, false);
assert.strictEqual(ep.connected, true);
});

/* mediajam rejects a token-2 older than 40s; the feature-server sets the same
token again on each later provisional and on the 200 OK. */
const expiringValidator = () => {
let validated = 0;
return (cmd) => {
if (cmd === 'licensing.generate-token') return { token: 'TOK1' };
if (cmd === 'licensing.validate-token-2') {
if (validated++ > 0) throw new Error('session token 2 invalid');
return { valid: true };
}
return {};
};
};

test('the same token-2 is validated once: a re-set after a long ring does not tear down', async () => {
const { ep, calls } = makeEp(expiringValidator());
await ep.api('uuid_jambonz_licensing', 'generate-session-token ep-1 call-123');
let destroyed = false;
ep.on('destroy', () => { destroyed = true; });
await ep.set('jambonz_session_token_2', 'TOK2'); // 180, fresh
await ep.set('jambonz_session_token_2', 'TOK2'); // 183 at 45s, same token
await ep.set('jambonz_session_token_2', 'TOK2'); // 200 OK, same token
assert.strictEqual(calls.filter((c) => c.cmd === 'licensing.validate-token-2').length, 1);
assert.strictEqual(destroyed, false);
assert.strictEqual(ep.connected, true);
});

test('a different token-2 is still validated (and torn down if invalid)', async () => {
const { ep, calls } = makeEp(expiringValidator());
await ep.api('uuid_jambonz_licensing', 'generate-session-token ep-1 call-123');
let destroyed = null;
ep.on('destroy', (evt) => { destroyed = evt; });
await ep.set('jambonz_session_token_2', 'TOK2');
await ep.set('jambonz_session_token_2', 'OTHER');
assert.strictEqual(calls.filter((c) => c.cmd === 'licensing.validate-token-2').length, 2);
assert.strictEqual(ep.connected, false);
assert.strictEqual(destroyed.reason, 'license-violation');
});

test('a token-2 that failed validation is not remembered as validated', async () => {
const { ep, calls } = makeEp((cmd) => {
if (cmd === 'licensing.generate-token') return { token: 'TOK1' };
if (cmd === 'licensing.validate-token-2') throw new Error('session token 2 invalid');
return {};
});
await ep.api('uuid_jambonz_licensing', 'generate-session-token ep-1 call-123');
await ep.set('jambonz_session_token_2', 'BAD');
assert.strictEqual(ep._validatedToken2 || '', '');
assert.strictEqual(calls.filter((c) => c.cmd === 'licensing.validate-token-2').length, 1);
});

test('minting a new token-1 clears the remembered token-2', async () => {
const { ep, calls } = makeEp((cmd) => (cmd === 'licensing.generate-token' ? { token: 'TOK1' } : { valid: true }));
await ep.api('uuid_jambonz_licensing', 'generate-session-token ep-1 call-123');
await ep.set('jambonz_session_token_2', 'TOK2');
await ep.api('uuid_jambonz_licensing', 'generate-session-token ep-1 call-456');
await ep.set('jambonz_session_token_2', 'TOK2');
assert.strictEqual(calls.filter((c) => c.cmd === 'licensing.validate-token-2').length, 2);
});
Loading