"use strict"; // License: MIT import MimeType from "whatwg-mimetype"; // eslint-disable-next-line no-unused-vars import { Download } from "./download"; import { CHROME, webRequest } from "../browser"; import { CDHeaderParser } from "../cdheaderparser"; import { sanitizePath, parsePath } from "../util"; import { MimeDB } from "../mime"; const PREROLL_HEURISTICS = /dl|attach|download|name|file|get|retr|^n$|\.(php|asp|py|pl|action|htm|shtm)/i; const PREROLL_HOSTS = /4cdn|chan/; const PREROLL_TIMEOUT = 10000; const PREROLL_NOPE = new Set(); /* eslint-disable no-magic-numbers */ const NOPE_STATUSES = Object.freeze(new Set([ 400, 401, 402, 405, 416, ])); /* eslint-enable no-magic-numbers */ const PREROLL_SEARCHEXTS = Object.freeze(new Set([ "php", "asp", "aspx", "inc", "py", "pl", "action", "htm", "html", "shtml" ])); const NAME_TESTER = /\.[a-z0-9]{1,5}$/i; const CDPARSER = new CDHeaderParser(); export interface PrerollResults { error?: string; name?: string; mime?: string; finalURL?: string; } export class Preroller { private readonly download: Download constructor(download: Download) { this.download = download; } get shouldPreroll() { if (CHROME) { return false; } const {uURL, renamer} = this.download; const {pathname, search, host} = uURL; if (PREROLL_NOPE.has(host)) { return false; } if (!renamer.p_ext) { return true; } if (search.length) { return true; } if (uURL.pathname.endsWith("/")) { return true; } if (PREROLL_HEURISTICS.test(pathname)) { return true; } if (PREROLL_HOSTS.test(host)) { return true; } return false; } async roll() { try { return await (CHROME ? this.prerollChrome() : this.prerollFirefox()); } catch (ex) { console.error("Failed to preroll", this, ex.toString(), ex.stack, ex); } return null; } private async prerollFirefox() { const controller = new AbortController(); const {signal} = controller; const {uURL, uReferrer} = this.download; const res = await fetch(uURL.toString(), { method: "GET", headers: new Headers({ Range: "bytes=0-1", }), mode: "same-origin", signal, referrer: (uReferrer || uURL).toString(), }); if (res.body) { res.body.cancel(); } controller.abort(); const {headers} = res; return this.finalize(headers, res); } private async prerollChrome() { let rid = ""; const {uURL, uReferrer} = this.download; const rurl = uURL.toString(); let listener: any; const wr = new Promise(resolve => { listener = (details: any) => { const {url, requestId, statusCode} = details; if (rid !== requestId && url !== rurl) { return; } // eslint-disable-next-line no-magic-numbers if (statusCode >= 300 && statusCode < 400) { // Redirect, continue tracking; rid = requestId; return; } resolve(details.responseHeaders); }; webRequest.onHeadersReceived.addListener( listener, {urls: [""]}, ["responseHeaders"]); }); const p = Promise.race([ wr, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), PREROLL_TIMEOUT)) ]); p.finally(() => { webRequest.onHeadersReceived.removeListener(listener); }); const controller = new AbortController(); const {signal} = controller; const res = await fetch(rurl, { method: "GET", headers: new Headers({ "Range": "bytes=0-1", "X-DTA-ID": this.download.sessionId.toString(), }), signal, referrer: (uReferrer || uURL).toString(), }); if (res.body) { res.body.cancel(); } controller.abort(); const headers = await p; return this.finalize( new Headers(headers.map(i => [i.name, i.value])), res); } private finalize(headers: Headers, res: Response): PrerollResults { const rv: PrerollResults = {}; const type = MimeType.parse(headers.get("content-type") || ""); if (type) { rv.mime = type.essence; } const dispHeader = headers.get("content-disposition"); if (dispHeader) { const file = CDPARSER.parse(dispHeader); // Sanitize rv.name = sanitizePath(file.replace(/[/\\]+/g, "-")); } else { const detected = Preroller.maybeFindNameFromSearchParams( this.download, rv); if (detected) { rv.name = detected; } } rv.finalURL = res.url; /* eslint-disable no-magic-numbers */ const {status} = res; if (status === 404) { rv.error = "SERVER_BAD_CONTENT"; } else if (status === 403) { rv.error = "SERVER_FORBIDDEN"; } else if (status === 402 || status === 407) { rv.error = "SERVER_UNAUTHORIZED"; } else if (NOPE_STATUSES.has(status)) { PREROLL_NOPE.add(this.download.uURL.host); if (PREROLL_NOPE.size > 1000) { PREROLL_NOPE.delete(PREROLL_NOPE.keys().next().value); } } else if (status > 400 && status < 500) { rv.error = "SERVER_FAILED"; } /* eslint-enable no-magic-numbers */ return rv; } static maybeFindNameFromSearchParams( download: Download, res: PrerollResults) { const {p_ext: ext} = download.renamer; if (ext && !PREROLL_SEARCHEXTS.has(ext.toLocaleLowerCase())) { return undefined; } return Preroller.findNameFromSearchParams(download.uURL, res.mime); } static findNameFromSearchParams(url: URL, mimetype?: string) { const {searchParams} = url; let detected = ""; for (const [, value] of searchParams) { if (!NAME_TESTER.test(value)) { continue; } const p = parsePath(value); if (!p.base || !p.ext) { continue; } if (!MimeDB.hasExtension(p.ext)) { continue; } if (mimetype) { const mime = MimeDB.getMime(mimetype); if (mime && !mime.extensions.has(p.ext.toLowerCase())) { continue; } } const sanitized = sanitizePath(p.name); if (sanitized.length <= detected.length) { continue; } detected = sanitized; } return detected; } }