zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
614 lines • 30 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAxios = exports.getNewImage = exports.updateToLatest = exports.isNewImageAvailable = exports.isUpdateAvailable = exports.parseImage = exports.getOverrideIndexFile = exports.processCustomCaBundle = exports.getFirmwareFile = exports.readLocalFile = exports.isValidUrl = exports.setDataDir = exports.upgradeFileIdentifier = void 0;
const crypto_1 = __importDefault(require("crypto"));
const https_proxy_agent_1 = require("https-proxy-agent");
const assert_1 = __importDefault(require("assert"));
const buffer_crc32_1 = __importDefault(require("buffer-crc32"));
const axios_1 = __importDefault(require("axios"));
const URI = __importStar(require("uri-js"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const zigbee_herdsman_1 = require("zigbee-herdsman");
const logger_1 = require("../logger");
const https_1 = __importDefault(require("https"));
const tls_1 = __importDefault(require("tls"));
const NS = 'zhc:ota:common';
let dataDir = null;
const maxTimeout = 2147483647; // +- 24 days
const imageBlockResponseDelay = 250;
const endRequestCodeLookup = {
0x00: 'success',
0x95: 'aborted by device',
0x7E: 'not authorized',
0x96: 'invalid image',
0x97: 'no data available',
0x98: 'no image available',
0x80: 'malformed command',
0x81: 'unsupported cluster command',
0x99: 'requires more image files',
};
exports.upgradeFileIdentifier = Buffer.from([0x1E, 0xF1, 0xEE, 0x0B]);
const validSilabsCrc = 0x2144DF1C;
const eblTagHeader = 0x0;
const eblTagEncHeader = 0xfb05;
const eblTagEnd = 0xfc04;
const eblPadding = 0xff;
const eblImageSignature = 0xe350;
const gblTagHeader = 0xeb17a603;
const gblTagEnd = 0xfc0404fc;
/**
* Helper functions
*/
const setDataDir = (dir) => {
dataDir = dir;
};
exports.setDataDir = setDataDir;
function isValidUrl(url) {
let parsed;
try {
parsed = URI.parse(url);
}
catch (_) {
return false;
}
return parsed.scheme === 'http' || parsed.scheme === 'https';
}
exports.isValidUrl = isValidUrl;
function readLocalFile(fileName) {
// If the file name is not a full path, then treat it as a relative to the data directory
if (!path_1.default.isAbsolute(fileName) && dataDir) {
fileName = path_1.default.join(dataDir, fileName);
}
logger_1.logger.debug(`Getting local firmware file '${fileName}'`, NS);
return fs_1.default.readFileSync(fileName);
}
exports.readLocalFile = readLocalFile;
async function getFirmwareFile(image) {
const urlOrName = image.url;
// First try to download firmware file with the URL provided
if (isValidUrl(urlOrName)) {
logger_1.logger.debug(`Downloading firmware image from '${urlOrName}'`, NS);
return await getAxios().get(urlOrName, { responseType: 'arraybuffer' });
}
logger_1.logger.debug(`Try to read firmware image from local file '${urlOrName}'`, NS);
return { data: readLocalFile(urlOrName) };
}
exports.getFirmwareFile = getFirmwareFile;
async function processCustomCaBundle(uri) {
let rawCaBundle = '';
if (isValidUrl(uri)) {
rawCaBundle = (await axios_1.default.get(uri)).data;
}
else {
if (!path_1.default.isAbsolute(uri) && dataDir) {
uri = path_1.default.join(dataDir, uri);
}
rawCaBundle = fs_1.default.readFileSync(uri, { encoding: 'utf-8' });
}
// Parse the raw CA bundle into clean, separate CA certs
const lines = rawCaBundle.split('\n');
const caBundle = [];
let inCert = false;
let currentCert = '';
for (const line of lines) {
if (line === '-----BEGIN CERTIFICATE-----') {
inCert = true;
}
if (inCert) {
currentCert = currentCert + line + '\n';
}
if (line === '-----END CERTIFICATE-----') {
inCert = false;
caBundle.push(currentCert);
currentCert = '';
}
}
return caBundle;
}
exports.processCustomCaBundle = processCustomCaBundle;
async function getOverrideIndexFile(urlOrName) {
if (isValidUrl(urlOrName)) {
const { data: index } = await getAxios().get(urlOrName);
if (!index) {
throw new Error(`OTA: Error getting override index file from '${urlOrName}'`);
}
return index;
}
return JSON.parse(fs_1.default.readFileSync(urlOrName, 'utf-8'));
}
exports.getOverrideIndexFile = getOverrideIndexFile;
/**
* OTA functions
*/
function getOTAEndpoint(device) {
return device.endpoints.find((e) => e.supportsOutputCluster('genOta'));
}
function parseSubElement(buffer, position) {
const tagID = buffer.readUInt16LE(position);
const length = buffer.readUInt32LE(position + 2);
const data = buffer.slice(position + 6, position + 6 + length);
return { tagID, length, data };
}
function parseImage(buffer, suppressElementImageParseFailure = false) {
const header = {
otaUpgradeFileIdentifier: buffer.subarray(0, 4),
otaHeaderVersion: buffer.readUInt16LE(4),
otaHeaderLength: buffer.readUInt16LE(6),
otaHeaderFieldControl: buffer.readUInt16LE(8),
manufacturerCode: buffer.readUInt16LE(10),
imageType: buffer.readUInt16LE(12),
fileVersion: buffer.readUInt32LE(14),
zigbeeStackVersion: buffer.readUInt16LE(18),
otaHeaderString: buffer.toString('utf8', 20, 52),
totalImageSize: buffer.readUInt32LE(52),
};
let headerPos = 56;
if (header.otaHeaderFieldControl & 1) {
header.securityCredentialVersion = buffer.readUInt8(headerPos);
headerPos += 1;
}
if (header.otaHeaderFieldControl & 2) {
header.upgradeFileDestination = buffer.subarray(headerPos, headerPos + 8);
headerPos += 8;
}
if (header.otaHeaderFieldControl & 4) {
header.minimumHardwareVersion = buffer.readUInt16LE(headerPos);
headerPos += 2;
header.maximumHardwareVersion = buffer.readUInt16LE(headerPos);
headerPos += 2;
}
const raw = buffer.slice(0, header.totalImageSize);
(0, assert_1.default)(Buffer.compare(header.otaUpgradeFileIdentifier, exports.upgradeFileIdentifier) === 0, `Not an OTA file`);
let position = header.otaHeaderLength;
const elements = [];
try {
while (position < header.totalImageSize) {
const element = parseSubElement(buffer, position);
elements.push(element);
position += element.data.length + 6;
}
}
catch (error) {
if (!suppressElementImageParseFailure) {
throw error;
}
logger_1.logger.debug('Partially failed to parse the image, continuing anyway...', NS);
}
(0, assert_1.default)(position === header.totalImageSize, `Size mismatch`);
return { header, elements, raw };
}
exports.parseImage = parseImage;
function validateImageData(image) {
for (const element of image.elements) {
const { data } = element;
if (data.readUInt32BE(0) === gblTagHeader) {
validateSilabsGbl(data);
}
else {
const tag = data.readUInt16BE(0);
if ((tag === eblTagHeader && data.readUInt16BE(6) === eblImageSignature) || tag === eblTagEncHeader) {
validateSilabsEbl(data);
}
}
}
}
function validateSilabsEbl(data) {
const dataLength = data.length;
let position = 0;
while (position + 4 <= dataLength) {
const tag = data.readUInt16BE(position);
const len = data.readUInt16BE(position + 2);
position += 4 + len;
if (tag !== eblTagEnd) {
continue;
}
for (let position2 = position; position2 < dataLength; position2++) {
(0, assert_1.default)(data.readUInt8(position2) === eblPadding, `Image padding contains invalid bytes`);
}
const calculatedCrc32 = buffer_crc32_1.default.unsigned(data.slice(0, position));
(0, assert_1.default)(calculatedCrc32 === validSilabsCrc, `Image CRC-32 is invalid`);
return;
}
throw new Error(`OTA: Image is truncated, not long enough to contain a valid tag`);
}
function validateSilabsGbl(data) {
const dataLength = data.length;
let position = 0;
while (position + 8 <= dataLength) {
const tag = data.readUInt32BE(position);
const len = data.readUInt32LE(position + 4);
position += 8 + len;
if (tag !== gblTagEnd) {
continue;
}
const calculatedCrc32 = buffer_crc32_1.default.unsigned(data.slice(0, position));
(0, assert_1.default)(calculatedCrc32 === validSilabsCrc, `Image CRC-32 is invalid`);
return;
}
throw new Error(`OTA: Image is truncated, not long enough to contain a valid tag`);
}
function cancelWaiters(waiters) {
for (const waiter of Object.values(waiters)) {
if (waiter) {
waiter.cancel();
}
}
}
function sendQueryNextImageResponse(endpoint, image, requestTransactionSequenceNumber) {
const payload = {
status: 0,
manufacturerCode: image.header.manufacturerCode,
imageType: image.header.imageType,
fileVersion: image.header.fileVersion,
imageSize: image.header.totalImageSize,
};
endpoint.commandResponse('genOta', 'queryNextImageResponse', payload, null, requestTransactionSequenceNumber).catch((e) => {
logger_1.logger.debug(`Failed to send queryNextImageResponse (${e.message})`, NS);
});
}
function imageNotify(endpoint) {
return endpoint.commandResponse('genOta', 'imageNotify', { payloadType: 0, queryJitter: 100 }, { sendPolicy: 'immediate' });
}
async function requestOTA(endpoint) {
// Some devices (e.g. Insta) take very long trying to discover the correct coordinator EP for OTA.
const queryNextImageRequest = endpoint.waitForCommand('genOta', 'queryNextImageRequest', null, 60000);
try {
await imageNotify(endpoint);
// @ts-expect-error
return await queryNextImageRequest.promise;
}
catch (e) {
queryNextImageRequest.cancel();
throw new Error(`OTA: Device didn't respond to OTA request`);
}
}
function getImageBlockResponsePayload(image, imageBlockRequest, pageOffset, pageSize) {
let start = imageBlockRequest.payload.fileOffset + pageOffset;
// When the data size is too big, OTA gets unstable, so default it to 50 bytes maximum.
// - Insta devices, OTA only works for data sizes 40 and smaller (= manufacturerCode 4474).
// - Legrand devices (newer firmware) require up to 64 bytes (= manufacturerCode 4129).
let maximumDataSize = 50;
if (imageBlockRequest.payload.manufacturerCode === 4474)
maximumDataSize = 40;
else if (imageBlockRequest.payload.manufacturerCode === 4129)
maximumDataSize = Infinity;
let dataSize = Math.min(maximumDataSize, imageBlockRequest.payload.maximumDataSize);
// Hack for https://github.com/Koenkk/zigbee-OTA/issues/328 (Legrand OTA not working)
if (imageBlockRequest.payload.manufacturerCode === 4129 &&
imageBlockRequest.payload.fileOffset === 50 &&
imageBlockRequest.payload.maximumDataSize === 12) {
logger_1.logger.info(`Detected Legrand firmware issue, attempting to reset the OTA stack`, NS);
// The following vector seems to buffer overflow the device to reset the OTA stack!
start = 78;
dataSize = 64;
}
if (pageSize) {
dataSize = Math.min(dataSize, pageSize - pageOffset);
}
let end = start + dataSize;
if (end > image.raw.length) {
end = image.raw.length;
}
logger_1.logger.debug(`Request offsets: fileOffset=${imageBlockRequest.payload.fileOffset} pageOffset=${pageOffset} \
dataSize=${imageBlockRequest.payload.maximumDataSize}`, NS);
logger_1.logger.debug(`Payload offsets: start=${start} end=${end} dataSize=${dataSize}`, NS);
return {
status: 0,
manufacturerCode: imageBlockRequest.payload.manufacturerCode,
imageType: imageBlockRequest.payload.imageType,
fileVersion: imageBlockRequest.payload.fileVersion,
fileOffset: start,
dataSize: end - start,
data: image.raw.slice(start, end),
};
}
function callOnProgress(startTime, lastUpdate, imageBlockRequest, image, onProgress) {
const now = Date.now();
// Call on progress every +- 30 seconds
if (lastUpdate === null || (now - lastUpdate) > 30000) {
const totalDuration = (now - startTime) / 1000; // in seconds
const bytesPerSecond = imageBlockRequest.payload.fileOffset / totalDuration;
const remaining = (image.header.totalImageSize - imageBlockRequest.payload.fileOffset) / bytesPerSecond;
let percentage = imageBlockRequest.payload.fileOffset / image.header.totalImageSize;
percentage = Math.round(percentage * 10000) / 100;
logger_1.logger.debug(`Update at ${percentage}%, remaining ${remaining} seconds`, NS);
onProgress(percentage, remaining === Infinity ? null : remaining);
return now;
}
else {
return lastUpdate;
}
}
async function isUpdateAvailable(device, requestPayload, isNewImageAvailable = null, getImageMeta = null) {
logger_1.logger.debug(`Checking if an update is available for '${device.ieeeAddr}' (${device.modelID})`, NS);
if (requestPayload === null) {
const endpoint = getOTAEndpoint(device);
(0, assert_1.default)(endpoint != null, `Failed to find an endpoint which supports the OTA cluster`);
logger_1.logger.debug(`Using endpoint '${endpoint.ID}'`, NS);
const request = await requestOTA(endpoint);
logger_1.logger.debug(`Got request '${JSON.stringify(request.payload)}'`, NS);
requestPayload = request.payload;
}
const availableResult = await isNewImageAvailable(requestPayload, device, getImageMeta);
logger_1.logger.debug(`Update available for '${device.ieeeAddr}' (${device.modelID}): ${availableResult.available < 0 ? 'YES' : 'NO'}`, NS);
if (availableResult.available > 0) {
logger_1.logger.warning(`Firmware on '${device.ieeeAddr}' (${device.modelID}) is newer than latest firmware online.`, NS);
}
return { ...availableResult, available: availableResult.available < 0 };
}
exports.isUpdateAvailable = isUpdateAvailable;
async function isNewImageAvailable(current, device, getImageMeta) {
const currentS = JSON.stringify(current);
logger_1.logger.debug(`Is new image available for '${device.ieeeAddr}' (${device.modelID}), current '${currentS}'`, NS);
const meta = await getImageMeta(current, device);
// Soft-fail because no images in repo/URL for specified device
if (!meta) {
const metaS = `device '${device.modelID}', hardwareVersion '${device.hardwareVersion}', manufacturerName ${device.manufacturerName}`;
logger_1.logger.warning(`Images currently unavailable for ${metaS}, ${currentS}'`, NS);
return {
available: 0,
currentFileVersion: current.fileVersion,
otaFileVersion: current.fileVersion,
};
}
logger_1.logger.debug(`Is new image available for '${device.ieeeAddr}' (${device.modelID}), latest meta '${JSON.stringify(meta)}'`, NS);
// Negative number means the new firmware is 'newer' than current one
return {
available: meta.force ? -1 : Math.sign(current.fileVersion - meta.fileVersion),
currentFileVersion: current.fileVersion,
otaFileVersion: meta.fileVersion,
};
}
exports.isNewImageAvailable = isNewImageAvailable;
async function updateToLatest(device, onProgress, getNewImage, getImageMeta = null, downloadImage = null, suppressElementImageParseFailure = false) {
logger_1.logger.debug(`Updating to latest '${device.ieeeAddr}' (${device.modelID})`, NS);
const endpoint = getOTAEndpoint(device);
(0, assert_1.default)(endpoint != null, `Failed to find an endpoint which supports the OTA cluster`);
logger_1.logger.debug(`Using endpoint '${endpoint.ID}'`, NS);
const request = await requestOTA(endpoint);
logger_1.logger.debug(`Got request '${JSON.stringify(request.payload)}'`, NS);
const image = await getNewImage(request.payload, device, getImageMeta, downloadImage, suppressElementImageParseFailure);
logger_1.logger.debug(`Got new image for '${device.ieeeAddr}' (${device.modelID})`, NS);
const waiters = {};
let lastUpdate = null;
let lastImageBlockResponse = null;
const startTime = Date.now();
return new Promise((resolve, reject) => {
const answerNextImageBlockOrPageRequest = () => {
let imageBlockOrPageRequestTimeoutMs = 150000;
// increase the upgradeEndReq wait time to solve the problem of OTA timeout failure of Sonoff Devices
// (https://github.com/Koenkk/zigbee-herdsman-converters/issues/6657)
if (request.payload.manufacturerCode == 4742 && request.payload.imageType == 8199) {
imageBlockOrPageRequestTimeoutMs = 3600000;
}
// Bosch transmits the firmware updates in the background in their native implementation.
// According to the app, this can take up to 2 days. Therefore, we assume to get at least
// one package request per hour from the device here.
if (request.payload.manufacturerCode == zigbee_herdsman_1.Zcl.ManufacturerCode.ROBERT_BOSCH_GMBH) {
imageBlockOrPageRequestTimeoutMs = 60 * 60 * 1000;
}
// Increase the timeout for Legrand devices, so that they will re-initiate and update themselves
// Newer firmwares have ackward behaviours when it comes to the handling of the last bytes of OTA updates
if (request.payload.manufacturerCode === 4129) {
imageBlockOrPageRequestTimeoutMs = 30 * 60 * 1000;
}
const imageBlockRequest = endpoint.waitForCommand('genOta', 'imageBlockRequest', null, imageBlockOrPageRequestTimeoutMs);
const imagePageRequest = endpoint.waitForCommand('genOta', 'imagePageRequest', null, imageBlockOrPageRequestTimeoutMs);
waiters.imageBlockOrPageRequest = {
promise: Promise.race([imageBlockRequest.promise, imagePageRequest.promise]),
cancel: () => {
imageBlockRequest.cancel();
imagePageRequest.cancel();
},
};
waiters.imageBlockOrPageRequest.promise.then((imageBlockOrPageRequest) => {
let pageOffset = 0;
let pageSize = 0;
const sendImageBlockResponse = (imageBlockRequest, thenCallback, transactionSequenceNumber) => {
const payload = getImageBlockResponsePayload(image, imageBlockRequest, pageOffset, pageSize);
const now = Date.now();
const timeSinceLastImageBlockResponse = now - lastImageBlockResponse;
// Reduce network congestion by only sending imageBlockResponse min every 250ms.
const cooldownTime = Math.max(imageBlockResponseDelay - timeSinceLastImageBlockResponse, 0);
setTimeout(() => {
endpoint.commandResponse('genOta', 'imageBlockResponse', payload, null, transactionSequenceNumber).then(() => {
pageOffset += payload.dataSize;
lastImageBlockResponse = Date.now();
thenCallback();
}, (e) => {
// Shit happens, device will probably do a new imageBlockRequest so don't care.
lastImageBlockResponse = Date.now();
thenCallback();
logger_1.logger.debug(`Image block response failed (${e.message})`, NS);
});
}, cooldownTime);
lastUpdate = callOnProgress(startTime, lastUpdate, imageBlockRequest, image, onProgress);
};
if ('pageSize' in imageBlockOrPageRequest.payload) {
// imagePageRequest
pageSize = imageBlockOrPageRequest.payload.pageSize;
const handleImagePageRequestBlocks = (imagePageRequest) => {
if (pageOffset < pageSize) {
sendImageBlockResponse(imagePageRequest, () => handleImagePageRequestBlocks(imagePageRequest), imagePageRequest.header.transactionSequenceNumber);
}
else {
answerNextImageBlockOrPageRequest();
}
};
handleImagePageRequestBlocks(imageBlockOrPageRequest);
}
else {
// imageBlockRequest
sendImageBlockResponse(imageBlockOrPageRequest, answerNextImageBlockOrPageRequest, imageBlockOrPageRequest.header.transactionSequenceNumber);
}
}, () => {
cancelWaiters(waiters);
reject(new Error(`OTA: Timeout, device did not request any image blocks`));
});
};
const answerNextImageRequest = () => {
waiters.nextImageRequest = endpoint.waitForCommand('genOta', 'queryNextImageRequest', null, maxTimeout);
waiters.nextImageRequest.promise.then((payload) => {
answerNextImageRequest();
sendQueryNextImageResponse(endpoint, image, payload.header.transactionSequenceNumber);
});
};
// No need to timeout here, will already be done in answerNextImageBlockRequest
waiters.upgradeEndRequest = endpoint.waitForCommand('genOta', 'upgradeEndRequest', null, maxTimeout);
waiters.upgradeEndRequest.promise.then((data) => {
logger_1.logger.debug(`Got upgrade end request for '${device.ieeeAddr}' (${device.modelID}): ${JSON.stringify(data.payload)}`, NS);
cancelWaiters(waiters);
if (data.payload.status === 0) {
const payload = {
manufacturerCode: image.header.manufacturerCode, imageType: image.header.imageType,
fileVersion: image.header.fileVersion, currentTime: 0, upgradeTime: 1,
};
endpoint.commandResponse('genOta', 'upgradeEndResponse', payload, null, data.header.transactionSequenceNumber).then(() => {
logger_1.logger.debug(`Update succeeded, waiting for device announce`, NS);
onProgress(100, null);
let timer = null;
const cb = () => {
logger_1.logger.debug(`Got device announce or timed out, call resolve`, NS);
clearInterval(timer);
device.removeListener('deviceAnnounce', cb);
resolve(image.header.fileVersion);
};
timer = setTimeout(cb, 120 * 1000); // timeout after 2 minutes
device.once('deviceAnnounce', cb);
}, (e) => {
const message = `OTA: Upgrade end response failed (${e.message})`;
logger_1.logger.debug(message, NS);
reject(new Error(message));
});
}
else {
// @ts-expect-error
const error = `Update failed with reason: '${endRequestCodeLookup[data.payload.status]}'`;
logger_1.logger.debug(error, NS);
reject(new Error(error));
}
});
logger_1.logger.debug(`Starting upgrade`, NS);
answerNextImageBlockOrPageRequest();
answerNextImageRequest();
// Notify client once more about new image, client should start sending queryNextImageRequest now
imageNotify(endpoint).catch((e) => logger_1.logger.debug(`Image notify failed (${e})`, NS));
});
}
exports.updateToLatest = updateToLatest;
async function getNewImage(current, device, getImageMeta, downloadImage, suppressElementImageParseFailure) {
const meta = await getImageMeta(current, device);
(0, assert_1.default)(meta, `Images for '${device.ieeeAddr}' (${device.modelID}) currently unavailable`);
logger_1.logger.debug(`Getting new image for '${device.ieeeAddr}' (${device.modelID}), latest meta ${JSON.stringify(meta)}`, NS);
(0, assert_1.default)(meta.fileVersion > current.fileVersion || meta.force, `No new image available`);
const download = downloadImage ? await downloadImage(meta) :
await getAxios().get(meta.url, { responseType: 'arraybuffer' });
const checksum = (meta.sha512 || meta.sha256);
if (checksum) {
const hash = crypto_1.default.createHash(meta.sha512 ? 'sha512' : 'sha256');
hash.update(download.data);
(0, assert_1.default)(hash.digest('hex') === checksum, `File checksum validation failed`);
logger_1.logger.debug(`Update checksum validation succeeded for '${device.ieeeAddr}' (${device.modelID})`, NS);
}
const start = download.data.indexOf(exports.upgradeFileIdentifier);
const image = parseImage(download.data.slice(start), suppressElementImageParseFailure);
logger_1.logger.debug(`Get new image for '${device.ieeeAddr}' (${device.modelID}), image header ${JSON.stringify(image.header)}`, NS);
(0, assert_1.default)(image.header.fileVersion === meta.fileVersion, `File version mismatch`);
(0, assert_1.default)(!meta.fileSize || image.header.totalImageSize === meta.fileSize, `Image size mismatch`);
(0, assert_1.default)(image.header.manufacturerCode === current.manufacturerCode, `Manufacturer code mismatch`);
(0, assert_1.default)(image.header.imageType === current.imageType, `Image type mismatch`);
if ('minimumHardwareVersion' in image.header && 'maximumHardwareVersion' in image.header) {
(0, assert_1.default)(image.header.minimumHardwareVersion <= device.hardwareVersion &&
device.hardwareVersion <= image.header.maximumHardwareVersion, `Hardware version mismatch`);
}
validateImageData(image);
return image;
}
exports.getNewImage = getNewImage;
function getAxios(caBundle = null) {
let config = {};
const httpsAgentOptions = {};
if (caBundle !== null) {
// We also include all system default CAs, as setting custom CAs fully replaces the default list
httpsAgentOptions.ca = [...tls_1.default.rootCertificates, ...caBundle];
}
const proxy = process.env.HTTPS_PROXY;
if (proxy) {
config = {
proxy: false,
httpsAgent: new https_proxy_agent_1.HttpsProxyAgent(proxy, httpsAgentOptions),
headers: {
'Accept-Encoding': '*',
},
};
}
else {
config = {
httpsAgent: new https_1.default.Agent(httpsAgentOptions),
};
}
const axiosInstance = axios_1.default.create(config);
axiosInstance.defaults.maxRedirects = 0; // Set to 0 to prevent automatic redirects
// Add work with 302 redirects without hostname in Location header
axiosInstance.interceptors.response.use((response) => response, (error) => {
// get domain from basic url
if (error.response && [301, 302].includes(error.response.status)) {
let redirectUrl = error.response.headers.location;
try {
const parsedUrl = new URL(redirectUrl);
if (!parsedUrl.protocol || !parsedUrl.host) {
throw new Error(`OTA: Get Axios, no scheme or domain`);
}
}
catch {
// Prepend scheme and domain from the original request's base URL
const baseURL = new URL(error.config.url);
redirectUrl = `${baseURL.origin}${redirectUrl}`;
}
return axiosInstance.get(redirectUrl, { responseType: error.config.responseType || 'arraybuffer' });
}
});
return axiosInstance;
}
exports.getAxios = getAxios;
exports.upgradeFileIdentifier = exports.upgradeFileIdentifier;
exports.isUpdateAvailable = isUpdateAvailable;
exports.parseImage = parseImage;
exports.validateImageData = validateImageData;
exports.isNewImageAvailable = isNewImageAvailable;
exports.updateToLatest = updateToLatest;
exports.getNewImage = getNewImage;
exports.getAxios = getAxios;
exports.isValidUrl = isValidUrl;
exports.setDataDir = exports.setDataDir;
exports.getFirmwareFile = getFirmwareFile;
exports.readLocalFile = readLocalFile;
exports.getOverrideIndexFile = getOverrideIndexFile;
//# sourceMappingURL=common.js.map