# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
Merge server changes with more recent app changes.
This commit is contained in:
litter 2020-01-02 12:41:39 -05:00
commit a87e81f128
10 changed files with 2580 additions and 450 deletions

View File

@ -655,6 +655,9 @@ async function index (req, res, next) {
} catch (err) { } catch (err) {
return next(err) return next(err)
} }
res.end(data)
return next()
} }
function init () { function init () {

View File

@ -1,254 +1,247 @@
'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_1 = require("util");
const util = require('util') const os_1 = require("os");
const os = require('os') const fs_extra_1 = require("fs-extra");
const log = require('../log')('ble');
const log = require('../log')('ble') const wifi_1 = require("../wifi");
const wifi = 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_1.networkInterfaces(); //?type?
const NETWORK = os.networkInterfaces() 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;
} }
if (prop.indexOf('write') !== -1) { if (prop.indexOf('write') !== -1) {
characteristic.prototype.onWriteRequest = write characteristic.prototype.onWriteRequest = write;
} }
chars.push(new characteristic()) chars.push(new characteristic());
} }
function createChars(onWrite, onRead) {
function createChars (onWrite, onRead) { const permissions = ['read', 'write'];
createChar('intval3', CHAR_ID, ['read', 'write'], onWrite, onRead) createChar('intval3', CHAR_ID, permissions, onWrite, onRead);
createChar('wifi', WIFI_ID, ['read', 'write'], onWifiWrite, onWifiRead) createChar('wifi', WIFI_ID, permissions, onWifiWrite, onWifiRead);
} }
async function onWifiWrite(data, offset) {
function onWifiWrite (data, offset, withoutResponse, callback) { 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 catch (err) {
return callback(result) log.error('Error hashing wifi password', err);
} result = bleno_1.default.Characteristic.RESULT_UNLIKELY_ERROR;
return wifi.setNetwork(ssid, plaintext, hash, (err, data) => { return result;
if (err) { }
log.error('Error configuring wifi', err) try {
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR await wifi.setNetwork(ssid, psk.plaintext, psk.hash);
return callback(result) }
} catch (err) {
currentWifi = ssid log.error('Error configuring wifi', err);
currentAddr = getIp() result = bleno_1.default.Characteristic.RESULT_UNLIKELY_ERROR;
log.info(`Connected to AP`, { ssid : ssid, ip : currentAddr }) return result;
result = bleno.Characteristic.RESULT_SUCCESS }
return callback(result) currentWifi = ssid;
}) currentAddr = getIp();
}) log.info(`Connected to AP`, { ssid, ip: currentAddr });
result = bleno_1.default.Characteristic.RESULT_SUCCESS;
return result;
} }
async function onWifiRead(offset, callback) {
function onWifiRead (offset, callback) { let result = bleno_1.default.Characteristic.RESULT_SUCCESS;
let result = bleno.Characteristic.RESULT_SUCCESS let wifiRes = {};
let wifiRes = {} let data;
let data let list;
wifi.list((err, list) => { try {
if (err) { list = await wifi.list();
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR }
return callback(result) catch (err) {
} result = bleno_1.default.Characteristic.RESULT_UNLIKELY_ERROR;
wifiRes.available = list return callback(result);
wifiRes.current = currentWifi }
wifiRes.ip = currentAddr wifiRes.available = list;
log.info('Discovered available APs', { found : list.length }) wifiRes.current = currentWifi;
data = new Buffer(JSON.stringify(wifiRes)) wifiRes.ip = currentAddr;
callback(result, data.slice(offset, data.length)) log.info('Discovered available APs', { found: list.length });
}) data = new Buffer(JSON.stringify(wifiRes));
return callback(result, data.slice(offset, data.length));
} }
function getMac() {
function getMac () { const colonRe = new RegExp(':', 'g');
const colonRe = new RegExp(':', 'g') if (NETWORK && NETWORK.wlan0 && NETWORK.wlan0[0] && NETWORK.wlan0[0].mac) {
if (NETWORK && NETWORK.wlan0 && NETWORK.wlan0[0] && NETWORK.wlan0[0].mac) { return NETWORK.wlan0[0].mac.replace(colonRe, '');
return NETWORK.wlan0[0].mac.replace(colonRe, '') }
} return undefined;
return undefined
} }
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_1.homedir() + '/.intval3id';
const IDFILE = os.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_extra_1.existsSync(IDFILE)) {
if (fs.existsSync(IDFILE)) { return fs_extra_1.readFileSync(IDFILE, 'utf8');
return fs.readFileSync(IDFILE, 'utf8') }
} uuid = require('uuid').v4;
uuid = require('uuid').v4 UUIDPATH = require.resolve('uuid');
UUIDPATH = require.resolve('uuid') delete require.cache[UUIDPATH];
delete require.cache[UUIDPATH] 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_extra_1.writeFileSync(IDFILE, MACTMP, 'utf8');
fs.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_1.networkInterfaces();
const ifaces = os.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') { return iface;
return iface }
} });
}) if (ipv4.length === 1) {
if (ipv4.length === 1) { addr = ipv4[0].address;
addr = ipv4[0].address }
} }
} return addr;
return addr
} }
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
function capitalize (s) {
return s[0].toUpperCase() + s.slice(1)
} }
/** Class representing the bluetooth interface */ /** Class representing the bluetooth interface */
class BLE { class BLE {
/** /**
* Establishes Bluetooth Low Energy services, accessible to process through this class * Establishes Bluetooth Low Energy services, accessible to process through this class
* *
* @constructor * @constructor
*/ */
constructor (bleGetState) { constructor(bleGetState) {
log.info('Starting bluetooth service') this.listeners = {};
log.info('Starting bluetooth service');
getState = bleGetState getState = bleGetState;
bleno_1.default.on('stateChange', state => {
bleno.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_1.default.startAdvertising(DEVICE_NAME, [CHAR_ID]);
bleno.startAdvertising(DEVICE_NAME, [CHAR_ID]) }
} else { else {
bleno.stopAdvertising() bleno_1.default.stopAdvertising();
} }
}) });
bleno_1.default.on('advertisingStart', err => {
bleno.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_1.default.setServices([
bleno.setServices([ new bleno_1.default.PrimaryService({
new bleno.PrimaryService({ uuid: SERVICE_ID,
uuid : SERVICE_ID, //hardcoded across panels characteristics: chars
characteristics : chars })
}) ]);
]) }
} });
}) bleno_1.default.on('accept', clientAddress => {
log.info('accept', { clientAddress: clientAddress });
bleno.on('accept', clientAddress => { });
log.info('accept', { clientAddress : clientAddress }) bleno_1.default.on('disconnect', clientAddress => {
}) log.info('disconnect', { clientAddress: clientAddress });
});
bleno.on('disconnect', clientAddress => { this._refreshWifi();
log.info('disconnect', { clientAddress : clientAddress }) }
}) async _refreshWifi() {
let ssid;
wifi.getNetwork((err, ssid) => { try {
if (err) { ssid = await wifi.getNetwork();
return log.error('wifi.getNetwork', err) }
} catch (err) {
currentWifi = ssid return log.error('wifi.getNetwork', err);
currentAddr = getIp() }
log.info('wifi.getNetwork', {ssid : ssid, ip : currentAddr }) currentWifi = ssid;
}) currentAddr = getIp();
} log.info('wifi.getNetwork', { ssid: ssid, ip: currentAddr });
_onWrite (data, offset, withoutResponse, callback) { }
let result = {} _onWrite(data, offset, withoutResponse, callback) {
let utf8 let result = {};
let obj let utf8;
let fn let obj;
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)}` if (obj.type && this.listeners[obj.type]) {
if (obj.type && this[fn]) { return this.listeners[obj.type](obj, () => {
return this[fn](obj, () => { callback(result);
callback(result) });
}) }
} else { else {
return callback(result) return callback(result);
} }
}
} _onRead(offset, callback) {
_onRead (offset, callback) { const result = bleno_1.default.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 )) callback(result, data.slice(offset, data.length));
callback(result, data.slice(offset, data.length)) }
} /**
/** * Binds functions to events that are triggered by BLE messages
* Binds functions to events that are triggered by BLE messages *
* * @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, callback) { this.listeners[eventName] = callback;
this[`_on${capitalize(eventName)}`] = callback }
}
} }
module.exports = BLE;
module.exports = BLE //# sourceMappingURL=index.js.map

1
lib/ble/index.js.map Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,209 +1,239 @@
'use strict' 'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
const networkPattern = /network[\s\S]*?=[\s\S]*?{([\s\S]*?)}/gi const networkPattern = /network[\s\S]*?=[\s\S]*?{([\s\S]*?)}/gi;
const quoteRe = new RegExp('"', 'g') const quoteRe = new RegExp('"', 'g');
const filePath = '/etc/wpa_supplicant/wpa_supplicant.conf';
const filePath = '/etc/wpa_supplicant/wpa_supplicant.conf' const reconfigure = '/sbin/wpa_cli reconfigure';
const reconfigure = '/sbin/wpa_cli reconfigure' const refresh = 'ip link set wlan0 down && ip link set wlan0 up';
const refresh = 'ip link set wlan0 down && ip link set wlan0 up' const iwlist = '/sbin/iwlist wlan0 scanning | grep "ESSID:"';
const iwlist = '/sbin/iwlist wlan0 scanning | grep "ESSID:"' const iwgetid = '/sbin/iwgetid';
const iwgetid = '/sbin/iwgetid' const log = require('../log')('wifi');
const child_process_1 = require("child_process");
const log = require('../log')('wifi') const fs_extra_1 = require("fs-extra");
const exec = require('child_process').exec
const fs = require('fs')
let _entry = null
let _ssid = null
let _cb = null
/** Class representing the wifi features */ /** Class representing the wifi features */
class Wifi { class Wifi {
constructor () { constructor() {
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) { */
exec(iwlist, (err, stdout, stderr) => { async list() {
if (err) { return new Promise((resolve, reject) => {
console.error(err) return child_process_1.exec(iwlist, (err, stdout, stderr) => {
return callback(err) if (err) {
} log.error('list', err);
const limit = 20; return reject(err);
const lines = stdout.split('\n') }
let output = [] const limit = 20;
let line const lines = stdout.split('\n');
let i = 0 let output = [];
for (let l of lines) { let line;
line = l.replace('ESSID:', '').trim() let i = 0;
if (line !== '""' && i < limit) { for (let l of lines) {
line = line.replace(quoteRe, '') line = l.replace('ESSID:', '').trim();
output.push(line) if (line !== '""' && i < limit) {
} line = line.replace(quoteRe, '');
i++ output.push(line);
} }
output = output.filter(ap => { i++;
if (ap !== '') return ap }
}) output = output.filter(ap => {
return callback(null, output) if (ap !== '')
}) return ap;
} });
/** return resolve(output);
* (internal function) Invoked after config file is read, });
* 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 * (internal function) Invoked after config file is read,
*/ * then invokes file write on the config file
_readConfigCb (err, data) { *
let parsed */
let current async _readConfig() {
if (err) { let data;
console.error(err) let parsed;
return _cb(err) let current;
} try {
parsed = this._parseConfig(data) data = await fs_extra_1.readFile(filePath, 'utf8');
current = parsed.find(network => { }
return network.ssid === _ssid catch (err) {
}) log.error('_readConfig', err);
if (typeof current !== 'undefined') { throw err;
data = data.replace(current.raw, _entry) }
} else { parsed = this._parseConfig(data);
data += '\n\n' + _entry current = parsed.find((network) => {
} return network.ssid === this._ssid;
_entry = null });
fs.writeFile(filePath, data, 'utf8', this._writeConfigCb.bind(this)) if (typeof current !== 'undefined') {
} data = data.replace(current.raw, this._entry);
/** }
* (internal function) Invoked after config file is written, else {
* then executes reconfiguration command data += '\n\n' + this._entry;
* }
* @param {object} err (optional) Error object only present if problem writing config file this._entry = null;
*/ return data;
_writeConfigCb (err) { }
if (err) { /**
console.error(err) * (internal function) Invoked after config file is written,
return _cb(err) * then executes reconfiguration command
} *
exec(reconfigure, this._reconfigureCb.bind(this)) */
} async _writeConfig(data) {
/** try {
* (internal function) Invoked after reconfiguration command is complete await fs_extra_1.writeFile(filePath, data, 'utf8');
* }
* @param {object} err (optional) Error object only present if configuration command fails catch (err) {
* @param {string} stdout Standard output from reconfiguration command log.error('_readConfigCb', err);
* @param {string} stderr Error output from command if fails throw err;
*/ }
_reconfigureCb (err, stdout, stderr) { }
if (err) { /**
console.error(err) * (internal function) Invoked after reconfiguration command is complete
return _cb(err) *
} */
console.log('Wifi reconfigured') async _reconfigure() {
exec(refresh, this._refreshCb.bind(this)) return new Promise((resolve, reject) => {
} return child_process_1.exec(reconfigure, (err, stdout, stderr) => {
/** if (err) {
* (internal function) Invoked after wifi refresh command is complete return reject(err);
* }
* @param {object} err (optional) Error object only present if refresh command fails log.info('Wifi reconfigured');
* @param {string} stdout Standard output from refresh command return resolve(true);
* @param {string} stderr Error output from command if fails });
*/ });
_refreshCb (err, stdout, stderr) { }
if (err) { /**
console.error(err) * (internal function) Invoked after wifi refresh command is complete
return _cb(err) *
} */
console.log('Wifi refreshed') async _refresh() {
_cb(null, { ssid : _ssid }) return new Promise((resolve, reject) => {
_cb = () => {} return child_process_1.exec(refresh, (err, stdout, stderr) => {
} if (err) {
_parseConfig (str) { return reject(err);
const networks = [] }
const lines = str.split('\n') log.info('Wifi refreshed');
let network = {} return resolve({ ssid: this._ssid });
for (let line of lines) { });
if (line.substring(0, 9) === 'network={') { });
network = {} }
network.raw = line _parseConfig(str) {
} else if (network.raw && line.indexOf('ssid=') !== -1) { const networks = [];
network.ssid = line.replace('ssid=', '').trim().replace(quoteRe, '') const lines = str.split('\n');
if (network.raw) { let network = {};
network.raw += '\n' + line for (let line of lines) {
} if (line.substring(0, 9) === 'network={') {
} else if (network.raw && line.substring(0, 1) === '}') { network = {};
network.raw += '\n' + line network.raw = line;
networks.push(network) }
network = {} else if (network.raw && line.indexOf('ssid=') !== -1) {
} else if (network.raw) { network.ssid = line.replace('ssid=', '').trim().replace(quoteRe, '');
network.raw += '\n' + line if (network.raw) {
} network.raw += '\n' + line;
} }
return networks }
} else if (network.raw && line.substring(0, 1) === '}') {
/** network.raw += '\n' + line;
* Create sanitized wpa_supplicant.conf stanza for networks.push(network);
* configuring wifi without storing plaintext passwords network = {};
* @example }
* network={ else if (network.raw) {
* ssid="YOUR_SSID" network.raw += '\n' + line;
* #psk="YOUR_PASSWORD" }
* psk=6a24edf1592aec4465271b7dcd204601b6e78df3186ce1a62a31f40ae9630702 }
* } return networks;
* }
* @param {string} ssid SSID of wifi network /**
* @param {string} pwd Plaintext passphrase of wifi network * Create sanitized wpa_supplicant.conf stanza for
* @param {function} callback Function called after psk hash is generated * configuring wifi without storing plaintext passwords
*/ * @example
createPSK (ssid, pwd, callback) { * network={
const cmd = `wpa_passphrase '${ssid.replace(/'/g, `'\\''`)}' '${pwd.replace(/'/g, `'\\''`)}' | grep "psk="` * ssid="YOUR_SSID"
let lines * #psk="YOUR_PASSWORD"
let hash * psk=6a24edf1592aec4465271b7dcd204601b6e78df3186ce1a62a31f40ae9630702
let plaintext * }
exec(cmd, (err, stdout, stderr) => { *
if (err) { * @param {string} ssid SSID of wifi network
return callback(err) * @param {string} pwd Plaintext passphrase of wifi network
} */
lines = stdout.replace('#psk=', '').split('psk=') createPSK(ssid, pwd) {
hash = lines[1] const cmd = `wpa_passphrase '${ssid.replace(/'/g, `'\\''`)}' '${pwd.replace(/'/g, `'\\''`)}' | grep "psk="`;
plaintext = lines[0] let lines;
callback(null, hash.trim(), plaintext.trim()) let hash;
}) let plaintext;
} return new Promise((resolve, reject) => {
/** return child_process_1.exec(cmd, (err, stdout, stderr) => {
* Function which initializes the processes for adding a wifi access point authentication if (err) {
* return reject(err);
* @param {string} ssid SSID of network to configure }
* @param {string} pwd Password of access point, plaintext to be masked lines = stdout.replace('#psk=', '').split('psk=');
* @param {string} hash Password/SSID of access point, securely hashed hash = lines[1];
* @param {function} callback Function invoked after process is complete, or fails plaintext = lines[0];
*/ return resolve({ hash: hash.trim(), plaintext: plaintext.trim() });
setNetwork (ssid, pwd, hash, callback) { });
let masked = pwd.split('').map(char => { return char !== '"' ? '*' : '"' }).join('') });
_entry = `network={\n\tssid="${ssid}"\n\t#psk=${masked}\n\tpsk=${hash}\n}\n` }
_cb = callback /**
_ssid = ssid * Function which initializes the processes for adding a wifi access point authentication
fs.readFile(filePath, 'utf8', this._readConfigCb.bind(this)) *
} * @param {string} ssid SSID of network to configure
/** * @param {string} pwd Password of access point, plaintext to be masked
* Executes command which gets the currently connected network * @param {string} hash Password/SSID of access point, securely hashed
* */
* @param {function} callback Function which is invoked after command is completed async setNetwork(ssid, pwd, hash) {
*/ let masked = pwd.split('').map(char => { return char !== '"' ? '*' : '"'; }).join('');
getNetwork (callback) { let data;
let output this._entry = `network={\n\tssid="${ssid}"\n\t#psk=${masked}\n\tpsk=${hash}\n}\n`;
exec(iwgetid, (err, stdout, stderr) => { this._ssid = ssid;
if (err) { try {
return callback(err) data = await this._readConfig();
} }
output = stdout.split('ESSID:')[1].replace(quoteRe, '').trim() catch (err) {
callback(null, output) 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
*
* @param {function} callback Function which is invoked after command is completed
*/
async getNetwork() {
let output;
return new Promise((resolve, reject) => {
return child_process_1.exec(iwgetid, (err, stdout, stderr) => {
if (err) {
return reject(err);
}
output = stdout.split('ESSID:')[1].replace(quoteRe, '').trim();
return resolve(output);
});
});
}
} }
exports.Wifi = Wifi;
module.exports = new Wifi() module.exports.Wifi = Wifi;
//# sourceMappingURL=index.js.map

1
lib/wifi/index.js.map Normal file

File diff suppressed because one or more lines are too long

1556
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,17 @@
{ {
"name": "intval3", "name": "intval3",
"version": "3.1.0", "version": "3.1.3",
"description": "Intervalometer for the Bolex", "description": "Intervalometer for the Bolex",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "./node_modules/.bin/qunit", "test": "./node_modules/.bin/qunit",
"docs": "sh docs.sh", "docs": "sh docs.sh",
"build": "./node_modules/.bin/tsc -p tsconfig.json" "compile": "./node_modules/.bin/tsc -p tsconfig.json",
"pretest": "",
"version": "",
"postversion": "git push && git push --tags",
"u": "npm run git -- -m \"update\"",
"git": "npm version patch --force"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -38,11 +43,13 @@
"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",
"@types/uuid": "^3.4.5", "@types/uuid": "^3.4.5",
"jsdoc-to-markdown": "^5.0.2", "jsdoc-to-markdown": "^5.0.2",
"pkg": "^4.4.0",
"qunit": "^2.9.3", "qunit": "^2.9.3",
"typescript": "^3.6.4" "typescript": "^3.6.4"
} }

291
src/ble/index.ts Normal file
View File

@ -0,0 +1,291 @@
'use strict'
/** @module ble */
/** Bluetooth Low Energy module */
import { inherits } from 'util'
import { networkInterfaces, homedir } from 'os'
import { readFileSync, existsSync, writeFileSync } from 'fs-extra'
const log = require('../log')('ble')
import { Wifi } from '../wifi'
const wifi = new Wifi()
const DEVICE_NAME : string = process.env.DEVICE_NAME || 'intval3'
const SERVICE_ID : string = process.env.SERVICE_ID || 'intval3_ble'
const CHAR_ID : string = process.env.CHAR_ID || 'intval3char'
const WIFI_ID : string = process.env.WIFI_ID || 'wifichar'
const NETWORK : any = networkInterfaces() //?type?
const MAC : string = getMac() || spoofMac()
//Give the device a unique device name, needs to be in env
process.env.BLENO_DEVICE_NAME += '_' + MAC
import bleno from 'bleno'
const { Characteristic } = bleno
let currentWifi : string = 'disconnected'
let currentAddr : string = null
let getState : Function
const chars : any[] = []
interface WifiInfo {
ssid : string
pwd : string
}
interface WifiResponse {
available : string[]
current : string
ip : string
}
function createChar(name : string, uuid : string, prop : string[], write : Function, read : Function) {
const characteristic : any = function () {
Characteristic.call(this, {
uuid,
properties: prop
})
}
inherits(characteristic, Characteristic)
if (prop.indexOf('read') !== -1) {
//data, offset, withoutResponse, callback
characteristic.prototype.onReadRequest = read
}
if (prop.indexOf('write') !== -1) {
characteristic.prototype.onWriteRequest = write
}
chars.push(new characteristic())
}
function createChars (onWrite : Function, onRead : Function) {
const permissions : string[] = ['read', 'write'];
createChar('intval3', CHAR_ID, permissions, onWrite, onRead)
createChar('wifi', WIFI_ID, permissions, onWifiWrite, onWifiRead)
}
async function onWifiWrite (data : any, offset : number) {
let result : any
let utf8 : string
let obj : WifiInfo = {} as WifiInfo
let ssid : string
let pwd : string
let psk : any
if (offset) {
log.warn(`Offset scenario`)
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG
return result
}
utf8 = data.toString('utf8')
obj = JSON.parse(utf8)
ssid = obj.ssid
pwd = obj.pwd
log.info(`connecting to AP`, { ssid : ssid })
try {
psk = await wifi.createPSK(ssid, pwd)
} catch (err) {
log.error('Error hashing wifi password', err)
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return result
}
try {
await wifi.setNetwork(ssid, psk.plaintext, psk.hash)
} catch (err) {
log.error('Error configuring wifi', err)
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
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
return callback(result)
}
wifiRes.available = list
wifiRes.current = currentWifi
wifiRes.ip = currentAddr
log.info('Discovered available APs', { found : list.length })
data = new Buffer(JSON.stringify(wifiRes))
return callback(result, data.slice(offset, data.length))
}
function getMac () {
const colonRe = new RegExp(':', 'g')
if (NETWORK && NETWORK.wlan0 && NETWORK.wlan0[0] && NETWORK.wlan0[0].mac) {
return NETWORK.wlan0[0].mac.replace(colonRe, '')
}
return undefined
}
function spoofMac () {
const fs = require('fs')
const FSPATH = require.resolve('uuid')
const IDFILE = homedir() + '/.intval3id'
let uuid
let UUIDPATH
let TMP
let MACTMP
let dashRe
delete require.cache[FSPATH]
if (existsSync(IDFILE)) {
return readFileSync(IDFILE, 'utf8')
}
uuid = require('uuid').v4
UUIDPATH = require.resolve('uuid')
delete require.cache[UUIDPATH]
TMP = uuid()
MACTMP = TMP.replace(dashRe, '').substring(0, 12)
dashRe = new RegExp('-', 'g')
writeFileSync(IDFILE, MACTMP, 'utf8')
return MACTMP
}
function getIp () {
let addr = null
let ipv4
const ifaces = networkInterfaces()
if (ifaces && ifaces.wlan0) {
ipv4 = ifaces.wlan0.filter(iface => {
if (iface.family === 'IPv4') {
return iface
}
})
if (ipv4.length === 1) {
addr = ipv4[0].address
}
}
return addr
}
function capitalize (str : string) {
return str[0].toUpperCase() + str.slice(1)
}
type functionKeys = "_onRead" | "_onWrite";
/** Class representing the bluetooth interface */
class BLE {
listeners : any = {}
/**
* Establishes Bluetooth Low Energy services, accessible to process through this class
*
* @constructor
*/
constructor (bleGetState : Function) {
log.info('Starting bluetooth service')
getState = bleGetState
bleno.on('stateChange', state => {
log.info('stateChange', { state : state })
if (state === 'poweredOn') {
log.info('Starting advertising', { DEVICE_NAME, DEVICE_ID : process.env.BLENO_DEVICE_NAME })
bleno.startAdvertising(DEVICE_NAME, [CHAR_ID])
} else {
bleno.stopAdvertising()
}
})
bleno.on('advertisingStart', err => {
log.info('advertisingStart', { res : (err ? 'error ' + err : 'success') })
createChars(this._onWrite.bind(this), this._onRead.bind(this))
if (!err) {
bleno.setServices([
new bleno.PrimaryService({
uuid : SERVICE_ID, //hardcoded across panels
characteristics : chars
})
])
}
})
bleno.on('accept', clientAddress => {
log.info('accept', { clientAddress : clientAddress })
})
bleno.on('disconnect', clientAddress => {
log.info('disconnect', { clientAddress : clientAddress })
})
this._refreshWifi()
}
private async _refreshWifi () {
let ssid : string
try {
ssid = await wifi.getNetwork() as string
} catch (err) {
return log.error('wifi.getNetwork', err)
}
currentWifi = ssid
currentAddr = getIp()
log.info('wifi.getNetwork', {ssid : ssid, ip : currentAddr })
}
private _onWrite (data : any, offset : number, withoutResponse : Function, callback : Function) {
let result : any = {}
let utf8 : string
let obj : any
if (offset) {
log.warn(`Offset scenario`)
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG
return callback(result)
}
utf8 = data.toString('utf8')
obj = JSON.parse(utf8)
result = bleno.Characteristic.RESULT_SUCCESS
if (obj.type && this.listeners[obj.type]) {
return this.listeners[obj.type](obj, () => {
callback(result)
})
} else {
return callback(result)
}
}
private _onRead (offset : number, callback : Function) {
const result = bleno.Characteristic.RESULT_SUCCESS
const state = getState()
const data = new Buffer(JSON.stringify( state ))
callback(result, data.slice(offset, data.length))
}
/**
* Binds functions to events that are triggered by BLE messages
*
* @param {string} eventName Name of the event to to bind
* @param {function} callback Invoked when the event is triggered
*/
on (eventName : string, callback : Function) {
this.listeners[eventName] = callback
}
}
module.exports = BLE

247
src/wifi/index.ts Normal file
View File

@ -0,0 +1,247 @@
'use strict'
const networkPattern : RegExp = /network[\s\S]*?=[\s\S]*?{([\s\S]*?)}/gi
const quoteRe : RegExp = new RegExp('"', 'g')
const filePath : string = '/etc/wpa_supplicant/wpa_supplicant.conf'
const reconfigure : string = '/sbin/wpa_cli reconfigure'
const refresh : string = 'ip link set wlan0 down && ip link set wlan0 up'
const iwlist : string = '/sbin/iwlist wlan0 scanning | grep "ESSID:"'
const iwgetid : string = '/sbin/iwgetid'
const log : any = require('../log')('wifi')
import { exec } from 'child_process'
import { readFile, writeFile } from 'fs-extra'
import { reject } from 'q'
interface Network {
raw : string
ssid : string
}
/** Class representing the wifi features */
export class Wifi {
private _ssid : string = null
private _entry : string = null
constructor () {
}
/**
* List available wifi access points
*
* @param {function} callback Function which gets invoked after list is returned
*/
public async list () {
return new Promise ((resolve : Function, reject : Function) => {
return exec(iwlist, (err, stdout, stderr) => {
if (err) {
log.error('list', err)
return reject(err)
}
const limit : number = 20;
const lines : string[] = stdout.split('\n')
let output : string[] = []
let line : string
let i = 0
for (let l of lines) {
line = l.replace('ESSID:', '').trim()
if (line !== '""' && i < limit) {
line = line.replace(quoteRe, '')
output.push(line)
}
i++
}
output = output.filter(ap => {
if (ap !== '') return ap
})
return resolve(output)
})
})
}
/**
* (internal function) Invoked after config file is read,
* then invokes file write on the config file
*
*/
async _readConfig () {
let data : string
let parsed : Network[]
let current : Network
try {
data = await readFile(filePath, 'utf8')
} catch (err) {
log.error('_readConfig', err)
throw err
}
parsed = this._parseConfig(data)
current = parsed.find((network : Network) => {
return network.ssid === this._ssid
})
if (typeof current !== 'undefined') {
data = data.replace(current.raw, this._entry)
} else {
data += '\n\n' + this._entry
}
this._entry = null
return data
}
/**
* (internal function) Invoked after config file is written,
* then executes reconfiguration command
*
*/
private async _writeConfig (data : string) {
try {
await writeFile(filePath, data, 'utf8')
} catch (err) {
log.error('_readConfigCb', err)
throw err
}
}
/**
* (internal function) Invoked after reconfiguration command is complete
*
*/
private async _reconfigure () {
return new Promise((resolve : Function, reject : Function) => {
return exec(reconfigure, (err : Error, stdout : string, stderr : string) => {
if (err) {
return reject(err)
}
log.info('Wifi reconfigured')
return resolve(true)
})
})
}
/**
* (internal function) Invoked after wifi refresh command is complete
*
*/
private async _refresh () {
return new Promise((resolve : Function, reject : Function) => {
return exec(refresh, (err : Error, stdout : string, stderr : string) => {
if (err) {
return reject(err)
}
log.info('Wifi refreshed')
return resolve({ ssid : this._ssid });
})
})
}
private _parseConfig (str : string) : Network[] {
const networks : Network[] = []
const lines = str.split('\n')
let network : Network = {} as Network
for (let line of lines) {
if (line.substring(0, 9) === 'network={') {
network = {} as Network
network.raw = line
} else if (network.raw && line.indexOf('ssid=') !== -1) {
network.ssid = line.replace('ssid=', '').trim().replace(quoteRe, '')
if (network.raw) {
network.raw += '\n' + line
}
} else if (network.raw && line.substring(0, 1) === '}') {
network.raw += '\n' + line
networks.push(network)
network = {} as Network
} else if (network.raw) {
network.raw += '\n' + line
}
}
return networks
}
/**
* Create sanitized wpa_supplicant.conf stanza for
* configuring wifi without storing plaintext passwords
* @example
* network={
* ssid="YOUR_SSID"
* #psk="YOUR_PASSWORD"
* psk=6a24edf1592aec4465271b7dcd204601b6e78df3186ce1a62a31f40ae9630702
* }
*
* @param {string} ssid SSID of wifi network
* @param {string} pwd Plaintext passphrase of wifi network
*/
createPSK (ssid : string, pwd : string) {
const cmd : string = `wpa_passphrase '${ssid.replace(/'/g, `'\\''`)}' '${pwd.replace(/'/g, `'\\''`)}' | grep "psk="`
let lines : string[]
let hash : string
let plaintext : string
return new Promise ((resolve : Function, reject : Function) => {
return exec(cmd, (err, stdout, stderr) => {
if (err) {
return reject(err)
}
lines = stdout.replace('#psk=', '').split('psk=')
hash = lines[1]
plaintext = lines[0]
return resolve({ hash : hash.trim(), plaintext : plaintext.trim()})
})
})
}
/**
* Function which initializes the processes for adding a wifi access point authentication
*
* @param {string} ssid SSID of network to configure
* @param {string} pwd Password of access point, plaintext to be masked
* @param {string} hash Password/SSID of access point, securely hashed
*/
async setNetwork (ssid : string, pwd : string, hash : string) {
let masked : string = pwd.split('').map(char => { return char !== '"' ? '*' : '"' }).join('')
let data : string
this._entry = `network={\n\tssid="${ssid}"\n\t#psk=${masked}\n\tpsk=${hash}\n}\n`
this._ssid = ssid
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
*
* @param {function} callback Function which is invoked after command is completed
*/
public async getNetwork () {
let output : string
return new Promise((resolve : Function, reject : Function) => {
return exec(iwgetid, (err : Error, stdout : string, stderr : string) => {
if (err) {
return reject(err)
}
output = stdout.split('ESSID:')[1].replace(quoteRe, '').trim()
return resolve(output)
})
})
}
}
module.exports.Wifi = Wifi

View File

@ -12,8 +12,11 @@
"outDir": "./lib/", "outDir": "./lib/",
"rootDir" : "./src/", "rootDir" : "./src/",
"paths" : { "paths" : {
"log" : ["./lib/log"], "log" : [ "./lib/log" ],
"delay" : [ "./lib/delay"] "delay" : [ "./lib/delay" ],
"intval" : [ "./lib/intval" ],
"ble" : [ "./lib/ble" ],
"wifi" : [ "./lib/wifi" ]
} }
}, },
"exclude" : [ "exclude" : [