AutoCatBackend/data_providers/avtocod.js

152 lines
4.0 KiB
JavaScript

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';
const baseUrl = 'https://avtocod.ru/api/v3';
const tokenRefreshUrl = 'https://avtocod.ru/api/centrifuge/refresh';
let deviceToken = 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 = 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'));
}
}, 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(/<meta name="app-version-hash" content="(.*?)" \/>/);
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;