From d9a76c9a9f968b3e33b80543446bdc6d78ec7f9c Mon Sep 17 00:00:00 2001 From: ale Date: Sun, 1 Dec 2024 16:36:36 +0100 Subject: [PATCH] initial commit Signed-off-by: ale --- Dockerfile | 8 +++ README.md | 23 ++++++++ docker-compose.yml | 20 +++++++ wble/ble-index-mapping.json | 85 +++++++++++++++++++++++++++ wble/index.ts | 111 ++++++++++++++++++++++++++++++++++++ wble/package.json | 20 +++++++ wble/tsconfig.json | 23 ++++++++ 7 files changed, 290 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 wble/ble-index-mapping.json create mode 100644 wble/index.ts create mode 100644 wble/package.json create mode 100644 wble/tsconfig.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cc84a05 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM node:22-bookworm +RUN apt update && apt install -y bluetooth bluez libbluetooth-dev libudev-dev libcap2-bin && apt clean +RUN setcap cap_net_raw+eip $(eval readlink -f `which node`) +USER node +COPY --chown=node:node ./wble /wble +WORKDIR /wble +RUN yarn && yarn build +ENTRYPOINT ["yarn", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e21fbd --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# wble + +### Small program that scan BLE (Bluetooth Low Energy) devices using `NodeJS` and `Docker` with storage in `ElasticSearch` + +## Config + + edit environment variables with your settings + +## Build + + docker compose build + +## Start + + docker compose up + +## Stop + + docker compose down + +## License + + MIT \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..701a289 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +services: + wble: + image: wble + build: ./ + hostname: wble + container_name: wble + restart: always + environment: + - MACHINEID="Pi4" + - ELASTICINDEX="ble" + - SECONDS=60 + - COUNTERCHANGEMAC=90 + - WAITSTARTSCAN=3 + - ELASTICSEARCHNODE="https://elasticsearch-node:9200" + cap_add: + - NET_ADMIN + - SYS_ADMIN + # volumes: + # - ./wble:/wble + network_mode: host diff --git a/wble/ble-index-mapping.json b/wble/ble-index-mapping.json new file mode 100644 index 0000000..627d2bd --- /dev/null +++ b/wble/ble-index-mapping.json @@ -0,0 +1,85 @@ +{ + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "@version": { + "type": "text" + }, + "address": { + "type": "keyword" + }, + "addressType": { + "type": "keyword" + }, + "advertisement": { + "properties": { + "localName": { + "type": "keyword" + }, + "manufacturerData": { + "type": "keyword" + }, + "serviceData": { + "properties": { + "data": { + "properties": { + "data": { + "type": "long" + }, + "type": { + "type": "text" + } + } + }, + "uuid": { + "type": "keyword" + } + } + }, + "serviceSolicitationUuids": { + "type": "text" + }, + "serviceUuids": { + "type": "text" + }, + "txPowerLevel": { + "type": "long" + } + } + }, + "connectable": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "machine": { + "type": "keyword" + }, + "mtu": { + "type": "long" + }, + "rssi": { + "type": "keyword" + }, + "scannable": { + "type": "boolean" + }, + "seen": { + "type": "date" + }, + "services": { + "type": "keyword" + }, + "state": { + "type": "keyword" + }, + "uuid": { + "type": "text", + "fielddata": true + } + } + } +} \ No newline at end of file diff --git a/wble/index.ts b/wble/index.ts new file mode 100644 index 0000000..9aedfae --- /dev/null +++ b/wble/index.ts @@ -0,0 +1,111 @@ +import { default as noble } from '@stoprocent/noble'; +import randomMac from 'random-mac'; +import { spawnSync } from 'node:child_process'; +import { Client } from '@elastic/elasticsearch'; +const machineId = process.env.MACHINEID || 'Pi4', + indexBle = process.env.ELASTICINDEX || 'ble', + seconds: any = process.env.SECONDS || 60, + counterChangeMac: any = process.env.COUNTERCHANGEMAC || 90, + waitStartScan: any = process.env.WAITSTARTSCAN || 3 + +let peers = [], + counter = 0 + +const changeMacBle = () => { + const newMac = randomMac('00:12:34').split(':').reverse().map(data => '0x' + data) + spawnSync('hcitool', ['-i', 'hci0', 'cmd', '0x3f', '0x001'].concat(newMac)) + spawnSync('hciconfig', ['hci0', 'reset']) +}, + transformObject = item => { + if (item && typeof item === 'object') { + Object.entries(item).map(([key, value]) => { + if (value && Buffer.isBuffer(value)) { + item[key] = Buffer.from(value).toString('hex') + } else { + item[key] = transformObject(value) + } + }) + } + return item + }, + mainBle = (client: Client) => { + noble.on('discover', peripheral => { + if (peripheral && peripheral.id && peripheral.uuid) { + const peer = transformObject({ + id: peripheral.id, + uuid: peripheral.uuid, + address: peripheral.address, + addressType: peripheral.addressType, + connectable: peripheral.connectable, + advertisement: peripheral.advertisement, + rssi: peripheral.rssi, + services: peripheral.services, + mtu: peripheral.mtu, + state: peripheral.state, + seen: new Date(), + machine: machineId + }) + peers.push(peer) + } + }) + noble.on('scanStart', () => { + peers = [] + setTimeout(() => { + noble.stopScanning() + }, seconds * 1000) + }) + noble.on('scanStop', async () => { + if (counter >= counterChangeMac) { + changeMacBle() + counter = 0 + } else { + counter++ + } + const conn = await client.ping() + if (conn) { + client.helpers.bulk({ + datasource: peers, + onDocument(doc) { + return { + index: { _index: indexBle } + } + } + }) + } else { + throw new Error('disconnected') + } + setTimeout(() => { + noble.startScanning([], false, err => { + if (err) { + throw err + } + }) + }, waitStartScan * 1000) + }) + noble.on('stateChange', state => { + if (state === 'poweredOn') { + noble.startScanning([], false, err => { + if (err) { + throw err + } + }) + } + }) + } + +(async () => { + try { + const client = new Client({ node: process.env.ELASTICSEARCHNODE || 'http://elasticsearch-node:9200' }) + if (!(await client.indices.exists({ index: indexBle }))) { + await client.indices.create({ + index: indexBle, + mappings: require('./ble-index-mapping.json') + }) + } + changeMacBle() + mainBle(client) + } catch (e) { + console.error(e) + process.exit(1) + } +})() diff --git a/wble/package.json b/wble/package.json new file mode 100644 index 0000000..3bff42b --- /dev/null +++ b/wble/package.json @@ -0,0 +1,20 @@ +{ + "name": "wble", + "version": "1.0.0", + "main": "index.js", + "author": "ale", + "license": "MIT", + "private": true, + "scripts": { + "build": "tsc --build", + "start": "node index.js" + }, + "dependencies": { + "@elastic/elasticsearch": "^8.16.2", + "@stoprocent/noble": "^1.15.1", + "@types/node": "^20.17.9", + "random-mac": "^0.0.5", + "typescript": "^5.7.2" + }, + "type": "module" +} diff --git a/wble/tsconfig.json b/wble/tsconfig.json new file mode 100644 index 0000000..a8c54c9 --- /dev/null +++ b/wble/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", /* Specify ECMAScript target + version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES201 +9', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "es2020", /* Specify module code gener +ation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNex +t'. */ + "strict": false, /* Enable all strict type-c +hecking options. */ + "baseUrl": "./", /* Base directory to resolve no +n-absolute module names. */ + "esModuleInterop": true, /* Enables emit interoperabi +lity between CommonJS and ES Modules via creation of namespace objects for all i +mports. Implies 'allowSyntheticDefaultImports'. */ + "skipLibCheck": true, /* Skip type checking of dec +laration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently- +cased references to the same file. */ + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +}