import { createHash, createDecipheriv } from 'crypto'; import Vehicle from '../models/vehicle.js'; import DebugInfo from '../models/DebugInfo.js'; import { Centrifuge } from 'centrifuge'; import WebSocket from 'ws'; import { v4 as uuidv4 } from 'uuid'; import crypto from 'crypto'; const baseUrl = 'https://avtocod.ru/api/v4'; const tokenRefreshUrl = 'https://avtocod.ru/api/centrifuge/refresh'; const minimalVersionUrl = 'https://avtocod.ru/api/v4/info/minimal_version?platform=ios'; // Just for getting cookies const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0'; const sign = { version: '4.6.2', sign: '3343fc07110c5df19b840d688bc45b78', platform: 'Win32', browsers: ['chrome'], score: 0.6, }; let deviceToken = uuidv4().replaceAll('-', '').toUpperCase(); var cookie = null; setInterval(() => { cookie = null; }, 30*60*1000); const myWs = function (options) { return class wsClass extends WebSocket { constructor(...args) { super(...[...args, ...[options]]); } }; }; function findValue(key, str) { let result = str.match(new RegExp(`"(${key}=.*?)"`)); if(result == null) { throw Error(`Cannot find ${key} data`); } return result[1]; } function getSpsnCookie(html, sign) { let signStr = JSON.stringify(sign); let signHex = Buffer.from(signStr).toString('hex'); let spsnStart = findValue('spsn', html); return spsnStart + signHex + ';'; } function getSpidCookie(html) { return findValue('spid', html) + ';'; } function getSpscCookie(html) { let keyResult = html.match(/-----BEGIN.*?KEY-----.*?-----END.*?KEY-----/s); if(keyResult == null) { throw Error('Cannot find key for decrypting spsc data'); } let key = keyResult[0]; let srcResult = html.match(/decrypt\("(.*?)"/); if(srcResult == null) { throw Error('Cannot find spsc data'); } let dataToDecrypt = Buffer.from(srcResult[1], 'hex'); let decrypted = crypto.privateDecrypt({ key, padding: crypto.constants.RSA_PKCS1_PADDING }, dataToDecrypt); return 'spsc=' + decrypted.toString() + ';'; } function fromBase64(data) { return Buffer.from(data, 'base64').toString('binary'); } async function getPage(url) { let result = await fetch(url, { headers: { 'User-Agent': userAgent, 'cookie': await getCookies() } }); return await result.text(); } async function getCookies() { if(cookie != null) { return cookie; } let result = await fetch(minimalVersionUrl, { headers: { 'User-Agent': userAgent } }); let html = await result.text(); let spsnCookie = getSpsnCookie(html, sign); let spidCookie = getSpidCookie(html); let spscCookie = getSpscCookie(html); cookie = [spsnCookie, spidCookie, spscCookie].join(' '); return cookie; } async function getJson(url) { let jsonResult = await fetch(url, { headers: { 'User-Agent': userAgent, 'cookie': await getCookies() } }); return await jsonResult.json(); } function decryptReport(report, hash) { let dataLength = report.length/2; let encryptedObject = JSON.parse(fromBase64(report.slice(-dataLength).concat(report.slice(0, -dataLength)))); let iv = Buffer.from(encryptedObject.iv, 'base64'); let data = Buffer.from(encryptedObject.value, 'base64'); let key = createHash('sha256').update(hash).digest(); let decipher = createDecipheriv('aes-256-cbc', key, iv); let decrypted = decipher.update(data); decrypted = Buffer.concat([decrypted, decipher.final()]); return JSON.parse(decrypted.toString()); } function getToken(ctx) { console.log('+++++ getToken'); return new Promise((resolve, reject) => { fetch(tokenRefreshUrl, { method: 'POST', headers: new Headers({ 'Content-Type': 'application/json', 'cookie': cookie }), body: JSON.stringify(ctx) }) .then(res => { if (!res.ok) { throw new Error(`Unexpected status code ${res.status}`); } return res.json(); }) .then(data => { resolve(data.token); }) .catch(err => { reject(err); }); }); } function waitForReport(centrifugoConfig, channel) { return new Promise((resolve, reject) => { let report = null; let centrifuge = new Centrifuge([{ transport: 'websocket', endpoint: centrifugoConfig.uri }], { websocket: myWs({ headers: { 'Sec-Fetch-Site': 'same-site', 'Sec-Fetch-Mode': 'websocket', 'Sec-Fetch-Dest': 'websocket', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0' } }), token: centrifugoConfig.token, getToken: getToken, debug: false }); let timeout = setTimeout(() => { centrifuge.disconnect(); if(report != null) { resolve(report); } else { reject(new Error('Request to avtocod timed out')); } }, 15000); const sub = centrifuge.newSubscription(channel); sub.on('publication', function(message) { if(message.data.event == 'report-ready') { clearTimeout(timeout); centrifuge.disconnect(); resolve(message.data); } else if(message.data.event == 'report-updated') { report = message.data; } }); sub.subscribe(); centrifuge.connect(); }); } class AvtocodProvider { static async getReport(number) { try { let url = `${baseUrl}/auto/generate?number=${encodeURIComponent(number)}&device_token=${deviceToken}&type=GRZ`; let resp = await getJson(url); let html = await getPage(resp.report_uri); let result = html.match(//); if(result == null) { throw Error('Error getting api version hash'); } let hash = result[1]; result = html.match(/value:(.*report_container.*)/); if(result == null) { throw Error('Error getting report data'); } let mainData = JSON.parse(result[1]); let report = decryptReport(mainData.report_container.report, hash); if(report == null) { report = await waitForReport(mainData.centrifugo, mainData.report_container.channel); } let vehicle = Vehicle.fromAvtocod(report); console.log('Avtocod found vehicle: ', vehicle?.brand?.name?.original); return vehicle; } catch(ex) { console.log('Avtocod error: ', ex.message); ex.debugInfo = { autocod: DebugInfo.fromError(ex.message) }; throw ex; } } } export default AvtocodProvider;