Skip to content

Commit b0a0b87

Browse files
committed
Implement Plasma Dash
1 parent 0761526 commit b0a0b87

7 files changed

+206
-4
lines changed

.babelrc

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"whitelist": [
3+
"asyncToGenerator",
4+
"es6.classes",
5+
"es6.destructuring",
6+
"es6.modules",
7+
"es6.parameters",
8+
"es6.properties.shorthand",
9+
"es6.spread",
10+
"es7.asyncFunctions",
11+
"es7.classProperties",
12+
"es7.trailingFunctionCommas",
13+
"flow",
14+
"runtime",
15+
"strict",
16+
]
17+
}

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Plasma Dash is designed to run on a Raspberry Pi. Specifically, it runs on [Rasp
88

99
Plasma Dash's runs on Node 4 and up on OS X and Linux. It depends on [libpcap](http://www.tcpdump.org/):
1010

11-
```
11+
```sh
1212
# Ubuntu and Debian
1313
sudo apt-get install libpcap-dev
1414
# Fedora and CentOS
@@ -17,7 +17,7 @@ sudo yum install libpcap-devel
1717

1818
Install Plasma Dash in your project using npm:
1919

20-
```
20+
```sh
2121
npm install --save plasma-dash
2222
```
2323

gulpfile.babel.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import gulp from 'gulp';
2+
import babel from 'gulp-babel';
3+
import changed from 'gulp-changed';
4+
import rimraf from 'rimraf';
5+
6+
const paths = {
7+
source: 'src/**/*.js',
8+
build: 'build',
9+
};
10+
11+
function build() {
12+
return gulp.src(paths.source)
13+
.pipe(changed(paths.build))
14+
.pipe(babel())
15+
.pipe(gulp.dest(paths.build));
16+
}
17+
18+
function watch(done) {
19+
gulp.watch(paths.source, build);
20+
done();
21+
}
22+
23+
gulp.task(build);
24+
gulp.task('watch', gulp.parallel(build, watch));
25+
gulp.task('clean', done => {
26+
rimraf(paths.build, done);
27+
});

package.json

+19-2
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,25 @@
33
"version": "0.0.1",
44
"description": "A small server that reacts to Amazon Dash buttons on your WiFi network",
55
"main": "build/PlasmaDash.js",
6+
"bin": {
7+
"plasma-dash": "build/cli.js"
8+
},
69
"engines": {
710
"node": ">=4"
811
},
912
"scripts": {
13+
"build": "gulp build",
14+
"watch": "gulp watch",
1015
"test": "jest"
1116
},
1217
"repository": {
1318
"type": "git",
1419
"url": "git+https://github.com/ide/plasma-dash.git"
1520
},
1621
"keywords": [
17-
"dash-button"
22+
"amazon",
23+
"dash",
24+
"button"
1825
],
1926
"author": "James Ide",
2027
"license": "MIT",
@@ -23,7 +30,17 @@
2330
},
2431
"homepage": "https://github.com/ide/plasma-dash#readme",
2532
"devDependencies": {
33+
"babel-core": "^5.8.25",
2634
"babel-eslint": "^4.1.3",
27-
"eslint": "^1.6.0"
35+
"eslint": "^1.6.0",
36+
"gulp": "gulpjs/gulp#4.0",
37+
"gulp-babel": "^5.2.1",
38+
"gulp-changed": "^1.3.0",
39+
"rimraf": "^2.4.3"
40+
},
41+
"dependencies": {
42+
"babel-runtime": "^5.8.25",
43+
"pcap": "ide/node_pcap#node-4",
44+
"yargs": "^3.26.0"
2845
}
2946
}

src/NetworkInterfaces.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import os from 'os';
2+
3+
export default class NetworkInterfaces {
4+
static getDefault() {
5+
let interfaces = os.networkInterfaces();
6+
let names = Object.keys(interfaces);
7+
for (let name of names) {
8+
if (interfaces[name].every(iface => !iface.internal)) {
9+
return name;
10+
}
11+
}
12+
return null;
13+
}
14+
}

src/PlasmaDash.js

+99
Original file line numberDiff line numberDiff line change
@@ -1 +1,100 @@
1+
import assert from 'assert';
2+
import pcap from 'pcap';
13

4+
import NetworkInterfaces from './NetworkInterfaces';
5+
6+
type Options = {
7+
networkInterface?: string,
8+
};
9+
10+
let pcapSession;
11+
12+
function getPcapSession(interfaceName: string) {
13+
if (!pcapSession) {
14+
pcapSession = pcap.createSession(interfaceName);
15+
} else {
16+
assert.equal(
17+
interfaceName, pcapSession.device_name,
18+
'The existing pcap session must be listening on the specified interface',
19+
);
20+
}
21+
return pcapSession;
22+
}
23+
24+
export default class PlasmaDash {
25+
constructor(macAddress: string, options?: Options = {}) {
26+
this._macAddress = macAddress;
27+
this._networkInterface = options.networkInterface ||
28+
NetworkInterfaces.getDefault();
29+
this._packetListener = this._handlePacket.bind(this);
30+
this._dashListeners = new Set();
31+
this._isResponding = false;
32+
}
33+
34+
addListener(listener): Subscription {
35+
if (!this._dashListeners.size) {
36+
let session = getPcapSession();
37+
session.addListener('packet', this._packetListener);
38+
}
39+
40+
// We run the listeners with Promise.all, which rejects early as soon as
41+
// any of its promises are rejected. Since we want to wait for all of the
42+
// listeners to finish we need to catch any errors they may throw.
43+
let guardedListener = this._createGuardedListener(listener);
44+
this._dashListeners.add(guardedListener);
45+
46+
return new Subscription(() => {
47+
this._dashListeners.remove(guardedListener);
48+
if (!this._dashListeners.size) {
49+
let session = getPcapSession();
50+
session.removeListener('packet', this._packetListener);
51+
if (!session.listenerCount('packet')) {
52+
session.close();
53+
}
54+
}
55+
});
56+
}
57+
58+
_createGuardedListener(listener) {
59+
return async function(...args) {
60+
try {
61+
return await listener(...args);
62+
} catch (error) {
63+
return error;
64+
}
65+
};
66+
}
67+
68+
async _handlePacket(rawPacket) {
69+
if (this._isResponding) {
70+
return;
71+
}
72+
73+
let packet = pcap.decode(rawPacket);
74+
75+
this._isResponding = true;
76+
try {
77+
// The listeners are guarded so this should never throw, but wrap it in
78+
// try-catch to be defensive
79+
await Promise.all(Array.from(this._dashListeners).map(
80+
listener => listener(packet)
81+
));
82+
} finally {
83+
this._isResponding = false;
84+
}
85+
}
86+
}
87+
88+
class Subscription {
89+
constructor(onRemove) {
90+
this._remove = onRemove;
91+
}
92+
93+
remove() {
94+
if (this._remove) {
95+
return;
96+
}
97+
this._remove();
98+
delete this._remove;
99+
}
100+
}

src/cli.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pcap from 'pcap';
2+
import yargs from 'yargs';
3+
4+
import NetworkInterfaces from './NetworkInterfaces';
5+
6+
if (require.main === module) {
7+
let argv = yargs
8+
.usage('Usage: $0 <command> [options]')
9+
.command('scan', 'Scan for ARP probes')
10+
.example(
11+
'$0 scan -i wlan0',
12+
'Scan for ARP probes on the given network interface'
13+
)
14+
.alias('i', 'interface')
15+
.nargs('i', 1)
16+
.default('i', NetworkInterfaces.getDefault())
17+
.describe('i', 'The network interface on which to listen')
18+
.help('h')
19+
.alias('h', 'help')
20+
.argv;
21+
let commands = new Set(argv._);
22+
if (commands.has('scan')) {
23+
let pcapSession = pcap.createSession(argv.interface, 'arp');
24+
pcapSession.addListener('packet', rawPacket => {
25+
console.log(rawPacket);
26+
});
27+
}
28+
}

0 commit comments

Comments
 (0)