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'; const baseUrl = 'https://avtocod.ru/api/v4'; const tokenRefreshUrl = 'https://avtocod.ru/api/centrifuge/refresh'; const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0'; let deviceToken = uuidv4().replaceAll('-', '').toUpperCase(); const myWs = function (options) { return class wsClass extends WebSocket { constructor(...args) { super(...[...args, ...[options]]); } }; }; function fromBase64(data) { return Buffer.from(data, 'base64').toString('binary'); } async function getPage(url) { let result = await fetch(url, { headers: { 'User-Agent': userAgent } }); return await result.text(); } async function getJson(url) { let jsonResult = await fetch(url, { headers: { 'User-Agent': userAgent } }); 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' }), 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')); } }, 20000); 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, type, htmlReport) { try { let html = ''; if(htmlReport) { html = Buffer.from(htmlReport, 'base64').toString('utf-8'); } else { let numberType = type ?? 'GRZ'; let url = `${baseUrl}/auto/generate?number=${encodeURIComponent(number)}&device_token=${deviceToken}&type=${numberType}`; let resp = await getJson(url); 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;