Add bleno types.

This commit is contained in:
mmcwilliams 2019-11-26 21:16:13 -05:00
parent a627143c22
commit de481ee185
8 changed files with 467 additions and 342 deletions

View File

@ -1,31 +1,38 @@
'use strict'; 'use strict';
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/** @module ble */ /** @module ble */
/** Bluetooth Low Energy module */ /** Bluetooth Low Energy module */
const util = require('util'); const util_1 = require("util");
const os = require('os'); const os_1 = require("os");
const fs_extra_1 = require("fs-extra");
const log = require('../log')('ble'); const log = require('../log')('ble');
const wifi = require('../wifi'); const wifi_1 = require("../wifi");
const wifi = new wifi_1.Wifi();
const DEVICE_NAME = process.env.DEVICE_NAME || 'intval3'; const DEVICE_NAME = process.env.DEVICE_NAME || 'intval3';
const SERVICE_ID = process.env.SERVICE_ID || 'intval3_ble'; const SERVICE_ID = process.env.SERVICE_ID || 'intval3_ble';
const CHAR_ID = process.env.CHAR_ID || 'intval3char'; const CHAR_ID = process.env.CHAR_ID || 'intval3char';
const WIFI_ID = process.env.WIFI_ID || 'wifichar'; const WIFI_ID = process.env.WIFI_ID || 'wifichar';
const NETWORK = os.networkInterfaces(); const NETWORK = os_1.networkInterfaces(); //?type?
const MAC = getMac() || spoofMac(); const MAC = getMac() || spoofMac();
//Give the device a unique device name, needs to be in env //Give the device a unique device name, needs to be in env
process.env.BLENO_DEVICE_NAME += '_' + MAC; process.env.BLENO_DEVICE_NAME += '_' + MAC;
const bleno = require('bleno'); const bleno_1 = __importDefault(require("bleno"));
const { Characteristic } = bleno_1.default;
let currentWifi = 'disconnected'; let currentWifi = 'disconnected';
let currentAddr = null; let currentAddr = null;
let getState; let getState;
const chars = []; const chars = [];
function createChar(name, uuid, prop, write, read) { function createChar(name, uuid, prop, write, read) {
function characteristic() { const characteristic = function () {
bleno.Characteristic.call(this, { Characteristic.call(this, {
uuid: uuid, uuid,
properties: prop properties: prop
}); });
} };
util.inherits(characteristic, bleno.Characteristic); util_1.inherits(characteristic, Characteristic);
if (prop.indexOf('read') !== -1) { if (prop.indexOf('read') !== -1) {
//data, offset, withoutResponse, callback //data, offset, withoutResponse, callback
characteristic.prototype.onReadRequest = read; characteristic.prototype.onReadRequest = read;
@ -36,52 +43,59 @@ function createChar(name, uuid, prop, write, read) {
chars.push(new characteristic()); chars.push(new characteristic());
} }
function createChars(onWrite, onRead) { function createChars(onWrite, onRead) {
createChar('intval3', CHAR_ID, ['read', 'write'], onWrite, onRead); const permissions = ['read', 'write'];
createChar('wifi', WIFI_ID, ['read', 'write'], onWifiWrite, onWifiRead); createChar('intval3', CHAR_ID, permissions, onWrite, onRead);
createChar('wifi', WIFI_ID, permissions, onWifiWrite, onWifiRead);
} }
function onWifiWrite(data, offset, withoutResponse, callback) { async function onWifiWrite(data, offset) {
let result; let result;
let utf8; let utf8;
let obj; let obj = {};
let ssid; let ssid;
let pwd; let pwd;
let psk;
if (offset) { if (offset) {
log.warn(`Offset scenario`); log.warn(`Offset scenario`);
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG; result = bleno_1.default.Characteristic.RESULT_ATTR_NOT_LONG;
return callback(result); return result;
} }
utf8 = data.toString('utf8'); utf8 = data.toString('utf8');
obj = JSON.parse(utf8); obj = JSON.parse(utf8);
ssid = obj.ssid; ssid = obj.ssid;
pwd = obj.pwd; pwd = obj.pwd;
log.info(`connecting to AP`, { ssid: ssid }); log.info(`connecting to AP`, { ssid: ssid });
return wifi.createPSK(ssid, pwd, (err, hash, plaintext) => { try {
if (err) { psk = await wifi.createPSK(ssid, pwd);
log.error('Error hashing wifi password', err);
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR;
return callback(result);
} }
return wifi.setNetwork(ssid, plaintext, hash, (err, data) => { catch (err) {
if (err) { log.error('Error hashing wifi password', err);
result = bleno_1.default.Characteristic.RESULT_UNLIKELY_ERROR;
return result;
}
try {
await wifi.setNetwork(ssid, psk.plaintext, psk.hash);
}
catch (err) {
log.error('Error configuring wifi', err); log.error('Error configuring wifi', err);
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR; result = bleno_1.default.Characteristic.RESULT_UNLIKELY_ERROR;
return callback(result); return result;
} }
currentWifi = ssid; currentWifi = ssid;
currentAddr = getIp(); currentAddr = getIp();
log.info(`Connected to AP`, { ssid: ssid, ip: currentAddr }); log.info(`Connected to AP`, { ssid, ip: currentAddr });
result = bleno.Characteristic.RESULT_SUCCESS; result = bleno_1.default.Characteristic.RESULT_SUCCESS;
return callback(result); return result;
});
});
} }
function onWifiRead(offset, callback) { async function onWifiRead(offset, callback) {
let result = bleno.Characteristic.RESULT_SUCCESS; let result = bleno_1.default.Characteristic.RESULT_SUCCESS;
let wifiRes = {}; let wifiRes = {};
let data; let data;
wifi.list((err, list) => { let list;
if (err) { try {
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR; list = await wifi.list();
}
catch (err) {
result = bleno_1.default.Characteristic.RESULT_UNLIKELY_ERROR;
return callback(result); return callback(result);
} }
wifiRes.available = list; wifiRes.available = list;
@ -89,8 +103,7 @@ function onWifiRead(offset, callback) {
wifiRes.ip = currentAddr; wifiRes.ip = currentAddr;
log.info('Discovered available APs', { found: list.length }); log.info('Discovered available APs', { found: list.length });
data = new Buffer(JSON.stringify(wifiRes)); data = new Buffer(JSON.stringify(wifiRes));
callback(result, data.slice(offset, data.length)); return callback(result, data.slice(offset, data.length));
});
} }
function getMac() { function getMac() {
const colonRe = new RegExp(':', 'g'); const colonRe = new RegExp(':', 'g');
@ -102,15 +115,15 @@ function getMac() {
function spoofMac() { function spoofMac() {
const fs = require('fs'); const fs = require('fs');
const FSPATH = require.resolve('uuid'); const FSPATH = require.resolve('uuid');
const IDFILE = os.homedir() + '/.intval3id'; const IDFILE = os_1.homedir() + '/.intval3id';
let uuid; let uuid;
let UUIDPATH; let UUIDPATH;
let TMP; let TMP;
let MACTMP; let MACTMP;
let dashRe; let dashRe;
delete require.cache[FSPATH]; delete require.cache[FSPATH];
if (fs.existsSync(IDFILE)) { if (fs_extra_1.existsSync(IDFILE)) {
return fs.readFileSync(IDFILE, 'utf8'); return fs_extra_1.readFileSync(IDFILE, 'utf8');
} }
uuid = require('uuid').v4; uuid = require('uuid').v4;
UUIDPATH = require.resolve('uuid'); UUIDPATH = require.resolve('uuid');
@ -118,13 +131,13 @@ function spoofMac() {
TMP = uuid(); TMP = uuid();
MACTMP = TMP.replace(dashRe, '').substring(0, 12); MACTMP = TMP.replace(dashRe, '').substring(0, 12);
dashRe = new RegExp('-', 'g'); dashRe = new RegExp('-', 'g');
fs.writeFileSync(IDFILE, MACTMP, 'utf8'); fs_extra_1.writeFileSync(IDFILE, MACTMP, 'utf8');
return MACTMP; return MACTMP;
} }
function getIp() { function getIp() {
let addr = null; let addr = null;
let ipv4; let ipv4;
const ifaces = os.networkInterfaces(); const ifaces = os_1.networkInterfaces();
if (ifaces && ifaces.wlan0) { if (ifaces && ifaces.wlan0) {
ipv4 = ifaces.wlan0.filter(iface => { ipv4 = ifaces.wlan0.filter(iface => {
if (iface.family === 'IPv4') { if (iface.family === 'IPv4') {
@ -137,8 +150,8 @@ function getIp() {
} }
return addr; return addr;
} }
function capitalize(s) { function capitalize(str) {
return s[0].toUpperCase() + s.slice(1); return str[0].toUpperCase() + str.slice(1);
} }
/** Class representing the bluetooth interface */ /** Class representing the bluetooth interface */
class BLE { class BLE {
@ -150,42 +163,47 @@ class BLE {
constructor(bleGetState) { constructor(bleGetState) {
log.info('Starting bluetooth service'); log.info('Starting bluetooth service');
getState = bleGetState; getState = bleGetState;
bleno.on('stateChange', state => { bleno_1.default.on('stateChange', state => {
log.info('stateChange', { state: state }); log.info('stateChange', { state: state });
if (state === 'poweredOn') { if (state === 'poweredOn') {
log.info('Starting advertising', { DEVICE_NAME, DEVICE_ID: process.env.BLENO_DEVICE_NAME }); log.info('Starting advertising', { DEVICE_NAME, DEVICE_ID: process.env.BLENO_DEVICE_NAME });
bleno.startAdvertising(DEVICE_NAME, [CHAR_ID]); bleno_1.default.startAdvertising(DEVICE_NAME, [CHAR_ID]);
} }
else { else {
bleno.stopAdvertising(); bleno_1.default.stopAdvertising();
} }
}); });
bleno.on('advertisingStart', err => { bleno_1.default.on('advertisingStart', err => {
log.info('advertisingStart', { res: (err ? 'error ' + err : 'success') }); log.info('advertisingStart', { res: (err ? 'error ' + err : 'success') });
createChars(this._onWrite.bind(this), this._onRead.bind(this)); createChars(this._onWrite.bind(this), this._onRead.bind(this));
if (!err) { if (!err) {
bleno.setServices([ bleno_1.default.setServices([
new bleno.PrimaryService({ new bleno_1.default.PrimaryService({
uuid: SERVICE_ID, uuid: SERVICE_ID,
characteristics: chars characteristics: chars
}) })
]); ]);
} }
}); });
bleno.on('accept', clientAddress => { bleno_1.default.on('accept', clientAddress => {
log.info('accept', { clientAddress: clientAddress }); log.info('accept', { clientAddress: clientAddress });
}); });
bleno.on('disconnect', clientAddress => { bleno_1.default.on('disconnect', clientAddress => {
log.info('disconnect', { clientAddress: clientAddress }); log.info('disconnect', { clientAddress: clientAddress });
}); });
wifi.getNetwork((err, ssid) => { this._refreshWifi();
if (err) { }
async _refreshWifi() {
let ssid;
try {
ssid = await wifi.getNetwork();
}
catch (err) {
return log.error('wifi.getNetwork', err); return log.error('wifi.getNetwork', err);
} }
currentWifi = ssid; currentWifi = ssid;
currentAddr = getIp(); currentAddr = getIp();
log.info('wifi.getNetwork', { ssid: ssid, ip: currentAddr }); log.info('wifi.getNetwork', { ssid: ssid, ip: currentAddr });
});
} }
_onWrite(data, offset, withoutResponse, callback) { _onWrite(data, offset, withoutResponse, callback) {
let result = {}; let result = {};
@ -194,12 +212,12 @@ class BLE {
let fn; let fn;
if (offset) { if (offset) {
log.warn(`Offset scenario`); log.warn(`Offset scenario`);
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG; result = bleno_1.default.Characteristic.RESULT_ATTR_NOT_LONG;
return callback(result); return callback(result);
} }
utf8 = data.toString('utf8'); utf8 = data.toString('utf8');
obj = JSON.parse(utf8); obj = JSON.parse(utf8);
result = bleno.Characteristic.RESULT_SUCCESS; result = bleno_1.default.Characteristic.RESULT_SUCCESS;
fn = `_on${capitalize(obj.type)}`; fn = `_on${capitalize(obj.type)}`;
if (obj.type && this[fn]) { if (obj.type && this[fn]) {
return this[fn](obj, () => { return this[fn](obj, () => {
@ -211,7 +229,7 @@ class BLE {
} }
} }
_onRead(offset, callback) { _onRead(offset, callback) {
const result = bleno.Characteristic.RESULT_SUCCESS; const result = bleno_1.default.Characteristic.RESULT_SUCCESS;
const state = getState(); const state = getState();
const data = new Buffer(JSON.stringify(state)); const data = new Buffer(JSON.stringify(state));
callback(result, data.slice(offset, data.length)); callback(result, data.slice(offset, data.length));

File diff suppressed because one or more lines are too long

View File

@ -10,23 +10,23 @@ const iwgetid = '/sbin/iwgetid';
const log = require('../log')('wifi'); const log = require('../log')('wifi');
const child_process_1 = require("child_process"); const child_process_1 = require("child_process");
const fs_1 = require("fs"); const fs_1 = require("fs");
let _entry = null;
let _ssid = null;
/** Class representing the wifi features */ /** Class representing the wifi features */
class Wifi { class Wifi {
constructor() { constructor() {
this._cb = null; this._ssid = null;
this._entry = null;
} }
/** /**
* List available wifi access points * List available wifi access points
* *
* @param {function} callback Function which gets invoked after list is returned * @param {function} callback Function which gets invoked after list is returned
*/ */
list(callback) { async list() {
child_process_1.exec(iwlist, (err, stdout, stderr) => { return new Promise((resolve, reject) => {
return child_process_1.exec(iwlist, (err, stdout, stderr) => {
if (err) { if (err) {
console.error(err); log.error('list', err);
return callback(err); return reject(err);
} }
const limit = 20; const limit = 20;
const lines = stdout.split('\n'); const lines = stdout.split('\n');
@ -45,79 +45,82 @@ class Wifi {
if (ap !== '') if (ap !== '')
return ap; return ap;
}); });
return callback(null, output); return resolve(output);
});
}); });
} }
/** /**
* (internal function) Invoked after config file is read, * (internal function) Invoked after config file is read,
* then invokes file write on the config file * then invokes file write on the config file
* *
* @param {object} err (optional) Error object only present if problem reading config file
* @param {string} data Contents of the config file
*/ */
_readConfigCb(err, data) { async _readConfig() {
let data;
let parsed; let parsed;
let current; let current;
if (err) { try {
console.error(err); data = await fs_1.readFile(filePath, 'utf8');
return this._cb(err); }
catch (err) {
log.error('_readConfig', err);
throw err;
} }
parsed = this._parseConfig(data); parsed = this._parseConfig(data);
current = parsed.find((network) => { current = parsed.find((network) => {
return network.ssid === _ssid; return network.ssid === this._ssid;
}); });
if (typeof current !== 'undefined') { if (typeof current !== 'undefined') {
data = data.replace(current.raw, _entry); data = data.replace(current.raw, this._entry);
} }
else { else {
data += '\n\n' + _entry; data += '\n\n' + this._entry;
} }
_entry = null; this._entry = null;
fs_1.writeFile(filePath, data, 'utf8', this._writeConfigCb.bind(this)); return data;
} }
/** /**
* (internal function) Invoked after config file is written, * (internal function) Invoked after config file is written,
* then executes reconfiguration command * then executes reconfiguration command
* *
* @param {object} err (optional) Error object only present if problem writing config file
*/ */
_writeConfigCb(err) { async _writeConfig(data) {
if (err) { try {
console.error(err); await fs_1.writeFile(filePath, data, { encoding: 'utf-8' });
return this._cb(err); }
catch (err) {
log.error('_readConfigCb', err);
throw err;
} }
child_process_1.exec(reconfigure, this._reconfigureCb.bind(this));
} }
/** /**
* (internal function) Invoked after reconfiguration command is complete * (internal function) Invoked after reconfiguration command is complete
* *
* @param {object} err (optional) Error object only present if configuration command fails
* @param {string} stdout Standard output from reconfiguration command
* @param {string} stderr Error output from command if fails
*/ */
_reconfigureCb(err, stdout, stderr) { async _reconfigure() {
return new Promise((resolve, reject) => {
return child_process_1.exec(reconfigure, (err, stdout, stderr) => {
if (err) { if (err) {
console.error(err); return reject(err);
return this._cb(err);
} }
log.info('Wifi reconfigured'); log.info('Wifi reconfigured');
child_process_1.exec(refresh, this._refreshCb.bind(this)); return resolve(true);
});
});
} }
/** /**
* (internal function) Invoked after wifi refresh command is complete * (internal function) Invoked after wifi refresh command is complete
* *
* @param {object} err (optional) Error object only present if refresh command fails
* @param {string} stdout Standard output from refresh command
* @param {string} stderr Error output from command if fails
*/ */
_refreshCb(err, stdout, stderr) { async _refresh() {
return new Promise((resolve, reject) => {
return child_process_1.exec(refresh, (err, stdout, stderr) => {
if (err) { if (err) {
console.error(err); return reject(err);
return this._cb(err);
} }
log.info('Wifi refreshed'); log.info('Wifi refreshed');
this._cb(null, { ssid: _ssid }); return resolve({ ssid: this._ssid });
this._cb = () => { }; });
});
} }
_parseConfig(str) { _parseConfig(str) {
const networks = []; const networks = [];
@ -157,21 +160,22 @@ class Wifi {
* *
* @param {string} ssid SSID of wifi network * @param {string} ssid SSID of wifi network
* @param {string} pwd Plaintext passphrase of wifi network * @param {string} pwd Plaintext passphrase of wifi network
* @param {function} callback Function called after psk hash is generated
*/ */
createPSK(ssid, pwd, callback) { createPSK(ssid, pwd) {
const cmd = `wpa_passphrase '${ssid.replace(/'/g, `'\\''`)}' '${pwd.replace(/'/g, `'\\''`)}' | grep "psk="`; const cmd = `wpa_passphrase '${ssid.replace(/'/g, `'\\''`)}' '${pwd.replace(/'/g, `'\\''`)}' | grep "psk="`;
let lines; let lines;
let hash; let hash;
let plaintext; let plaintext;
child_process_1.exec(cmd, (err, stdout, stderr) => { return new Promise((resolve, reject) => {
return child_process_1.exec(cmd, (err, stdout, stderr) => {
if (err) { if (err) {
return callback(err); return reject(err);
} }
lines = stdout.replace('#psk=', '').split('psk='); lines = stdout.replace('#psk=', '').split('psk=');
hash = lines[1]; hash = lines[1];
plaintext = lines[0]; plaintext = lines[0];
callback(null, hash.trim(), plaintext.trim()); return resolve({ hash: hash.trim(), plaintext: plaintext.trim() });
});
}); });
} }
/** /**
@ -180,30 +184,56 @@ class Wifi {
* @param {string} ssid SSID of network to configure * @param {string} ssid SSID of network to configure
* @param {string} pwd Password of access point, plaintext to be masked * @param {string} pwd Password of access point, plaintext to be masked
* @param {string} hash Password/SSID of access point, securely hashed * @param {string} hash Password/SSID of access point, securely hashed
* @param {function} callback Function invoked after process is complete, or fails
*/ */
setNetwork(ssid, pwd, hash, callback) { async setNetwork(ssid, pwd, hash) {
let masked = pwd.split('').map(char => { return char !== '"' ? '*' : '"'; }).join(''); let masked = pwd.split('').map(char => { return char !== '"' ? '*' : '"'; }).join('');
_entry = `network={\n\tssid="${ssid}"\n\t#psk=${masked}\n\tpsk=${hash}\n}\n`; let data;
this._cb = callback; this._entry = `network={\n\tssid="${ssid}"\n\t#psk=${masked}\n\tpsk=${hash}\n}\n`;
_ssid = ssid; this._ssid = ssid;
fs_1.readFile(filePath, 'utf8', this._readConfigCb.bind(this)); try {
data = await this._readConfig();
}
catch (err) {
log.error(err);
}
try {
await this._writeConfig(data);
}
catch (err) {
log.error(err);
}
try {
await this._reconfigure();
}
catch (err) {
log.error(err);
}
try {
await this._refresh();
}
catch (err) {
log.error(err);
}
return { ssid: this._ssid };
} }
/** /**
* Executes command which gets the currently connected network * Executes command which gets the currently connected network
* *
* @param {function} callback Function which is invoked after command is completed * @param {function} callback Function which is invoked after command is completed
*/ */
getNetwork(callback) { async getNetwork() {
let output; let output;
child_process_1.exec(iwgetid, (err, stdout, stderr) => { return new Promise((resolve, reject) => {
return child_process_1.exec(iwgetid, (err, stdout, stderr) => {
if (err) { if (err) {
return callback(err); return reject(err);
} }
output = stdout.split('ESSID:')[1].replace(quoteRe, '').trim(); output = stdout.split('ESSID:')[1].replace(quoteRe, '').trim();
callback(null, output); return resolve(output);
});
}); });
} }
} }
module.exports = new Wifi(); exports.Wifi = Wifi;
module.exports.Wifi = Wifi;
//# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

9
package-lock.json generated
View File

@ -57,6 +57,15 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true "dev": true
}, },
"@types/bleno": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/bleno/-/bleno-0.4.1.tgz",
"integrity": "sha512-CY4CG5jFURnigEi1qQVnKgtyxg7jQEAmTjBpawejmt91XRThKowP90IvZksv/Qrjfh5IgZIGLe0SXGMUpwS6ZA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/events": { "@types/events": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",

View File

@ -43,6 +43,7 @@
"winston": "^3.2.1" "winston": "^3.2.1"
}, },
"devDependencies": { "devDependencies": {
"@types/bleno": "^0.4.1",
"@types/fs-extra": "^8.0.0", "@types/fs-extra": "^8.0.0",
"@types/node": "^12.7.12", "@types/node": "^12.7.12",
"@types/node-persist": "0.0.33", "@types/node-persist": "0.0.33",

View File

@ -3,38 +3,51 @@
/** @module ble */ /** @module ble */
/** Bluetooth Low Energy module */ /** Bluetooth Low Energy module */
const util = require('util') import { inherits } from 'util'
const os = require('os') import { networkInterfaces, homedir } from 'os'
import { readFileSync, existsSync, writeFileSync } from 'fs-extra'
const log = require('../log')('ble') const log = require('../log')('ble')
const wifi = require('../wifi') import { Wifi } from '../wifi'
const wifi = new Wifi()
const DEVICE_NAME = process.env.DEVICE_NAME || 'intval3' const DEVICE_NAME : string = process.env.DEVICE_NAME || 'intval3'
const SERVICE_ID = process.env.SERVICE_ID || 'intval3_ble' const SERVICE_ID : string = process.env.SERVICE_ID || 'intval3_ble'
const CHAR_ID = process.env.CHAR_ID || 'intval3char' const CHAR_ID : string = process.env.CHAR_ID || 'intval3char'
const WIFI_ID = process.env.WIFI_ID || 'wifichar' const WIFI_ID : string = process.env.WIFI_ID || 'wifichar'
const NETWORK = os.networkInterfaces() const NETWORK : any = networkInterfaces() //?type?
const MAC = getMac() || spoofMac() const MAC : string = getMac() || spoofMac()
//Give the device a unique device name, needs to be in env //Give the device a unique device name, needs to be in env
process.env.BLENO_DEVICE_NAME += '_' + MAC process.env.BLENO_DEVICE_NAME += '_' + MAC
const bleno = require('bleno') import bleno from 'bleno'
const { Characteristic } = bleno
let currentWifi : string = 'disconnected'
let currentAddr : string = null
let getState : Function
let currentWifi = 'disconnected' const chars : any[] = []
let currentAddr = null
let getState
const chars = [] interface WifiInfo {
ssid : string
pwd : string
}
function createChar(name, uuid, prop, write, read) { interface WifiResponse {
function characteristic () { available : string[]
bleno.Characteristic.call(this, { current : string
uuid : uuid, ip : string
}
function createChar(name : string, uuid : string, prop : string[], write : Function, read : Function) {
const characteristic : any = function () {
Characteristic.call(this, {
uuid,
properties: prop properties: prop
}) })
} }
util.inherits(characteristic, bleno.Characteristic) inherits(characteristic, Characteristic)
if (prop.indexOf('read') !== -1) { if (prop.indexOf('read') !== -1) {
//data, offset, withoutResponse, callback //data, offset, withoutResponse, callback
characteristic.prototype.onReadRequest = read characteristic.prototype.onReadRequest = read
@ -45,64 +58,76 @@ function createChar(name, uuid, prop, write, read) {
chars.push(new characteristic()) chars.push(new characteristic())
} }
function createChars (onWrite, onRead) { function createChars (onWrite : Function, onRead : Function) {
createChar('intval3', CHAR_ID, ['read', 'write'], onWrite, onRead) const permissions : string[] = ['read', 'write'];
createChar('wifi', WIFI_ID, ['read', 'write'], onWifiWrite, onWifiRead) createChar('intval3', CHAR_ID, permissions, onWrite, onRead)
createChar('wifi', WIFI_ID, permissions, onWifiWrite, onWifiRead)
} }
function onWifiWrite (data, offset, withoutResponse, callback) { async function onWifiWrite (data : any, offset : number) {
let result let result : any
let utf8 let utf8 : string
let obj let obj : WifiInfo = {} as WifiInfo
let ssid let ssid : string
let pwd let pwd : string
let psk : any
if (offset) { if (offset) {
log.warn(`Offset scenario`) log.warn(`Offset scenario`)
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG result = bleno.Characteristic.RESULT_ATTR_NOT_LONG
return callback(result) return result
} }
utf8 = data.toString('utf8') utf8 = data.toString('utf8')
obj = JSON.parse(utf8) obj = JSON.parse(utf8)
ssid = obj.ssid ssid = obj.ssid
pwd = obj.pwd pwd = obj.pwd
log.info(`connecting to AP`, { ssid : ssid }) log.info(`connecting to AP`, { ssid : ssid })
return wifi.createPSK(ssid, pwd, (err, hash, plaintext) => {
if (err) { try {
psk = await wifi.createPSK(ssid, pwd)
} catch (err) {
log.error('Error hashing wifi password', err) log.error('Error hashing wifi password', err)
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return callback(result) return result
}
return wifi.setNetwork(ssid, plaintext, hash, (err, data) => {
if (err) {
log.error('Error configuring wifi', err)
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return callback(result)
}
currentWifi = ssid
currentAddr = getIp()
log.info(`Connected to AP`, { ssid : ssid, ip : currentAddr })
result = bleno.Characteristic.RESULT_SUCCESS
return callback(result)
})
})
} }
function onWifiRead (offset, callback) { try {
let result = bleno.Characteristic.RESULT_SUCCESS await wifi.setNetwork(ssid, psk.plaintext, psk.hash)
let wifiRes = {} } catch (err) {
let data log.error('Error configuring wifi', err)
wifi.list((err, list) => { result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
if (err) { return result
}
currentWifi = ssid
currentAddr = getIp()
log.info(`Connected to AP`, { ssid, ip : currentAddr })
result = bleno.Characteristic.RESULT_SUCCESS
return result
}
async function onWifiRead (offset : number, callback : Function) {
let result : any = bleno.Characteristic.RESULT_SUCCESS
let wifiRes : WifiResponse = {} as WifiResponse
let data : any
let list : any
try {
list = await wifi.list()
} catch (err) {
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return callback(result) return callback(result)
} }
wifiRes.available = list wifiRes.available = list
wifiRes.current = currentWifi wifiRes.current = currentWifi
wifiRes.ip = currentAddr wifiRes.ip = currentAddr
log.info('Discovered available APs', { found : list.length }) log.info('Discovered available APs', { found : list.length })
data = new Buffer(JSON.stringify(wifiRes)) data = new Buffer(JSON.stringify(wifiRes))
callback(result, data.slice(offset, data.length))
}) return callback(result, data.slice(offset, data.length))
} }
function getMac () { function getMac () {
@ -116,15 +141,15 @@ function getMac () {
function spoofMac () { function spoofMac () {
const fs = require('fs') const fs = require('fs')
const FSPATH = require.resolve('uuid') const FSPATH = require.resolve('uuid')
const IDFILE = os.homedir() + '/.intval3id' const IDFILE = homedir() + '/.intval3id'
let uuid let uuid
let UUIDPATH let UUIDPATH
let TMP let TMP
let MACTMP let MACTMP
let dashRe let dashRe
delete require.cache[FSPATH] delete require.cache[FSPATH]
if (fs.existsSync(IDFILE)) { if (existsSync(IDFILE)) {
return fs.readFileSync(IDFILE, 'utf8') return readFileSync(IDFILE, 'utf8')
} }
uuid = require('uuid').v4 uuid = require('uuid').v4
UUIDPATH = require.resolve('uuid') UUIDPATH = require.resolve('uuid')
@ -132,14 +157,14 @@ function spoofMac () {
TMP = uuid() TMP = uuid()
MACTMP = TMP.replace(dashRe, '').substring(0, 12) MACTMP = TMP.replace(dashRe, '').substring(0, 12)
dashRe = new RegExp('-', 'g') dashRe = new RegExp('-', 'g')
fs.writeFileSync(IDFILE, MACTMP, 'utf8') writeFileSync(IDFILE, MACTMP, 'utf8')
return MACTMP return MACTMP
} }
function getIp () { function getIp () {
let addr = null let addr = null
let ipv4 let ipv4
const ifaces = os.networkInterfaces() const ifaces = networkInterfaces()
if (ifaces && ifaces.wlan0) { if (ifaces && ifaces.wlan0) {
ipv4 = ifaces.wlan0.filter(iface => { ipv4 = ifaces.wlan0.filter(iface => {
if (iface.family === 'IPv4') { if (iface.family === 'IPv4') {
@ -154,8 +179,8 @@ function getIp () {
} }
function capitalize (s) { function capitalize (str : string) {
return s[0].toUpperCase() + s.slice(1) return str[0].toUpperCase() + str.slice(1)
} }
/** Class representing the bluetooth interface */ /** Class representing the bluetooth interface */
@ -165,7 +190,7 @@ class BLE {
* *
* @constructor * @constructor
*/ */
constructor (bleGetState) { constructor (bleGetState : Function) {
log.info('Starting bluetooth service') log.info('Starting bluetooth service')
getState = bleGetState getState = bleGetState
@ -201,29 +226,38 @@ class BLE {
log.info('disconnect', { clientAddress : clientAddress }) log.info('disconnect', { clientAddress : clientAddress })
}) })
wifi.getNetwork((err, ssid) => { this._refreshWifi()
if (err) { }
private async _refreshWifi () {
let ssid : string
try {
ssid = await wifi.getNetwork() as string
} catch (err) {
return log.error('wifi.getNetwork', err) return log.error('wifi.getNetwork', err)
} }
currentWifi = ssid currentWifi = ssid
currentAddr = getIp() currentAddr = getIp()
log.info('wifi.getNetwork', {ssid : ssid, ip : currentAddr }) log.info('wifi.getNetwork', {ssid : ssid, ip : currentAddr })
})
} }
_onWrite (data, offset, withoutResponse, callback) { private _onWrite (data : any, offset : number, withoutResponse : Function, callback : Function) {
let result = {} let result = {}
let utf8 let utf8
let obj let obj
let fn let fn
if (offset) { if (offset) {
log.warn(`Offset scenario`) log.warn(`Offset scenario`)
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG result = bleno.Characteristic.RESULT_ATTR_NOT_LONG
return callback(result) return callback(result)
} }
utf8 = data.toString('utf8') utf8 = data.toString('utf8')
obj = JSON.parse(utf8) obj = JSON.parse(utf8)
result = bleno.Characteristic.RESULT_SUCCESS result = bleno.Characteristic.RESULT_SUCCESS
fn = `_on${capitalize(obj.type)}` fn = `_on${capitalize(obj.type)}`
if (obj.type && this[fn]) { if (obj.type && this[fn]) {
return this[fn](obj, () => { return this[fn](obj, () => {
callback(result) callback(result)
@ -233,7 +267,7 @@ class BLE {
} }
} }
_onRead (offset, callback) { private _onRead (offset : number, callback : Function) {
const result = bleno.Characteristic.RESULT_SUCCESS const result = bleno.Characteristic.RESULT_SUCCESS
const state = getState() const state = getState()
const data = new Buffer(JSON.stringify( state )) const data = new Buffer(JSON.stringify( state ))
@ -245,7 +279,7 @@ class BLE {
* @param {string} eventName Name of the event to to bind * @param {string} eventName Name of the event to to bind
* @param {function} callback Invoked when the event is triggered * @param {function} callback Invoked when the event is triggered
*/ */
on (eventName, callback) { on (eventName : string, callback : Function) {
this[`_on${capitalize(eventName)}`] = callback this[`_on${capitalize(eventName)}`] = callback
} }

View File

@ -12,9 +12,7 @@ const iwgetid : string = '/sbin/iwgetid'
const log : any = require('../log')('wifi') const log : any = require('../log')('wifi')
import { exec } from 'child_process' import { exec } from 'child_process'
import { readFile, writeFile } from 'fs' import { readFile, writeFile } from 'fs'
import { reject } from 'q'
let _entry : string = null
let _ssid : string = null
interface Network { interface Network {
raw : string raw : string
@ -22,8 +20,10 @@ interface Network {
} }
/** Class representing the wifi features */ /** Class representing the wifi features */
class Wifi { export class Wifi {
private _cb : Function = null private _ssid : string = null
private _entry : string = null
constructor () { constructor () {
} }
@ -32,16 +32,17 @@ class Wifi {
* *
* @param {function} callback Function which gets invoked after list is returned * @param {function} callback Function which gets invoked after list is returned
*/ */
list (callback : Function) { public async list () {
exec(iwlist, (err, stdout, stderr) => { return new Promise ((resolve : Function, reject : Function) => {
return exec(iwlist, (err, stdout, stderr) => {
if (err) { if (err) {
console.error(err) log.error('list', err)
return callback(err) return reject(err)
} }
const limit = 20; const limit : number = 20;
const lines = stdout.split('\n') const lines : string[] = stdout.split('\n')
let output = [] let output : string[] = []
let line let line : string
let i = 0 let i = 0
for (let l of lines) { for (let l of lines) {
line = l.replace('ESSID:', '').trim() line = l.replace('ESSID:', '').trim()
@ -54,80 +55,87 @@ class Wifi {
output = output.filter(ap => { output = output.filter(ap => {
if (ap !== '') return ap if (ap !== '') return ap
}) })
return callback(null, output) return resolve(output)
})
}) })
} }
/** /**
* (internal function) Invoked after config file is read, * (internal function) Invoked after config file is read,
* then invokes file write on the config file * then invokes file write on the config file
* *
* @param {object} err (optional) Error object only present if problem reading config file
* @param {string} data Contents of the config file
*/ */
_readConfigCb (err : Error, data : string) { async _readConfig () {
let data : string
let parsed : Network[] let parsed : Network[]
let current : Network let current : Network
if (err) {
console.error(err) try {
return this._cb(err) data = await readFile(filePath, 'utf8')
} catch (err) {
log.error('_readConfig', err)
throw err
} }
parsed = this._parseConfig(data) parsed = this._parseConfig(data)
current = parsed.find((network : Network) => { current = parsed.find((network : Network) => {
return network.ssid === _ssid return network.ssid === this._ssid
}) })
if (typeof current !== 'undefined') { if (typeof current !== 'undefined') {
data = data.replace(current.raw, _entry) data = data.replace(current.raw, this._entry)
} else { } else {
data += '\n\n' + _entry data += '\n\n' + this._entry
} }
_entry = null this._entry = null
writeFile(filePath, data, 'utf8', this._writeConfigCb.bind(this))
return data
} }
/** /**
* (internal function) Invoked after config file is written, * (internal function) Invoked after config file is written,
* then executes reconfiguration command * then executes reconfiguration command
* *
* @param {object} err (optional) Error object only present if problem writing config file
*/ */
_writeConfigCb (err : Error) { private async _writeConfig (data : string) {
if (err) { try {
console.error(err) await writeFile(filePath, data, { encoding : 'utf-8' })
return this._cb(err) } catch (err) {
log.error('_readConfigCb', err)
throw err
} }
exec(reconfigure, this._reconfigureCb.bind(this))
} }
/** /**
* (internal function) Invoked after reconfiguration command is complete * (internal function) Invoked after reconfiguration command is complete
* *
* @param {object} err (optional) Error object only present if configuration command fails
* @param {string} stdout Standard output from reconfiguration command
* @param {string} stderr Error output from command if fails
*/ */
_reconfigureCb (err : Error, stdout : string, stderr : string) { private async _reconfigure () {
return new Promise((resolve : Function, reject : Function) => {
return exec(reconfigure, (err : Error, stdout : string, stderr : string) => {
if (err) { if (err) {
console.error(err) return reject(err)
return this._cb(err)
} }
log.info('Wifi reconfigured') log.info('Wifi reconfigured')
exec(refresh, this._refreshCb.bind(this)) return resolve(true)
})
})
} }
/** /**
* (internal function) Invoked after wifi refresh command is complete * (internal function) Invoked after wifi refresh command is complete
* *
* @param {object} err (optional) Error object only present if refresh command fails
* @param {string} stdout Standard output from refresh command
* @param {string} stderr Error output from command if fails
*/ */
_refreshCb (err : Error, stdout : string, stderr : string) { private async _refresh () {
return new Promise((resolve : Function, reject : Function) => {
return exec(refresh, (err : Error, stdout : string, stderr : string) => {
if (err) { if (err) {
console.error(err) return reject(err)
return this._cb(err)
} }
log.info('Wifi refreshed') log.info('Wifi refreshed')
this._cb(null, { ssid : _ssid }) return resolve({ ssid : this._ssid });
this._cb = () => {} })
})
} }
_parseConfig (str : string) : Network[] {
private _parseConfig (str : string) : Network[] {
const networks : Network[] = [] const networks : Network[] = []
const lines = str.split('\n') const lines = str.split('\n')
let network : Network = {} as Network let network : Network = {} as Network
@ -162,21 +170,22 @@ class Wifi {
* *
* @param {string} ssid SSID of wifi network * @param {string} ssid SSID of wifi network
* @param {string} pwd Plaintext passphrase of wifi network * @param {string} pwd Plaintext passphrase of wifi network
* @param {function} callback Function called after psk hash is generated
*/ */
createPSK (ssid : string, pwd : string, callback : Function) { createPSK (ssid : string, pwd : string) {
const cmd : string = `wpa_passphrase '${ssid.replace(/'/g, `'\\''`)}' '${pwd.replace(/'/g, `'\\''`)}' | grep "psk="` const cmd : string = `wpa_passphrase '${ssid.replace(/'/g, `'\\''`)}' '${pwd.replace(/'/g, `'\\''`)}' | grep "psk="`
let lines : string[] let lines : string[]
let hash : string let hash : string
let plaintext : string let plaintext : string
exec(cmd, (err, stdout, stderr) => { return new Promise ((resolve : Function, reject : Function) => {
return exec(cmd, (err, stdout, stderr) => {
if (err) { if (err) {
return callback(err) return reject(err)
} }
lines = stdout.replace('#psk=', '').split('psk=') lines = stdout.replace('#psk=', '').split('psk=')
hash = lines[1] hash = lines[1]
plaintext = lines[0] plaintext = lines[0]
callback(null, hash.trim(), plaintext.trim()) return resolve({ hash : hash.trim(), plaintext : plaintext.trim()})
})
}) })
} }
/** /**
@ -185,30 +194,54 @@ class Wifi {
* @param {string} ssid SSID of network to configure * @param {string} ssid SSID of network to configure
* @param {string} pwd Password of access point, plaintext to be masked * @param {string} pwd Password of access point, plaintext to be masked
* @param {string} hash Password/SSID of access point, securely hashed * @param {string} hash Password/SSID of access point, securely hashed
* @param {function} callback Function invoked after process is complete, or fails
*/ */
setNetwork (ssid : string, pwd : string, hash : string, callback : Function) { async setNetwork (ssid : string, pwd : string, hash : string) {
let masked : string = pwd.split('').map(char => { return char !== '"' ? '*' : '"' }).join('') let masked : string = pwd.split('').map(char => { return char !== '"' ? '*' : '"' }).join('')
_entry = `network={\n\tssid="${ssid}"\n\t#psk=${masked}\n\tpsk=${hash}\n}\n` let data : string
this._cb = callback this._entry = `network={\n\tssid="${ssid}"\n\t#psk=${masked}\n\tpsk=${hash}\n}\n`
_ssid = ssid this._ssid = ssid
readFile(filePath, 'utf8', this._readConfigCb.bind(this))
try {
data = await this._readConfig()
} catch (err) {
log.error(err)
}
try {
await this._writeConfig(data)
} catch (err) {
log.error(err)
}
try {
await this._reconfigure()
} catch (err) {
log.error(err)
}
try {
await this._refresh()
} catch (err) {
log.error(err)
}
return { ssid : this._ssid }
} }
/** /**
* Executes command which gets the currently connected network * Executes command which gets the currently connected network
* *
* @param {function} callback Function which is invoked after command is completed * @param {function} callback Function which is invoked after command is completed
*/ */
getNetwork (callback : Function) { public async getNetwork () {
let output let output : string
exec(iwgetid, (err, stdout, stderr) => { return new Promise((resolve : Function, reject : Function) => {
return exec(iwgetid, (err : Error, stdout : string, stderr : string) => {
if (err) { if (err) {
return callback(err) return reject(err)
} }
output = stdout.split('ESSID:')[1].replace(quoteRe, '').trim() output = stdout.split('ESSID:')[1].replace(quoteRe, '').trim()
callback(null, output) return resolve(output)
}) })
})
} }
} }
module.exports = new Wifi() module.exports.Wifi = Wifi