From 0fa69c7aaaca44ea83c4019dab342396e742a581 Mon Sep 17 00:00:00 2001 From: Thibault Friedrich Date: Fri, 3 Jul 2020 07:51:35 -0400 Subject: [PATCH] fix(macos/scan): parse networks when shifted A line was shifted in airport output (cf scan-shifted.log) and it was breaking parser for this line. It is now fixed --- .travis.yml | 3 +- package.json | 2 +- src/mac-scan.js | 38 ++------- src/macOS/scan/__logs__/scan-shifted.log | 7 ++ src/macOS/scan/__logs__/scan-space.log | 5 ++ src/macOS/scan/__test__/parser.spec.js | 97 ++++++++++++++++++++++- src/macOS/scan/command.js | 4 +- src/macOS/scan/parser.js | 99 +++++++++++++----------- src/utils/__test__/promiser.spec.js | 27 ++++++- src/utils/executer.js | 2 +- src/utils/promiser.js | 18 ++--- test/scan.js | 6 +- 12 files changed, 206 insertions(+), 102 deletions(-) create mode 100644 src/macOS/scan/__logs__/scan-shifted.log create mode 100644 src/macOS/scan/__logs__/scan-space.log diff --git a/.travis.yml b/.travis.yml index 53b413a..78a145f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,11 @@ # travis.yml language: node_js node_js: - - '8' + - '10' install: - npm install script: - npm run commitlint - npm run lint - npm run format + - npm run test diff --git a/package.json b/package.json index 93cadb9..81491ad 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "type": "commonjs", "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" }, "dependencies": { "command-line-args": "^3.0.1", diff --git a/src/mac-scan.js b/src/mac-scan.js index cedf731..8894e6e 100644 --- a/src/mac-scan.js +++ b/src/mac-scan.js @@ -1,35 +1,9 @@ -var execFile = require('child_process').execFile; -var env = require('./env'); -const getCommand = require('./macOS/scan/command.js'); +const execute = require('./utils/executer'); +const promiser = require('./utils/promiser'); +const command = require('./macOS/scan/command.js'); const parse = require('./macOS/scan/parser'); -function scanWifi(config, callback) { - const { cmd, args } = getCommand(config); +const scanWifi = config => + execute(command(config)).then(output => parse(output)); - execFile(cmd, args, { env }, function(err, scanResults) { - if (err) { - callback && callback(err); - } - - var resp = parse(scanResults); - callback && callback(null, resp); - }); -} - -module.exports = function(config) { - return function(callback) { - if (callback) { - scanWifi(config, callback); - } else { - return new Promise(function(resolve, reject) { - scanWifi(config, function(err, networks) { - if (err) { - reject(err); - } else { - resolve(networks); - } - }); - }); - } - }; -}; +module.exports = promiser(scanWifi); diff --git a/src/macOS/scan/__logs__/scan-shifted.log b/src/macOS/scan/__logs__/scan-shifted.log new file mode 100644 index 0000000..5a80f1d --- /dev/null +++ b/src/macOS/scan/__logs__/scan-shifted.log @@ -0,0 +1,7 @@ +$ /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s + SSID BSSID RSSI CHANNEL HT CC SECURITY (auth/unicast/group) + NERMNET 18:ff:7b:43:b5:26 -64 149 Y US WPA(PSK/TKIP,AES/TKIP) WPA2(PSK/TKIP,AES/TKIP) + Linksys02787-invité 12:23:03:18:9f:1c -33 11 Y US NONE + Linksys02787 10:23:03:1a:9f:1c -33 11 Y US WPA2(PSK/AES/AES) + NERMNET 18:ff:7b:43:b5:27 -53 1,+1 Y -- WPA2(PSK/TKIP,AES/TKIP) + \ No newline at end of file diff --git a/src/macOS/scan/__logs__/scan-space.log b/src/macOS/scan/__logs__/scan-space.log new file mode 100644 index 0000000..607b9f5 --- /dev/null +++ b/src/macOS/scan/__logs__/scan-space.log @@ -0,0 +1,5 @@ +$ /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s + SSID BSSID RSSI CHANNEL HT CC SECURITY (auth/unicast/group) + Linksys02787 10:23:03:1a:9f:1c -33 11 Y US WPA2(PSK/AES/AES) + Terminus 1 1e:27:e2:fa:c6:32 -26 4 Y -- WPA2(PSK/AES/AES) + Linksys02787_5GHz 10:23:03:1a:9f:1d -51 36 Y US WPA2(PSK/AES/AES) diff --git a/src/macOS/scan/__test__/parser.spec.js b/src/macOS/scan/__test__/parser.spec.js index 374dbf9..507eb19 100644 --- a/src/macOS/scan/__test__/parser.spec.js +++ b/src/macOS/scan/__test__/parser.spec.js @@ -5,7 +5,7 @@ const parse = require('../parser'); const log = filename => path.resolve(__dirname, `../__logs__/`, filename); describe('parse macOS scan output', () => { - it('should return right wifi networks', async () => { + it('should return wifi networks', async () => { const output = await unlog(log('scan-01.log')); const networks = parse(output); @@ -57,4 +57,99 @@ describe('parse macOS scan output', () => { } ]); }); + + it('should return wifi networks with shifted lines', async () => { + const output = await unlog(log('scan-shifted.log')); + + const networks = parse(output); + + expect(networks).toEqual([ + { + mac: '18:ff:7b:43:b5:26', + bssid: '18:ff:7b:43:b5:26', + ssid: 'NERMNET', + channel: 149, + frequency: 5745, + quality: -132, + signal_level: '-64', + security: 'WPA WPA2', + security_flags: ['(PSK/TKIP,AES/TKIP)', '(PSK/TKIP,AES/TKIP)'] + }, + { + mac: '12:23:03:18:9f:1c', + bssid: '12:23:03:18:9f:1c', + ssid: 'Linksys02787-invité', + channel: 11, + frequency: 2462, + quality: -116.5, + signal_level: '-33', + security: 'NONE', + security_flags: [] + }, + { + mac: '10:23:03:1a:9f:1c', + bssid: '10:23:03:1a:9f:1c', + ssid: 'Linksys02787', + channel: 11, + frequency: 2462, + quality: -116.5, + signal_level: '-33', + security: 'WPA2', + security_flags: ['(PSK/AES/AES)'] + }, + { + mac: '18:ff:7b:43:b5:27', + bssid: '18:ff:7b:43:b5:27', + ssid: 'NERMNET', + channel: 1, + frequency: 2412, + quality: -126.5, + signal_level: '-53', + security: 'WPA2', + security_flags: ['(PSK/TKIP,AES/TKIP)'] + } + ]); + }); + + it('should return wifi networks with space in ssid', async () => { + const output = await unlog(log('scan-space.log')); + + const networks = parse(output); + + expect(networks).toEqual([ + { + mac: '10:23:03:1a:9f:1c', + bssid: '10:23:03:1a:9f:1c', + ssid: 'Linksys02787', + channel: 11, + frequency: 2462, + quality: -116.5, + signal_level: '-33', + security: 'WPA2', + security_flags: ['(PSK/AES/AES)'] + }, + { + mac: '1e:27:e2:fa:c6:32', + bssid: '1e:27:e2:fa:c6:32', + ssid: 'Terminus 1', + channel: 4, + frequency: 2427, + quality: -113, + signal_level: '-26', + security: 'WPA2', + security_flags: ['(PSK/AES/AES)'] + }, + { + mac: '10:23:03:1a:9f:1d', + bssid: '10:23:03:1a:9f:1d', + ssid: 'Linksys02787_5GHz', + channel: 36, + frequency: 5180, + quality: -125.5, + signal_level: '-51', + security: 'WPA2', + security_flags: ['(PSK/AES/AES)'] + } + ]); + }); }); diff --git a/src/macOS/scan/command.js b/src/macOS/scan/command.js index eb89b53..03806f6 100644 --- a/src/macOS/scan/command.js +++ b/src/macOS/scan/command.js @@ -1,7 +1,7 @@ -const getCommand = () => ({ +const command = () => ({ cmd: '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport', args: ['-s'] }); -module.exports = getCommand; +module.exports = command; diff --git a/src/macOS/scan/parser.js b/src/macOS/scan/parser.js index 2cc5ff7..edcf0b8 100644 --- a/src/macOS/scan/parser.js +++ b/src/macOS/scan/parser.js @@ -1,55 +1,60 @@ var networkUtils = require('../../network-utils.js'); -const terms = { - BSSID: 'BSSID', - RSSI: 'RSSI', - CHANNEL: 'CHANNEL', - HT: 'HT', - SECURITY: 'SECURITY', - CC: 'CC' +const isNotEmpty = line => line.trim() !== ''; + +const parseSecurity = security => { + const securities = + security === 'NONE' + ? [{ protocole: 'NONE', flag: '' }] + : security + .split(' ') + .map(s => s.match(/(.*)\((.*)\)/)) + .filter(Boolean) + .map(([, protocole, flag]) => ({ + protocole, + flag + })); + + return { + security: securities.map(s => s.protocole).join(' '), + security_flags: securities.filter(s => s.flag).map(s => `(${s.flag})`) + }; }; const parse = stdout => { - var lines = stdout.split('\n'); - var colMac = lines[0].indexOf(terms.BSSID); - var colRssi = lines[0].indexOf(terms.RSSI); - var colChannel = lines[0].indexOf(terms.CHANNEL); - var colHt = lines[0].indexOf(terms.HT); - var colSec = lines[0].indexOf(terms.SECURITY); - //var colCC = lines[0].indexOf(terms.CC); - - var wifis = []; - for (var i = 1, l = lines.length; i < l; i++) { - var bssid = lines[i].substr(colMac, colRssi - colMac).trim(); - var securityFlags = lines[i].substr(colSec).trim(); - var security = 'none'; - if (securityFlags != 'NONE') { - security = securityFlags.replace(/\(.*?\)/g, ''); - securityFlags = securityFlags.match(/\((.*?)\)/g); - } else { - security = 'none'; - securityFlags = []; - } - wifis.push({ - mac: bssid, // for retrocompatibility - bssid: bssid, - ssid: lines[i].substr(0, colMac).trim(), - channel: parseInt(lines[i].substr(colChannel, colHt - colChannel)), - frequency: parseInt( - networkUtils.frequencyFromChannel( - lines[i].substr(colChannel, colHt - colChannel).trim() - ) - ), - signal_level: lines[i].substr(colRssi, colChannel - colRssi).trim(), - quality: networkUtils.dBFromQuality( - lines[i].substr(colRssi, colChannel - colRssi).trim() - ), - security: security, - security_flags: securityFlags - }); - } - wifis.pop(); - return wifis; + const lines = stdout.split('\n'); + + const [, ...otherLines] = lines; + + const networks = otherLines + .filter(isNotEmpty) + .map(line => line.trim()) + .map(line => { + const match = line.match( + /(.*)\s+([a-zA-Z0-9]{2}:[a-zA-Z0-9]{2}:[a-zA-Z0-9]{2}:[a-zA-Z0-9]{2}:[a-zA-Z0-9]{2}:[a-zA-Z0-9]{2})\s+(-[0-9]+)\s+([0-9]+).*\s+([A-Z]+)\s+([a-zA-Z-]+)\s+([A-Z0-9(,)\s/]+)/ + ); + + if (match) { + // eslint-disable-next-line no-unused-vars + const [, ssid, bssid, rssi, channel, ht, countryCode, security] = match; + + return { + mac: bssid, // for retrocompatibility + bssid: bssid, + ssid, + channel: parseInt(channel), + frequency: parseInt(networkUtils.frequencyFromChannel(channel)), + signal_level: rssi, + quality: networkUtils.dBFromQuality(rssi), + ...parseSecurity(security) + }; + } + + return false; + }) + .filter(Boolean); + + return networks; }; module.exports = parse; diff --git a/src/utils/__test__/promiser.spec.js b/src/utils/__test__/promiser.spec.js index f1393bf..e41ffa3 100644 --- a/src/utils/__test__/promiser.spec.js +++ b/src/utils/__test__/promiser.spec.js @@ -4,23 +4,42 @@ const config = { foo: 'foo' }; describe('promiser', () => { it('should execute function without error and return promise if no callback provided', async () => { - const func = jest.fn((config, callback) => callback(null, 'bar')); + const func = jest.fn(() => Promise.resolve('bar')); const result = await promiser(func)(config)(); - expect(func).toHaveBeenCalledWith(config, expect.anything()); + expect(func).toHaveBeenCalledWith(config); expect(result).toEqual('bar'); }); it('should execute function with error and return promise if no callback provided', async () => { expect.assertions(2); - const func = jest.fn((config, callback) => callback('error')); + const func = jest.fn(() => Promise.reject('error')); try { await promiser(func)(config)(); } catch (error) { - expect(func).toHaveBeenCalledWith(config, expect.anything()); + expect(func).toHaveBeenCalledWith(config); expect(error).toEqual('error'); } }); + + it('should execute function without error and call callback', done => { + const func = jest.fn(() => Promise.resolve('bar')); + + promiser(func)(config)((error, result) => { + expect(func).toHaveBeenCalledWith(config); + expect(result).toEqual('bar'); + done(); + }); + }); + + it('should execute function with error and return promise if no callback provided', done => { + const func = jest.fn(() => Promise.reject('error')); + promiser(func)(config)(error => { + expect(func).toHaveBeenCalledWith(config); + expect(error).toEqual('error'); + done(); + }); + }); }); diff --git a/src/utils/executer.js b/src/utils/executer.js index 1b05fe3..ebc9396 100644 --- a/src/utils/executer.js +++ b/src/utils/executer.js @@ -1,7 +1,7 @@ const { execFile } = require('child_process'); const env = require('../env'); -module.exports = (cmd, args) => +module.exports = ({ cmd, args }) => new Promise((resolve, reject) => { execFile(cmd, args, { env }, (error, output) => { if (error) { diff --git a/src/utils/promiser.js b/src/utils/promiser.js index 8a29c1c..e33fe4d 100644 --- a/src/utils/promiser.js +++ b/src/utils/promiser.js @@ -1,15 +1,13 @@ module.exports = func => config => callback => { if (typeof callback === 'function') { - func(config, callback); - } else { - return new Promise((resolve, reject) => { - func(config, (err, result) => { - if (err) { - reject(err); - } else { - resolve(result); - } + func(config) + .then(response => { + callback(null, response); + }) + .catch(error => { + callback(error); }); - }); + } else { + return func(config); } }; diff --git a/test/scan.js b/test/scan.js index c328f9b..d881b2b 100644 --- a/test/scan.js +++ b/test/scan.js @@ -1,10 +1,10 @@ const execute = require('../src/utils/executer'); -const getCommand = require('../src/macOS/scan/command'); +const command = require('../src/macOS/scan/command'); -const { cmd, args } = getCommand(); +const { cmd, args } = command(); console.log(`$ ${cmd} ${args.join(' ')}`); -execute(cmd, args) +execute({ cmd, args }) .then(output => console.log(output)) .catch(error => console.error(error));