mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
fix(downloads): refine artwork URL checks
This commit is contained in:
1 parent
3e7de1ebc9
commit
1d20a36640
4 files changed
+92
-14
No files matched your search
@@ -33,15 +33,43 @@ const CREDENTIAL_QUERY_TERMS = [
|
||||
'token',
|
||||
];
|
||||
|
||||
const CREDENTIAL_PATH_KEYS = new Set([
|
||||
'accesskey',
|
||||
'accesstoken',
|
||||
'apikey',
|
||||
'auth',
|
||||
'authentication',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'credential',
|
||||
'credentials',
|
||||
'devicemac',
|
||||
'key',
|
||||
'mac',
|
||||
'macaddress',
|
||||
'oauth',
|
||||
'password',
|
||||
'passwd',
|
||||
'privatekey',
|
||||
'refreshtoken',
|
||||
'secret',
|
||||
'secretkey',
|
||||
'session',
|
||||
'sig',
|
||||
'signature',
|
||||
'signingkey',
|
||||
'token',
|
||||
]);
|
||||
|
||||
function invalidArtworkUrl(): never {
|
||||
throw new Error('Invalid download metadata snapshot');
|
||||
}
|
||||
|
||||
function isCredentialQueryKey(key: string): boolean {
|
||||
return isCredentialToken(normalizeToken(key));
|
||||
return isCredentialQueryToken(normalizeToken(key));
|
||||
}
|
||||
|
||||
function isCredentialToken(normalized: string): boolean {
|
||||
function isCredentialQueryToken(normalized: string): boolean {
|
||||
if (
|
||||
normalized === 'auth' ||
|
||||
normalized === 'key' ||
|
||||
@@ -64,6 +92,15 @@ function isCredentialToken(normalized: string): boolean {
|
||||
].some((term) => normalized.includes(term));
|
||||
}
|
||||
|
||||
function isCredentialPathSegment(segment: string): boolean {
|
||||
const keyValueSeparator = segment.search(/[=:]/);
|
||||
const key =
|
||||
keyValueSeparator === -1
|
||||
? segment
|
||||
: segment.slice(0, keyValueSeparator);
|
||||
return CREDENTIAL_PATH_KEYS.has(normalizeToken(key));
|
||||
}
|
||||
|
||||
function normalizeToken(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
@@ -105,6 +142,7 @@ export function getNormalizedDownloadUrlIdentity(
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
return undefined;
|
||||
}
|
||||
url.hash = '';
|
||||
return url.href;
|
||||
} catch {
|
||||
return undefined;
|
||||
@@ -135,7 +173,7 @@ export function normalizeDownloadArtworkUrl(
|
||||
const hasCredentialPathToken = decodedPathname
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.some((segment) => isCredentialToken(normalizeToken(segment)));
|
||||
.some(isCredentialPathSegment);
|
||||
if (
|
||||
(url.protocol !== 'http:' && url.protocol !== 'https:') ||
|
||||
url.username !== '' ||
|
||||
|
||||
@@ -252,6 +252,22 @@ describe('download metadata snapshot', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts artwork filenames containing title-like credential words', () => {
|
||||
const snapshot: DownloadMetadataSnapshot = {
|
||||
...validSnapshot,
|
||||
posterUrl: 'https://images.example.test/posters/secret-garden.jpg',
|
||||
episode: {
|
||||
episodeNumber: 9,
|
||||
seasonNumber: 1,
|
||||
stillUrl: 'https://images.example.test/stills/session-9.jpg',
|
||||
},
|
||||
};
|
||||
|
||||
expect(JSON.parse(encodeDownloadMetadataSnapshot(snapshot))).toEqual(
|
||||
snapshot
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes blank optional artwork sentinels as absent', () => {
|
||||
expect(
|
||||
JSON.parse(
|
||||
@@ -366,6 +382,8 @@ describe('download metadata snapshot', () => {
|
||||
'AUTH',
|
||||
'device_mac',
|
||||
'api-key',
|
||||
'access_token=secret',
|
||||
'token:abc',
|
||||
])('rejects credential-like artwork path token %s', (pathToken) => {
|
||||
expect(() =>
|
||||
encodeDownloadMetadataSnapshot({
|
||||
|
||||
@@ -367,20 +367,20 @@ describe('downloads events: managed metadata updates', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects metadata artwork that reuses the stored stream URL', async () => {
|
||||
const url = 'https://streams.example.test/images/live.jpg';
|
||||
it('rejects metadata artwork when the stored stream differs only by a fragment', async () => {
|
||||
const artworkUrl = 'https://streams.example.test/images/live.jpg';
|
||||
const database = createSingleRowDatabase({
|
||||
contentType: 'vod',
|
||||
id: 42,
|
||||
playlistId: 'playlist-a',
|
||||
seriesXtreamId: null,
|
||||
url,
|
||||
url: `${artworkUrl}#player`,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getHandler('DOWNLOADS_UPDATE_METADATA')(null, 42, {
|
||||
...movieSnapshot,
|
||||
posterUrl: url,
|
||||
posterUrl: artworkUrl,
|
||||
})
|
||||
).resolves.toEqual({
|
||||
error: 'Invalid download metadata snapshot',
|
||||
@@ -393,7 +393,7 @@ describe('downloads events: managed metadata updates', () => {
|
||||
});
|
||||
|
||||
it('rejects a group update before writing when artwork reuses a member stream URL', async () => {
|
||||
const reusedUrl = 'https://streams.example.test/images/episode-2.jpg';
|
||||
const artworkUrl = 'https://streams.example.test/images/episode-2.jpg';
|
||||
const database = createGroupedDatabase(
|
||||
{
|
||||
contentType: 'episode',
|
||||
@@ -417,7 +417,7 @@ describe('downloads events: managed metadata updates', () => {
|
||||
metadataSnapshot: null,
|
||||
seasonNumber: 1,
|
||||
title: 'Two',
|
||||
url: reusedUrl,
|
||||
url: `${artworkUrl}#player`,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -426,7 +426,7 @@ describe('downloads events: managed metadata updates', () => {
|
||||
getHandler('DOWNLOADS_UPDATE_METADATA')(null, 41, {
|
||||
...movieSnapshot,
|
||||
mediaKind: 'series',
|
||||
posterUrl: reusedUrl,
|
||||
posterUrl: artworkUrl,
|
||||
})
|
||||
).resolves.toEqual({
|
||||
error: 'Invalid download metadata snapshot',
|
||||
|
||||
@@ -293,9 +293,31 @@ describe('download request metadata snapshots', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects a new download when its stream differs from artwork only by a fragment', async () => {
|
||||
const artworkUrl = 'https://streams.example.test/images/live.jpg';
|
||||
const request = await setupStartMetadataRequest(undefined);
|
||||
|
||||
await expect(
|
||||
request.startDownloadRequest(
|
||||
{
|
||||
...startPayload({
|
||||
...metadataSnapshot,
|
||||
posterUrl: artworkUrl,
|
||||
}),
|
||||
url: `${artworkUrl}#player`,
|
||||
},
|
||||
request.authorizer
|
||||
)
|
||||
).rejects.toThrow('Invalid download metadata snapshot');
|
||||
|
||||
expect(request.db.insert).not.toHaveBeenCalled();
|
||||
expect(request.db.update).not.toHaveBeenCalled();
|
||||
expect(request.enqueueDownload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a restart whose poster reuses the normalized stored stream URL', async () => {
|
||||
const storedUrl =
|
||||
'https://STREAMS.example.test:443/images/section/../live.jpg';
|
||||
'https://STREAMS.example.test:443/images/section/../live.jpg#player';
|
||||
const posterUrl = 'https://streams.example.test/images/live.jpg';
|
||||
const request = await setupStartMetadataRequest({
|
||||
contentType: 'vod',
|
||||
@@ -328,8 +350,8 @@ describe('download request metadata snapshots', () => {
|
||||
});
|
||||
|
||||
it('rejects a restart whose poster reuses the replacement stream URL', async () => {
|
||||
const replacementUrl =
|
||||
'https://replacement.example.test/images/live.jpg';
|
||||
const artworkUrl = 'https://replacement.example.test/images/live.jpg';
|
||||
const replacementUrl = `${artworkUrl}#player`;
|
||||
const request = await setupStartMetadataRequest({
|
||||
contentType: 'vod',
|
||||
filePath: null,
|
||||
@@ -347,7 +369,7 @@ describe('download request metadata snapshots', () => {
|
||||
{
|
||||
...startPayload({
|
||||
...metadataSnapshot,
|
||||
posterUrl: replacementUrl,
|
||||
posterUrl: artworkUrl,
|
||||
}),
|
||||
url: replacementUrl,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user