290 lines
8.0 KiB
JavaScript
290 lines
8.0 KiB
JavaScript
#!/usr/bin/env node
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const puppeteer = require('puppeteer');
|
|
|
|
const SHALOM_SECRET = '.Ov3rsku112024l4r43l.';
|
|
const SHALOM_ENCRYPTION_KEY_B64 = 'uQn/bQ94PXBEfId70zjN+VE1hSU7kh9VBXTOUd68Ssc=';
|
|
const SHALOM_TRACKING_HOME = 'https://shalom.com.pe/rastrea';
|
|
const SHALOM_SEARCH_URL = 'https://serviceswebapi.shalomcontrol.com/api/v1/web/rastrea/buscar';
|
|
const SHALOM_STATUS_URL = 'https://serviceswebapi.shalomcontrol.com/api/v1/web/rastrea/estados';
|
|
|
|
function detectChromeExecutable() {
|
|
const candidates = [];
|
|
const cacheRoot = '/home/ubuntu/.cache/puppeteer/chrome';
|
|
|
|
try {
|
|
if (fs.existsSync(cacheRoot)) {
|
|
const versions = fs.readdirSync(cacheRoot).sort().reverse();
|
|
for (const version of versions) {
|
|
candidates.push(path.join(cacheRoot, version, 'chrome-linux64', 'chrome'));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Ignoramos y seguimos con rutas fijas.
|
|
}
|
|
|
|
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
|
|
candidates.unshift(process.env.PUPPETEER_EXECUTABLE_PATH);
|
|
}
|
|
|
|
candidates.push(
|
|
'/usr/bin/google-chrome-stable',
|
|
'/usr/bin/google-chrome',
|
|
'/usr/bin/chromium',
|
|
'/usr/bin/chromium-browser'
|
|
);
|
|
|
|
return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || null;
|
|
}
|
|
|
|
function uuidv4() {
|
|
const bytes = crypto.randomBytes(16);
|
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
const hex = bytes.toString('hex');
|
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
}
|
|
|
|
function buildAuthorizationHeader() {
|
|
const token = `web-${uuidv4()}`;
|
|
const expiresAt = Math.floor(Date.now() / 1000) + 300;
|
|
const payload = `${token}@${expiresAt}`;
|
|
const signature = crypto.createHmac('sha256', SHALOM_SECRET).update(payload).digest('hex');
|
|
return `Bearer ${payload}@${signature}`;
|
|
}
|
|
|
|
function decryptShalomPayload(payload) {
|
|
if (!payload || payload.encrypted !== true || !payload.data) {
|
|
return payload;
|
|
}
|
|
|
|
const key = Buffer.from(SHALOM_ENCRYPTION_KEY_B64, 'base64');
|
|
if (key.length !== 32) {
|
|
throw new Error('No se pudo preparar la clave de descifrado.');
|
|
}
|
|
|
|
const cipherBuffer = Buffer.from(String(payload.data), 'base64');
|
|
if (cipherBuffer.length <= 16) {
|
|
throw new Error('La respuesta cifrada de Shalom es inválida.');
|
|
}
|
|
|
|
const iv = cipherBuffer.subarray(0, 16);
|
|
const ciphertext = cipherBuffer.subarray(16);
|
|
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
const plainText = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
|
|
try {
|
|
return JSON.parse(plainText);
|
|
} catch (error) {
|
|
return plainText;
|
|
}
|
|
}
|
|
|
|
async function obtainRecaptchaToken(page) {
|
|
try {
|
|
await page.goto(SHALOM_TRACKING_HOME, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 60000,
|
|
});
|
|
} catch (error) {
|
|
// La página puede reintentar/cargar de nuevo por el limpiador de service workers.
|
|
}
|
|
|
|
await page.waitForFunction(() => window.RECAPTCHA_SITE_KEY && window.grecaptcha, {
|
|
timeout: 60000,
|
|
});
|
|
|
|
return page.evaluate(async () => {
|
|
await new Promise((resolve) => window.grecaptcha.ready(resolve));
|
|
return window.grecaptcha.execute(window.RECAPTCHA_SITE_KEY, { action: 'rastrea_buscar' });
|
|
});
|
|
}
|
|
|
|
async function postShalomApi(url, fields) {
|
|
const form = new FormData();
|
|
for (const [key, value] of Object.entries(fields)) {
|
|
if (value === undefined || value === null) continue;
|
|
form.append(key, String(value));
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
body: form,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: buildAuthorizationHeader(),
|
|
Origin: 'https://shalom.com.pe',
|
|
Referer: 'https://shalom.com.pe/',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
});
|
|
|
|
const rawText = await response.text();
|
|
let wrappedPayload = null;
|
|
try {
|
|
wrappedPayload = JSON.parse(rawText);
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: `Respuesta no JSON de Shalom (${response.status}).`,
|
|
details: rawText.trim() ? rawText.slice(0, 1200) : 'Sin contenido útil.',
|
|
};
|
|
}
|
|
|
|
let plainPayload = wrappedPayload;
|
|
try {
|
|
plainPayload = decryptShalomPayload(wrappedPayload);
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: `No se pudo descifrar la respuesta de Shalom (${response.status}).`,
|
|
details: error.message,
|
|
};
|
|
}
|
|
|
|
if (response.status >= 400) {
|
|
const message = plainPayload && typeof plainPayload === 'object'
|
|
? String(plainPayload.message || plainPayload.error || '').trim()
|
|
: '';
|
|
|
|
return {
|
|
success: false,
|
|
error: message || `Shalom respondió HTTP ${response.status}.`,
|
|
details: plainPayload,
|
|
};
|
|
}
|
|
|
|
if (!plainPayload || typeof plainPayload !== 'object') {
|
|
return {
|
|
success: false,
|
|
error: 'Shalom devolvió una respuesta vacía o inválida.',
|
|
details: plainPayload,
|
|
};
|
|
}
|
|
|
|
if (plainPayload.success === false) {
|
|
return {
|
|
success: false,
|
|
error: String(plainPayload.message || plainPayload.error || 'Shalom rechazó la solicitud.'),
|
|
details: plainPayload,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: plainPayload,
|
|
};
|
|
}
|
|
|
|
function printJson(payload) {
|
|
console.log(JSON.stringify(payload));
|
|
}
|
|
|
|
async function main() {
|
|
const orderNumber = String(process.argv[2] || '').trim();
|
|
const orderCode = String(process.argv[3] || '').trim();
|
|
|
|
if (!orderNumber || !orderCode) {
|
|
printJson({
|
|
success: false,
|
|
error: 'Número de orden y código de orden son requeridos.',
|
|
});
|
|
process.exit(1);
|
|
}
|
|
|
|
let browser = null;
|
|
|
|
try {
|
|
const executablePath = detectChromeExecutable();
|
|
if (!executablePath) {
|
|
printJson({
|
|
success: false,
|
|
error: 'No se encontró un navegador Chromium compatible para consultar Shalom.',
|
|
});
|
|
process.exit(2);
|
|
}
|
|
|
|
browser = await puppeteer.launch({
|
|
headless: 'new',
|
|
executablePath,
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-features=SSLCommonNameMismatchHandling',
|
|
],
|
|
});
|
|
|
|
const page = await browser.newPage();
|
|
await page.setViewport({ width: 1280, height: 900 });
|
|
await page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36');
|
|
|
|
const recaptchaToken = await obtainRecaptchaToken(page);
|
|
|
|
const searchResult = await postShalomApi(SHALOM_SEARCH_URL, {
|
|
numero: orderNumber,
|
|
codigo: orderCode,
|
|
ose_id: '',
|
|
recaptcha_token: recaptchaToken,
|
|
});
|
|
|
|
if (!searchResult.success) {
|
|
printJson(searchResult);
|
|
process.exit(3);
|
|
}
|
|
|
|
const searchPayload = searchResult.data;
|
|
const oseId = searchPayload && searchPayload.data ? searchPayload.data.ose_id : null;
|
|
if (!oseId) {
|
|
printJson({
|
|
success: false,
|
|
error: 'La búsqueda de Shalom no devolvió un N° interno válido.',
|
|
details: searchPayload,
|
|
});
|
|
process.exit(4);
|
|
}
|
|
|
|
const statusResult = await postShalomApi(SHALOM_STATUS_URL, {
|
|
ose_id: String(oseId),
|
|
});
|
|
|
|
if (!statusResult.success) {
|
|
printJson(statusResult);
|
|
process.exit(5);
|
|
}
|
|
|
|
const statusPayload = statusResult.data;
|
|
const trackingUrl = `${SHALOM_TRACKING_HOME}/${encodeURIComponent(String(oseId))}/${encodeURIComponent(orderCode)}`;
|
|
|
|
printJson({
|
|
success: true,
|
|
source: 'shalom_search_api',
|
|
search: searchPayload,
|
|
status: statusPayload,
|
|
resolved: {
|
|
ose_id: oseId,
|
|
tracking_url: trackingUrl,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
printJson({
|
|
success: false,
|
|
error: error && error.message ? error.message : 'Error al consultar Shalom.',
|
|
details: error && error.stack ? error.stack.split('\n').slice(0, 4).join(' | ') : undefined,
|
|
});
|
|
process.exit(6);
|
|
} finally {
|
|
if (browser) {
|
|
try {
|
|
await browser.close();
|
|
} catch (error) {
|
|
// Ignoramos errores al cerrar el navegador.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
main();
|