const crypto = require('crypto'); const fetch = require('node-fetch'); const Vehicle = require('../models/vehicle'); const DebugInfo = require('../models/DebugInfo'); const { Centrifuge } = require('centrifuge'); const SockJS = require('sockjs-client'); const WebSocket = require('ws'); const baseUrl = 'https://avtocod.ru/api/v3'; const tokenRefreshUrl = 'https://avtocod.ru/api/centrifuge/refresh'; let deviceToken = crypto.createHash('sha256').update(Date.now().toString()).digest().toString('hex'); 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); return await result.text(); } async function getJson(url) { let result = await fetch(url); return await result.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 = crypto.createHash('sha256').update(hash).digest(); let decipher = crypto.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')); } }, 10000); 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}`; 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) { ex.debugInfo = { autocod: DebugInfo.fromError(ex.message) }; throw ex; } } } module.exports = AvtocodProvider;