Compare commits
	
		
			115 Commits
		
	
	
		
			2a0c1e5a3b
			...
			6e05a0b45c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6e05a0b45c | |||
| d14e469ef4 | |||
| 99a3e13d77 | |||
| b5c895674e | |||
| 86d2b8c1cf | |||
| 7ff6556d51 | |||
| 16388c73e5 | |||
| 8211f55b89 | |||
| c28bbaaada | |||
| 4ddbe3f06f | |||
| 0d84079ce1 | |||
| 1bf761970f | |||
| 5a0118aedd | |||
| 0709db0ddf | |||
| c27761322c | |||
| 45a11753de | |||
| d482001cdc | |||
| a681bbd2d2 | |||
| d9ee804c3b | |||
| e320d8670b | |||
| 2d824543d1 | |||
| 6e080907d1 | |||
| 91c3aca9e2 | |||
| 31ab10c3e1 | |||
| 8ff7211f0f | |||
| 2ae85ababb | |||
| 227ba127f8 | |||
| 8417de5756 | |||
| 54eadf3bc3 | |||
| e39ebaac23 | |||
| 54d627d469 | |||
| ca3c37be0f | |||
| 96b52e63a0 | |||
| 3c5e941cba | |||
| a468d7a57b | |||
| 4ad5eba7e0 | |||
| 2646c9787e | |||
| 873f00b21b | |||
| 271554719e | |||
| e18de63d7c | |||
| 2356040572 | |||
| b0bbf0c71a | |||
| 37b78e7373 | |||
| c5e1bb4c64 | |||
| 3c29ed2000 | |||
| ddf39b9433 | |||
| 3a927688d0 | |||
| c51cfc1b14 | |||
| e1b2a7e016 | |||
| fc5900b0ba | |||
| 354ca32a61 | |||
| d10e9bb2c6 | |||
| 44cd3288cf | |||
| 3af4bb7cc6 | |||
| 2a662e0bd1 | |||
| d7a9530b68 | |||
| 7de2250983 | |||
| bb3d843895 | |||
| 9472ed9198 | |||
| dcd0ce8111 | |||
| 1a9ced0bb8 | |||
| d77e3f8844 | |||
| e715cc1cac | |||
| cf1b300f6a | |||
| 450f162cda | |||
| 7ebcf573b9 | |||
| 38985ea9e2 | |||
| 1976838d8a | |||
| a1ce7a848b | |||
| 21e4ff1a66 | |||
| 25297ed4d9 | |||
| 68541b1191 | |||
| 7bc5a3530c | |||
| d0ee1a7044 | |||
| 57394882cc | |||
| b41d728ba9 | |||
| d6135a02e9 | |||
| ffe14e3f53 | |||
| 09bea81058 | |||
| 66c0fdfe12 | |||
| b32b5cbed7 | |||
| e86b5fca9a | |||
| d2178f3b73 | |||
| fe5dec7860 | |||
| 0d472e8cb3 | |||
| bba5da2599 | |||
| c2210dfb59 | |||
| 3c3ab18410 | |||
| aa9d5431f8 | |||
| 62b55b94f1 | |||
| 025edf8465 | |||
| 59e7188543 | |||
| 333a514363 | |||
| d714b342ed | |||
| 1aae9d5e71 | |||
| 6bc248b667 | |||
| 0cbbecbd2c | |||
| 368b6585ba | |||
| 4f1463eb4f | |||
| d11aac5599 | |||
| fa8d630283 | |||
| 9c28ed53e3 | |||
| cc8b106157 | |||
| 8ee189f52b | |||
| 79efcc3951 | |||
| 7dd131d6b5 | |||
| 1b6721d242 | |||
| d13df7b03d | |||
| 34c73c82c8 | |||
| 93f78aafe0 | |||
| 184432f642 | |||
| 7562ff2268 | |||
| 20ed3d0928 | |||
| 6ccd02af6c | |||
| f862c362fe | 
							
								
								
									
										3
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | node_modules | ||||||
|  | npm-debug.log | ||||||
|  | .* | ||||||
							
								
								
									
										17
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -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"] | ||||||
							
								
								
									
										266
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -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==" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @ -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" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										105
									
								
								src/dto/Packet.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/dto/Packet.js
									
									
									
									
									
										Normal file
									
								
							| @ -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, | ||||||
|  | }; | ||||||
							
								
								
									
										13
									
								
								src/helper/env.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/helper/env.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | }; | ||||||
							
								
								
									
										18
									
								
								src/helper/exec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/helper/exec.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | }; | ||||||
							
								
								
									
										24
									
								
								src/helper/hexConverter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/helper/hexConverter.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								src/helper/influx-checks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/helper/influx-checks.js
									
									
									
									
									
										Normal file
									
								
							| @ -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, | ||||||
|  | }; | ||||||
							
								
								
									
										11
									
								
								src/helper/logger.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/helper/logger.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
							
								
								
									
										46
									
								
								src/helper/userHelper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/helper/userHelper.js
									
									
									
									
									
										Normal file
									
								
							| @ -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, | ||||||
|  | }; | ||||||
							
								
								
									
										43
									
								
								src/helper/wifiStateAnalyzer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/helper/wifiStateAnalyzer.js
									
									
									
									
									
										Normal file
									
								
							| @ -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, | ||||||
|  | }; | ||||||
							
								
								
									
										131
									
								
								src/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/main.js
									
									
									
									
									
										Normal file
									
								
							| @ -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"); | ||||||
|  | })(); | ||||||
							
								
								
									
										38
									
								
								src/streamHandler/InfluxPointWriter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/streamHandler/InfluxPointWriter.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | }; | ||||||
							
								
								
									
										85
									
								
								src/streamHandler/PacketInfluxPointFactory.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/streamHandler/PacketInfluxPointFactory.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | }; | ||||||
							
								
								
									
										152
									
								
								src/streamHandler/PacketStreamFactory.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								src/streamHandler/PacketStreamFactory.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | }; | ||||||
							
								
								
									
										67
									
								
								src/streamHandler/RegexBlockStream.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/streamHandler/RegexBlockStream.js
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user