source/test/run_module.beta.js
2025-12-08 19:27:48 +01:00

319 lines
11 KiB
JavaScript

#!/usr/bin/env node
// Simple CLI to load a module JS file and run one of its functions.
// Made by Cufiy
// Works with CommonJS exports or files that define top-level global functions (executed in a VM sandbox).
// Usage examples (PowerShell-safe quoting):
// node run_module.js --path 'temp_modules/AniCrush_(ENG_SUB).js' --function searchResults --param '"naruto"'
// node run_module.js --path 'temp_modules/AniCrush_(ENG_SUB).js' --function extractStreamUrl --param '"_movieId=123&ep=1&sv=4&sc=sub"'
const fs = require('fs');
const path = require('path');
const vm = require('vm');
function parseArgs() {
const args = process.argv.slice(2);
const out = {};
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === '--path' && args[i+1]) { out.path = args[++i]; }
else if (a === '--function' && args[i+1]) { out.function = args[++i]; }
else if (a === '--param' && args[i+1]) { out.param = args[++i]; }
else if (a === '--quiet' || a === '-q') { out.quiet = true; }
else if (a === '--debug' || a === '-d') { out.debug = true; }
else if (a === '--debug-full') { out.debugFull = true; }
else if (a === '--sandbox') { out.sandbox = true; }
else if (a === '--help') { out.help = true; }
}
return out;
}
async function loadModuleWithSandbox(filePath, debugLog, opts = {}) {
// Read file and execute in a VM context so top-level functions become properties on the sandbox.
const code = await fs.promises.readFile(filePath, 'utf8');
// Provide fetchv2 as a wrapper that mimics the module's expected behavior
// helper for conditional logging based on flags
const pushDebug = (msg, level = 'info', source = 'RUNNER') => {
if (opts.quiet && level !== 'error') return;
if (!opts.debug && level === 'debug') return;
const tag = `[${String(level || 'info').toUpperCase()}]`;
const src = `[${String(source || 'RUNNER').toUpperCase()}]`;
const entry = `${tag} ${src} ${String(msg)}`;
debugLog.push(entry);
};
const fetchv2 = async (url, options) => {
// fetch calls initiated by the module — mark as MODULE
pushDebug('[FETCH REQUEST] ' + url + ' ' + formatArg(options), 'debug', 'MODULE');
try {
const response = await fetch(url, options);
// Try to read a clone of the body for transparent logging without consuming the original.
// Attach an 'error' handler to the clone stream to avoid unhandled stream errors
let bodyText = null;
try {
if (response && typeof response.clone === 'function') {
const clone = response.clone();
// If the clone exposes a Node.js stream, capture errors to avoid crashing the process
try {
if (clone && clone.body && typeof clone.body.on === 'function') {
clone.body.on('error', (err) => {
pushDebug('[FETCH RESPONSE BODY STREAM ERROR] ' + (err && err.message), 'error', 'MODULE');
});
}
} catch (e) {
// ignore
}
try {
bodyText = await clone.text();
} catch (e) {
// fallback to arrayBuffer -> string conversion
try {
const buf = await clone.arrayBuffer();
bodyText = Buffer.from(buf).toString('utf8');
} catch (e2) {
bodyText = '[unreadable body]';
}
}
} else if (response && typeof response.text === 'function') {
// fallback (may consume body)
try {
bodyText = await response.text();
} catch (e) {
bodyText = '[unreadable body]';
}
}
} catch (e) {
bodyText = '[unreadable body]';
}
// truncate response body for logs unless debug+debugFull are set
let displayBody = bodyText;
try {
const isFull = Boolean(opts && opts.debug && opts.debugFull);
if (typeof bodyText === 'string' && !isFull) {
displayBody = bodyText.length > 100 ? bodyText.slice(0,100) + '…' : bodyText;
}
} catch (e) {
// ignore
}
pushDebug('[FETCH RESPONSE] ' + url + ' status=' + (response && response.status) + ' body=' + formatArg(displayBody), 'debug', 'MODULE');
return response;
} catch (error) {
pushDebug('[FETCH ERROR] ' + url + ' ' + (error && error.message), 'error', 'MODULE');
throw error;
}
};
const formatArg = (arg) => {
if (typeof arg === 'object' && arg !== null) {
try {
return JSON.stringify(arg);
} catch (e) {
return String(arg);
}
}
return String(arg);
};
const sandbox = {
console: {
log: (...args) => pushDebug(args.map(formatArg).join(' '), 'info', 'MODULE'),
error: (...args) => pushDebug('[ERROR] ' + args.map(formatArg).join(' '), 'error', 'MODULE'),
warn: (...args) => pushDebug('[WARN] ' + args.map(formatArg).join(' '), 'warn', 'MODULE'),
info: (...args) => pushDebug('[INFO] ' + args.map(formatArg).join(' '), 'info', 'MODULE'),
debug: (...args) => pushDebug('[DEBUG] ' + args.map(formatArg).join(' '), 'debug', 'MODULE')
},
// allow access to require and module/exports in case the script uses CommonJS patterns
require: require,
module: { exports: {} },
exports: {},
// override fetch with our logging wrapper (and also expose fetchv2)
fetch: fetchv2,
fetchv2: fetchv2,
// Provide soraFetch as an alias in case modules use it
soraFetch: fetchv2,
// Inject small obfuscated helper if modules expect it
_0xB4F2: function() { return "accinrxxxxxxxxxx"; },
// Encoding functions
encodeURIComponent: encodeURIComponent,
decodeURIComponent: decodeURIComponent,
// JSON operations
JSON: JSON,
// Promise support
Promise: Promise,
// small helper to avoid accidental infinite loops
setTimeout,
setInterval,
clearTimeout,
clearInterval
};
const context = vm.createContext(sandbox);
try {
const script = new vm.Script(code, { filename: filePath });
script.runInContext(context, { timeout: 5000 });
} catch (e) {
// running the file might fail if it expects browser globals — that's okay, we still return what we have
// silently continue
}
// Merge module.exports/exports into sandbox for easier lookup
const mod = Object.assign({}, sandbox.module && sandbox.module.exports ? sandbox.module.exports : {}, sandbox.exports || {});
// Also attach any top-level functions that were defined (e.g., function searchResults() { ... })
// We need to iterate over sandbox properties, not context
for (const k of Object.keys(sandbox)) {
if (!(k in mod) && typeof sandbox[k] === 'function') {
mod[k] = sandbox[k];
}
}
return mod;
}
async function main() {
const opts = parseArgs();
const debugLog = [];
if (opts.help || !opts.path || !opts.function) {
console.log(JSON.stringify({
status: 'error',
message: 'Usage: node run_module.js --path <file> --function <fnName> [--param <jsonOrString>]',
data: null,
debug: []
}));
process.exit(opts.help ? 0 : 1);
}
const filePath = path.resolve(opts.path);
if (!fs.existsSync(filePath)) {
console.log(JSON.stringify({
status: 'error',
message: 'File not found: ' + filePath,
data: null,
debug: []
}));
process.exit(2);
}
let mod = {};
// Optionally prefer sandbox execution (avoids require side-effects and lets us hook internal functions)
// Load module: prefer sandbox first (so we can hook top-level functions). If sandbox doesn't expose the requested
// function, fall back to require(). If --sandbox is set, only use sandbox.
if (opts.sandbox) {
mod = await loadModuleWithSandbox(filePath, debugLog, opts);
} else {
try {
mod = await loadModuleWithSandbox(filePath, debugLog, opts);
} catch (e) {
// ignore sandbox errors
}
if (!mod || !(opts.function in mod)) {
try {
delete require.cache[require.resolve(filePath)];
const required = require(filePath);
if (required && (typeof required === 'object' || typeof required === 'function')) {
mod = required;
}
} catch (e) {
// ignore require errors
}
}
}
// helper for main-scope debug logging (used by wrappers)
const pushMainDebug = (msg, level = 'info') => {
if (opts.quiet && level !== 'error') return;
if (!opts.debug && level === 'debug') return;
const tag = `[${String(level || 'info').toUpperCase()}]`;
const src = `[RUNNER]`;
debugLog.push(`${tag} ${src} ${String(msg)}`);
};
// If module exposes multiExtractor, wrap it to log providers when --debug
const wrapMulti = (fn) => {
const _orig = fn;
return async function(providers) {
if (opts && opts.debug) {
try {
pushMainDebug('[multiExtractor providers] ' + (typeof providers === 'object' ? JSON.stringify(providers) : String(providers)), 'debug');
} catch (e) {
pushMainDebug('[multiExtractor providers] [unserializable]', 'debug');
}
}
return await _orig.apply(this, arguments);
};
};
if (mod) {
if (typeof mod.multiExtractor === 'function') mod.multiExtractor = wrapMulti(mod.multiExtractor);
if (typeof mod === 'function' && mod.name === 'multiExtractor') mod = { multiExtractor: wrapMulti(mod) };
if (mod.default && typeof mod.default === 'function' && mod.default.name === 'multiExtractor') {
mod.default = wrapMulti(mod.default);
mod.multiExtractor = mod.default;
}
}
let fn = mod[opts.function];
// also check default export
if (!fn && mod.default && typeof mod.default === 'object') fn = mod.default[opts.function];
if (typeof fn !== 'function') {
console.log(JSON.stringify({
status: 'error',
message: `Function '${opts.function}' not found in module`,
data: null,
debug: debugLog
}));
process.exit(4);
}
let param = opts.param;
// try parse JSON, fallback to string
try {
if (param) param = JSON.parse(param);
} catch (e) {
// leave as string
}
try {
const maybeResult = fn.length === 0 ? fn() : fn(param);
// Check if it's a Promise - handle cross-context Promises
const result = (maybeResult && typeof maybeResult.then === 'function') ? await maybeResult : maybeResult;
// Parse the result if it's a JSON string (modules return JSON.stringify'd data)
let data = result;
if (typeof result === 'string') {
try {
data = JSON.parse(result);
} catch (e) {
// if it's not valid JSON, keep it as string
data = result;
}
}
// Prepare debug output: if quiet -> empty; if debug -> all entries; otherwise filter out DEBUG-level entries
let outDebug = [];
if (opts.quiet) outDebug = [];
else if (opts.debug) outDebug = debugLog;
else outDebug = debugLog.filter(d => !String(d).startsWith('[DEBUG]'));
console.log(JSON.stringify({
status: 'success',
message: 'Fetch successful',
data: data,
debug: outDebug
}));
} catch (e) {
console.log(JSON.stringify({
status: 'error',
message: 'Error executing function: ' + e.message,
data: null,
debug: debugLog
}));
process.exit(5);
}
}
main();