253 lines
6.2 KiB
TypeScript
253 lines
6.2 KiB
TypeScript
"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<string>();
|
|
|
|
/* 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<string>([
|
|
"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<any[]>(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: ["<all_urls>"]}, ["responseHeaders"]);
|
|
});
|
|
const p = Promise.race([
|
|
wr,
|
|
new Promise<any[]>((_, 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;
|
|
}
|
|
}
|