170 lines
4.4 KiB
JavaScript
170 lines
4.4 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';
|
|
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(/<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;
|