mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 09:26:14 -08:00
fix(stalker): check the session fingerprint on the playback token path too
Two more places where the endpoint/identity/credential binding was defined but not enforced: - Collection playback read the RAW token cache, which skips the fingerprint check `ensureToken()` performs. After editing the MAC, endpoint or login, opening a direct-URL radio favorite before any other request put the previous account's token into the stream headers. It now always goes through `ensureToken()`, which returns the cached token when it is still valid, so a warm session costs nothing. - The repair's atomic row guard compared URL, mode and device identity but not credentials, so a login saved during a 45-second discovery let the outcome negotiated for the OLD account commit and adopt its token. The guard now matches `repairSourceFingerprint()`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9799558ad7
commit
2e5cc3d901
4 files changed
+46
-6
No files matched your search
@@ -794,7 +794,10 @@ describe('StreamResolverService', () => {
|
||||
isFullStalkerPortal: true,
|
||||
} satisfies Partial<Playlist>)
|
||||
);
|
||||
stalkerSession.getCachedToken.mockReturnValue('TOKEN77');
|
||||
// Playback goes through ensureToken, never the raw cache accessor:
|
||||
// that one skips the endpoint/identity/credential check, so an
|
||||
// edited playlist would put the old account's token in the headers.
|
||||
stalkerSession.ensureToken.mockResolvedValue({ token: 'TOKEN77' });
|
||||
stalkerSession.makeAuthenticatedRequest.mockResolvedValue({
|
||||
js: { cmd: 'ffmpeg https://stalker.example.com:8080/live/88.ts' },
|
||||
});
|
||||
@@ -810,7 +813,7 @@ describe('StreamResolverService', () => {
|
||||
stalkerCmd: 'ffrt3 http://stalker.example.com/media/88.mpg',
|
||||
} satisfies UnifiedCollectionItem);
|
||||
|
||||
expect(stalkerSession.getCachedToken).toHaveBeenCalledWith('stalker-1');
|
||||
expect(stalkerSession.ensureToken).toHaveBeenCalled();
|
||||
expect(playback.headers?.['Cookie']).toContain(
|
||||
'mac=00:11:22:33:44:55'
|
||||
);
|
||||
|
||||
@@ -568,15 +568,19 @@ export class StreamResolverService {
|
||||
playlistId: string,
|
||||
playlist: Playlist | undefined
|
||||
): Promise<string | null> {
|
||||
const cached = this.stalkerSession.getCachedToken(playlistId);
|
||||
// The shared mode contract, not the raw flag: a legacy row with an
|
||||
// absent flag but a canonical URL IS a full portal, and reading the
|
||||
// property directly would skip authentication for it — a restored
|
||||
// older backup opens a direct-URL radio favorite with no Bearer.
|
||||
if (cached || !playlist || !isFullStalkerPortalPlaylist(playlist)) {
|
||||
return cached;
|
||||
if (!playlist || !isFullStalkerPortalPlaylist(playlist)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Always through ensureToken, never the raw cache accessor: that one
|
||||
// skips the endpoint/identity/credential check, so after an edit this
|
||||
// playback path would put the previous account's token into the
|
||||
// stream headers. ensureToken returns the cached token when it is
|
||||
// still valid for this playlist, so a warm session costs nothing.
|
||||
try {
|
||||
const { token } = await this.stalkerSession.ensureToken(playlist);
|
||||
return token;
|
||||
|
||||
@@ -286,6 +286,33 @@ describe('StalkerPortalRepairService', () => {
|
||||
expect(writtenRow).toBeNull();
|
||||
});
|
||||
|
||||
it('discards a repair whose credentials changed while probing', async () => {
|
||||
// Discovery can run for tens of seconds; a login saved meanwhile
|
||||
// means the outcome was negotiated for an account the row no
|
||||
// longer belongs to, so committing it would adopt the wrong
|
||||
// session.
|
||||
discover.mockResolvedValue({
|
||||
status: 'resolved',
|
||||
portalUrl: 'http://panel.example/server/load.php',
|
||||
isFullStalkerPortal: true,
|
||||
token: 'WRONG-ACCOUNT',
|
||||
});
|
||||
// The row the transform sees carries the NEW login.
|
||||
persistedRow = {
|
||||
...MISCLASSIFIED,
|
||||
username: 'edited-mid-probe',
|
||||
} as Playlist;
|
||||
|
||||
expect(
|
||||
await service.repairPortal({
|
||||
...MISCLASSIFIED,
|
||||
username: 'original',
|
||||
})
|
||||
).toBeNull();
|
||||
expect(writtenRow).toBeNull();
|
||||
expect(adoptDiscoveredSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adopts the cadence the repair confirmation discovered', async () => {
|
||||
// Caching the token alone satisfies the retry, so NO
|
||||
// authentication path would ever apply the profile outcome — the
|
||||
|
||||
@@ -507,7 +507,13 @@ export class StalkerPortalRepairService implements StalkerPortalRepairApi {
|
||||
row.portalUrl === playlist.portalUrl &&
|
||||
isFullStalkerPortalPlaylist(row) === sourceMode &&
|
||||
stalkerIdentityFingerprint(row) ===
|
||||
stalkerIdentityFingerprint(playlist)
|
||||
stalkerIdentityFingerprint(playlist) &&
|
||||
// Credentials too, matching repairSourceFingerprint(): discovery
|
||||
// can run for tens of seconds, and a login saved meanwhile means
|
||||
// the outcome was negotiated for an account the row no longer
|
||||
// belongs to — committing it would adopt the wrong session.
|
||||
(row.username ?? '') === (playlist.username ?? '') &&
|
||||
(row.password ?? '') === (playlist.password ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user