mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
fix(stalker): preserve exact portal principals
This commit is contained in:
1 parent
dfdce824d9
commit
6d38a7cc4b
6 files changed
+120
-36
No files matched your search
@@ -113,6 +113,7 @@ describe('StalkerAuthSession', () => {
|
||||
});
|
||||
|
||||
it('runs do_auth only after status 2, then sends the second profile with step 1', async () => {
|
||||
const exactUsername = ' confirmed-user ';
|
||||
const { auth, calls } = harness(
|
||||
{
|
||||
kind: 'credentials-required',
|
||||
@@ -126,7 +127,6 @@ describe('StalkerAuthSession', () => {
|
||||
if (request.params?.['action'] === 'get_profile') {
|
||||
return success({
|
||||
js: {
|
||||
login: 'confirmed-user',
|
||||
status: 0,
|
||||
},
|
||||
});
|
||||
@@ -140,24 +140,21 @@ describe('StalkerAuthSession', () => {
|
||||
kind: 'credentials-required',
|
||||
});
|
||||
const outcome = await auth.submitCredentials({
|
||||
username: 'confirmed-user',
|
||||
username: exactUsername,
|
||||
password: 'confirmed-password',
|
||||
});
|
||||
|
||||
expect(outcome).toEqual({
|
||||
accountSummary: {
|
||||
name: 'confirmed-user',
|
||||
status: '0',
|
||||
},
|
||||
accountSummary: { status: '0' },
|
||||
kind: 'ready',
|
||||
});
|
||||
expect(auth.getPrincipalKey()).toBe('confirmed-user');
|
||||
expect(auth.getPrincipalKey()).toBe(exactUsername);
|
||||
expect(calls.map((call) => call.params?.['action'])).toEqual([
|
||||
'do_auth',
|
||||
'get_profile',
|
||||
]);
|
||||
expect(calls[0].params).toMatchObject({
|
||||
login: 'confirmed-user',
|
||||
login: exactUsername,
|
||||
password: 'confirmed-password',
|
||||
});
|
||||
expect(calls[1].params?.['auth_second_step']).toBe(1);
|
||||
|
||||
@@ -507,10 +507,10 @@ function normalizeCredentials(
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const username = value.username.trim();
|
||||
const username = value.username;
|
||||
const password = value.password;
|
||||
if (
|
||||
username.length === 0 ||
|
||||
username.trim().length === 0 ||
|
||||
password.length === 0 ||
|
||||
Buffer.byteLength(username, 'utf8') > USERNAME_MAX_BYTES ||
|
||||
Buffer.byteLength(password, 'utf8') > PASSWORD_MAX_BYTES
|
||||
|
||||
+51
-6
@@ -42,11 +42,58 @@ describe('createStalkerSavedCredentialsLoader', () => {
|
||||
|
||||
await expect(load(DESCRIPTOR)).resolves.toEqual({
|
||||
password: 'saved password',
|
||||
username: 'saved-user',
|
||||
username: ' saved-user ',
|
||||
});
|
||||
expect(readPlaylist).toHaveBeenCalledWith('playlist-1');
|
||||
});
|
||||
|
||||
it.each([
|
||||
'userAgent',
|
||||
'xUserAgent',
|
||||
'referer',
|
||||
'origin',
|
||||
'refererPolicy',
|
||||
'originPolicy',
|
||||
'locale',
|
||||
'language',
|
||||
'timezone',
|
||||
] as const)(
|
||||
'rejects a %s transport-identity mismatch',
|
||||
async (transportKey) => {
|
||||
const load = createStalkerSavedCredentialsLoader(async () => ({
|
||||
...SAVED_PLAYLIST,
|
||||
stalkerTransportConfiguration: {
|
||||
[transportKey]: 'saved-value',
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(load(DESCRIPTOR)).resolves.toBeUndefined();
|
||||
}
|
||||
);
|
||||
|
||||
it('matches legacy transport columns when the structured configuration is absent', async () => {
|
||||
const load = createStalkerSavedCredentialsLoader(async () => ({
|
||||
...SAVED_PLAYLIST,
|
||||
origin: 'https://portal.example',
|
||||
referrer: 'https://portal.example/c/',
|
||||
userAgent: 'Saved User Agent',
|
||||
}));
|
||||
|
||||
await expect(
|
||||
load({
|
||||
...DESCRIPTOR,
|
||||
transportConfiguration: {
|
||||
origin: 'https://portal.example',
|
||||
referer: 'https://portal.example/c/',
|
||||
userAgent: 'Saved User Agent',
|
||||
},
|
||||
})
|
||||
).resolves.toEqual({
|
||||
password: 'saved password',
|
||||
username: ' saved-user ',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['source', { stalkerSourceUrl: 'https://other.example/c/' }],
|
||||
['MAC', { macAddress: '00:1A:79:AA:BB:CC' }],
|
||||
@@ -91,10 +138,8 @@ describe('createStalkerSavedCredentialsLoader', () => {
|
||||
});
|
||||
|
||||
it('matches legacy identity columns when the structured override is absent', async () => {
|
||||
const {
|
||||
stalkerIdentityOverrides: _structured,
|
||||
...legacyPlaylist
|
||||
} = SAVED_PLAYLIST;
|
||||
const { stalkerIdentityOverrides: _structured, ...legacyPlaylist } =
|
||||
SAVED_PLAYLIST;
|
||||
const load = createStalkerSavedCredentialsLoader(async () => ({
|
||||
...legacyPlaylist,
|
||||
stalkerDeviceId1: 'DEVICE-1',
|
||||
@@ -103,7 +148,7 @@ describe('createStalkerSavedCredentialsLoader', () => {
|
||||
|
||||
await expect(load(DESCRIPTOR)).resolves.toEqual({
|
||||
password: 'saved password',
|
||||
username: 'saved-user',
|
||||
username: ' saved-user ',
|
||||
});
|
||||
});
|
||||
});
|
||||
+55
-9
@@ -6,12 +6,11 @@ import {
|
||||
LEGACY_DEFAULT_STALKER_SERIAL,
|
||||
type StalkerSessionConnectionDescriptor,
|
||||
type StalkerSessionIdentityOverrides,
|
||||
type StalkerSessionTransportConfiguration,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import type { StalkerAuthCredentials } from './stalker-auth-session';
|
||||
|
||||
type SavedPlaylistReader = (
|
||||
playlistRef: string
|
||||
) => Promise<unknown>;
|
||||
type SavedPlaylistReader = (playlistRef: string) => Promise<unknown>;
|
||||
|
||||
const IDENTITY_KEYS = [
|
||||
'apiSignature',
|
||||
@@ -29,6 +28,18 @@ const IDENTITY_KEYS = [
|
||||
'videoOutput',
|
||||
] as const satisfies readonly (keyof StalkerSessionIdentityOverrides)[];
|
||||
|
||||
const TRANSPORT_KEYS = [
|
||||
'language',
|
||||
'locale',
|
||||
'origin',
|
||||
'originPolicy',
|
||||
'referer',
|
||||
'refererPolicy',
|
||||
'timezone',
|
||||
'userAgent',
|
||||
'xUserAgent',
|
||||
] as const satisfies readonly (keyof StalkerSessionTransportConfiguration)[];
|
||||
|
||||
export function createStalkerSavedCredentialsLoader(
|
||||
readPlaylist: SavedPlaylistReader
|
||||
): (
|
||||
@@ -39,7 +50,7 @@ export function createStalkerSavedCredentialsLoader(
|
||||
if (!isRecord(value) || !matchesDescriptor(value, descriptor)) {
|
||||
return undefined;
|
||||
}
|
||||
const username = presentString(value['username'])?.trim();
|
||||
const username = presentString(value['username']);
|
||||
const password = value['password'];
|
||||
if (
|
||||
username === undefined ||
|
||||
@@ -60,12 +71,12 @@ function matchesDescriptor(
|
||||
descriptor: StalkerSessionConnectionDescriptor
|
||||
): boolean {
|
||||
if (
|
||||
(playlist['type'] !== undefined &&
|
||||
playlist['type'] !== 'stalker') ||
|
||||
(playlist['type'] !== undefined && playlist['type'] !== 'stalker') ||
|
||||
!matchesSource(playlist, descriptor.sourceUrl) ||
|
||||
!matchesMac(playlist['macAddress'], descriptor.macAddress) ||
|
||||
!matchesProfile(playlist['stalkerProfilePreset'], descriptor) ||
|
||||
!matchesIdentity(playlist, descriptor.identityOverrides)
|
||||
!matchesIdentity(playlist, descriptor.identityOverrides) ||
|
||||
!matchesTransport(playlist, descriptor.transportConfiguration)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -114,8 +125,7 @@ function matchesProfile(
|
||||
): boolean {
|
||||
if (value === undefined) {
|
||||
return (
|
||||
descriptor.profilePreset.id ===
|
||||
'mag250-public-5_1-minimal-v1' &&
|
||||
descriptor.profilePreset.id === 'mag250-public-5_1-minimal-v1' &&
|
||||
descriptor.profilePreset.version === 1
|
||||
);
|
||||
}
|
||||
@@ -137,6 +147,17 @@ function matchesIdentity(
|
||||
);
|
||||
}
|
||||
|
||||
function matchesTransport(
|
||||
playlist: Readonly<Record<string, unknown>>,
|
||||
descriptorTransport: StalkerSessionTransportConfiguration | undefined
|
||||
): boolean {
|
||||
const saved = readSavedTransport(playlist);
|
||||
const descriptor = descriptorTransport ?? {};
|
||||
return TRANSPORT_KEYS.every(
|
||||
(key) => saved[key] === presentString(descriptor[key])
|
||||
);
|
||||
}
|
||||
|
||||
function readSavedIdentity(
|
||||
playlist: Readonly<Record<string, unknown>>
|
||||
): StalkerSessionIdentityOverrides {
|
||||
@@ -157,6 +178,20 @@ function readSavedIdentity(
|
||||
});
|
||||
}
|
||||
|
||||
function readSavedTransport(
|
||||
playlist: Readonly<Record<string, unknown>>
|
||||
): StalkerSessionTransportConfiguration {
|
||||
const structured = playlist['stalkerTransportConfiguration'];
|
||||
if (isRecord(structured)) {
|
||||
return copyTransport(structured);
|
||||
}
|
||||
return copyTransport({
|
||||
origin: playlist['origin'],
|
||||
referer: playlist['referrer'],
|
||||
userAgent: playlist['userAgent'],
|
||||
});
|
||||
}
|
||||
|
||||
function copyIdentity(
|
||||
value: Readonly<Record<string, unknown>>
|
||||
): StalkerSessionIdentityOverrides {
|
||||
@@ -168,6 +203,17 @@ function copyIdentity(
|
||||
) as StalkerSessionIdentityOverrides;
|
||||
}
|
||||
|
||||
function copyTransport(
|
||||
value: Readonly<Record<string, unknown>>
|
||||
): StalkerSessionTransportConfiguration {
|
||||
return Object.fromEntries(
|
||||
TRANSPORT_KEYS.flatMap((key) => {
|
||||
const present = presentString(value[key]);
|
||||
return present === undefined ? [] : [[key, present]];
|
||||
})
|
||||
) as StalkerSessionTransportConfiguration;
|
||||
}
|
||||
|
||||
function firstPresentString(...values: readonly unknown[]): string | undefined {
|
||||
for (const value of values) {
|
||||
const present = presentString(value);
|
||||
|
||||
+5
-7
@@ -108,14 +108,12 @@ describe('Stalker route connection dialogs', () => {
|
||||
const component = fixture.componentInstance;
|
||||
const password = (
|
||||
fixture.nativeElement as HTMLElement
|
||||
).querySelector<HTMLInputElement>(
|
||||
'input[formControlName="password"]'
|
||||
);
|
||||
).querySelector<HTMLInputElement>('input[formControlName="password"]');
|
||||
|
||||
expect(password?.type).toBe('password');
|
||||
expect(
|
||||
(fixture.nativeElement as HTMLElement).textContent
|
||||
).toContain('The saved credentials were rejected.');
|
||||
expect((fixture.nativeElement as HTMLElement).textContent).toContain(
|
||||
'The saved credentials were rejected.'
|
||||
);
|
||||
|
||||
component.passwordVisible.set(true);
|
||||
fixture.detectChanges();
|
||||
@@ -128,7 +126,7 @@ describe('Stalker route connection dialogs', () => {
|
||||
component.submit();
|
||||
expect(dialogRef.close).toHaveBeenCalledWith({
|
||||
password: ' password with spaces ',
|
||||
username: 'replacement-user',
|
||||
username: ' replacement-user ',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-4
@@ -127,9 +127,7 @@ export interface StalkerCredentialsDialogResult {
|
||||
{{ 'CANCEL' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button type="submit">
|
||||
{{
|
||||
'HOME.STALKER_PORTAL.VALIDATE_CREDENTIALS' | translate
|
||||
}}
|
||||
{{ 'HOME.STALKER_PORTAL.VALIDATE_CREDENTIALS' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
@@ -162,7 +160,7 @@ export class StalkerCredentialsDialogComponent {
|
||||
const value = this.form.getRawValue();
|
||||
this.dialogRef.close({
|
||||
password: value.password,
|
||||
username: value.username.trim(),
|
||||
username: value.username,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user