mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
feat(sources): add the shared source row and credential-free formatting
One app-source-row (regular for the Sources page, compact for the playlist switcher) with an app-source-tile, built on pure helpers: - resolvePlaylistSourceKind: one provider rule for icons, filters and counts (the list filter and the panel counts used to disagree). - sourceDisplayName / sourceDisplayUrl: a name or URL never shows a password, token or MAC. URL imports that stored "get.php?username=…&password=…" as the title now show the host. - stripUrlCredentials (shared/logging): drops what the log redactor would mask, plus short user/pass keys. - formatSourceDate / sourceMetaParts / sourceStatusTag: one meta line order and Intl dates and durations in the UI language. - SourceStatusService + SourceHealthHistoryService: one status per source (desktop health check, browser Xtream portal status) and the last conclusive result kept across restarts, so "not responding for 3 days" survives a relaunch. No URL or credential is stored. Also fixes playlistNameHasCredentials answering differently on repeated calls (global regex lastIndex), and adds the UI-kit button, menu and segmented-control mixins (libs/ui/styles/_app-controls.scss); menus opt in with class="app-menu", styled by the components that use them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6fe684af51
commit
028a3db880
51 files changed
+3455
-25
No files matched your search
@@ -1,4 +1,5 @@
|
||||
export {
|
||||
isSensitiveKey,
|
||||
REDACTED_VALUE,
|
||||
redactSensitiveData,
|
||||
} from './lib/redact-sensitive-data';
|
||||
@@ -24,3 +25,4 @@ export type {
|
||||
RendererPerformancePhaseMetadata,
|
||||
RendererPerformancePhaseOutcome,
|
||||
} from './lib/renderer-performance-phase';
|
||||
export { stripUrlCredentials } from './lib/strip-url-credentials';
|
||||
@@ -54,7 +54,7 @@ const SENSITIVE_KEY_SUFFIXES = [
|
||||
'username',
|
||||
];
|
||||
|
||||
const XTREAM_CREDENTIAL_PATH_SEGMENTS = new Set([
|
||||
export const XTREAM_CREDENTIAL_PATH_SEGMENTS: ReadonlySet<string> = new Set([
|
||||
'live',
|
||||
'movie',
|
||||
'series',
|
||||
@@ -79,7 +79,7 @@ function normalizeKey(key: string): string {
|
||||
return key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
function isSensitiveKey(key: string): boolean {
|
||||
export function isSensitiveKey(key: string): boolean {
|
||||
const normalized = normalizeKey(key);
|
||||
return (
|
||||
SENSITIVE_KEY_NAMES.has(normalized) ||
|
||||
@@ -196,7 +196,7 @@ function redactEmbeddedSensitivePairs(value: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
function looksLikeSearchParams(value: string): boolean {
|
||||
export function looksLikeSearchParams(value: string): boolean {
|
||||
return /^[^=&\s]+=[^&]*(?:&[^=&\s]+=[^&]*)*$/u.test(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { stripUrlCredentials } from './strip-url-credentials';
|
||||
|
||||
describe('stripUrlCredentials', () => {
|
||||
it.each([
|
||||
[
|
||||
'http://host.tv:8080/get.php?username=joe&password=hunter2&type=m3u_plus',
|
||||
'http://host.tv:8080/get.php?type=m3u_plus',
|
||||
],
|
||||
['http://joe:hunter2@host.tv/list.m3u', 'http://host.tv/list.m3u'],
|
||||
[
|
||||
'http://host.tv/live/joe/hunter2/123.ts',
|
||||
'http://host.tv/live/123.ts',
|
||||
],
|
||||
[
|
||||
'https://cdn.tv/playlist.m3u8?token=abc123&quality=hd',
|
||||
'https://cdn.tv/playlist.m3u8?quality=hd',
|
||||
],
|
||||
['http://portal.tv/c/?mac=00:1A:79:00:00:01', 'http://portal.tv/c/'],
|
||||
[
|
||||
'http://host.tv/list.m3u#user=joe&pass=hunter2',
|
||||
'http://host.tv/list.m3u',
|
||||
],
|
||||
['http://[::1]:3211/get.php?password=p', 'http://[::1]:3211/get.php'],
|
||||
[
|
||||
'https://iptv-org.github.io/iptv/index.m3u',
|
||||
'https://iptv-org.github.io/iptv/index.m3u',
|
||||
],
|
||||
])('shows %s as %s', (input, expected) => {
|
||||
expect(stripUrlCredentials(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('never returns a password from a credential-bearing URL', () => {
|
||||
const urls = [
|
||||
'http://joe:hunter2@host.tv/get.php?username=joe&password=hunter2',
|
||||
'http://host.tv/movie/joe/hunter2/9.mkv?auth=hunter2',
|
||||
'http://host.tv/x#password=hunter2',
|
||||
];
|
||||
for (const url of urls) {
|
||||
expect(stripUrlCredentials(url)).not.toMatch(/hunter2/);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns null for text that is not an absolute URL', () => {
|
||||
expect(stripUrlCredentials('get.php?username=joe')).toBeNull();
|
||||
expect(stripUrlCredentials('')).toBeNull();
|
||||
expect(stripUrlCredentials('not a url')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
isSensitiveKey,
|
||||
looksLikeSearchParams,
|
||||
XTREAM_CREDENTIAL_PATH_SEGMENTS,
|
||||
} from './redact-sensitive-data';
|
||||
|
||||
/**
|
||||
* Short account keys some playlist URLs use. A log line keeps them for
|
||||
* diagnosis; a list on screen has no reason to.
|
||||
*/
|
||||
const SHORT_ACCOUNT_KEYS = new Set(['user', 'usr', 'pass', 'pas']);
|
||||
|
||||
function isDisplaySensitiveKey(key: string): boolean {
|
||||
return SHORT_ACCOUNT_KEYS.has(key.toLowerCase()) || isSensitiveKey(key);
|
||||
}
|
||||
|
||||
function withoutSensitiveParams(params: URLSearchParams): string {
|
||||
const kept = new URLSearchParams();
|
||||
params.forEach((value, key) => {
|
||||
if (!isDisplaySensitiveKey(key)) {
|
||||
kept.append(key, value);
|
||||
}
|
||||
});
|
||||
return kept.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* A URL safe to show on screen. Where the log redactor replaces secrets with
|
||||
* a marker, this drops them: the userinfo, every query or fragment parameter
|
||||
* the redactor treats as sensitive (`username`, `password`, `token`, `mac`,
|
||||
* …), and the two account segments after `/live/`, `/movie/`, `/series/` or
|
||||
* `/timeshift/` in an Xtream stream path. A list can then say where a source
|
||||
* points without any part of its credentials.
|
||||
*
|
||||
* Returns null for text that is not an absolute URL, so a caller never
|
||||
* renders the unparsed original by mistake.
|
||||
*/
|
||||
export function stripUrlCredentials(value: string): string | null {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value.trim());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
url.username = '';
|
||||
url.password = '';
|
||||
|
||||
const segments = url.pathname.split('/');
|
||||
const kept: string[] = [];
|
||||
for (let index = 0; index < segments.length; index += 1) {
|
||||
kept.push(segments[index]);
|
||||
if (
|
||||
XTREAM_CREDENTIAL_PATH_SEGMENTS.has(segments[index]) &&
|
||||
index + 2 < segments.length
|
||||
) {
|
||||
index += 2;
|
||||
}
|
||||
}
|
||||
url.pathname = kept.join('/');
|
||||
|
||||
const search = withoutSensitiveParams(url.searchParams);
|
||||
url.search = search ? `?${search}` : '';
|
||||
|
||||
const fragment = url.hash.slice(1);
|
||||
if (looksLikeSearchParams(fragment)) {
|
||||
const hash = withoutSensitiveParams(new URLSearchParams(fragment));
|
||||
url.hash = hash ? `#${hash}` : '';
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
Reference in new issue
Block a user