mirror of
https://git.luna-app.eu/50n50/sources
synced 2025-12-21 21:26:19 +01:00
319 lines
11 KiB
JavaScript
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();
|