fix: detect and report Cloudflare challenge blocks
This commit is contained in:
parent
62098f8e84
commit
0638ce0a8c
9 changed files with 91 additions and 13 deletions
1
src/error/CloudflareChallengeError.ts
Normal file
1
src/error/CloudflareChallengeError.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export class CloudflareChallengeError extends Error {}
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from './CloudflareChallengeError';
|
||||
export * from './NotFoundError';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { DoodStream } from './DoodStream';
|
|||
import { Dropload } from './Dropload';
|
||||
import { SuperVideo } from './SuperVideo';
|
||||
import { ExternalUrl } from './ExternalUrl';
|
||||
import { NotFoundError } from '../error';
|
||||
import { CloudflareChallengeError, NotFoundError } from '../error';
|
||||
|
||||
export class ExtractorRegistry {
|
||||
private readonly logger: winston.Logger;
|
||||
|
|
@ -43,6 +43,20 @@ export class ExtractorRegistry {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (error instanceof CloudflareChallengeError) {
|
||||
this.logger.warn(`${extractor.id}: Request was blocked by Cloudflare challenge.`, ctx);
|
||||
|
||||
return {
|
||||
url: url,
|
||||
isExternal: true,
|
||||
cloudflareChallenge: true,
|
||||
label: url.host,
|
||||
sourceId: '',
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.warn(`${extractor.id} error: ${error}`, ctx);
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ export interface Meta {
|
|||
export interface UrlResult {
|
||||
url: URL;
|
||||
isExternal?: boolean;
|
||||
cloudflareChallenge?: boolean;
|
||||
label: string;
|
||||
sourceId: string;
|
||||
sourceId?: string;
|
||||
meta: Meta;
|
||||
requestHeaders?: Record<string, string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import axios, { AxiosError } from 'axios';
|
|||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import { Fetcher } from './Fetcher';
|
||||
import { Context } from '../types';
|
||||
import { NotFoundError } from '../error';
|
||||
import { CloudflareChallengeError, NotFoundError } from '../error';
|
||||
|
||||
const axiosMock = new AxiosMockAdapter(axios);
|
||||
|
||||
|
|
@ -125,6 +125,12 @@ describe('fetch', () => {
|
|||
await expect(fetcher.text(ctx, new URL('https://some-url.test/'))).rejects.toBeInstanceOf(NotFoundError);
|
||||
});
|
||||
|
||||
test('converts Cloudflare challenge block to custom CloudflareChallengeError', async () => {
|
||||
axiosMock.onGet().reply(403, undefined, { 'cf-mitigated': 'challenge' });
|
||||
|
||||
await expect(fetcher.text(ctx, new URL('https://some-url.test/'))).rejects.toBeInstanceOf(CloudflareChallengeError);
|
||||
});
|
||||
|
||||
test('passes through other errors', async () => {
|
||||
axiosMock.onGet().networkError();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import TTLCache from '@isaacs/ttlcache';
|
|||
import UserAgent from 'user-agents';
|
||||
import winston from 'winston';
|
||||
import { Context } from '../types';
|
||||
import { NotFoundError } from '../error';
|
||||
import { CloudflareChallengeError, NotFoundError } from '../error';
|
||||
|
||||
export class Fetcher {
|
||||
private readonly axios: AxiosInstance;
|
||||
|
|
@ -69,9 +69,16 @@ export class Fetcher {
|
|||
try {
|
||||
return await callable();
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === 404) {
|
||||
throw new NotFoundError();
|
||||
if (isAxiosError(error)) {
|
||||
if (error.response?.status === 404) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
if (error.response?.headers['cf-mitigated'] === 'challenge') {
|
||||
throw new CloudflareChallengeError();
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ExtractorRegistry } from '../extractor';
|
|||
import { StreamResolver } from './StreamResolver';
|
||||
import { Handler, MeineCloud, MostraGuarda } from '../handler';
|
||||
import { Fetcher } from './Fetcher';
|
||||
import { Context } from '../types';
|
||||
import { Context, CountryCode, UrlResult } from '../types';
|
||||
import { NotFoundError } from '../error';
|
||||
jest.mock('../utils/Fetcher');
|
||||
|
||||
|
|
@ -57,6 +57,34 @@ describe('resolve', () => {
|
|||
expect(streams).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('adds Cloudflare blocked info', async () => {
|
||||
class MockHandler implements Handler {
|
||||
readonly id = 'mockhandler';
|
||||
|
||||
readonly label = 'MockHandler';
|
||||
|
||||
readonly contentTypes = ['movie'];
|
||||
|
||||
readonly countryCodes: CountryCode[] = ['de'];
|
||||
|
||||
readonly handle = async (): Promise<(UrlResult | undefined)[]> => {
|
||||
return [{
|
||||
url: new URL('https://example.com'),
|
||||
isExternal: true,
|
||||
cloudflareChallenge: true,
|
||||
label: 'example.com',
|
||||
sourceId: '',
|
||||
meta: {
|
||||
countryCode: 'de',
|
||||
},
|
||||
}];
|
||||
};
|
||||
}
|
||||
|
||||
const streams = await streamResolver.resolve(ctx, [new MockHandler()], 'movie', 'tt11655566');
|
||||
expect(streams).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('ignores not found errors', async () => {
|
||||
const mockHandler: Handler = {
|
||||
id: 'mockhandler',
|
||||
|
|
|
|||
|
|
@ -85,14 +85,24 @@ export class StreamResolver {
|
|||
titleSecondLineEntries.push(`💾 ${bytes.format(urlResult.meta.bytes, { unitSeparator: ' ' })}`);
|
||||
}
|
||||
titleSecondLineEntries.push(`⚙️ ${urlResult.label}`);
|
||||
const title = [urlResult.meta.title ?? '', titleSecondLineEntries.join(' ')].join('\n');
|
||||
|
||||
const titleLines = [];
|
||||
if (urlResult.meta.title) {
|
||||
titleLines.push(urlResult.meta.title);
|
||||
}
|
||||
titleLines.push(titleSecondLineEntries.join(' '));
|
||||
if (urlResult.cloudflareChallenge) {
|
||||
titleLines.push('Request was blocked by Cloudflare.');
|
||||
}
|
||||
|
||||
const title = titleLines.join('\n');
|
||||
|
||||
return {
|
||||
[urlResult.isExternal ? 'externalUrl' : 'url']: urlResult.url.href,
|
||||
name,
|
||||
title,
|
||||
behaviorHints: {
|
||||
bingeGroup: `webstreamr-${urlResult.sourceId}`,
|
||||
...(urlResult.sourceId && { bingeGroup: `webstreamr-${urlResult.sourceId}` }),
|
||||
...(urlResult.requestHeaders !== undefined && {
|
||||
notWebReady: true,
|
||||
proxyHeaders: { request: urlResult.requestHeaders },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,17 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`resolve adds Cloudflare blocked info 1`] = `
|
||||
[
|
||||
{
|
||||
"behaviorHints": {},
|
||||
"externalUrl": "https://example.com/",
|
||||
"name": "WebStreamr 🇩🇪 external",
|
||||
"title": "⚙️ example.com
|
||||
Request was blocked by Cloudflare.",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`resolve returns sorted results 1`] = `
|
||||
[
|
||||
{
|
||||
|
|
@ -78,8 +90,7 @@ exports[`resolve returns sorted results 1`] = `
|
|||
},
|
||||
"externalUrl": "https://mixdrop.ag/e/3nzwveprim63or6",
|
||||
"name": "WebStreamr 🇩🇪 external",
|
||||
"title": "
|
||||
⚙️ mixdrop.ag",
|
||||
"title": "⚙️ mixdrop.ag",
|
||||
},
|
||||
{
|
||||
"behaviorHints": {
|
||||
|
|
@ -87,8 +98,7 @@ exports[`resolve returns sorted results 1`] = `
|
|||
},
|
||||
"externalUrl": "https://mixdrop.ag/e/vk196d6xfzwwo1",
|
||||
"name": "WebStreamr 🇮🇹 external",
|
||||
"title": "
|
||||
⚙️ mixdrop.ag",
|
||||
"title": "⚙️ mixdrop.ag",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue