homebridge-xfinityhome
Version:
A homebridge plugin to control your Xfinity Home security system.
271 lines • 13.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable no-console */
const debug_1 = __importDefault(require("debug"));
const events_1 = require("events");
const fs_1 = require("fs");
const http_mitm_proxy_1 = require("http-mitm-proxy");
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const qrcode_1 = __importDefault(require("qrcode"));
const plugin_ui_utils_1 = require("@homebridge/plugin-ui-utils");
class PluginUiServer extends plugin_ui_utils_1.HomebridgePluginUiServer {
constructor() {
var _a, _b, _c, _d, _e;
super();
const events = new events_1.EventEmitter();
const plugin = 'homebridge-xfinityhome';
const platform = 'XfinityHomePlatform';
const storagePath = (_a = this.homebridgeStoragePath) !== null && _a !== void 0 ? _a : '';
const configPath = (_b = this.homebridgeConfigPath) !== null && _b !== void 0 ? _b : '';
const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8')).platforms.find((plugin) => plugin.platform === platform);
/*
A native method getCachedAccessories() was introduced in config-ui-x v4.37.0
The following is for users who have a lower version of config-ui-x
*/
const cachedAccessoriesDir = path_1.default.join(storagePath, '/accessories/cachedAccessories') +
(((_c = config === null || config === void 0 ? void 0 : config._bridge) === null || _c === void 0 ? void 0 : _c.username) ? ('.' + ((_e = (_d = config === null || config === void 0 ? void 0 : config._bridge) === null || _d === void 0 ? void 0 : _d.username) === null || _e === void 0 ? void 0 : _e.split(':').join(''))) : '');
this.onRequest('/getCachedAccessories', async () => {
try {
// Define the plugin and create the array to return
if ((0, fs_1.existsSync)(cachedAccessoriesDir)) {
return JSON.parse((0, fs_1.readFileSync)(cachedAccessoriesDir, 'utf-8')).filter(accessory => accessory.plugin === plugin);
}
else {
return [];
}
}
catch (err) {
// Just return an empty accessory list in case of any errors
console.log(err);
return [];
}
});
this.onRequest('/getGeneralLog', async () => {
return path_1.default.join(storagePath, 'XfinityHome', 'General.log');
});
this.onRequest('/getLogs', async (payload) => {
try {
return (0, fs_1.readFileSync)(payload.logPath).toString().replace(/\n/g, '<br>');
}
catch (err) {
return `Failed To Load Logs From ${payload.logPath}`;
}
});
this.onRequest('/getRelativePath', (payload) => {
return path_1.default.relative(path_1.default.join(__dirname, '/public/index.html'), payload.path);
});
this.onRequest('/deleteLog', async (payload) => {
try {
return (0, fs_1.rmSync)(payload.logPath, { force: true });
}
catch (err) {
return err;
}
});
this.onRequest('/watchLog', async (payload) => {
try {
return await watchFilePromise(payload.path);
}
catch (err) {
return Promise.reject(err);
}
});
const watchFilePromise = async (file) => {
return new Promise((resolve, reject) => {
if (!(0, fs_1.existsSync)(file)) {
reject('File does not exist: ' + file);
return;
}
try {
const aborter = new AbortController();
const watcher = (0, fs_1.watch)(file, { signal: aborter.signal });
watcher.once('change', () => {
aborter.abort();
resolve((0, fs_1.readFileSync)(file));
});
watcher.once('error', err => {
console.error(err);
aborter.abort();
(0, fs_1.watchFile)(file, () => {
(0, fs_1.unwatchFile)(file);
resolve((0, fs_1.readFileSync)(file));
});
});
}
catch (_a) {
(0, fs_1.watchFile)(file, () => {
(0, fs_1.unwatchFile)(file);
resolve((0, fs_1.readFileSync)(file));
});
}
});
};
this.onRequest('/watchAccessory', async (payload) => {
const loop = async () => {
const oldFile = JSON.parse((0, fs_1.readFileSync)(cachedAccessoriesDir, 'utf-8')).filter(accessory => accessory.plugin === plugin);
await watchFilePromise(cachedAccessoriesDir);
const newFile = JSON.parse((0, fs_1.readFileSync)(cachedAccessoriesDir, 'utf-8')).filter(accessory => accessory.plugin === plugin);
const oldAccessory = oldFile.find(accessory => accessory.UUID === payload.accessory.UUID);
const newAccessory = newFile.find(accessory => accessory.UUID === payload.accessory.UUID);
if (JSON.stringify(oldAccessory) !== JSON.stringify(newAccessory)) {
return newAccessory;
}
else {
loop();
}
};
return loop();
});
this.onRequest('/proxyActive', async () => {
return new Promise((resolve) => {
events.on('proxy', () => resolve(''));
});
});
/*this.onRequest('/sslActive', async () => {
return new Promise((resolve) => {
events.on('ssl', () => resolve());
});
});*/
this.onRequest('/token', async () => {
return new Promise((resolve) => {
events.on('token', token => resolve(token));
});
});
this.onRequest('/startProxy', async () => {
// Disable debug messages from the proxy
try {
debug_1.default.disable();
}
catch (err) {
//Do nothing
}
const ROOT = path_1.default.join(storagePath, 'XfinityHome');
if (!(0, fs_1.existsSync)(ROOT)) {
(0, fs_1.mkdirSync)(ROOT);
}
const pemFile = path_1.default.join(ROOT, 'certs', 'ca.pem');
const localIPs = [];
const ifaces = os_1.default.networkInterfaces();
Object.keys(ifaces).forEach(name => {
var _a;
(_a = ifaces[name]) === null || _a === void 0 ? void 0 : _a.forEach(network => {
const familyV4Value = typeof network.family === 'string' ? 'IPv4' : 4;
if (network.family === familyV4Value && !network.internal) {
localIPs.push(network.address);
}
});
});
localIPs.push(os_1.default.hostname() + os_1.default.hostname().endsWith('.local') ? '' : '.local');
const proxy = new http_mitm_proxy_1.Proxy();
const localIPPorts = localIPs.map(ip => `${ip}:${585}`);
proxy.onError((ctx, err) => {
switch (err === null || err === void 0 ? void 0 : err.name) {
case 'ERR_STREAM_DESTROYED':
case 'ECONNRESET':
return;
case 'ECONNREFUSED':
console.error('Failed to intercept secure communications. This could happen due to bad CA certificate.');
return;
case 'EACCES':
console.error(`Permission was denied to use port ${585}.`);
return;
default:
//console.error('Error:', err.code, err);
}
});
proxy.onRequest((ctx, callback) => {
var _a;
if (ctx.clientToProxyRequest.method === 'GET' && ctx.clientToProxyRequest.url === '/cert' &&
localIPPorts.includes((_a = ctx.clientToProxyRequest.headers.host) !== null && _a !== void 0 ? _a : '')) {
ctx.use(http_mitm_proxy_1.Proxy.gunzip);
console.log('Intercepted certificate request');
ctx.proxyToClientResponse.writeHead(200, {
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=0',
'Content-Type': 'application/x-x509-ca-cert',
'Content-Disposition': 'attachment; filename=cert.pem',
'Content-Transfer-Encoding': 'binary',
'Content-Length': (0, fs_1.statSync)(pemFile).size,
'Connection': 'keep-alive',
});
//ctx.proxyToClientResponse.end(fs.readFileSync(path.join(ROOT, 'certs', 'ca.pem')));
ctx.proxyToClientResponse.write((0, fs_1.readFileSync)(pemFile));
ctx.proxyToClientResponse.end();
return;
}
else if (ctx.clientToProxyRequest.method === 'POST' && ctx.clientToProxyRequest.headers.host === 'oauth.xfinity.com' &&
ctx.clientToProxyRequest.url === '/oauth/token') {
ctx.use(http_mitm_proxy_1.Proxy.gunzip);
ctx.onRequestData((ctx, chunk, callback) => {
return callback(undefined, chunk);
});
ctx.onRequestEnd((ctx, callback) => {
callback();
});
const chunks = [];
ctx.onResponseData((ctx, chunk, callback) => {
chunks.push(chunk);
return callback(undefined, chunk);
});
ctx.onResponseEnd((ctx, callback) => {
events.emit('token', JSON.parse(Buffer.concat(chunks).toString()).refresh_token);
//token = JSON.parse(Buffer.concat(chunks).toString()).refresh_token;
//this.pushEvent('token', { refreshToken: JSON.parse(Buffer.concat(chunks).toString()).refresh_token });
//emitter.emit('tuya-config', Buffer.concat(chunks).toString());
callback();
});
}
else {
//this.pushEvent('proxy', {});
events.emit('proxy');
/*ctx.onRequestData(function (ctx, chunk, callback) {
ctx.onResponseData(function (ctx, chunk, callback) {
//this.pushEvent('sslProxy', {});
events.emit('ssl');
});
});*/
}
return callback();
});
/*emitter.on('tuya-config', body => {
//if (body.indexOf('tuya.m.my.group.device.list') === -1) return;
console.log('Intercepted token from Xfinity Home');
try {
console.log('Your refresh token is: ' + JSON.parse(body).refresh_token);
} catch (err) {
console.error(err);
}
});*/
this.onRequest('/stopProxy', () => {
if (proxy && typeof proxy.close === 'function') {
proxy.close();
}
if ((0, fs_1.existsSync)(path_1.default.join(ROOT, 'certs'))) {
(0, fs_1.rmSync)(path_1.default.join(ROOT, 'certs'), { recursive: true, force: true });
}
if ((0, fs_1.existsSync)(path_1.default.join(ROOT, 'keys'))) {
(0, fs_1.rmSync)(path_1.default.join(ROOT, 'keys'), { recursive: true, force: true });
}
return '';
});
return new Promise((resolve) => {
proxy.listen({ host: '::', port: 585, sslCaDir: ROOT }, async (err) => {
if (err) {
console.error('Error starting proxy: ' + err);
}
const address = localIPs[0];
const port = 585;
qrcode_1.default.toString(`http://${address}:${port}/cert`, { type: 'svg' })
.then(qrcode => resolve({ ip: address, port: port, qrcode: qrcode }));
});
});
});
this.ready();
}
}
(() => new PluginUiServer())();
//# sourceMappingURL=server.js.map