commit
6e05a0b45c
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.*
|
@ -0,0 +1,17 @@
|
|||||||
|
FROM node:16
|
||||||
|
|
||||||
|
# Create app directory
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Install app dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get -y install \
|
||||||
|
tcpdump
|
||||||
|
|
||||||
|
# Bundle app source
|
||||||
|
COPY ./src/ .
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
@ -0,0 +1,266 @@
|
|||||||
|
{
|
||||||
|
"name": "rfmon-to-influx",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "rfmon-to-influx",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@influxdata/influxdb-client": "^1.20.0",
|
||||||
|
"@influxdata/influxdb-client-apis": "^1.20.0",
|
||||||
|
"log4js": "^6.3.0",
|
||||||
|
"luxon": "^2.1.1",
|
||||||
|
"string-argv": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@influxdata/influxdb-client": {
|
||||||
|
"version": "1.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.20.0.tgz",
|
||||||
|
"integrity": "sha512-jaKSI63hmQ5VSkJrFJkYIXaKlhoF+mGd4HmOf7v/X7pmEi69ReHp922Wyx6/OeCrpndRMbsadk+XmGNdd43cFw=="
|
||||||
|
},
|
||||||
|
"node_modules/@influxdata/influxdb-client-apis": {
|
||||||
|
"version": "1.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.20.0.tgz",
|
||||||
|
"integrity": "sha512-KMTmXH4rbpS+NWGpqDjxcKTyan2rbiT2IM5AdRElKhH2sHbH96xwLgziaxeC+OCJLeNAdehJgae3I8WiZjbwdg==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@influxdata/influxdb-client": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/date-format": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/flatted": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA=="
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6 <7 || >=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
||||||
|
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
|
||||||
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||||
|
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log4js": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==",
|
||||||
|
"dependencies": {
|
||||||
|
"date-format": "^3.0.0",
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"flatted": "^2.0.1",
|
||||||
|
"rfdc": "^1.1.4",
|
||||||
|
"streamroller": "^2.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/luxon": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-6VQVNw7+kQu3hL1ZH5GyOhnk8uZm21xS7XJ/6vDZaFNcb62dpFDKcH8TI5NkoZOdMRxr7af7aYGrJlE/Wv0i1w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
|
},
|
||||||
|
"node_modules/rfdc": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
|
||||||
|
},
|
||||||
|
"node_modules/streamroller": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"date-format": "^2.1.0",
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"fs-extra": "^8.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/streamroller/node_modules/date-format": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-argv": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@influxdata/influxdb-client": {
|
||||||
|
"version": "1.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.20.0.tgz",
|
||||||
|
"integrity": "sha512-jaKSI63hmQ5VSkJrFJkYIXaKlhoF+mGd4HmOf7v/X7pmEi69ReHp922Wyx6/OeCrpndRMbsadk+XmGNdd43cFw=="
|
||||||
|
},
|
||||||
|
"@influxdata/influxdb-client-apis": {
|
||||||
|
"version": "1.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.20.0.tgz",
|
||||||
|
"integrity": "sha512-KMTmXH4rbpS+NWGpqDjxcKTyan2rbiT2IM5AdRElKhH2sHbH96xwLgziaxeC+OCJLeNAdehJgae3I8WiZjbwdg==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
|
"date-format": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w=="
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flatted": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA=="
|
||||||
|
},
|
||||||
|
"fs-extra": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graceful-fs": {
|
||||||
|
"version": "4.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
|
||||||
|
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg=="
|
||||||
|
},
|
||||||
|
"jsonfile": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||||
|
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log4js": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==",
|
||||||
|
"requires": {
|
||||||
|
"date-format": "^3.0.0",
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"flatted": "^2.0.1",
|
||||||
|
"rfdc": "^1.1.4",
|
||||||
|
"streamroller": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"luxon": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-6VQVNw7+kQu3hL1ZH5GyOhnk8uZm21xS7XJ/6vDZaFNcb62dpFDKcH8TI5NkoZOdMRxr7af7aYGrJlE/Wv0i1w=="
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
|
},
|
||||||
|
"rfdc": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
|
||||||
|
},
|
||||||
|
"streamroller": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==",
|
||||||
|
"requires": {
|
||||||
|
"date-format": "^2.1.0",
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"fs-extra": "^8.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"date-format": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"string-argv": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg=="
|
||||||
|
},
|
||||||
|
"universalify": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "rfmon-to-influx",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Writing (mostly meta-) data received in Wireless-Monitor-Mode into an InfluxDB",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node main.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.ruekov.eu/Ruakij/rfmon-to-influx"
|
||||||
|
},
|
||||||
|
"author": "Ruakij",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@influxdata/influxdb-client": "^1.20.0",
|
||||||
|
"@influxdata/influxdb-client-apis": "^1.20.0",
|
||||||
|
"log4js": "^6.3.0",
|
||||||
|
"luxon": "^2.1.1",
|
||||||
|
"string-argv": "^0.3.1"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
const PacketType = {
|
||||||
|
Beacon: 'Beacon',
|
||||||
|
ProbeRequest: 'ProbeRequest',
|
||||||
|
ProbeResponse: 'ProbeResponse',
|
||||||
|
Data: 'Data',
|
||||||
|
RequestToSend: 'RequestToSend',
|
||||||
|
ClearToSend: 'ClearToSend',
|
||||||
|
Acknowledgment: 'Acknowledgment',
|
||||||
|
BlockAcknowledgment: 'BlockAcknowledgment',
|
||||||
|
NoData: 'NoData',
|
||||||
|
Authentication: 'Authentication',
|
||||||
|
AssociationRequest: 'AssociationRequest',
|
||||||
|
AssociationResponse: 'AssociationResponse',
|
||||||
|
Disassociation: 'Disassociation',
|
||||||
|
Handshake: 'Handshake',
|
||||||
|
Unknown: 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlagType = {
|
||||||
|
MoreFragments: "MoreFragments",
|
||||||
|
Retry: "Retry",
|
||||||
|
PwrMgt: "PwrMgt",
|
||||||
|
MoreData: "MoreData",
|
||||||
|
Protected: "Protected",
|
||||||
|
Order: "Order"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Packet{
|
||||||
|
timestampMicros;
|
||||||
|
|
||||||
|
flags = {};
|
||||||
|
|
||||||
|
srcMac;
|
||||||
|
dstMac;
|
||||||
|
bssid;
|
||||||
|
|
||||||
|
signal;
|
||||||
|
frequency;
|
||||||
|
dataRate;
|
||||||
|
|
||||||
|
durationMicros;
|
||||||
|
|
||||||
|
payloadData;
|
||||||
|
get payloadSize(){
|
||||||
|
return this.payloadData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
packetType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions of Packet
|
||||||
|
class PacketWithSSID extends Packet{
|
||||||
|
ssid;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BeaconPacket extends PacketWithSSID{}
|
||||||
|
class ProbeRequestPacket extends PacketWithSSID{}
|
||||||
|
class ProbeResponsePacket extends PacketWithSSID{}
|
||||||
|
|
||||||
|
const AuthenticationType = {
|
||||||
|
OpenSystem_1: 'OpenSystem_1',
|
||||||
|
OpenSystem_2: 'OpenSystem_2',
|
||||||
|
Unknown: 'Unknown',
|
||||||
|
}
|
||||||
|
class AuthenticationPacket extends Packet{
|
||||||
|
authenticationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssociationRequestPacket extends PacketWithSSID{}
|
||||||
|
class AssociationResponsePacket extends Packet{
|
||||||
|
associationIsSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DisassociationPacket extends Packet{
|
||||||
|
disassociationReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const HandshakeStage = {
|
||||||
|
1: '1',
|
||||||
|
2: '2',
|
||||||
|
3: '3',
|
||||||
|
4: '4'
|
||||||
|
}
|
||||||
|
class HandshakePacket extends Packet{
|
||||||
|
handshakeStage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
PacketType,
|
||||||
|
FlagType,
|
||||||
|
Packet,
|
||||||
|
PacketWithSSID,
|
||||||
|
BeaconPacket,
|
||||||
|
ProbeRequestPacket,
|
||||||
|
ProbeResponsePacket,
|
||||||
|
AuthenticationType,
|
||||||
|
AuthenticationPacket,
|
||||||
|
AssociationRequestPacket,
|
||||||
|
AssociationResponsePacket,
|
||||||
|
DisassociationPacket,
|
||||||
|
HandshakeStage,
|
||||||
|
HandshakePacket,
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
function requireEnvVars(requiredEnv){
|
||||||
|
// Ensure required ENV vars are set
|
||||||
|
let unsetEnv = requiredEnv.filter((env) => !(typeof process.env[env] !== 'undefined'));
|
||||||
|
|
||||||
|
if (unsetEnv.length > 0) {
|
||||||
|
return "Required ENV variables are not set: [" + unsetEnv.join(', ') + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
requireEnvVars
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
const logger = require("./logger.js")("exec");
|
||||||
|
|
||||||
|
const { spawn } = require("child_process");
|
||||||
|
const { parseArgsStringToArgv } = require('string-argv');
|
||||||
|
|
||||||
|
|
||||||
|
function exec(cmd, options){
|
||||||
|
const [bin, ...args] = parseArgsStringToArgv(cmd);
|
||||||
|
|
||||||
|
logger.addContext("binary", "bin");
|
||||||
|
logger.debug(`Spawn process '${cmd}'`);
|
||||||
|
return spawn(bin, args, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
exec
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
// From https://stackoverflow.com/a/34356351
|
||||||
|
|
||||||
|
// Convert a hex string to a byte array
|
||||||
|
function hexToBytes(hex) {
|
||||||
|
for (var bytes = [], c = 0; c < hex.length; c += 2)
|
||||||
|
bytes.push(parseInt(hex.substr(c, 2), 16));
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a byte array to a hex string
|
||||||
|
function bytesToHex(bytes) {
|
||||||
|
for (var hex = [], i = 0; i < bytes.length; i++) {
|
||||||
|
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
|
||||||
|
hex.push((current >>> 4).toString(16));
|
||||||
|
hex.push((current & 0xF).toString(16));
|
||||||
|
}
|
||||||
|
return hex.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
hexToBytes,
|
||||||
|
bytesToHex
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
const logger = require.main.require("./helper/logger.js")("influx-checks");
|
||||||
|
|
||||||
|
const Os = require("os");
|
||||||
|
const { InfluxDB, Point } = require('@influxdata/influxdb-client')
|
||||||
|
const Influx = require('@influxdata/influxdb-client-apis');
|
||||||
|
|
||||||
|
|
||||||
|
function checkHealth(influxDb){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
new Influx.HealthAPI(influxDb) // Check influx health
|
||||||
|
.getHealth()
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error("Could not communicate with Influx:");
|
||||||
|
logger.error(`Error [${err.code}]:`, err.message);
|
||||||
|
reject();
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
logger.debug("Server healthy.", "Version: ", res.version);
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkBucket(influxDb, options){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
new Influx.BucketsAPI(influxDb).getBuckets(options)
|
||||||
|
.catch((err) => { // Weirdly the influx-Api returns 404 for searches of non-existing buckets
|
||||||
|
logger.error("Could not get bucket:");
|
||||||
|
logger.error(`Error [${err.code}]:`, err.message);
|
||||||
|
reject();
|
||||||
|
}).then((res) => { // But an empty list when the bucket exists, but token does not have permission to get details
|
||||||
|
logger.debug("Bucket found");
|
||||||
|
resolve(res);
|
||||||
|
// Now we know the bucket exists and we have some kind of permission.. but we still dont know if we are able to write to it..
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkWriteApi(influxDb, options){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const writeApi = influxDb.getWriteApi(options.org, options.bucket); // Get WriteAPI
|
||||||
|
writeApi.writePoint(new Point("worker_connectionTest").tag("hostname", Os.hostname())) // Write point
|
||||||
|
writeApi.close()
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error("Could not get writeApi:");
|
||||||
|
logger.error(`Error [${err.code}]:`, err.message);
|
||||||
|
reject();
|
||||||
|
}).then((res) => {
|
||||||
|
logger.debug("Writing ok");
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
checkHealth,
|
||||||
|
checkBucket,
|
||||||
|
checkWriteApi,
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
const log4js = require("log4js");
|
||||||
|
|
||||||
|
|
||||||
|
function setup(category = "unknown"){
|
||||||
|
const logger = log4js.getLogger(category);
|
||||||
|
logger.level = process.env.LOGLEVEL ?? "INFO";
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = setup;
|
@ -0,0 +1,46 @@
|
|||||||
|
// This file specifies functions to help a user with e.g. configuration-errors
|
||||||
|
|
||||||
|
function detectStreamData(stream, timeout = 5000){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let timeoutHandler;
|
||||||
|
if(timeout){
|
||||||
|
timeoutHandler = setTimeout(() => {
|
||||||
|
reject('timeout');
|
||||||
|
remListeners();
|
||||||
|
},
|
||||||
|
timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remListeners(){
|
||||||
|
stream.removeListener('error', errorHandler);
|
||||||
|
stream.removeListener('data', dataHandler);
|
||||||
|
if(timeoutHandler) clearTimeout(timeoutHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorHandler(err) {
|
||||||
|
remListeners();
|
||||||
|
}
|
||||||
|
function dataHandler(data) {
|
||||||
|
resolve(data);
|
||||||
|
remListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.on('error', errorHandler);
|
||||||
|
stream.on('data', dataHandler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectStreamsData(streams, timeout = 5000){
|
||||||
|
let promises = [];
|
||||||
|
streams.forEach((stream) => {
|
||||||
|
promises.push(detectStreamData(stream, timeout));
|
||||||
|
})
|
||||||
|
return promises;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
detectStreamData,
|
||||||
|
detectStreamsData,
|
||||||
|
};
|
@ -0,0 +1,43 @@
|
|||||||
|
const { HandshakeStage } = require.main.require('./dto/Packet.js');
|
||||||
|
|
||||||
|
function keyInfoFromRaw(keyInfoRaw) {
|
||||||
|
return {
|
||||||
|
"KeyDescriptorVersion": keyInfoRaw>>0 & 0b111,
|
||||||
|
"KeyType": keyInfoRaw>>3 & 0b1,
|
||||||
|
"KeyIndex": keyInfoRaw>>4 & 0b11,
|
||||||
|
"Install": keyInfoRaw>>6 & 0b1,
|
||||||
|
"KeyACK": keyInfoRaw>>7 & 0b1,
|
||||||
|
"KeyMIC": keyInfoRaw>>8 & 0b1,
|
||||||
|
"Secure": keyInfoRaw>>9 & 0b1,
|
||||||
|
"Error": keyInfoRaw>>10 & 0b1,
|
||||||
|
"Request": keyInfoRaw>>11 & 0b1,
|
||||||
|
"EncryptedKeyData": keyInfoRaw>>12 & 0b1,
|
||||||
|
"SMKMessage": keyInfoRaw>>13 & 0b1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const HANDSHAKE_STAGE_KEYINFO = {
|
||||||
|
"keys": ["Install", "KeyACK", "KeyMIC", "Secure"],
|
||||||
|
"0100": HandshakeStage[1],
|
||||||
|
"0010": HandshakeStage[2],
|
||||||
|
"1111": HandshakeStage[3],
|
||||||
|
"0011": HandshakeStage[4],
|
||||||
|
};
|
||||||
|
function handshakeStageFromKeyInfo(keyInfo){
|
||||||
|
|
||||||
|
// Extract compare-keys
|
||||||
|
let keyData = "";
|
||||||
|
for (const key of HANDSHAKE_STAGE_KEYINFO['keys']) {
|
||||||
|
keyData += keyInfo[key].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and return stage
|
||||||
|
return HANDSHAKE_STAGE_KEYINFO[keyData];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
keyInfoFromRaw,
|
||||||
|
handshakeStageFromKeyInfo,
|
||||||
|
};
|
@ -0,0 +1,131 @@
|
|||||||
|
"use strict";
|
||||||
|
const logFactory = require("./helper/logger.js");
|
||||||
|
const logger = logFactory("main");
|
||||||
|
|
||||||
|
const { requireEnvVars } = require("./helper/env.js");
|
||||||
|
const { exit } = require("process");
|
||||||
|
const { exec } = require("./helper/exec.js");
|
||||||
|
|
||||||
|
const { InfluxDB } = require('@influxdata/influxdb-client');
|
||||||
|
const InfluxChecks = require('./helper/influx-checks.js');
|
||||||
|
|
||||||
|
const { RegexBlockStream } = require("./streamHandler/RegexBlockStream.js");
|
||||||
|
const { PacketStreamFactory } = require("./streamHandler/PacketStreamFactory.js");
|
||||||
|
const { PacketInfluxPointFactory } = require("./streamHandler/PacketInfluxPointFactory.js");
|
||||||
|
const { InfluxPointWriter } = require("./streamHandler/InfluxPointWriter.js");
|
||||||
|
|
||||||
|
const userHelper = require("./helper/userHelper.js");
|
||||||
|
|
||||||
|
|
||||||
|
/// Setup ENVs
|
||||||
|
const env = process.env;
|
||||||
|
// Defaults
|
||||||
|
{
|
||||||
|
env.LOGLEVEL ??= "INFO";
|
||||||
|
env.WIFI_INTERFACE ??= "wlan0";
|
||||||
|
}
|
||||||
|
// Required vars
|
||||||
|
let errorMsg = requireEnvVars([
|
||||||
|
"INFLUX_URL", "INFLUX_TOKEN",
|
||||||
|
"INFLUX_ORG", "INFLUX_BUCKET"
|
||||||
|
]);
|
||||||
|
if(errorMsg){
|
||||||
|
logger.fatal(errorMsg);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function() {
|
||||||
|
logger.info("Setup Influx..");
|
||||||
|
const influxDb = new InfluxDB({url: env.INFLUX_URL, token: env.INFLUX_TOKEN});
|
||||||
|
|
||||||
|
await InfluxChecks.checkHealth(influxDb)
|
||||||
|
.then((res) => {return InfluxChecks.checkBucket(influxDb, {
|
||||||
|
org: env.INFLUX_ORG,
|
||||||
|
name: env.INFLUX_BUCKET
|
||||||
|
})})
|
||||||
|
.then((res) => {return InfluxChecks.checkWriteApi(influxDb, {
|
||||||
|
org: env.INFLUX_ORG,
|
||||||
|
bucket: env.INFLUX_BUCKET
|
||||||
|
})})
|
||||||
|
.catch((err) => {
|
||||||
|
if(err) {
|
||||||
|
logger.error("Error whilst checking influx:");
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
logger.fatal("Setup influx failed!");
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Influx ok");
|
||||||
|
|
||||||
|
logger.info("Starting tcpdump..");
|
||||||
|
const TCPDUMP_BASECMD = "tcpdump -vvv -e -n -X -s0 -i"
|
||||||
|
let cmd = `${TCPDUMP_BASECMD} ${env.WIFI_INTERFACE}`;
|
||||||
|
|
||||||
|
let proc = exec(cmd);
|
||||||
|
logger.debug("Creating & Attaching streams..");
|
||||||
|
let regexBlockStream = new RegexBlockStream(/^\d{2}:\d{2}:\d{2}.\d{6}.*(\n( {4,8}|\t\t?).*)+\n/gm);
|
||||||
|
let packetStreamFactory = new PacketStreamFactory();
|
||||||
|
let packetInfluxPointFactory = new PacketInfluxPointFactory();
|
||||||
|
let influxPointWriter = new InfluxPointWriter(influxDb, env.INFLUX_ORG, env.INFLUX_BUCKET);
|
||||||
|
proc.stdout
|
||||||
|
.setEncoding("utf8")
|
||||||
|
.pipe(regexBlockStream)
|
||||||
|
.pipe(packetStreamFactory)
|
||||||
|
.pipe(packetInfluxPointFactory)
|
||||||
|
.pipe(influxPointWriter);
|
||||||
|
|
||||||
|
logger.debug("Attaching error-logger..");
|
||||||
|
const loggerTcpdump = logFactory("tcpdump");
|
||||||
|
proc.stderr.setEncoding("utf8").on("data", (data) => {
|
||||||
|
if(!data.match(/^(tcpdump: )?listening on /i) || !data.match(/^\d+ packets captured/i)) { // Catch start-error
|
||||||
|
loggerTcpdump.debug(data);
|
||||||
|
}
|
||||||
|
else loggerTcpdump.error(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: This is a hacky workaround to not let errors from subprocess bubble up and terminate our process
|
||||||
|
regexBlockStream.on('error', (err) => {});
|
||||||
|
|
||||||
|
proc.on("error", (err) => {
|
||||||
|
loggerTcpdump.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loggerPacketStream = logFactory("PacketStreamFactory");
|
||||||
|
userHelper.detectStreamData(proc.stdout, 10000) // Expect tcpdump-logs to have data after max. 10s
|
||||||
|
.then(() => {
|
||||||
|
loggerTcpdump.debug("Got first data");
|
||||||
|
userHelper.detectStreamData(packetStreamFactory, 10000) // Expect then to have packets after further 10s
|
||||||
|
.then(() => {
|
||||||
|
loggerPacketStream.debug("Got first packet");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if(err == 'timeout') loggerPacketStream.warn("No packets");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if(err == 'timeout') loggerTcpdump.warn("No data after 10s! Wrong configuration?");
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug("Attaching exit-handler..");
|
||||||
|
proc.on("exit", (code) => {
|
||||||
|
loggerTcpdump.debug(`tcpdump exited code: ${code}`);
|
||||||
|
if (code) {
|
||||||
|
loggerTcpdump.fatal(`tcpdump exited with non-zero code: ${code}`);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
logger.info("Shutdown");
|
||||||
|
exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle stop-signals for graceful shutdown
|
||||||
|
function shutdownReq() {
|
||||||
|
logger.info("Shutdown request received..");
|
||||||
|
logger.debug("Stopping subprocess tcpdump, then exiting myself..");
|
||||||
|
proc.kill(); // Kill process (send SIGTERM), then upper event-handler will stop self
|
||||||
|
}
|
||||||
|
process.on('SIGTERM', shutdownReq);
|
||||||
|
process.on('SIGINT', shutdownReq);
|
||||||
|
|
||||||
|
logger.info("Startup complete");
|
||||||
|
})();
|
@ -0,0 +1,38 @@
|
|||||||
|
const logger = require.main.require("./helper/logger.js")("InfluxPointWriter");
|
||||||
|
const { Writable } = require('stream');
|
||||||
|
const {InfluxDB, Point, HttpError} = require('@influxdata/influxdb-client')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get points and write them into influx
|
||||||
|
*/
|
||||||
|
class InfluxPointWriter extends Writable{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {InfluxDB} influxDb InfluxDb
|
||||||
|
* @param {string} org Organization to use
|
||||||
|
* @param {string} bucket Bucket to use
|
||||||
|
* @param {Partial<WriteOptions>} options Options for WriteApi
|
||||||
|
*/
|
||||||
|
constructor(influxDb, org, bucket, options){
|
||||||
|
super({
|
||||||
|
objectMode: true
|
||||||
|
});
|
||||||
|
this._api = influxDb.getWriteApi(org, bucket, 'us', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
_write(point, encoding, next){
|
||||||
|
this._api.writePoint(point);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
_flush(next){
|
||||||
|
this._api.flush(true)
|
||||||
|
.catch((err) => { next(new Error(`WriteApi rejected promise for flush: ${err}`)); })
|
||||||
|
.then(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
InfluxPointWriter
|
||||||
|
};
|
@ -0,0 +1,85 @@
|
|||||||
|
const logger = require.main.require("./helper/logger.js")("PacketStreamFactory");
|
||||||
|
const { Transform } = require('stream');
|
||||||
|
const {Point} = require('@influxdata/influxdb-client')
|
||||||
|
|
||||||
|
/** Keys to always use as tags */
|
||||||
|
const TAG_LIST = [
|
||||||
|
"srcMac",
|
||||||
|
"dstMac",
|
||||||
|
"bssid",
|
||||||
|
"frequency",
|
||||||
|
"flags",
|
||||||
|
"packetType",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Measurement-name and corresponding field-key */
|
||||||
|
const MEASUREMENT_MAP = new Map([
|
||||||
|
["Signal", "signal"],
|
||||||
|
["PayloadSize", "payloadSize"],
|
||||||
|
["DataRate", "dataRate"],
|
||||||
|
["SSID", "ssid"],
|
||||||
|
["AuthenticationType", "authenticationType"],
|
||||||
|
["AssociationSuccess", "associationIsSuccessful"],
|
||||||
|
["DisassociationReason", "disassociationReason"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get packets and convert them into influx-points
|
||||||
|
*/
|
||||||
|
class PacketInfluxPointFactory extends Transform{
|
||||||
|
constructor(){
|
||||||
|
super({
|
||||||
|
readableObjectMode: true,
|
||||||
|
writableObjectMode: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_transform(packet, encoding, next){
|
||||||
|
// Create measurements
|
||||||
|
MEASUREMENT_MAP.forEach((objKey, measurement) => {
|
||||||
|
if(packet[objKey] == null) return;
|
||||||
|
|
||||||
|
let point = new Point(measurement); // Create point
|
||||||
|
|
||||||
|
// Set tags
|
||||||
|
TAG_LIST.filter(tag => Object.keys(packet).includes(tag)) // Filter tags available on object
|
||||||
|
.filter(tag => packet[tag] != null) // Filter tags not falsy on object
|
||||||
|
.forEach(tag => {
|
||||||
|
tagObjectRecursively(point, tag, packet[tag]);
|
||||||
|
});
|
||||||
|
|
||||||
|
point.setField('value', packet[objKey]); // Set field
|
||||||
|
|
||||||
|
this.push(point); // Push point into stream
|
||||||
|
});
|
||||||
|
|
||||||
|
next(); // Get next packet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagObjectRecursively(point, tag, field, suffix = ""){
|
||||||
|
if(typeof(field) == "object"){
|
||||||
|
// TODO: Convert boolean-arrays like "packet.flags" to key: value
|
||||||
|
Object.entries(field).map(([key, value]) => {
|
||||||
|
tagObjectRecursively(point, tag, value, `_${key}${suffix}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else point.tag(tag+suffix, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mapping for type -> field-method */
|
||||||
|
const POINT_FIELD_TYPE = new Map([
|
||||||
|
['boolean', function(key, value){ return this.booleanField(key, value); }],
|
||||||
|
['number', function(key, value){ return this.intField(key, value); }],
|
||||||
|
['string', function(key, value){ return this.stringField(key, value); }],
|
||||||
|
]);
|
||||||
|
Point.prototype.setField = function(key, value){
|
||||||
|
let setField = POINT_FIELD_TYPE.get(typeof value);
|
||||||
|
return setField.apply(this, [key, value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
PacketInfluxPointFactory
|
||||||
|
};
|
@ -0,0 +1,152 @@
|
|||||||
|
const logger = require.main.require("./helper/logger.js")("PacketStreamFactory");
|
||||||
|
const { Transform } = require('stream');
|
||||||
|
const { DateTime } = require("luxon");
|
||||||
|
const { PacketType, FlagType, Packet, PacketWithSSID, BeaconPacket, ProbeRequestPacket, ProbeResponsePacket, AuthenticationPacket, AuthenticationType, AssociationResponsePacket, DisassociationPacket, HandshakePacket, HandshakeStage } = require.main.require('./dto/Packet.js');
|
||||||
|
const hexConv = require.main.require("./helper/hexConverter.js");
|
||||||
|
const wifiStateAnalyser = require.main.require("./helper/wifiStateAnalyzer.js");
|
||||||
|
|
||||||
|
const PACKET_TYPE_MAP = {
|
||||||
|
"Beacon": PacketType.Beacon,
|
||||||
|
"Probe Request": PacketType.ProbeRequest,
|
||||||
|
"Probe Response": PacketType.ProbeResponse,
|
||||||
|
"Data": PacketType.Data,
|
||||||
|
"Request-To-Send": PacketType.RequestToSend,
|
||||||
|
"Clear-To-Send": PacketType.ClearToSend,
|
||||||
|
"Acknowledgment": PacketType.Acknowledgment,
|
||||||
|
"BA": PacketType.BlockAcknowledgment,
|
||||||
|
"Authentication": PacketType.Authentication,
|
||||||
|
"Assoc Request": PacketType.AssociationRequest,
|
||||||
|
"Assoc Response": PacketType.AssociationResponse,
|
||||||
|
"Disassociation:": PacketType.Disassociation,
|
||||||
|
"EAPOL": PacketType.Handshake,
|
||||||
|
};
|
||||||
|
const PACKET_TYPES_REGEX = Object.keys(PACKET_TYPE_MAP).join('|');
|
||||||
|
|
||||||
|
const AUTHENTICATION_TYPE_MAP = {
|
||||||
|
"(Open System)-1": AuthenticationType.OpenSystem_1,
|
||||||
|
"(Open System)-2": AuthenticationType.OpenSystem_2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const FLAG_TYPE_MAP = {
|
||||||
|
"Retry": FlagType.Retry,
|
||||||
|
"Pwr Mgmt": FlagType.PwrMgt,
|
||||||
|
"More Data": FlagType.MoreData,
|
||||||
|
"Protected": FlagType.Protected,
|
||||||
|
}
|
||||||
|
const FLAG_TYPE_MAPS_REGEX = Object.keys(FLAG_TYPE_MAP).join('|');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read data from text-blocks and convert them to Packet
|
||||||
|
*/
|
||||||
|
class PacketStreamFactory extends Transform{
|
||||||
|
constructor(){
|
||||||
|
super({
|
||||||
|
readableObjectMode: true,
|
||||||
|
writableObjectMode: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_transform(chunk, encoding, next){
|
||||||
|
let packet = new Packet();
|
||||||
|
|
||||||
|
const lines = chunk.split('\n');
|
||||||
|
const header = lines.splice(0, 1)[0]; // Grab first line, 'lines' is now the payload
|
||||||
|
packet = this._handleHeader(packet, header);
|
||||||
|
packet = this._handlePayload(packet, lines);
|
||||||
|
|
||||||
|
next(null, packet); // Get next chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleHeader(packet, data){
|
||||||
|
// Convert time to epoch-micros Unfortunately luxon doesnt use micros, but millis as smallest time-unit requiring some "hacks"
|
||||||
|
packet.timestampMicros = DateTime.fromISO(data.slice(0, 12)).toSeconds() + data.slice(12, 15)/1000000;
|
||||||
|
|
||||||
|
// Find flags
|
||||||
|
data.match(data.match(new RegExp('(?<=^|\\s)('+ FLAG_TYPE_MAPS_REGEX +')(?=$|\\s)', 'ig'))
|
||||||
|
?.forEach(match => packet.flags[FLAG_TYPE_MAP[match]] = true) // Set them to true in flags
|
||||||
|
);
|
||||||
|
|
||||||
|
packet.dataRate = Number(data.match(/(?<=^|\s)\d+(\.\d+)?(?=\sMb\/?s($|\s))/i)?.[0]) || null;
|
||||||
|
packet.frequency = Number(data.match(/(?<=^|\s)\d{4}(?=\sMHz($|\s))/i)?.[0]) || null;
|
||||||
|
|
||||||
|
packet.durationMicros = Number(data.match(/(?<=^|\s)\d{1,4}(?=us($|\s))/i)?.[0]) || null;
|
||||||
|
|
||||||
|
packet.signal = Number(data.match(/(?<=^|\s)-\d{2,3}(?=dBm\sSignal($|\s))/i)?.[0]) || null;
|
||||||
|
|
||||||
|
let packetTypeStr = data.match(new RegExp('(?<=^|\\s)('+ PACKET_TYPES_REGEX +')(?=$|\\s)', 'i'))?.[0];
|
||||||
|
if(packetTypeStr)
|
||||||
|
packet.packetType = PACKET_TYPE_MAP[packetTypeStr];
|
||||||
|
else if(data.match(/(SA|TA|DA|RA|BSSID):.{17}\s*$/i)){
|
||||||
|
packet.packetType = PacketType.NoData
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
packet.packetType = PacketType.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.srcMac = data.match(/(?<=(^|\s)(SA|TA):).{17}(?=$|\s)/i)?.[0] ?? null;
|
||||||
|
|
||||||
|
packet.dstMac = data.match(/(?<=(^|\s)(DA|RA):).{17}(?=$|\s)/i)?.[0] ?? null;
|
||||||
|
|
||||||
|
packet.bssid = data.match(/(?<=(^|\s)BSSID:).{17}(?=$|\s)/i)?.[0] ?? null;
|
||||||
|
|
||||||
|
// Cover special cases with more data
|
||||||
|
let newPacket;
|
||||||
|
switch(packet.packetType){
|
||||||
|
case PacketType.Beacon:
|
||||||
|
case PacketType.ProbeRequest:
|
||||||
|
case PacketType.ProbeResponse:
|
||||||
|
case PacketType.AssociationRequest:
|
||||||
|
newPacket = new PacketWithSSID();
|
||||||
|
newPacket.ssid = data.match(new RegExp('(?<=(^|\\s)'+ packetTypeStr +'\\s\\().{0,32}(?=\\)($|\\s))', 'i'))?.[0] ?? null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PacketType.Authentication:
|
||||||
|
newPacket = new AuthenticationPacket();
|
||||||
|
newPacket.authenticationType = AUTHENTICATION_TYPE_MAP[data.match(/(?<=(^|\s)Authentication\s).{3,}(?=\:(\s|$))/i)[0]] ?? AuthenticationType.Unknown;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PacketType.AssociationResponse:
|
||||||
|
newPacket = new AssociationResponsePacket();
|
||||||
|
newPacket.associationIsSuccessful = data.match(/(?<=(^|\s)Assoc\sResponse\s.{0,30})Successful(?=\s|$)/i) ? true : false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PacketType.Disassociation:
|
||||||
|
newPacket = new DisassociationPacket();
|
||||||
|
newPacket.disassociationReason = data.match(/(?<=(^|\s)Disassociation:\s).*$/i)?.[0] ?? null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(newPacket) packet = Object.assign(newPacket, packet); // Use new, more specific, packet and copy old data over
|
||||||
|
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePayload(packet, data){
|
||||||
|
data = data.join('');
|
||||||
|
|
||||||
|
// Get payload-Hex-Data. If there is no data: empty
|
||||||
|
packet.payloadData = hexConv.hexToBytes(data.match(/(?<=\s)([A-F0-9]{1,4}(?=\s))/igm)?.join('') ?? '');
|
||||||
|
packet.payloadData.splice(packet.payloadData.length-4, 4); // Remove FrameCheck sequence
|
||||||
|
|
||||||
|
// Cover special cases with more data
|
||||||
|
let newPacket;
|
||||||
|
switch(packet.packetType){
|
||||||
|
case PacketType.Handshake:
|
||||||
|
newPacket = new HandshakePacket();
|
||||||
|
|
||||||
|
// Read key-information
|
||||||
|
const keyInfoRaw = (packet.payloadData[0x5]<<0x8) + packet.payloadData[0x6];
|
||||||
|
const keyInfo = wifiStateAnalyser.keyInfoFromRaw(keyInfoRaw); // Convert
|
||||||
|
|
||||||
|
newPacket.handshakeStage = wifiStateAnalyser.handshakeStageFromKeyInfo(keyInfo); // Get stage
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(newPacket) packet = Object.assign(newPacket, packet);
|
||||||
|
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
PacketStreamFactory
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
const logger = require.main.require("./helper/logger.js")("RegexBlockStream");
|
||||||
|
const { Transform } = require('stream')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches whole blocks as regex and passes them on
|
||||||
|
*/
|
||||||
|
class RegexBlockStream extends Transform{
|
||||||
|
matcher;
|
||||||
|
withholdLastBlock;
|
||||||
|
matchAllOnFlush;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RegExp} matcher Block-match
|
||||||
|
* @param {boolean} withholdLastBlock When true, the last matches block will not be submitted to prevent submitting incomplete blocks.
|
||||||
|
* @param {boolean} matchAllOnFlush (Only in combination with withholdLastBlock) When enabled, the buffer will be matched on last time on _flush (stream deconstruction) and write any, also incomplete, blocks
|
||||||
|
* @remarks WARNING: It should match a clean-block (including e.g. newline)! Otherwise buffer will get dirty and use more and more resources.
|
||||||
|
*/
|
||||||
|
constructor(matcher, withholdLastBlock = true, matchAllOnFlush = false){
|
||||||
|
super({
|
||||||
|
readableObjectMode: true,
|
||||||
|
writableObjectMode: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this.matcher = matcher;
|
||||||
|
this.withholdLastBlock = withholdLastBlock;
|
||||||
|
this.matchAllOnFlush = matchAllOnFlush;
|
||||||
|
}
|
||||||
|
|
||||||
|
_transform(chunk, encoding, next){
|
||||||
|
chunk = this.readableBuffer.length? this.readableBuffer.join('') + chunk: chunk; // Add previous buffer to current chunk
|
||||||
|
this.readableBuffer.length && this.readableBuffer.clear(); // Clear buffer once we read it
|
||||||
|
|
||||||
|
let matches = chunk.match(this.matcher); // Match
|
||||||
|
if(matches){
|
||||||
|
if(this.withholdLastBlock) matches.pop(); // Remove last if we want to withhold it
|
||||||
|
chunk = this._writeMatches(matches, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.readableBuffer.push(chunk); // Store remaining data in buffer
|
||||||
|
next(); // Get next chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeMatches(matches, chunk = null){
|
||||||
|
if(matches){
|
||||||
|
matches.forEach((match) => {
|
||||||
|
this.push(match); // Write match to stream
|
||||||
|
if(chunk) chunk = chunk.replace(match, ''); // Remove match from chunks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(chunk) return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
_flush(next){
|
||||||
|
if(matchAllOnFlush){ // When requested, we'll match one last time over the remaining buffer
|
||||||
|
let chunk = this.readableBuffer.join('');
|
||||||
|
let matches = chunk.match(this.matcher); // Match remaining buffer
|
||||||
|
_writeMatches(matches); // Write matches including last element
|
||||||
|
}
|
||||||
|
|
||||||
|
next(); // Tell system we are done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify exports
|
||||||
|
module.exports = {
|
||||||
|
RegexBlockStream
|
||||||
|
};
|
Loading…
Reference in New Issue