Enforce request policies for fetch() or GM_xmlhttpRequest methods with fallback routing
Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta
// @require https://update.greasyfork.org/scripts/591898/1906160/Transport%20policy%20enforcer.js
// ==UserScript==
// @name Transport policy enforcer
// @namespace 861ddd094884eac5bea7a3b12e074f34
// @author Anonymous, Codex
// @version 1.0.0
// @license BSD-0
// @grant none
// ==/UserScript==
(function (global) {
'use strict';
const TRANSPORTS = ['gm-first', 'fetch-first', 'gm-only', 'fetch-only'];
const config = {
transport: 'gm-first',
fallbackOn: {
transportError: true,
statuses: [403, 429, [500, 599]],
predicate: null,
},
gmProvider: () =>
(typeof GM !== 'undefined' && GM.xmlHttpRequest) ||
(typeof GM_xmlhttpRequest === 'function' && GM_xmlhttpRequest) ||
null,
fetchProvider: () =>
(typeof window !== 'undefined' && window.fetch)
? window.fetch.bind(window)
: (typeof fetch === 'function' ? fetch : null),
timeoutMs: 30000,
onTransportEvent: null,
};
function configure(options) {
return Object.assign(config, options || {});
}
function matchesStatus(status, list) {
if (!Array.isArray(list)) return false;
const value = Number(status);
return list.some((entry) => {
if (Array.isArray(entry)) {
return entry.length === 2 && value >= Number(entry[0]) && value <= Number(entry[1]);
}
return value === Number(entry);
});
}
function emit(event) {
if (typeof config.onTransportEvent !== 'function') return;
try { config.onTransportEvent(event); } catch (e) { /* host hook is best-effort */ }
}
function asError(value, message) {
if (value instanceof Error) return value;
const error = new Error(value && value.message ? value.message : message);
if (value !== undefined) error.cause = value;
return error;
}
function transportError(value, transport, details, kind) {
const error = asError(value, `TransportPolicy: ${kind} using ${transport} for ${details.url}`);
try { error.transport = transport; } catch (e) { /* a frozen provider error is replaced below */ }
if (error.transport !== transport) {
const replacement = new Error(error.message);
replacement.cause = error;
replacement.transport = transport;
if (kind === 'timeout') replacement.timedOut = true;
return replacement;
}
if (kind === 'timeout') error.timedOut = true;
return error;
}
function normaliseGmResponse(raw, details) {
raw = raw || {};
const responseText = typeof raw.responseText === 'string' ? raw.responseText : '';
return {
status: Number(raw.status) || 0,
statusText: raw.statusText || '',
responseText,
response: raw.response !== undefined ? raw.response : responseText,
responseHeaders: raw.responseHeaders || '',
finalUrl: raw.finalUrl || details.url,
transport: 'gm',
fellBack: false,
};
}
function gmRequest(details, provider) {
return new Promise((resolve, reject) => {
const options = Object.assign({}, details, {
timeout: details.timeout ?? config.timeoutMs,
onload: (response) => resolve(normaliseGmResponse(response, details)),
onerror: (error) => reject(transportError(error, 'gm', details, 'network error')),
ontimeout: (error) => reject(transportError(error, 'gm', details, 'timeout')),
onabort: (error) => reject(transportError(error, 'gm', details, 'abort')),
});
try {
provider(options);
} catch (error) {
reject(transportError(error, 'gm', details, 'provider error'));
}
});
}
function headerString(headers) {
let lines = '';
if (headers && typeof headers.forEach === 'function') {
headers.forEach((value, name) => { lines += `${name}: ${value}\r\n`; });
}
return lines;
}
async function fetchRequest(details, provider) {
const timeout = details.timeout ?? config.timeoutMs;
const init = {
method: details.method || 'GET',
credentials: 'include',
cache: 'no-store',
redirect: 'follow',
};
if (details.headers) init.headers = Object.assign({}, details.headers);
if (details.data != null) init.body = details.data;
let controller;
let timer;
if (timeout > 0 && typeof AbortController === 'function') {
controller = new AbortController();
init.signal = controller.signal;
timer = setTimeout(() => controller.abort(), timeout);
}
try {
const raw = await provider(details.url, init);
const responseType = details.responseType || 'text';
let response;
let responseText = '';
if (responseType === 'json') {
responseText = await raw.text();
response = JSON.parse(responseText);
} else if (responseType === 'arraybuffer') {
response = await raw.arrayBuffer();
} else if (responseType === 'blob') {
response = await raw.blob();
} else {
responseText = await raw.text();
response = responseText;
}
return {
status: Number(raw.status) || 0,
statusText: raw.statusText || '',
responseText,
response,
responseHeaders: headerString(raw.headers),
finalUrl: raw.url || details.url,
transport: 'fetch',
fellBack: false,
};
} catch (error) {
const timedOut = !!(controller && controller.signal.aborted);
throw transportError(error, 'fetch', details, timedOut ? 'timeout' : 'transport error');
} finally {
if (timer !== undefined) clearTimeout(timer);
}
}
function providerFor(transport) {
const factory = transport === 'gm' ? config.gmProvider : config.fetchProvider;
if (typeof factory !== 'function') return null;
return factory();
}
async function attempt(transport, details) {
emit({ type: 'attempt', transport, url: details.url, method: details.method || 'GET' });
let provider;
try {
provider = providerFor(transport);
} catch (error) {
throw transportError(error, transport, details, 'provider error');
}
if (typeof provider !== 'function') {
const error = transportError(undefined, transport, details, 'unavailable');
error.unavailable = true;
throw error;
}
return transport === 'gm'
? gmRequest(details, provider)
: fetchRequest(details, provider);
}
function ordering(policy) {
switch (policy) {
case 'gm-first': return ['gm', 'fetch'];
case 'fetch-first': return ['fetch', 'gm'];
case 'gm-only': return ['gm'];
case 'fetch-only': return ['fetch'];
default: throw new Error(`TransportPolicy: unknown transport policy ${policy}`);
}
}
function fallbackDecision(outcome, rules, transport) {
if (typeof rules.predicate === 'function') {
const verdict = rules.predicate(outcome.response, transport);
if (typeof verdict === 'boolean') {
return verdict ? { fallback: true, reason: 'predicate' } : { fallback: false };
}
}
if (outcome.error) {
return rules.transportError
? { fallback: true, reason: 'transport-error' }
: { fallback: false };
}
return matchesStatus(outcome.response.status, rules.statuses)
? { fallback: true, reason: 'status' }
: { fallback: false };
}
function primarySummary(transport, outcome) {
if (outcome.error) return { transport, error: outcome.error };
return { transport, status: outcome.response.status };
}
function finalEvent(type, transport, details, value, reason) {
const event = { type, transport, url: details.url, method: details.method || 'GET' };
if (value && typeof value.status === 'number') event.status = value.status;
if (value instanceof Error) event.error = value;
if (reason) event.reason = reason;
emit(event);
}
async function request(input) {
const details = input || {};
const policy = details.transport || config.transport;
const order = ordering(policy);
const rules = Object.assign({}, config.fallbackOn || {}, details.fallbackOn || {});
const { transport: ignoredTransport, fallbackOn: ignoredFallback, ...networkDetails } = details;
const primaryTransport = order[0];
let primary;
try {
primary = { response: await attempt(primaryTransport, networkDetails) };
} catch (error) {
primary = { error };
}
if (order.length === 1) {
if (primary.error) {
finalEvent('rejected', primaryTransport, networkDetails, primary.error,
primary.error.unavailable ? 'unavailable' : 'transport-error');
throw primary.error;
}
finalEvent('resolved', primaryTransport, networkDetails, primary.response);
return primary.response;
}
let decision;
if (primary.error && primary.error.unavailable) {
decision = { fallback: true, reason: 'unavailable' };
} else {
try {
decision = fallbackDecision(primary, rules, primaryTransport);
} catch (error) {
const rejection = transportError(error, primaryTransport, networkDetails, 'predicate error');
finalEvent('rejected', primaryTransport, networkDetails, rejection);
throw rejection;
}
}
if (!decision.fallback) {
if (primary.error) {
finalEvent('rejected', primaryTransport, networkDetails, primary.error, 'transport-error');
throw primary.error;
}
finalEvent('resolved', primaryTransport, networkDetails, primary.response);
return primary.response;
}
const fallbackEvent = {
type: 'fallback',
transport: primaryTransport,
url: networkDetails.url,
method: networkDetails.method || 'GET',
reason: decision.reason,
};
if (primary.response) fallbackEvent.status = primary.response.status;
if (primary.error) fallbackEvent.error = primary.error;
emit(fallbackEvent);
const secondaryTransport = order[1];
try {
const response = await attempt(secondaryTransport, networkDetails);
response.fellBack = true;
response.primary = primarySummary(primaryTransport, primary);
finalEvent('resolved', secondaryTransport, networkDetails, response);
return response;
} catch (error) {
error.transport = secondaryTransport;
if (primary.error) error.primaryError = primary.error;
else error.primaryResponse = primary.response;
finalEvent('rejected', secondaryTransport, networkDetails, error,
error.unavailable ? 'unavailable' : 'transport-error');
throw error;
}
}
function xmlHttpRequest(details) {
const { onload, onerror, ontimeout, ...requestDetails } = details;
request(requestDetails).then(
(response) => { if (typeof onload === 'function') onload(response); },
(error) => {
if (error.timedOut && typeof ontimeout === 'function') ontimeout(error);
else if (typeof onerror === 'function') onerror(error);
}
);
return { abort() {} };
}
global.TransportPolicy = {
configure,
config,
request,
xmlHttpRequest,
TRANSPORTS,
matchesStatus,
};
}(typeof globalThis !== 'undefined' ? globalThis : this));