mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
feat(stalker): protocol-correct auth lifecycle
Portals that answer get_profile with status 2 ("login/password required")
never authenticated: the status was treated as success and the do_auth path
had no callers. Parse js.status (numeric or stringified), run do_auth with the
credentials the import dialog now collects, and retry get_profile with
auth_second_step=1.
- Surface the portal's own msg/block_msg (markup-stripped) in the import
dialog and the workspace category error state instead of a generic message.
- Classify the plain-text failure bodies at the Electron transport boundary as
a structured marker rather than by JSON.stringify regex.
- Re-present the persisted stalkerToken in the handshake (which is
idempotent), skipping get_profile when it comes back unchanged and not
flagged not_valid, and write a renegotiated session back. The watchdog
cadence is persisted with the token because that reuse skips the only
response carrying it.
- Take the watchdog cadence from the profile (watchdog_timeout + timeslot),
defaulting to the documented 120 s instead of pinging every 25 s.
Splits stalker-session.service.ts into an auth API, a watchdog controller, a
portal-error module and a response classifier; the service is now a facade and
drops off the max-lines baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e197409b10
commit
28429580a5
51 files changed
+3626
-656
No files matched your search
@@ -58,6 +58,23 @@ export interface Playlist {
|
||||
serverTimezone?: string;
|
||||
/** Session token for full stalker portal authentication - persisted for session */
|
||||
stalkerToken?: string;
|
||||
/**
|
||||
* Identity fingerprint the persisted `stalkerToken` was negotiated for.
|
||||
* Re-presenting a token minted for a DIFFERENT identity (the user edited
|
||||
* the MAC, serial or device ids) would pair a new identity with an old
|
||||
* session — the same class of bug the in-memory cache guards against, so
|
||||
* reuse is refused unless this still matches the playlist.
|
||||
*/
|
||||
stalkerSessionIdentity?: string;
|
||||
/**
|
||||
* Watchdog cadence the portal advertised in `get_profile`, persisted
|
||||
* alongside the token: reusing a stored token skips the profile request
|
||||
* that carries these, so without persistence the keep-alive would fall
|
||||
* back to the 120 s default forever.
|
||||
*/
|
||||
stalkerWatchdogTimeout?: number;
|
||||
/** Per-user watchdog jitter (seconds) from `get_profile`. */
|
||||
stalkerTimeslot?: number;
|
||||
/** Serial number for stalker portal - generated once and stored for consistency */
|
||||
stalkerSerialNumber?: string;
|
||||
/** Optional device ID 1 for stalker portal - if not provided, auto-generated from MAC */
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
classifyStalkerAuthFailureBody,
|
||||
createStalkerAuthFailureMarker,
|
||||
extractStalkerAuthFailureBody,
|
||||
isStalkerAuthFailureBody,
|
||||
isStalkerAuthFailureMessage,
|
||||
isStalkerAuthFailureResponse,
|
||||
@@ -98,6 +100,58 @@ describe('isStalkerAuthFailureResponse', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('transport marker', () => {
|
||||
it('round-trips through create / detect / extract', () => {
|
||||
const marker = createStalkerAuthFailureMarker('Access denied.');
|
||||
expect(isStalkerAuthFailureResponse(marker)).toBe(true);
|
||||
expect(extractStalkerAuthFailureBody(marker)).toBe('Access denied.');
|
||||
});
|
||||
|
||||
it('is what keeps the repair trigger working once Electron classifies', () => {
|
||||
// The main process replaces the plain-text body with this marker, so
|
||||
// a detector that only knew the raw string would silently stop
|
||||
// triggering portal repair on the desktop app.
|
||||
expect(
|
||||
isStalkerAuthFailureResponse({
|
||||
stalkerAuthFailure: 'Authorization failed.',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('does not accept arbitrary objects as markers', () => {
|
||||
expect(isStalkerAuthFailureResponse({ stalkerAuthFailure: 'nope' })).toBe(
|
||||
false
|
||||
);
|
||||
expect(extractStalkerAuthFailureBody({})).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractStalkerAuthFailureBody', () => {
|
||||
it('extracts from the raw PWA payload string', () => {
|
||||
expect(extractStalkerAuthFailureBody('Authorization failed. 12')).toBe(
|
||||
'Authorization failed.'
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts from a {js: body} envelope', () => {
|
||||
expect(
|
||||
extractStalkerAuthFailureBody({ js: 'Authorization failed. 75' })
|
||||
).toBe('Authorization failed.');
|
||||
});
|
||||
|
||||
it('reports no canonical body for the structured panel wording', () => {
|
||||
// Still an auth failure — just not one of the middleware's bodies.
|
||||
const response = { js: { error: 'Invalid token' } };
|
||||
expect(extractStalkerAuthFailureBody(response)).toBeNull();
|
||||
expect(isStalkerAuthFailureResponse(response)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns null for ordinary responses', () => {
|
||||
expect(extractStalkerAuthFailureBody({ js: { data: [] } })).toBeNull();
|
||||
expect(extractStalkerAuthFailureBody(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStalkerAuthFailureMessage', () => {
|
||||
it('accepts messages our own auth layer produces', () => {
|
||||
expect(
|
||||
|
||||
@@ -128,17 +128,77 @@ export function isStalkerAuthFailureMessage(message: unknown): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a portal response is an authorization failure in EITHER wire
|
||||
* shape: the middleware's plain-text body, or the JSON envelope some panels
|
||||
* answer instead. Classification and the lazy-repair trigger must use this,
|
||||
* not the string-only primitive: a JSON-failing panel would otherwise be
|
||||
* persisted as token-free and never repaired.
|
||||
* Structured shape a transport returns in place of the raw plain-text body.
|
||||
*
|
||||
* Returned, never thrown: `ipcRenderer.invoke` strips custom properties from
|
||||
* rejected values, so a classified body would not survive the trip to the
|
||||
* renderer as an error. The Electron main process mints this because it is
|
||||
* where the body arrives; the PWA proxy still delivers the raw string, and
|
||||
* every consumer goes through the predicates below rather than caring which.
|
||||
*/
|
||||
export interface StalkerAuthFailureMarker {
|
||||
stalkerAuthFailure: StalkerAuthFailureBody;
|
||||
}
|
||||
|
||||
export function createStalkerAuthFailureMarker(
|
||||
body: StalkerAuthFailureBody
|
||||
): StalkerAuthFailureMarker {
|
||||
return { stalkerAuthFailure: body };
|
||||
}
|
||||
|
||||
function readAuthFailureMarker(
|
||||
value: unknown
|
||||
): StalkerAuthFailureBody | null {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return classifyStalkerAuthFailureBody(
|
||||
(value as StalkerAuthFailureMarker).stalkerAuthFailure
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the canonical failure body from any shape an auth failure reaches
|
||||
* a consumer in: the transport marker (Electron), the raw plain-text payload
|
||||
* (PWA proxy), or a `{js: '<body>'}` envelope. Returns null for everything
|
||||
* else, including the structured `{js: {error|msg}}` form — that one is a
|
||||
* panel's own wording, not one of the middleware's three bodies, so it has
|
||||
* no canonical body to report. Use `isStalkerAuthFailureResponse` when the
|
||||
* question is merely "did authorization fail?".
|
||||
*/
|
||||
export function extractStalkerAuthFailureBody(
|
||||
value: unknown
|
||||
): StalkerAuthFailureBody | null {
|
||||
return (
|
||||
readAuthFailureMarker(value) ??
|
||||
classifyStalkerAuthFailureBody(value) ??
|
||||
classifyStalkerAuthFailureBody(
|
||||
typeof value === 'object' && value !== null && 'js' in value
|
||||
? (value as { js?: unknown }).js
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a portal response is an authorization failure in ANY wire shape:
|
||||
* the middleware's plain-text body, the transport marker minted from it, or
|
||||
* the JSON envelope some panels answer instead. Classification and the
|
||||
* lazy-repair trigger must use this, not the string-only primitive: a
|
||||
* JSON-failing panel would otherwise be persisted as token-free and never
|
||||
* repaired, and an Electron-classified body would stop triggering repair at
|
||||
* all.
|
||||
*/
|
||||
export function isStalkerAuthFailureResponse(response: unknown): boolean {
|
||||
if (isStalkerAuthFailureBody(response)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (readAuthFailureMarker(response) !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
response === null ||
|
||||
typeof response !== 'object' ||
|
||||
|
||||
Reference in new issue
Block a user