mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-08-04 17:22:31 -04:00
A live multipart (mode=jpeg) stream <img> whose baked auth hash expires past AUTH_HASH_TTL is reconnected by the browser itself, reusing the same src (same connkey, same dead hash). Every native reconnect returns 403, so once the capture daemon drops the stream the client storms zms with auth failures for hours. Observed in production: a single connkey retried 84 times over 2.5h, 880 failures on one monitor whose zmc was timing out, while monitors with healthy zmc showed only baseline hash-rollover noise. img_onerror only blanked the <img> src inside its async refresh callback, which never ran once authRefreshAttempts reached the cap. On give-up the stale src stayed live and the browser kept native-retrying it, which is the storm. Blank src synchronously at the top of img_onerror so the browser's retry loop stops immediately, and reconnect with a fresh connkey (the zms process behind the old connkey has exited) after fetching a fresh hash. Extract the src rewrite into a pure rebuildStreamSrc() helper in auth-helpers.js with unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
104 lines
3.8 KiB
JavaScript
104 lines
3.8 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
const ZM = require(path.join(__dirname, '../../web/js/auth-helpers.js'));
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(' ok ' + name);
|
|
passed++;
|
|
} catch (e) {
|
|
console.error(' FAIL ' + name);
|
|
console.error(' ' + e.message);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
console.log('authFailureAction');
|
|
test('401 Unauthorized -> login', () => {
|
|
assert.strictEqual(ZM.authFailureAction(401), 'login');
|
|
});
|
|
test('403 Forbidden (stale auth hash, what zms returns) -> login', () => {
|
|
assert.strictEqual(ZM.authFailureAction(403), 'login');
|
|
});
|
|
test('0 network error -> retry', () => {
|
|
assert.strictEqual(ZM.authFailureAction(0), 'retry');
|
|
});
|
|
test('408 timeout -> retry', () => {
|
|
assert.strictEqual(ZM.authFailureAction(408), 'retry');
|
|
});
|
|
test('502 bad gateway -> retry', () => {
|
|
assert.strictEqual(ZM.authFailureAction(502), 'retry');
|
|
});
|
|
test('200 success -> ignore', () => {
|
|
assert.strictEqual(ZM.authFailureAction(200), 'ignore');
|
|
});
|
|
test('404 not found -> ignore', () => {
|
|
assert.strictEqual(ZM.authFailureAction(404), 'ignore');
|
|
});
|
|
|
|
console.log('loginRedirectUrl');
|
|
test('builds login url preserving console view', () => {
|
|
assert.strictEqual(
|
|
ZM.loginRedirectUrl('/zm/index.php', 'console'),
|
|
'/zm/index.php?view=login&postLoginQuery=view%3Dconsole');
|
|
});
|
|
test('preserves montage view', () => {
|
|
assert.strictEqual(
|
|
ZM.loginRedirectUrl('/zm/index.php', 'montage'),
|
|
'/zm/index.php?view=login&postLoginQuery=view%3Dmontage');
|
|
});
|
|
test('defaults to console when view missing', () => {
|
|
assert.strictEqual(
|
|
ZM.loginRedirectUrl('/zm/index.php', ''),
|
|
'/zm/index.php?view=login&postLoginQuery=view%3Dconsole');
|
|
});
|
|
|
|
console.log('rebuildStreamSrc');
|
|
test('replaces auth hash in place', () => {
|
|
assert.strictEqual(
|
|
ZM.rebuildStreamSrc('/zm/cgi-bin/nph-zms?monitor=35&auth=OLD123&connkey=816890&mode=jpeg', 'NEW456', 816890),
|
|
'/zm/cgi-bin/nph-zms?monitor=35&auth=NEW456&connkey=816890&mode=jpeg');
|
|
});
|
|
test('replaces connkey when a fresh one is supplied', () => {
|
|
assert.strictEqual(
|
|
ZM.rebuildStreamSrc('/zm/cgi-bin/nph-zms?monitor=35&auth=OLD123&connkey=816890&mode=jpeg', 'NEW456', 999999),
|
|
'/zm/cgi-bin/nph-zms?monitor=35&auth=NEW456&connkey=999999&mode=jpeg');
|
|
});
|
|
test('swaps both auth and connkey (the reconnect case)', () => {
|
|
// Regression: a broken montage <img> must reconnect with BOTH a fresh hash
|
|
// and a fresh connkey, never the stale baked pair that storms zms.
|
|
const broken = 'cgi-bin/nph-zms?monitor=35&auth=5c464e95&user=plaza&connkey=816890&scale=25&mode=jpeg';
|
|
const out = ZM.rebuildStreamSrc(broken, 'fresh99', 123456);
|
|
assert.strictEqual(out.indexOf('auth=5c464e95'), -1, 'stale auth must be gone');
|
|
assert.strictEqual(out.indexOf('connkey=816890'), -1, 'stale connkey must be gone');
|
|
assert.ok(out.indexOf('auth=fresh99') !== -1);
|
|
assert.ok(out.indexOf('connkey=123456') !== -1);
|
|
});
|
|
test('appends auth when the url has none', () => {
|
|
assert.strictEqual(
|
|
ZM.rebuildStreamSrc('cgi-bin/nph-zms?monitor=35&mode=jpeg', 'abc', null),
|
|
'cgi-bin/nph-zms?monitor=35&mode=jpeg&auth=abc');
|
|
});
|
|
test('appends auth with ? when no query string present', () => {
|
|
assert.strictEqual(
|
|
ZM.rebuildStreamSrc('cgi-bin/nph-zms', 'abc', null),
|
|
'cgi-bin/nph-zms?auth=abc');
|
|
});
|
|
test('leaves connkey untouched when none requested', () => {
|
|
assert.strictEqual(
|
|
ZM.rebuildStreamSrc('cgi-bin/nph-zms?auth=OLD&connkey=42', 'NEW'),
|
|
'cgi-bin/nph-zms?auth=NEW&connkey=42');
|
|
});
|
|
test('handles empty/undefined src safely', () => {
|
|
assert.strictEqual(ZM.rebuildStreamSrc('', 'abc', 7), '?auth=abc&connkey=7');
|
|
assert.strictEqual(ZM.rebuildStreamSrc(undefined, 'abc', null), '?auth=abc');
|
|
});
|
|
|
|
console.log('\n' + passed + ' passed, ' + failed + ' failed');
|
|
process.exit(failed ? 1 : 0);
|