Skip to content

Commit

Permalink
fix(macos/scan): parse networks when shifted
Browse files Browse the repository at this point in the history
A line was shifted in airport output (cf scan-shifted.log) and it was breaking parser for this line. It is now fixed
  • Loading branch information
friedrith committed Jul 3, 2020
1 parent 1066263 commit 0fa69c7
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 102 deletions.
3 changes: 2 additions & 1 deletion .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
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -31,7 +31,7 @@
},
"type": "commonjs",
"engines": {
"node": ">=8.0.0"
"node": ">=10.0.0"
},
"dependencies": {
"command-line-args": "^3.0.1",
Expand Down
38 changes: 6 additions & 32 deletions 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);
7 changes: 7 additions & 0 deletions 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)

5 changes: 5 additions & 0 deletions 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)
97 changes: 96 additions & 1 deletion src/macOS/scan/__test__/parser.spec.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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)']
}
]);
});
});
4 changes: 2 additions & 2 deletions 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;
99 changes: 52 additions & 47 deletions 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;
27 changes: 23 additions & 4 deletions src/utils/__test__/promiser.spec.js
Expand Up @@ -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();
});
});
});
2 changes: 1 addition & 1 deletion 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) {
Expand Down
18 changes: 8 additions & 10 deletions 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);
}
};
6 changes: 3 additions & 3 deletions 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));

0 comments on commit 0fa69c7

Please sign in to comment.