├── .gitmodules ├── src ├── c.d │ ├── manuf │ ├── config.yaml │ ├── base64.h │ ├── config_yaml.h │ ├── logger_thread.h │ ├── config.h.in │ ├── queue.h │ ├── manuf.h │ ├── parsers.h │ ├── platform.h │ ├── db.h │ ├── meson.build │ ├── queue.c │ ├── lruc.h │ ├── README.md │ ├── config_yaml.c │ ├── logger_thread.c │ ├── base64.c │ ├── radiotap_iter.h │ ├── manuf.c │ ├── radiotap.h │ ├── parsers.c │ ├── lruc.c │ ├── probemon.c │ ├── db.c │ └── radiotap.c ├── www │ ├── config.yaml │ ├── static │ │ ├── robots.txt │ │ ├── img │ │ │ ├── logo.png │ │ │ ├── favicon.png │ │ │ └── loading.gif │ │ ├── js │ │ │ └── package.json │ │ └── css │ │ │ └── bootstrap-datepicker3.min.css │ ├── requirements.txt │ ├── update-protobuf.sh │ ├── probe.proto │ ├── templates │ │ ├── error.html.j2 │ │ └── index.html.j2 │ ├── README.md │ ├── probe_pb2.py │ └── mapot.py ├── config.yaml ├── merge.py ├── consolidate-stats.py ├── probemon.py ├── stats.py └── plot.py ├── requirements.txt ├── screenshots ├── example.png ├── mapot-popup.png └── mapot-main-ui.png ├── .gitignore └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/c.d/manuf: -------------------------------------------------------------------------------- 1 | ../manuf -------------------------------------------------------------------------------- /src/c.d/config.yaml: -------------------------------------------------------------------------------- 1 | ../config.yaml -------------------------------------------------------------------------------- /src/www/config.yaml: -------------------------------------------------------------------------------- 1 | ../config.yaml -------------------------------------------------------------------------------- /src/www/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scapy 2 | manuf 3 | matplotlib 4 | lru-dict 5 | pyyaml 6 | -------------------------------------------------------------------------------- /src/www/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-Caching 3 | protobuf 4 | pyyaml 5 | -------------------------------------------------------------------------------- /screenshots/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solsticedhiver/probemon/HEAD/screenshots/example.png -------------------------------------------------------------------------------- /screenshots/mapot-popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solsticedhiver/probemon/HEAD/screenshots/mapot-popup.png -------------------------------------------------------------------------------- /src/www/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solsticedhiver/probemon/HEAD/src/www/static/img/logo.png -------------------------------------------------------------------------------- /screenshots/mapot-main-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solsticedhiver/probemon/HEAD/screenshots/mapot-main-ui.png -------------------------------------------------------------------------------- /src/www/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solsticedhiver/probemon/HEAD/src/www/static/img/favicon.png -------------------------------------------------------------------------------- /src/www/static/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solsticedhiver/probemon/HEAD/src/www/static/img/loading.gif -------------------------------------------------------------------------------- /src/www/static/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "browserify probe_pb.js -o protobuf.js" 4 | }, 5 | "dependencies": { 6 | "browserify": "^16.2.3", 7 | "google-protobuf": "^3.9.0-rc.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /archives/ 2 | /src/scripts/* 3 | /src/manuf 4 | probemon.db 5 | probemon.db-journal 6 | *.pyc 7 | *.bak 8 | node_modules/ 9 | package-lock.json 10 | static/js/probe_pb.js 11 | lost+found/ 12 | src/c.d/build 13 | /.vscode/ 14 | -------------------------------------------------------------------------------- /src/c.d/base64.h: -------------------------------------------------------------------------------- 1 | #ifndef BASE64_H 2 | #define BASE64_H 3 | 4 | char *base64_encode(const unsigned char *data, size_t input_length, size_t *output_length); 5 | unsigned char *base64_decode(const char *data, size_t input_length, size_t *output_length); 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /src/c.d/config_yaml.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_YAML_H 2 | #define CONFIG_YAML_H 3 | 4 | char **parse_config_yaml(const char *path, const char *keyname, int *count); 5 | uint64_t *parse_ignored_entries(char **entries, int count); 6 | int cmp_uint64_t(const void *u, const void*v); 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /src/www/update-protobuf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # helper script to update all things related to protobuf proto 4 | 5 | protoc --python_out=. probe.proto 6 | protoc --js_out=import_style=commonjs,binary:static/js probe.proto 7 | 8 | cd static/js 9 | if [[ ! -d node_modules ]]; then 10 | npm install 11 | fi 12 | npm run build 13 | -------------------------------------------------------------------------------- /src/config.yaml: -------------------------------------------------------------------------------- 1 | knownmac: # will be highlighted in red in plots 2 | - xx:xx:xx:xx:xx:xx # example mac address 3 | - xx:xx:xx:yy:yy:yy 4 | 5 | ignored: # list of your devices for example 6 | - yy:yy:yy:yy:yy:yy 7 | 8 | merged: # list of partial mac addresses for devices using randomization 9 | - 'da:a1:19:*' # well known LAA mac for android devices 10 | 11 | height: 1366 # in pixels 12 | width: 768 13 | dpi: 100 14 | -------------------------------------------------------------------------------- /src/c.d/logger_thread.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGGER_THREAD_H 2 | #define LOGGER_THREAD_H 3 | 4 | #include 5 | #include 6 | 7 | struct probereq { 8 | struct timeval tv; 9 | char *mac; 10 | char *vendor; 11 | uint8_t *ssid; 12 | uint8_t ssid_len; 13 | int rssi; 14 | }; 15 | typedef struct probereq probereq_t; 16 | 17 | void *process_queue(void *args); 18 | void free_probereq(probereq_t *pr); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/c.d/config.h.in: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #define NAME "probemon" 5 | #define VERSION "@version@" 6 | 7 | #define SNAP_LEN 512 8 | #define MAX_QUEUE_SIZE 128 9 | 10 | #define MAC_CACHE_SIZE 64 11 | #define SSID_CACHE_SIZE 64 12 | 13 | #define MAX_VENDOR_LENGTH 25 14 | #define MAX_SSID_LENGTH 15 15 | 16 | #define DB_NAME "./probemon.db" 17 | #define MANUF_NAME "./manuf" 18 | #define CONFIG_NAME "./config.yaml" 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/c.d/queue.h: -------------------------------------------------------------------------------- 1 | #ifndef SIMPLE_QUEUE 2 | #define SIMPLE_QUEUE 3 | 4 | struct Node { 5 | void *value; 6 | struct Node *next; 7 | }; 8 | 9 | typedef struct Queue { 10 | int size; 11 | int max_size; 12 | struct Node *head; 13 | struct Node *tail; 14 | } queue_t; 15 | 16 | extern queue_t *new_queue(int capacity); 17 | 18 | extern int enqueue(queue_t * q, void *value); 19 | 20 | extern void *dequeue(queue_t * q); 21 | 22 | extern void free_queue(queue_t * q); 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /src/www/probe.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package probemon; 4 | 5 | message Probes { 6 | string mac = 1; 7 | string vendor = 2; 8 | bool known = 3; 9 | message Ssid { 10 | string name = 1; 11 | } 12 | repeated Ssid ssids = 4; 13 | 14 | uint64 starting_ts = 5; 15 | message Probereq { 16 | int32 timestamp = 1; 17 | sint32 rssi = 2; 18 | int32 ssid = 3; 19 | } 20 | 21 | repeated Probereq probereq = 6; 22 | } 23 | 24 | message MyData { 25 | repeated Probes probes = 1; 26 | } 27 | -------------------------------------------------------------------------------- /src/c.d/manuf.h: -------------------------------------------------------------------------------- 1 | #ifndef MANUF_H 2 | #define MANUF_H 3 | 4 | #include 5 | 6 | struct manuf { 7 | uint64_t min; 8 | uint64_t max; 9 | char *short_oui; 10 | char *long_oui; 11 | char *comment; 12 | }; 13 | typedef struct manuf manuf_t; 14 | 15 | void free_manuf_t(manuf_t *m); 16 | manuf_t *parse_manuf_file(const char*path, size_t *ouidb_size); 17 | int lookup_oui(char *mac, manuf_t *ouidb, size_t ouidb_size); 18 | 19 | char *str_replace(const char *orig, const char *rep, const char *with); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/c.d/parsers.h: -------------------------------------------------------------------------------- 1 | #ifndef PARSERS_H 2 | #define PARSERS_H 3 | 4 | #include 5 | #include 6 | 7 | #include "logger_thread.h" 8 | 9 | int8_t parse_radiotap_header(const uint8_t * packet, uint16_t * freq, 10 | int8_t * rssi); 11 | 12 | void parse_probereq_frame(const uint8_t *packet, uint32_t header_len, 13 | int8_t offset, char **mac, uint8_t **ssid, uint8_t *ssid_len); 14 | 15 | char *probereq_to_str(probereq_t pr); 16 | 17 | bool is_utf8(const char * string); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /src/c.d/platform.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #if defined(__APPLE__) 4 | #include 5 | #else 6 | #include 7 | #endif 8 | 9 | #define le16_to_cpu le16toh 10 | #define le32_to_cpu le32toh 11 | #define get_unaligned(p) \ 12 | ({ \ 13 | struct packed_dummy_struct { \ 14 | typeof(*(p)) __val; \ 15 | } __attribute__((packed)) *__ptr = (void *) (p); \ 16 | \ 17 | __ptr->__val; \ 18 | }) 19 | #define get_unaligned_le16(p) le16_to_cpu(get_unaligned((uint16_t *)(p))) 20 | #define get_unaligned_le32(p) le32_to_cpu(get_unaligned((uint32_t *)(p))) 21 | -------------------------------------------------------------------------------- /src/c.d/db.h: -------------------------------------------------------------------------------- 1 | #ifndef DB_H 2 | #define DB_H 3 | 4 | #include 5 | #include "lruc.h" 6 | 7 | // to avoid SD-card wear, we avoid writing to disk every seconds, setting a delay between each transactions 8 | #define DB_CACHE_TIME 60 // time in second between transaction 9 | 10 | int init_probemon_db(const char *db_file, sqlite3 **db); 11 | int search_ssid(const char *ssid, sqlite3 *db); 12 | int insert_ssid(const char *ssid, sqlite3 *db); 13 | int search_vendor(const char *vendor, sqlite3 *db); 14 | int insert_vendor(const char *vendor, sqlite3 *db); 15 | int search_mac(const char *mac, sqlite3 *db); 16 | int insert_mac(const char *mac, int vendor_id, sqlite3 *db); 17 | int insert_probereq(probereq_t pr, sqlite3 *db, lruc *mac_pk_cache, lruc *ssid_pk_cache); 18 | int begin_txn(sqlite3 *db); 19 | int commit_txn(sqlite3 *db); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/c.d/meson.build: -------------------------------------------------------------------------------- 1 | project('probemon', 'c', version: '0.2') 2 | 3 | cc = meson.get_compiler('c') 4 | 5 | conf_data = configuration_data() 6 | conf_data.set('version', meson.project_version()) 7 | configure_file(input : 'config.h.in', 8 | output : 'config.h', 9 | configuration : conf_data) 10 | 11 | src = ['probemon.c', 'parsers.c', 'queue.c', 'radiotap.c', 12 | 'logger_thread.c', 'db.c', 'manuf.c', 'config_yaml.c', 'base64.c', 'lruc.c'] 13 | pcap_dep = dependency('pcap', version: '>1.0') 14 | pthread_dep = dependency('threads') 15 | sqlite3_dep = dependency('sqlite3') 16 | yaml_dep = dependency('yaml-0.1') 17 | 18 | if cc.has_header('sys/stat.h') 19 | add_project_arguments('-DHAS_SYS_STAT_H', language: 'c') 20 | endif 21 | 22 | executable('probemon', src, 23 | dependencies: [pcap_dep, pthread_dep, sqlite3_dep, yaml_dep], 24 | install: true) 25 | -------------------------------------------------------------------------------- /src/www/templates/error.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mapot 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 |
18 |
19 |

An error occured

20 |
21 |

{{ error.code }}   
{{ error.name }}

22 |

{{ error.description }}

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /src/c.d/queue.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "queue.h" 3 | 4 | queue_t *new_queue(int capacity) 5 | { 6 | queue_t *q; 7 | q = malloc(sizeof(queue_t)); 8 | 9 | if (q == NULL) { 10 | return q; 11 | } 12 | 13 | q->size = 0; 14 | q->max_size = capacity; 15 | q->head = NULL; 16 | q->tail = NULL; 17 | 18 | return q; 19 | } 20 | 21 | int enqueue(queue_t * q, void *value) 22 | { 23 | if ((q->size + 1) > q->max_size) { 24 | return q->size; 25 | } 26 | 27 | struct Node *node = malloc(sizeof(struct Node)); 28 | 29 | if (node == NULL) { 30 | return q->size; 31 | } 32 | 33 | node->value = value; 34 | node->next = NULL; 35 | 36 | if (q->head == NULL) { 37 | q->head = node; 38 | q->tail = node; 39 | q->size = 1; 40 | 41 | return q->size; 42 | } 43 | 44 | q->tail->next = node; 45 | q->tail = node; 46 | q->size += 1; 47 | 48 | return q->size; 49 | } 50 | 51 | void *dequeue(queue_t * q) 52 | { 53 | if (q->size == 0) { 54 | return NULL; 55 | } 56 | 57 | void *value = NULL; 58 | struct Node *tmp = NULL; 59 | 60 | value = q->head->value; 61 | tmp = q->head; 62 | q->head = q->head->next; 63 | q->size -= 1; 64 | 65 | free(tmp); 66 | 67 | return value; 68 | } 69 | 70 | void free_queue(queue_t * q) 71 | { 72 | if (q == NULL) { 73 | return; 74 | } 75 | 76 | while (q->head != NULL) { 77 | struct Node *tmp = q->head; 78 | q->head = q->head->next; 79 | if (tmp->value != NULL) { 80 | free(tmp->value); 81 | } 82 | 83 | free(tmp); 84 | } 85 | 86 | if (q->tail != NULL) { 87 | free(q->tail); 88 | } 89 | 90 | free(q); 91 | } 92 | -------------------------------------------------------------------------------- /src/c.d/lruc.h: -------------------------------------------------------------------------------- 1 | // https://github.com/willcannings/C-LRU-Cache.git 2 | #include 3 | #include 4 | #include 5 | 6 | #ifndef __lruc_header__ 7 | #define __lruc_header__ 8 | 9 | // ------------------------------------------ 10 | // errors 11 | // ------------------------------------------ 12 | typedef enum { 13 | LRUC_NO_ERROR = 0, 14 | LRUC_MISSING_CACHE, 15 | LRUC_MISSING_KEY, 16 | LRUC_MISSING_VALUE, 17 | LRUC_PTHREAD_ERROR, 18 | LRUC_VALUE_TOO_LARGE 19 | } lruc_error; 20 | 21 | // ------------------------------------------ 22 | // types 23 | // ------------------------------------------ 24 | typedef struct { 25 | void *value; 26 | void *key; 27 | uint32_t value_length; 28 | uint32_t key_length; 29 | uint64_t access_count; 30 | void *next; 31 | } lruc_item; 32 | 33 | typedef struct { 34 | lruc_item **items; 35 | uint64_t access_count; 36 | uint64_t free_memory; 37 | uint64_t total_memory; 38 | uint64_t average_item_length; 39 | uint32_t hash_table_size; 40 | time_t seed; 41 | lruc_item *free_items; 42 | pthread_mutex_t *mutex; 43 | } lruc; 44 | 45 | // ------------------------------------------ 46 | // api 47 | // ------------------------------------------ 48 | lruc *lruc_new(uint64_t cache_size, uint32_t average_length); 49 | lruc_error lruc_free(lruc *cache); 50 | lruc_error lruc_set(lruc *cache, void *key, uint32_t key_length, void *value, uint32_t value_length); 51 | lruc_error lruc_get(lruc *cache, void *key, uint32_t key_length, void **value); 52 | lruc_error lruc_delete(lruc *cache, void *key, uint32_t key_length); 53 | lruc_error lruc_print(lruc *cache); 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /src/www/README.md: -------------------------------------------------------------------------------- 1 | # The app 2 | `mapot.py` is a *flask* app that allows to serve in real time the stats and data collected by `probemon.py`. 3 | 4 | The app excepts the database to be in the current directory. This can be easily changed by changing the path of it in the `DATABASE` variable in `mapot.py`. 5 | 6 | Or use a symlink or hardlink towards the real db. 7 | 8 | The app connects to the db read-only. 9 | 10 | ## Running the app 11 | Even though it is possible to run it without any real webserver, this is not recommended, as per the documentation of **flask**. 12 | 13 | If you want to do it anyway, simply run: 14 | 15 | python3 mapot.py 16 | 17 | Howewer, using a real webserver is the way to go. 18 | 19 | This could be done simply with *gunicorn* for example. That you could install with: 20 | 21 | pip3 install gunicorn 22 | 23 | Then run it with: 24 | 25 | gunicorn -w 4 -b 127.0.0.1:5556 mapot:app 26 | 27 | You can look at other **options** on how to run the *app* in [the flask documentation](https://flask.palletsprojects.com/en/1.1.x/deploying/). 28 | 29 | ## The UI 30 | The main UI is a time chart with the probe requests displayed for the last 24 hours. 31 | 32 | ![Flask app main UI](../../screenshots/mapot-main-ui.png) 33 | 34 | On mobile, a table is displayed instead. 35 | 36 | You can choose another day in the datepicker, on the right. 37 | 38 | Refresh is done automatically if you go back to the tab/window and the current selected day is today. Otherwise, you can force a refresh by using the 'Refresh' button. 39 | 40 | By clicking on a mac in the legend on the left of the chart, one gets a popup with details about that particuliar mac: 41 | - probed SSIDs(s) list 42 | - RSSI values stats 43 | - a RSSI chart value over time 44 | - a complete probe requests log 45 | 46 | ![Popup over details](../../screenshots/mapot-popup.png) 47 | -------------------------------------------------------------------------------- /src/merge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sqlite3 4 | import sys 5 | import argparse 6 | 7 | parser = argparse.ArgumentParser(description='Merge one db into the current one') 8 | parser.add_argument('-o', '--output', default='probemon.db', help='file name of the target/output db') 9 | parser.add_argument('-i', '--input', help='file name of the input db', required=True) 10 | args = parser.parse_args() 11 | 12 | conn_in = sqlite3.connect(args.input) 13 | c_in = conn_in.cursor() 14 | 15 | conn_out = sqlite3.connect(args.output) 16 | c_out = conn_out.cursor() 17 | 18 | c_in.execute('select * from probemon') 19 | for row in c_in.fetchall(): 20 | time, mac, ssid, rssi = row 21 | 22 | c_in.execute('select address,vendor from mac where id = ?', (mac,)) 23 | mac_add, vendor_id = c_in.fetchone() 24 | c_in.execute('select name from vendor where id = ?', (vendor_id,)) 25 | vendor_name = c_in.fetchone()[0] 26 | c_out.execute('select id from vendor where name = ?', (vendor_name,)) 27 | r = c_out.fetchone() 28 | if r is None: 29 | c_out.execute('insert into vendor (name) values (?)', (vendor_name,)) 30 | c_out.execute('select id from vendor where name = ?', (vendor_name,)) 31 | r = c_out.fetchone() 32 | vendor_id = r[0] 33 | 34 | c_out.execute('select id from mac where address = ?', (mac_add,)) 35 | r = c_out.fetchone() 36 | if r is None: 37 | c_out.execute('insert into mac (address, vendor) values (?, ?)', (mac_add, vendor_id,)) 38 | c_out.execute('select id from mac where address = ?', (mac_add,)) 39 | r = c_out.fetchone() 40 | mac_id = r[0] 41 | 42 | c_in.execute('select name from ssid where id = ?' , (ssid,)) 43 | ssid_name = c_in.fetchone()[0] 44 | c_out.execute('select id from ssid where name = ?', (ssid_name,)) 45 | r = c_out.fetchone() 46 | if r is None: 47 | c_out.execute('insert into ssid (name) values (?)', (ssid_name,)) 48 | c_out.execute('select id from ssid where name = ?', (ssid_name,)) 49 | r = c_out.fetchone() 50 | ssid_id = r[0] 51 | 52 | c_out.execute('insert into probemon values (?, ?, ?, ?)', (time, mac_id, ssid_id, rssi)) 53 | 54 | conn_out.commit() 55 | 56 | conn_out.close() 57 | conn_in.close() 58 | -------------------------------------------------------------------------------- /src/c.d/README.md: -------------------------------------------------------------------------------- 1 | # A C implementation of probemon 2 | 3 | This is an impementation of *probemon* in **C**. It does exactly the same thing than *probemon.py* but this is written in C using *libpcap*, *libsqlite3* and *libyaml* libraries. You can except a lower CPU usage and a lower memory footprint, especially on low end hardware like a raspberry pi 0, for example. Otherwise, it should not matter much. 4 | 5 | The main implementation remains the *python* version but that could change in the future. 6 | 7 | ## Running 8 | You run *probemon* with: 9 | 10 | $ sudo ./build/probemon -i wlan0mon -c 1 11 | 12 | where *wlan0mon* is a wifi interface already put in monitor mode. 13 | 14 | The default output format is a **sqlite3** database, named *probemon.db*. You can choose another name by using the `-d` switch. 15 | 16 | You can query as usual the database with the python tool `stats.py` or create time plot with `plot.py`. 17 | 18 | The complete usage: 19 | 20 | Usage: probemon -i IFACE -c CHANNEL [-d DB_NAME] [-m MANUF_NAME] [-s] 21 | -i IFACE interface to use 22 | -c CHANNEL channel to sniff on 23 | -d DB_NAME explicitly set the db filename 24 | -m MANUF_NAME path to manuf file 25 | -s also log probe requests to stdout 26 | 27 | ## Dependencies 28 | *probemon* depends on the following libraries: 29 | 30 | - libpcap 31 | - libpthread 32 | - libsqlite3 33 | - libyaml 34 | - and on the `iw` executable 35 | 36 | This also relies on a *manuf* file; it can be found in the *wireshark* package under `/usr/share/wireshark/manuf` or you can directly download a fresh version at https://code.wireshark.org/review/gitweb?p=wireshark.git;a=blob_plain;f=manuf;hb=HEAD 37 | 38 | ### Examples 39 | On Ubuntu 18.04 or Raspbian, one needs to run the following command to install libraries and headers: 40 | 41 | sudo apt install pkg-config libpcap0.8 libpcap0.8-dev libsqlite3-dev libsqlite3-0 meson ninja-build libyaml-dev libyaml-0-2 42 | 43 | On archlinux-arm, this is: 44 | 45 | sudo pacman -S libpcap libyaml sqlite3 meson ninja 46 | 47 | ## Building 48 | To build the executable, you need *meson* and *ninja*. 49 | 50 | $ cd probemon/src/c.d 51 | $ meson build 52 | $ ninja -C build 53 | 54 | # to run it, use: 55 | $ sudo ./build/probemon .... 56 | -------------------------------------------------------------------------------- /src/c.d/config_yaml.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "manuf.h" 6 | 7 | // very basic yaml parser 8 | // it only scans for a given key (keyname) and return entries for that key 9 | char **parse_config_yaml(const char *path, const char *keyname, int *count) 10 | { 11 | yaml_parser_t parser; 12 | yaml_token_t token; 13 | 14 | FILE *fh = fopen(path, "r"); 15 | if (fh == NULL) { 16 | fprintf(stderr, "Error: failed to open file %s\n", path); 17 | return NULL; 18 | } 19 | if (!yaml_parser_initialize(&parser)) { 20 | fprintf(stderr, "Error: failed to initialize yaml parser\n"); 21 | return NULL; 22 | } 23 | yaml_parser_set_input_file(&parser, fh); 24 | 25 | int key = 0, start = 0, entry = 0; 26 | char **entries = NULL; 27 | do { 28 | yaml_parser_scan(&parser, &token); 29 | char *tk = (char *)token.data.scalar.value; 30 | switch(token.type) { 31 | case YAML_KEY_TOKEN: 32 | key = 1; 33 | break; 34 | case YAML_BLOCK_ENTRY_TOKEN: 35 | if (start) { 36 | entry = 1; 37 | } 38 | break; 39 | case YAML_BLOCK_END_TOKEN: 40 | if (start) { 41 | start = 0; 42 | } 43 | break; 44 | case YAML_SCALAR_TOKEN: 45 | if (key) { 46 | key = 0; 47 | if (strcmp(tk, keyname) == 0) { 48 | start = 1; 49 | } 50 | } 51 | if (entry) { 52 | entries = realloc(entries, sizeof(char *)*(*count+1)); 53 | entries[*count] = strdup(tk); 54 | *count += 1; 55 | entry = 0; 56 | } 57 | break; 58 | default: 59 | break; 60 | } 61 | if (token.type != YAML_STREAM_END_TOKEN) { 62 | yaml_token_delete(&token); 63 | } 64 | } while(token.type != YAML_STREAM_END_TOKEN); 65 | yaml_token_delete(&token); 66 | 67 | yaml_parser_delete(&parser); 68 | fclose(fh); 69 | 70 | return entries; 71 | } 72 | 73 | int cmp_uint64_t(const void *u, const void*v) 74 | { 75 | uint64_t a = *(uint64_t *)u; 76 | uint64_t b = *(uint64_t *)v; 77 | if (ab) return 1; 79 | // assert(a==b); 80 | return 0; 81 | } 82 | 83 | uint64_t *parse_ignored_entries(char **entries, int count) 84 | { 85 | 86 | if (entries == NULL || count == 0) { 87 | return NULL; 88 | } 89 | 90 | uint64_t *result = malloc(count*sizeof(uint64_t)); 91 | 92 | for (int i=0; i 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "parsers.h" 17 | #include "queue.h" 18 | #include "logger_thread.h" 19 | #include "db.h" 20 | #include "manuf.h" 21 | #include "config_yaml.h" 22 | #include "lruc.h" 23 | #include "config.h" 24 | 25 | extern pthread_mutex_t mutex_queue; 26 | extern queue_t *queue; 27 | extern sem_t queue_empty; 28 | extern sem_t queue_full; 29 | extern sqlite3 *db; 30 | struct timespec start_ts_cache; 31 | extern bool option_stdout; 32 | 33 | extern manuf_t *ouidb; 34 | extern size_t ouidb_size; 35 | 36 | extern uint64_t *ignored; 37 | extern int ignored_count; 38 | 39 | lruc *ssid_pk_cache = NULL, *mac_pk_cache = NULL; 40 | 41 | void free_probereq(probereq_t *pr) 42 | { 43 | if (pr == NULL) return; 44 | 45 | free(pr->mac); 46 | pr->mac = NULL; 47 | free(pr->ssid); 48 | pr->ssid = NULL; 49 | if (pr->vendor) free(pr->vendor); 50 | pr->vendor = NULL; 51 | free(pr); 52 | pr = NULL; 53 | return; 54 | } 55 | 56 | void *process_queue(void *args) 57 | { 58 | probereq_t *pr; 59 | struct timespec now; 60 | 61 | mac_pk_cache = lruc_new(MAC_CACHE_SIZE, 1); 62 | ssid_pk_cache = lruc_new(SSID_CACHE_SIZE, 1); 63 | 64 | clock_gettime(CLOCK_MONOTONIC, &start_ts_cache); 65 | 66 | while (true) { 67 | sem_wait(&queue_empty); 68 | pthread_mutex_lock(&mutex_queue); 69 | pr = (probereq_t *) dequeue(queue); 70 | pthread_mutex_unlock(&mutex_queue); 71 | sem_post(&queue_full); 72 | 73 | // look for vendor string in manuf 74 | int indx = lookup_oui(pr->mac, ouidb, ouidb_size); 75 | if (indx >= 0) { 76 | if (ouidb[indx].long_oui) { 77 | pr->vendor = strdup(ouidb[indx].long_oui); 78 | } else { 79 | pr->vendor = strdup(ouidb[indx].short_oui); 80 | } 81 | } else { 82 | pr->vendor = strdup("UNKNOWN"); 83 | } 84 | // check if mac is not in ignored list 85 | uint64_t *res = NULL; 86 | if (ignored != NULL) { 87 | char *tmp = str_replace(pr->mac, ":", ""); 88 | uint64_t mac_number = strtoll(tmp, NULL, 16); 89 | free(tmp); 90 | res = bsearch(&mac_number, ignored, ignored_count, sizeof(uint64_t), cmp_uint64_t); 91 | } 92 | if (res == NULL) { 93 | insert_probereq(*pr, db, mac_pk_cache, ssid_pk_cache); 94 | if (option_stdout) { 95 | char *pr_str = probereq_to_str(*pr); 96 | printf("%s\n", pr_str); 97 | free(pr_str); 98 | } 99 | } 100 | free_probereq(pr); 101 | if (option_stdout) { 102 | fflush(stdout); 103 | } 104 | clock_gettime(CLOCK_MONOTONIC, &now); 105 | if (now.tv_sec - start_ts_cache.tv_sec >= DB_CACHE_TIME) { 106 | // commit to db 107 | commit_txn(db); 108 | begin_txn(db); 109 | start_ts_cache = now; 110 | } 111 | } 112 | 113 | lruc_free(mac_pk_cache); 114 | lruc_free(ssid_pk_cache); 115 | 116 | return NULL; 117 | } 118 | -------------------------------------------------------------------------------- /src/c.d/base64.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // from https://stackoverflow.com/a/6782480/283067 5 | static char encoding_table[] = { 6 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 7 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 8 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 9 | 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 10 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 11 | 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 12 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', 13 | '4', '5', '6', '7', '8', '9', '+', '/' 14 | }; 15 | static char *decoding_table = NULL; 16 | static int mod_table[] = {0, 2, 1}; 17 | 18 | void build_decoding_table(void) { 19 | decoding_table = malloc(256); 20 | 21 | for (int i = 0; i < 64; i++) { 22 | decoding_table[(unsigned char) encoding_table[i]] = i; 23 | } 24 | } 25 | 26 | void base64_cleanup(void) { 27 | free(decoding_table); 28 | } 29 | 30 | char *base64_encode(const unsigned char *data, size_t input_length, size_t *output_length) 31 | { 32 | *output_length = 4 * ((input_length + 2) / 3); 33 | 34 | char *encoded_data = malloc(*output_length); 35 | if (encoded_data == NULL) return NULL; 36 | 37 | for (int i = 0, j = 0; i < input_length;) { 38 | uint32_t octet_a = i < input_length ? (unsigned char)data[i++] : 0; 39 | uint32_t octet_b = i < input_length ? (unsigned char)data[i++] : 0; 40 | uint32_t octet_c = i < input_length ? (unsigned char)data[i++] : 0; 41 | 42 | uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; 43 | 44 | encoded_data[j++] = encoding_table[(triple >> 3 * 6) & 0x3F]; 45 | encoded_data[j++] = encoding_table[(triple >> 2 * 6) & 0x3F]; 46 | encoded_data[j++] = encoding_table[(triple >> 1 * 6) & 0x3F]; 47 | encoded_data[j++] = encoding_table[(triple >> 0 * 6) & 0x3F]; 48 | } 49 | 50 | for (int i = 0; i < mod_table[input_length % 3]; i++) { 51 | encoded_data[*output_length - 1 - i] = '='; 52 | } 53 | 54 | return encoded_data; 55 | } 56 | 57 | unsigned char *base64_decode(const char *data, size_t input_length, size_t *output_length) 58 | { 59 | if (decoding_table == NULL) build_decoding_table(); 60 | 61 | if (input_length % 4 != 0) return NULL; 62 | 63 | *output_length = input_length / 4 * 3; 64 | if (data[input_length - 1] == '=') (*output_length)--; 65 | if (data[input_length - 2] == '=') (*output_length)--; 66 | 67 | unsigned char *decoded_data = malloc(*output_length); 68 | if (decoded_data == NULL) return NULL; 69 | 70 | for (int i = 0, j = 0; i < input_length;) { 71 | uint32_t sextet_a = data[i] == '=' ? 0 & i++ : decoding_table[(int)data[i++]]; 72 | uint32_t sextet_b = data[i] == '=' ? 0 & i++ : decoding_table[(int)data[i++]]; 73 | uint32_t sextet_c = data[i] == '=' ? 0 & i++ : decoding_table[(int)data[i++]]; 74 | uint32_t sextet_d = data[i] == '=' ? 0 & i++ : decoding_table[(int)data[i++]]; 75 | 76 | uint32_t triple = (sextet_a << 3 * 6) 77 | + (sextet_b << 2 * 6) 78 | + (sextet_c << 1 * 6) 79 | + (sextet_d << 0 * 6); 80 | 81 | if (j < *output_length) decoded_data[j++] = (triple >> 2 * 8) & 0xFF; 82 | if (j < *output_length) decoded_data[j++] = (triple >> 1 * 8) & 0xFF; 83 | if (j < *output_length) decoded_data[j++] = (triple >> 0 * 8) & 0xFF; 84 | } 85 | 86 | base64_cleanup(); 87 | 88 | return decoded_data; 89 | } 90 | -------------------------------------------------------------------------------- /src/c.d/radiotap_iter.h: -------------------------------------------------------------------------------- 1 | #ifndef __RADIOTAP_ITER_H 2 | #define __RADIOTAP_ITER_H 3 | 4 | #include 5 | #include "radiotap.h" 6 | 7 | /* Radiotap header iteration 8 | * implemented in radiotap.c 9 | */ 10 | 11 | struct radiotap_override { 12 | uint8_t field; 13 | uint8_t align:4, size:4; 14 | }; 15 | 16 | struct radiotap_align_size { 17 | uint8_t align:4, size:4; 18 | }; 19 | 20 | struct ieee80211_radiotap_namespace { 21 | const struct radiotap_align_size *align_size; 22 | int n_bits; 23 | uint32_t oui; 24 | uint8_t subns; 25 | }; 26 | 27 | struct ieee80211_radiotap_vendor_namespaces { 28 | const struct ieee80211_radiotap_namespace *ns; 29 | int n_ns; 30 | }; 31 | 32 | /** 33 | * struct ieee80211_radiotap_iterator - tracks walk thru present radiotap args 34 | * @this_arg_index: index of current arg, valid after each successful call 35 | * to ieee80211_radiotap_iterator_next() 36 | * @this_arg: pointer to current radiotap arg; it is valid after each 37 | * call to ieee80211_radiotap_iterator_next() but also after 38 | * ieee80211_radiotap_iterator_init() where it will point to 39 | * the beginning of the actual data portion 40 | * @this_arg_size: length of the current arg, for convenience 41 | * @current_namespace: pointer to the current namespace definition 42 | * (or internally %NULL if the current namespace is unknown) 43 | * @is_radiotap_ns: indicates whether the current namespace is the default 44 | * radiotap namespace or not 45 | * 46 | * @overrides: override standard radiotap fields 47 | * @n_overrides: number of overrides 48 | * 49 | * @_rtheader: pointer to the radiotap header we are walking through 50 | * @_max_length: length of radiotap header in cpu byte ordering 51 | * @_arg_index: next argument index 52 | * @_arg: next argument pointer 53 | * @_next_bitmap: internal pointer to next present u32 54 | * @_bitmap_shifter: internal shifter for curr u32 bitmap, b0 set == arg present 55 | * @_vns: vendor namespace definitions 56 | * @_next_ns_data: beginning of the next namespace's data 57 | * @_reset_on_ext: internal; reset the arg index to 0 when going to the 58 | * next bitmap word 59 | * 60 | * Describes the radiotap parser state. Fields prefixed with an underscore 61 | * must not be used by users of the parser, only by the parser internally. 62 | */ 63 | 64 | struct ieee80211_radiotap_iterator { 65 | struct ieee80211_radiotap_header *_rtheader; 66 | const struct ieee80211_radiotap_vendor_namespaces *_vns; 67 | const struct ieee80211_radiotap_namespace *current_namespace; 68 | 69 | unsigned char *_arg, *_next_ns_data; 70 | uint32_t *_next_bitmap; 71 | 72 | unsigned char *this_arg; 73 | const struct radiotap_override *overrides; /* Only for RADIOTAP_SUPPORT_OVERRIDES */ 74 | int n_overrides; /* Only for RADIOTAP_SUPPORT_OVERRIDES */ 75 | int this_arg_index; 76 | int this_arg_size; 77 | 78 | int is_radiotap_ns; 79 | 80 | int _max_length; 81 | int _arg_index; 82 | uint32_t _bitmap_shifter; 83 | int _reset_on_ext; 84 | }; 85 | 86 | #ifdef __cplusplus 87 | #define CALLING_CONVENTION "C" 88 | #else 89 | #define CALLING_CONVENTION 90 | #endif 91 | 92 | extern CALLING_CONVENTION int ieee80211_radiotap_iterator_init( 93 | struct ieee80211_radiotap_iterator *iterator, 94 | struct ieee80211_radiotap_header *radiotap_header, 95 | int max_length, const struct ieee80211_radiotap_vendor_namespaces *vns); 96 | 97 | extern CALLING_CONVENTION int ieee80211_radiotap_iterator_next( 98 | struct ieee80211_radiotap_iterator *iterator); 99 | 100 | #endif /* __RADIOTAP_ITER_H */ 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *all your probe requests belong to us* 2 | 3 | # probemon 4 | *probemon* is a simple command line tool for logging data from 802.11 probe request frames with additional tools to plot mac presence over time and get statistics. 5 | 6 | This rewritten version of *probemon* uses an sqlite3 DB like in *probeSniffer*, as the log can quickly grow over time. It does not hop on channels as you lose more traffic by hopping than by simply staying on a major channel (like 1, 6, 11). 7 | 8 | There is a **flask app** `mapot.py` to serve charts/plots and stats of the sqlite3 db, in real time. An alternative is to use the other provided python tools: `plot.py` and `stats.py`. 9 | 10 | The dependencies are: 11 | * for probemon.py: scapy, manuf, lru-dict 12 | * for mapot.py: flask, flask-caching 13 | * for stats.py: None 14 | * for plot.py: matplotlib, cycler 15 | 16 | ## probemon.py 17 | You must enable monitor mode on your interface before running `probemon.py`. You can use, for example, `airmon-ng start wlan0` where wlan0 is your interface name. Now, use *wlan0mon* with `probemon.py`. 18 | 19 | ``` 20 | usage: probemon.py [-h] [-c CHANNEL] [-d DB] [-i INTERFACE] [-I IGNORE] [-s] 21 | [-v] 22 | 23 | a command line tool for logging 802.11 probe request 24 | 25 | optional arguments: 26 | -h, --help show this help message and exit 27 | -c CHANNEL, --channel CHANNEL 28 | the channel to listen on 29 | -d DB, --db DB database file name to use 30 | -i INTERFACE, --interface INTERFACE 31 | the capture interface to use 32 | -I IGNORE, --ignore IGNORE 33 | mac address to ignore 34 | -s, --stdout also log probe request to stdout 35 | -v, --version show version and exit 36 | ``` 37 | 38 | #### Note about non utf-8 SSID 39 | For SSID that we can't decode in utf-8, we can't store them as is in the db. So we encode them in base64 and store prepended with `b64_`. 40 | 41 | #### in C too 42 | There is a C implementation of *probemon* in the `c.d` directory. It is almost complete and has the same features as the python script. 43 | 44 | ## mapot.py 45 | A flask *app* to serve in real time the charts and probe requests stats. 46 | 47 | You can run the *app* with: 48 | 49 | python mapot.py 50 | 51 | More details in the [README.md](src/www/README.md) in `src/www` 52 | 53 | ## Other tools 54 | 55 | - the `plot.py` script simplifies the analysis of the recorded data by drawing a chart that plots the presence of 56 | mac addresses via the recorded probe request. 57 | 58 | ![Image of chart plotted with plot.py](screenshots/example.png) 59 | When displayed by the script, one can hover the mouse on the plot to get the mac address, and the timestamp. 60 | When you export to an image, you lose that feature but you can add a legend instead. 61 | 62 | 63 | - the `stats.py` script allows you to request the database about a specific mac address and get statistics about it, 64 | or filter based on a RSSI value. You can also specify the start time and end time of your request. 65 | 66 | ## Locally Administered Addresses 67 | 68 | > A locally administered address is assigned to a device by a network administrator, overriding the burned-in address. 69 | 70 | > Universally administered and locally administered addresses are distinguished by setting the second-least-significant bit of the first octet of the address. This bit is also referred to as the U/L bit, short for Universal/Local, which identifies how the address is administered. 71 | (source Wikipedia) 72 | 73 | These type of MAC addresses are used by recent various OS/wifi stack to send probe requests anonymously, and using at the same time randomization. 74 | 75 | So it defeats tracking and render probemon useless in that case. But not all devices are using this randomization technique, yet. 76 | 77 | ## Device behavior 78 | It should be noted that not all devices are equal. They vary a lot in behavior regarding of the probe requests (PR). This should be taken into account when analyzing the data collected. 79 | 80 | Depending on the type of device (PC/laptop/..., printer, mobile phone/tablet, IoT device), the OS used (Linux, Windows, Android, MacOS/iOS, unknown embedded OS, ...) the wifi chipset and/or the wifi/network stack, one device behave differently from one another when sending probe request. 81 | 82 | Even phone using the same OS like android, can behave differently: some send PR every 30 seconds, while others only send PR when the screen is unlocked. 83 | 84 | # Legality 85 | 86 | I am not a lawyer and I can't give you any advice regarding the law. 87 | But it might not be legal to collect and store probe requests in your country. 88 | 89 | Even simply listening to probe requests might not be legal. For example, given the interpretation of *Section 18 U.S. Code § 2511*, it might not be legal to intercept the MAC addresses of devices in a network (?) or in the vicinity (?) in the U.S.A. 90 | 91 | In Europe, if you are operating as a business, storing MAC addresses, which are considered private data, might be subject to the *GDPR*. 92 | 93 | This is only straching the surface of the legality or illegality of this software. 94 | 95 | Use with caution. 96 | -------------------------------------------------------------------------------- /src/c.d/manuf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "manuf.h" 6 | 7 | static inline char *str_strip(const char *s) 8 | { 9 | char *ret; 10 | if (s[strlen(s)-1] == '\n') { 11 | ret = strndup(s, strlen(s)-1); 12 | } else { 13 | ret = strdup(s); 14 | } 15 | return ret; 16 | } 17 | 18 | // from https://stackoverflow.com/a/779960/283067 19 | char *str_replace(const char *orig, const char *rep, const char *with) 20 | { 21 | char *result; 22 | char *ins; 23 | char *tmp; 24 | int len_rep; 25 | int len_with; 26 | int len_front; 27 | int count; 28 | 29 | if (!orig || !rep) 30 | return NULL; 31 | len_rep = strlen(rep); 32 | if (len_rep == 0) 33 | return NULL; 34 | if (!with) 35 | with = ""; 36 | len_with = strlen(with); 37 | 38 | ins = (char *)orig; 39 | for (count = 0; (tmp = strstr(ins, rep)); ++count) { 40 | ins = tmp + len_rep; 41 | } 42 | 43 | tmp = result = malloc(strlen(orig) + (len_with - len_rep) * count + 1); 44 | 45 | if (!result) 46 | return NULL; 47 | 48 | while (count--) { 49 | ins = strstr(orig, rep); 50 | len_front = ins - orig; 51 | tmp = strncpy(tmp, orig, len_front) + len_front; 52 | tmp = strcpy(tmp, with) + len_with; 53 | orig += len_front + len_rep; 54 | } 55 | strcpy(tmp, orig); 56 | return result; 57 | } 58 | 59 | void free_manuf_t(manuf_t *m) 60 | { 61 | free(m->short_oui); 62 | free(m->long_oui); 63 | free(m->comment); 64 | free(m); 65 | } 66 | 67 | int cmp_manuf_t(const void *u, const void *v) 68 | { 69 | uint64_t a = ((manuf_t *)u)->min; 70 | uint64_t b = ((manuf_t *)v)->min; 71 | if (ab) return 1; 73 | //assert(a == b); 74 | return 0; 75 | } 76 | 77 | int parse_mac_field(char *mac, manuf_t *m) 78 | { 79 | char *smac = str_strip(mac); 80 | char *tmp = str_replace(smac, "-", ":"); 81 | free(smac); 82 | char *mr = str_replace(tmp, ":", ""); 83 | free(tmp); 84 | if (strlen(mr) == 6) { 85 | char *min = malloc(13 * sizeof(char)); 86 | strncpy(min, mr, 6); 87 | strcat(min, "000000"); 88 | m->min = strtoll(min, NULL, 16); 89 | char *max = malloc(13 * sizeof(char)); 90 | strncpy(max, mr, 6); 91 | strcat(max, "ffffff"); 92 | m->max = strtoll(max, NULL, 16); 93 | free(min); 94 | free(max); 95 | } else if (strlen(mr) == 12) { 96 | m->min = strtoll(mr, NULL, 16); 97 | m->max = m->min; 98 | } else if (strlen(mr) == 15) { 99 | mr[12] = '\0'; 100 | char *tmp = strndup(mr, 11); 101 | m->min = strtoll(tmp, NULL, 16); 102 | free(tmp); 103 | tmp = strndup(mr+13, 2); 104 | int mask = (int)strtol(tmp, NULL, 10); 105 | free(tmp); 106 | uint64_t t = 0; 107 | for (int i=0; i<(48-mask)/4; i++) { 108 | t += 0xf << i; 109 | } 110 | m->max = m->min+t; 111 | } else { 112 | // we failed ! 113 | //printf("%s\n", mr); 114 | } 115 | free(mr); 116 | 117 | return 0; 118 | } 119 | 120 | manuf_t *parse_manuf_file(const char*path, size_t *ouidb_size) 121 | { 122 | FILE *manuf; 123 | char *line = NULL; 124 | size_t len = 0; 125 | ssize_t read; 126 | 127 | manuf = fopen(path, "r"); 128 | if (manuf == NULL) { 129 | return NULL; 130 | } 131 | 132 | *ouidb_size = 30*1024; 133 | int count = 0; 134 | manuf_t *ouidb = malloc(*ouidb_size * sizeof(manuf_t)); 135 | 136 | while ((read = getline(&line, &len, manuf)) != -1) { 137 | if (line[0] == '#' || strlen(line) == 1) { 138 | continue; 139 | } 140 | char *token, *str = line; 141 | int indx = 0; 142 | if (count == *ouidb_size) { 143 | *ouidb_size += 1; 144 | ouidb = realloc(ouidb, *ouidb_size * sizeof(manuf_t)); 145 | } 146 | ouidb[count].short_oui = NULL; 147 | ouidb[count].long_oui = NULL; 148 | ouidb[count].comment = NULL; 149 | while ((token = strsep(&str, "\t"))) { 150 | if (indx == 0) { 151 | parse_mac_field(token, &ouidb[count]); 152 | } else if (indx == 1) { 153 | char *stoken = str_strip(token); 154 | ouidb[count].short_oui = stoken; 155 | } else if (indx == 2) { 156 | char *stoken = str_strip(token); 157 | ouidb[count].long_oui = stoken; 158 | } else if (indx == 3) { 159 | char *stoken = str_strip(token); 160 | ouidb[count].comment = stoken; 161 | } 162 | indx++; 163 | } 164 | count++; 165 | } 166 | fclose(manuf); 167 | 168 | qsort(ouidb, *ouidb_size, sizeof(manuf_t), cmp_manuf_t); 169 | 170 | if (line) { 171 | free(line); 172 | } 173 | 174 | return ouidb; 175 | } 176 | 177 | int lookup_oui(char *mac, manuf_t *ouidb, size_t ouidb_size) 178 | { 179 | char *tmp = str_replace(mac, ":", ""); 180 | uint64_t mac_number = strtoll(tmp, NULL, 16); 181 | free(tmp); 182 | 183 | int count = 0; 184 | uint64_t val = ouidb[count].max; 185 | 186 | while ((count < ouidb_size) && mac_number > val) { 187 | count++; 188 | val = ouidb[count].max; 189 | } 190 | 191 | if (count == ouidb_size) { 192 | return -1; 193 | } 194 | if (mac_number > ouidb[count].min) { 195 | return count; 196 | } else { 197 | return -1; 198 | } 199 | } 200 | 201 | /* 202 | int main(void) 203 | { 204 | size_t ouidb_size; 205 | manuf_t *ouidb = parse_manuf_file("../manuf", &ouidb_size); 206 | 207 | int count = lookup_oui("da:a1:19:ac:b7:cc", ouidb, ouidb_size); 208 | if (count >= 0) { 209 | printf("%s %s\n", ouidb[count].short_oui, ouidb[count].long_oui); 210 | } 211 | 212 | size_t mu = sizeof(manuf_t) * ouidb_size; 213 | for (int i=0; i? and date? and date 21 | #define bswap_16 OSSwapInt16 22 | #define bswap_32 OSSwapInt32 23 | #define bswap_64 OSSwapInt64 24 | #include 25 | #define le16toh(x) OSSwapLittleToHostInt16(x) 26 | #define le32toh(x) OSSwapLittleToHostInt32(x) 27 | #define le64toh(x) OSSwapLittleToHostInt64(x) 28 | #endif 29 | 30 | /** 31 | * struct ieee82011_radiotap_header - base radiotap header 32 | */ 33 | struct ieee80211_radiotap_header { 34 | /** 35 | * @it_version: radiotap version, always 0 36 | */ 37 | uint8_t it_version; 38 | 39 | /** 40 | * @it_pad: padding (or alignment) 41 | */ 42 | uint8_t it_pad; 43 | 44 | /** 45 | * @it_len: overall radiotap header length 46 | */ 47 | uint16_t it_len; 48 | 49 | /** 50 | * @it_present: (first) present word 51 | */ 52 | uint32_t it_present; 53 | }; 54 | 55 | /* version is always 0 */ 56 | #define PKTHDR_RADIOTAP_VERSION 0 57 | 58 | /* see the radiotap website for the descriptions */ 59 | enum ieee80211_radiotap_presence { 60 | IEEE80211_RADIOTAP_TSFT = 0, 61 | IEEE80211_RADIOTAP_FLAGS = 1, 62 | IEEE80211_RADIOTAP_RATE = 2, 63 | IEEE80211_RADIOTAP_CHANNEL = 3, 64 | IEEE80211_RADIOTAP_FHSS = 4, 65 | IEEE80211_RADIOTAP_DBM_ANTSIGNAL = 5, 66 | IEEE80211_RADIOTAP_DBM_ANTNOISE = 6, 67 | IEEE80211_RADIOTAP_LOCK_QUALITY = 7, 68 | IEEE80211_RADIOTAP_TX_ATTENUATION = 8, 69 | IEEE80211_RADIOTAP_DB_TX_ATTENUATION = 9, 70 | IEEE80211_RADIOTAP_DBM_TX_POWER = 10, 71 | IEEE80211_RADIOTAP_ANTENNA = 11, 72 | IEEE80211_RADIOTAP_DB_ANTSIGNAL = 12, 73 | IEEE80211_RADIOTAP_DB_ANTNOISE = 13, 74 | IEEE80211_RADIOTAP_RX_FLAGS = 14, 75 | IEEE80211_RADIOTAP_TX_FLAGS = 15, 76 | IEEE80211_RADIOTAP_RTS_RETRIES = 16, 77 | IEEE80211_RADIOTAP_DATA_RETRIES = 17, 78 | /* 18 is XChannel, but it's not defined yet */ 79 | IEEE80211_RADIOTAP_MCS = 19, 80 | IEEE80211_RADIOTAP_AMPDU_STATUS = 20, 81 | IEEE80211_RADIOTAP_VHT = 21, 82 | IEEE80211_RADIOTAP_TIMESTAMP = 22, 83 | 84 | /* valid in every it_present bitmap, even vendor namespaces */ 85 | IEEE80211_RADIOTAP_RADIOTAP_NAMESPACE = 29, 86 | IEEE80211_RADIOTAP_VENDOR_NAMESPACE = 30, 87 | IEEE80211_RADIOTAP_EXT = 31 88 | }; 89 | 90 | /* for IEEE80211_RADIOTAP_FLAGS */ 91 | enum ieee80211_radiotap_flags { 92 | IEEE80211_RADIOTAP_F_CFP = 0x01, 93 | IEEE80211_RADIOTAP_F_SHORTPRE = 0x02, 94 | IEEE80211_RADIOTAP_F_WEP = 0x04, 95 | IEEE80211_RADIOTAP_F_FRAG = 0x08, 96 | IEEE80211_RADIOTAP_F_FCS = 0x10, 97 | IEEE80211_RADIOTAP_F_DATAPAD = 0x20, 98 | IEEE80211_RADIOTAP_F_BADFCS = 0x40, 99 | }; 100 | 101 | /* for IEEE80211_RADIOTAP_CHANNEL */ 102 | enum ieee80211_radiotap_channel_flags { 103 | IEEE80211_CHAN_CCK = 0x0020, 104 | IEEE80211_CHAN_OFDM = 0x0040, 105 | IEEE80211_CHAN_2GHZ = 0x0080, 106 | IEEE80211_CHAN_5GHZ = 0x0100, 107 | IEEE80211_CHAN_DYN = 0x0400, 108 | IEEE80211_CHAN_HALF = 0x4000, 109 | IEEE80211_CHAN_QUARTER = 0x8000, 110 | }; 111 | 112 | /* for IEEE80211_RADIOTAP_RX_FLAGS */ 113 | enum ieee80211_radiotap_rx_flags { 114 | IEEE80211_RADIOTAP_F_RX_BADPLCP = 0x0002, 115 | }; 116 | 117 | /* for IEEE80211_RADIOTAP_TX_FLAGS */ 118 | enum ieee80211_radiotap_tx_flags { 119 | IEEE80211_RADIOTAP_F_TX_FAIL = 0x0001, 120 | IEEE80211_RADIOTAP_F_TX_CTS = 0x0002, 121 | IEEE80211_RADIOTAP_F_TX_RTS = 0x0004, 122 | IEEE80211_RADIOTAP_F_TX_NOACK = 0x0008, 123 | }; 124 | 125 | /* for IEEE80211_RADIOTAP_MCS "have" flags */ 126 | enum ieee80211_radiotap_mcs_have { 127 | IEEE80211_RADIOTAP_MCS_HAVE_BW = 0x01, 128 | IEEE80211_RADIOTAP_MCS_HAVE_MCS = 0x02, 129 | IEEE80211_RADIOTAP_MCS_HAVE_GI = 0x04, 130 | IEEE80211_RADIOTAP_MCS_HAVE_FMT = 0x08, 131 | IEEE80211_RADIOTAP_MCS_HAVE_FEC = 0x10, 132 | IEEE80211_RADIOTAP_MCS_HAVE_STBC = 0x20, 133 | }; 134 | 135 | enum ieee80211_radiotap_mcs_flags { 136 | IEEE80211_RADIOTAP_MCS_BW_MASK = 0x03, 137 | IEEE80211_RADIOTAP_MCS_BW_20 = 0, 138 | IEEE80211_RADIOTAP_MCS_BW_40 = 1, 139 | IEEE80211_RADIOTAP_MCS_BW_20L = 2, 140 | IEEE80211_RADIOTAP_MCS_BW_20U = 3, 141 | 142 | IEEE80211_RADIOTAP_MCS_SGI = 0x04, 143 | IEEE80211_RADIOTAP_MCS_FMT_GF = 0x08, 144 | IEEE80211_RADIOTAP_MCS_FEC_LDPC = 0x10, 145 | IEEE80211_RADIOTAP_MCS_STBC_MASK = 0x60, 146 | IEEE80211_RADIOTAP_MCS_STBC_1 = 1, 147 | IEEE80211_RADIOTAP_MCS_STBC_2 = 2, 148 | IEEE80211_RADIOTAP_MCS_STBC_3 = 3, 149 | IEEE80211_RADIOTAP_MCS_STBC_SHIFT = 5, 150 | }; 151 | 152 | /* for IEEE80211_RADIOTAP_AMPDU_STATUS */ 153 | enum ieee80211_radiotap_ampdu_flags { 154 | IEEE80211_RADIOTAP_AMPDU_REPORT_ZEROLEN = 0x0001, 155 | IEEE80211_RADIOTAP_AMPDU_IS_ZEROLEN = 0x0002, 156 | IEEE80211_RADIOTAP_AMPDU_LAST_KNOWN = 0x0004, 157 | IEEE80211_RADIOTAP_AMPDU_IS_LAST = 0x0008, 158 | IEEE80211_RADIOTAP_AMPDU_DELIM_CRC_ERR = 0x0010, 159 | IEEE80211_RADIOTAP_AMPDU_DELIM_CRC_KNOWN = 0x0020, 160 | }; 161 | 162 | /* for IEEE80211_RADIOTAP_VHT */ 163 | enum ieee80211_radiotap_vht_known { 164 | IEEE80211_RADIOTAP_VHT_KNOWN_STBC = 0x0001, 165 | IEEE80211_RADIOTAP_VHT_KNOWN_TXOP_PS_NA = 0x0002, 166 | IEEE80211_RADIOTAP_VHT_KNOWN_GI = 0x0004, 167 | IEEE80211_RADIOTAP_VHT_KNOWN_SGI_NSYM_DIS = 0x0008, 168 | IEEE80211_RADIOTAP_VHT_KNOWN_LDPC_EXTRA_OFDM_SYM = 0x0010, 169 | IEEE80211_RADIOTAP_VHT_KNOWN_BEAMFORMED = 0x0020, 170 | IEEE80211_RADIOTAP_VHT_KNOWN_BANDWIDTH = 0x0040, 171 | IEEE80211_RADIOTAP_VHT_KNOWN_GROUP_ID = 0x0080, 172 | IEEE80211_RADIOTAP_VHT_KNOWN_PARTIAL_AID = 0x0100, 173 | }; 174 | 175 | enum ieee80211_radiotap_vht_flags { 176 | IEEE80211_RADIOTAP_VHT_FLAG_STBC = 0x01, 177 | IEEE80211_RADIOTAP_VHT_FLAG_TXOP_PS_NA = 0x02, 178 | IEEE80211_RADIOTAP_VHT_FLAG_SGI = 0x04, 179 | IEEE80211_RADIOTAP_VHT_FLAG_SGI_NSYM_M10_9 = 0x08, 180 | IEEE80211_RADIOTAP_VHT_FLAG_LDPC_EXTRA_OFDM_SYM = 0x10, 181 | IEEE80211_RADIOTAP_VHT_FLAG_BEAMFORMED = 0x20, 182 | }; 183 | 184 | enum ieee80211_radiotap_vht_coding { 185 | IEEE80211_RADIOTAP_CODING_LDPC_USER0 = 0x01, 186 | IEEE80211_RADIOTAP_CODING_LDPC_USER1 = 0x02, 187 | IEEE80211_RADIOTAP_CODING_LDPC_USER2 = 0x04, 188 | IEEE80211_RADIOTAP_CODING_LDPC_USER3 = 0x08, 189 | }; 190 | 191 | /* for IEEE80211_RADIOTAP_TIMESTAMP */ 192 | enum ieee80211_radiotap_timestamp_unit_spos { 193 | IEEE80211_RADIOTAP_TIMESTAMP_UNIT_MASK = 0x000F, 194 | IEEE80211_RADIOTAP_TIMESTAMP_UNIT_MS = 0x0000, 195 | IEEE80211_RADIOTAP_TIMESTAMP_UNIT_US = 0x0001, 196 | IEEE80211_RADIOTAP_TIMESTAMP_UNIT_NS = 0x0003, 197 | IEEE80211_RADIOTAP_TIMESTAMP_SPOS_MASK = 0x00F0, 198 | IEEE80211_RADIOTAP_TIMESTAMP_SPOS_BEGIN_MDPU = 0x0000, 199 | IEEE80211_RADIOTAP_TIMESTAMP_SPOS_PLCP_SIG_ACQ = 0x0010, 200 | IEEE80211_RADIOTAP_TIMESTAMP_SPOS_EO_PPDU = 0x0020, 201 | IEEE80211_RADIOTAP_TIMESTAMP_SPOS_EO_MPDU = 0x0030, 202 | IEEE80211_RADIOTAP_TIMESTAMP_SPOS_UNKNOWN = 0x00F0, 203 | }; 204 | 205 | enum ieee80211_radiotap_timestamp_flags { 206 | IEEE80211_RADIOTAP_TIMESTAMP_FLAG_64BIT = 0x00, 207 | IEEE80211_RADIOTAP_TIMESTAMP_FLAG_32BIT = 0x01, 208 | IEEE80211_RADIOTAP_TIMESTAMP_FLAG_ACCURACY = 0x02, 209 | }; 210 | 211 | #endif /* __RADIOTAP_H */ 212 | -------------------------------------------------------------------------------- /src/c.d/parsers.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "radiotap_iter.h" 9 | #include "parsers.h" 10 | #include "logger_thread.h" 11 | #include "base64.h" 12 | #include "config.h" 13 | 14 | int8_t parse_radiotap_header(const uint8_t * packet, uint16_t * freq, int8_t * rssi) 15 | { 16 | // parse radiotap header to get frequency and rssi 17 | // returns radiotap header size or -1 on error 18 | struct ieee80211_radiotap_header *rtaphdr; 19 | rtaphdr = (struct ieee80211_radiotap_header *) (packet); 20 | int8_t offset = (int8_t) rtaphdr->it_len; 21 | 22 | struct ieee80211_radiotap_iterator iter; 23 | //uint16_t flags = 0; 24 | int8_t r; 25 | 26 | static const struct radiotap_align_size align_size_000000_00[] = { 27 | [0] = {.align = 1,.size = 4, }, 28 | [52] = {.align = 1,.size = 4, }, 29 | }; 30 | 31 | static const struct ieee80211_radiotap_namespace vns_array[] = { 32 | { 33 | .oui = 0x000000, 34 | .subns = 0, 35 | .n_bits = sizeof(align_size_000000_00), 36 | .align_size = align_size_000000_00, 37 | }, 38 | }; 39 | 40 | static const struct ieee80211_radiotap_vendor_namespaces vns = { 41 | .ns = vns_array, 42 | .n_ns = sizeof(vns_array) / sizeof(vns_array[0]), 43 | }; 44 | 45 | int err = 46 | ieee80211_radiotap_iterator_init(&iter, rtaphdr, rtaphdr->it_len, 47 | &vns); 48 | if (err) { 49 | printf("Error: malformed radiotap header (init returned %d)\n", err); 50 | return -1; 51 | } 52 | 53 | *freq = 0; 54 | *rssi = 0; 55 | // iterate through radiotap fields and look for frequency and rssi 56 | while (!(err = ieee80211_radiotap_iterator_next(&iter))) { 57 | if (iter.this_arg_index == IEEE80211_RADIOTAP_CHANNEL) { 58 | assert(iter.this_arg_size == 4); // XXX: why ? 59 | *freq = iter.this_arg[0] + (iter.this_arg[1] << 8); 60 | //flags = iter.this_arg[2] + (iter.this_arg[3] << 8); 61 | } 62 | if (iter.this_arg_index == IEEE80211_RADIOTAP_DBM_ANTSIGNAL) { 63 | r = (int8_t) * iter.this_arg; 64 | if (r != 0) 65 | *rssi = r; // XXX: why do we get multiple dBm_antSignal with 0 value after the first one ? 66 | } 67 | if (*freq != 0 && *rssi != 0) 68 | break; 69 | } 70 | return offset; 71 | } 72 | 73 | void parse_probereq_frame(const uint8_t *packet, uint32_t packet_len, 74 | int8_t offset, char **mac, uint8_t **ssid, uint8_t *ssid_len) 75 | { 76 | *mac = malloc(18 * sizeof(char)); 77 | // parse the probe request frame to look for mac and Information Element we need (ssid) 78 | // SA 79 | const uint8_t *sa_addr = packet + offset + 2 + 2 + 6; // FC + duration + DA 80 | sprintf(*mac, "%02x:%02x:%02x:%02x:%02x:%02x", sa_addr[0], 81 | sa_addr[1], sa_addr[2], sa_addr[3], sa_addr[4], sa_addr[5]); 82 | 83 | *ssid = NULL; 84 | uint8_t *ie = (uint8_t *)sa_addr + 6 + 6 + 2 ; // + SA + BSSID + Seqctl 85 | uint8_t ie_len = *(ie + 1); 86 | *ssid_len = 0; 87 | 88 | // iterate over Information Element to look for SSID 89 | while (ie < packet + packet_len) { 90 | if ((ie + ie_len + 2 <= packet + packet_len)) { // just double check that this is an IE with length inside packet 91 | if (*ie == 0) { // SSID aka IE with id 0 92 | *ssid_len = *(ie + 1); 93 | if (*ssid_len > 32) { 94 | fprintf(stderr, "Warning: detected SSID greater than 32 bytes. Cutting it to 32 bytes."); 95 | *ssid_len = 32; 96 | } 97 | *ssid = malloc((*ssid_len) * sizeof(uint8_t)); // AP name 98 | memcpy(*ssid, ie+2, *ssid_len); 99 | break; 100 | } 101 | } 102 | ie = ie + ie_len + 2; 103 | ie_len = *(ie + 1); 104 | } 105 | return; 106 | } 107 | 108 | char *probereq_to_str(probereq_t pr) 109 | { 110 | char tmp[1024], vendor[MAX_VENDOR_LENGTH+1], ssid[MAX_SSID_LENGTH+1], datetime[20], rssi[5]; 111 | char *pr_str; 112 | 113 | strftime(datetime, 20, "%Y-%m-%d %H:%M:%S", localtime(&pr.tv.tv_sec)); 114 | 115 | char *first = strndup(pr.mac, 2); 116 | bool is_laa = strtoll(first, NULL, 16) & 0x2; 117 | free(first); 118 | 119 | // cut or pad vendor string 120 | if (strlen(pr.vendor) >= MAX_VENDOR_LENGTH) { 121 | strncpy(vendor, pr.vendor, MAX_VENDOR_LENGTH-1); 122 | for (int i=MAX_VENDOR_LENGTH-3; i= MAX_SSID_LENGTH) { 147 | strncpy(ssid, tmp, MAX_SSID_LENGTH); 148 | for (int i=MAX_SSID_LENGTH-3; i 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "lruc.h" 9 | 10 | // ------------------------------------------ 11 | // private functions 12 | // ------------------------------------------ 13 | // MurmurHash2, by Austin Appleby 14 | // http://sites.google.com/site/murmurhash/ 15 | uint32_t lruc_hash(lruc *cache, void *key, uint32_t key_length) 16 | { 17 | uint32_t m = 0x5bd1e995; 18 | uint32_t r = 24; 19 | uint32_t h = cache->seed ^ key_length; 20 | char* data = (char *)key; 21 | 22 | while (key_length >= 4) { 23 | uint32_t k = *(uint32_t *)data; 24 | k *= m; 25 | k ^= k >> r; 26 | k *= m; 27 | h *= m; 28 | h ^= k; 29 | data += 4; 30 | key_length -= 4; 31 | } 32 | 33 | switch(key_length) { 34 | case 3: h ^= data[2] << 16; 35 | case 2: h ^= data[1] << 8; 36 | case 1: h ^= data[0]; 37 | h *= m; 38 | }; 39 | 40 | h ^= h >> 13; 41 | h *= m; 42 | h ^= h >> 15; 43 | return h % cache->hash_table_size; 44 | } 45 | 46 | // compare a key against an existing item's key 47 | int lruc_cmp_keys(lruc_item *item, void *key, uint32_t key_length) 48 | { 49 | if (key_length != item->key_length) 50 | return 1; 51 | else 52 | return memcmp(key, item->key, key_length); 53 | } 54 | 55 | // remove an item and push it to the free items queue 56 | void lruc_remove_item(lruc *cache, lruc_item *prev, lruc_item *item, uint32_t hash_index) 57 | { 58 | if (prev) 59 | prev->next = item->next; 60 | else 61 | cache->items[hash_index] = (lruc_item *) item->next; 62 | 63 | // free memory and update the free memory counter 64 | cache->free_memory += item->value_length; 65 | free(item->value); 66 | free(item->key); 67 | 68 | // push the item to the free items queue 69 | memset(item, 0, sizeof(lruc_item)); 70 | item->next = cache->free_items; 71 | cache->free_items = item; 72 | } 73 | 74 | // remove the least recently used item 75 | // TODO: we can optimise this by finding the n lru items, where n = required_space / average_length 76 | void lruc_remove_lru_item(lruc *cache) 77 | { 78 | lruc_item *min_item = NULL, *min_prev = NULL; 79 | lruc_item *item = NULL, *prev = NULL; 80 | uint32_t i = 0, min_index = -1; 81 | uint64_t min_access_count = -1; 82 | 83 | for (; i < cache->hash_table_size; i++) { 84 | item = cache->items[i]; 85 | prev = NULL; 86 | 87 | while (item) { 88 | if (item->access_count < min_access_count || min_access_count == -1) { 89 | min_access_count = item->access_count; 90 | min_item = item; 91 | min_prev = prev; 92 | min_index = i; 93 | } 94 | prev = item; 95 | item = item->next; 96 | } 97 | } 98 | 99 | if (min_item) 100 | lruc_remove_item(cache, min_prev, min_item, min_index); 101 | } 102 | 103 | // pop an existing item off the free queue, or create a new one 104 | lruc_item *lruc_pop_or_create_item(lruc *cache) 105 | { 106 | lruc_item *item = NULL; 107 | 108 | if (cache->free_items) { 109 | item = cache->free_items; 110 | cache->free_items = item->next; 111 | } else { 112 | item = (lruc_item *) calloc(sizeof(lruc_item), 1); 113 | } 114 | 115 | return item; 116 | } 117 | 118 | // error helpers 119 | #define error_for(conditions, error) if(conditions) {return error;} 120 | #define test_for_missing_cache() error_for(!cache, LRUC_MISSING_CACHE) 121 | #define test_for_missing_key() error_for(!key || key_length == 0, LRUC_MISSING_KEY) 122 | #define test_for_missing_value() error_for(!value || value_length == 0, LRUC_MISSING_VALUE) 123 | #define test_for_value_too_large() error_for(value_length > cache->total_memory, LRUC_VALUE_TOO_LARGE) 124 | 125 | // lock helpers 126 | #define lock_cache() if (pthread_mutex_lock(cache->mutex)) {\ 127 | perror("LRU Cache unable to obtain mutex lock");\ 128 | return LRUC_PTHREAD_ERROR;\ 129 | } 130 | 131 | #define unlock_cache() if (pthread_mutex_unlock(cache->mutex)) {\ 132 | perror("LRU Cache unable to release mutex lock");\ 133 | return LRUC_PTHREAD_ERROR;\ 134 | } 135 | 136 | // ------------------------------------------ 137 | // public API 138 | // ------------------------------------------ 139 | lruc *lruc_new(uint64_t cache_size, uint32_t average_length) 140 | { 141 | // create the cache 142 | lruc *cache = (lruc *) calloc(sizeof(lruc), 1); 143 | if (!cache) { 144 | perror("LRU Cache unable to create cache object"); 145 | return NULL; 146 | } 147 | cache->hash_table_size = cache_size / average_length; 148 | cache->average_item_length = average_length; 149 | cache->free_memory = cache_size; 150 | cache->total_memory = cache_size; 151 | cache->seed = time(NULL); 152 | 153 | // size the hash table to a guestimate of the number of slots required (assuming a perfect hash) 154 | cache->items = (lruc_item **) calloc(sizeof(lruc_item *), cache->hash_table_size); 155 | if (!cache->items) { 156 | perror("LRU Cache unable to create cache hash table"); 157 | free(cache); 158 | return NULL; 159 | } 160 | 161 | // all cache calls are guarded by a mutex 162 | cache->mutex = (pthread_mutex_t *) malloc(sizeof(pthread_mutex_t)); 163 | if (pthread_mutex_init(cache->mutex, NULL)) { 164 | perror("LRU Cache unable to initialise mutex"); 165 | free(cache->items); 166 | free(cache); 167 | return NULL; 168 | } 169 | return cache; 170 | } 171 | 172 | lruc_error lruc_free(lruc *cache) 173 | { 174 | test_for_missing_cache(); 175 | 176 | // free each of the cached items, and the hash table 177 | lruc_item *item = NULL, *next = NULL; 178 | uint32_t i = 0; 179 | if (cache->items) { 180 | for (; i < cache->hash_table_size; i++) { 181 | item = cache->items[i]; 182 | while (item) { 183 | next = (lruc_item *) item->next; 184 | free(item->key); 185 | free(item->value); 186 | free(item); 187 | item = next; 188 | } 189 | } 190 | free(cache->items); 191 | } 192 | 193 | if (cache->free_items) { 194 | item = cache->free_items; 195 | while(item) { 196 | next = (lruc_item *) item->next; 197 | free(item); 198 | item = next; 199 | } 200 | } 201 | 202 | // free the cache 203 | if (cache->mutex) { 204 | if (pthread_mutex_destroy(cache->mutex)) { 205 | perror("LRU Cache unable to destroy mutex"); 206 | return LRUC_PTHREAD_ERROR; 207 | } 208 | free(cache->mutex); 209 | } 210 | free(cache); 211 | 212 | return LRUC_NO_ERROR; 213 | } 214 | 215 | lruc_error lruc_set(lruc *cache, void *key, uint32_t key_length, void *value, uint32_t value_length) 216 | { 217 | test_for_missing_cache(); 218 | test_for_missing_key(); 219 | test_for_missing_value(); 220 | test_for_value_too_large(); 221 | lock_cache(); 222 | 223 | // see if the key already exists 224 | uint32_t hash_index = lruc_hash(cache, key, key_length), required = 0; 225 | lruc_item *item = NULL, *prev = NULL; 226 | item = cache->items[hash_index]; 227 | 228 | while (item && lruc_cmp_keys(item, key, key_length)) { 229 | prev = item; 230 | item = (lruc_item *) item->next; 231 | } 232 | 233 | if (item) { 234 | // update the value and value_lengths 235 | required = value_length - item->value_length; 236 | free(item->value); 237 | item->value = value; 238 | item->value_length = value_length; 239 | 240 | } else { 241 | // insert a new item 242 | item = lruc_pop_or_create_item(cache); 243 | item->value = value; 244 | item->key = key; 245 | item->value_length = value_length; 246 | item->key_length = key_length; 247 | required = value_length; 248 | 249 | if (prev) 250 | prev->next = item; 251 | else 252 | cache->items[hash_index] = item; 253 | } 254 | item->access_count = ++cache->access_count; 255 | 256 | // remove as many items as necessary to free enough space 257 | if (required > 0 && required > cache->free_memory) { 258 | while (cache->free_memory < required) 259 | lruc_remove_lru_item(cache); 260 | } 261 | cache->free_memory -= required; 262 | unlock_cache(); 263 | return LRUC_NO_ERROR; 264 | } 265 | 266 | lruc_error lruc_get(lruc *cache, void *key, uint32_t key_length, void **value) 267 | { 268 | test_for_missing_cache(); 269 | test_for_missing_key(); 270 | lock_cache(); 271 | 272 | // loop until we find the item, or hit the end of a chain 273 | uint32_t hash_index = lruc_hash(cache, key, key_length); 274 | lruc_item *item = cache->items[hash_index]; 275 | 276 | while (item && lruc_cmp_keys(item, key, key_length)) 277 | item = (lruc_item *) item->next; 278 | 279 | if (item) { 280 | *value = item->value; 281 | item->access_count = ++cache->access_count; 282 | } else { 283 | *value = NULL; 284 | } 285 | 286 | unlock_cache(); 287 | return LRUC_NO_ERROR; 288 | } 289 | 290 | lruc_error lruc_delete(lruc *cache, void *key, uint32_t key_length) 291 | { 292 | test_for_missing_cache(); 293 | test_for_missing_key(); 294 | lock_cache(); 295 | 296 | // loop until we find the item, or hit the end of a chain 297 | lruc_item *item = NULL, *prev = NULL; 298 | uint32_t hash_index = lruc_hash(cache, key, key_length); 299 | item = cache->items[hash_index]; 300 | 301 | while (item && lruc_cmp_keys(item, key, key_length)) { 302 | prev = item; 303 | item = (lruc_item *) item->next; 304 | } 305 | 306 | if (item) { 307 | lruc_remove_item(cache, prev, item, hash_index); 308 | } 309 | 310 | unlock_cache(); 311 | return LRUC_NO_ERROR; 312 | } 313 | 314 | lruc_error lruc_print(lruc *cache) 315 | { 316 | test_for_missing_cache(); 317 | lock_cache(); 318 | 319 | // loop over all the items 320 | lruc_item *item = NULL; 321 | uint32_t i = 0; 322 | int64_t *value; 323 | 324 | if (cache->items) { 325 | printf("["); 326 | int first = 1; 327 | for (; i < cache->hash_table_size; i++) { 328 | item = cache->items[i]; 329 | while (item) { 330 | value = (int64_t *)item->value; 331 | printf("%s%"PRId64, first ? "" : " ", *value); 332 | if (first) first = 0; 333 | item = (lruc_item *) item->next; 334 | } 335 | } 336 | printf("]\n"); 337 | } 338 | 339 | unlock_cache(); 340 | return LRUC_NO_ERROR; 341 | } 342 | -------------------------------------------------------------------------------- /src/www/templates/index.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mapot 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 36 | 37 | 38 |
39 |
40 |

MapotMac addresses probe requests over time

41 |

DB mtimeLast modification of database: 2001-01-01 00:00:00

42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
MACCountMinMaxAvgMedFirst seenLast seenProbed SSIDs

Please wait while we are collecting the data

57 |
58 | 59 |

Your browser does not support the canvas element

60 |
61 |
62 |
63 |
64 |
65 |

66 | Downloading data... 67 |

68 | Raw logs 69 |

70 |
71 |
72 |
73 | 74 | 75 | 154 | 155 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /src/c.d/probemon.c: -------------------------------------------------------------------------------- 1 | // this implementation borrows most of its code from the twin project ssid-logger 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #ifdef HAS_SYS_STAT_H 15 | #include 16 | #endif 17 | #include 18 | 19 | #include "queue.h" 20 | #include "parsers.h" 21 | #include "logger_thread.h" 22 | #include "db.h" 23 | #include "manuf.h" 24 | #include "config_yaml.h" 25 | #include "config.h" 26 | 27 | pcap_t *handle; // global, to use it in sigint_handler 28 | queue_t *queue; // queue to hold parsed ap infos 29 | 30 | pthread_t logger; 31 | pthread_mutex_t mutex_queue = PTHREAD_MUTEX_INITIALIZER; 32 | sem_t queue_empty; 33 | sem_t queue_full; 34 | struct timespec start_ts_queue; 35 | bool option_stdout; 36 | 37 | sqlite3 *db = NULL; 38 | int ret = 0; 39 | 40 | size_t ouidb_size; 41 | manuf_t *ouidb; 42 | uint64_t *ignored = NULL; 43 | int ignored_count = 0; 44 | 45 | void sigint_handler(int s) 46 | { 47 | // stop pcap capture loop 48 | pcap_breakloop(handle); 49 | } 50 | 51 | void process_packet(uint8_t * args, const struct pcap_pkthdr *header, const uint8_t *packet) 52 | { 53 | uint16_t freq; 54 | int8_t rssi; 55 | // parse radiotap header 56 | int8_t offset = parse_radiotap_header(packet, &freq, &rssi); 57 | if (offset < 0) { 58 | return; 59 | } 60 | 61 | char *mac; 62 | uint8_t ssid_len, *ssid; 63 | 64 | parse_probereq_frame(packet, header->len, offset, &mac, &ssid, &ssid_len); 65 | 66 | probereq_t *pr = malloc(sizeof(probereq_t)); 67 | pr->tv.tv_sec = header->ts.tv_sec; 68 | pr->tv.tv_usec = header->ts.tv_usec; 69 | pr->mac = mac; 70 | pr->ssid = ssid; 71 | pr->ssid_len = ssid_len; 72 | pr->vendor = NULL; 73 | pr->rssi = rssi; 74 | 75 | sem_wait(&queue_full); 76 | pthread_mutex_lock(&mutex_queue); 77 | enqueue(queue, pr); 78 | pthread_mutex_unlock(&mutex_queue); 79 | sem_post(&queue_empty); 80 | } 81 | 82 | void usage(void) 83 | { 84 | printf("Usage: probemon -i IFACE -c CHANNEL [-d DB_NAME] [-m MANUF_NAME] [-s]\n"); 85 | printf(" -i IFACE interface to use\n" 86 | " -c CHANNEL channel to sniff on\n" 87 | " -d DB_NAME explicitly set the db filename\n" 88 | " -m MANUF_NAME path to manuf file\n" 89 | " -s also log probe requests to stdout\n" 90 | ); 91 | } 92 | 93 | void parse_args(int argc, char *argv[], char **iface, uint8_t *channel, char **manuf_name, char **db_name, bool *option_stdout) 94 | { 95 | int opt; 96 | char *option_channel = NULL; 97 | char *option_db_name = NULL; 98 | char *option_manuf_name = NULL; 99 | 100 | *option_stdout = false; 101 | while ((opt = getopt(argc, argv, "c:hi:d:m:sV")) != -1) { 102 | switch (opt) { 103 | case 'h': 104 | usage(); 105 | exit(EXIT_SUCCESS); 106 | break; 107 | case 'i': 108 | *iface = optarg; 109 | break; 110 | case 'c': 111 | option_channel = optarg; 112 | break; 113 | case 'd': 114 | option_db_name = optarg; 115 | break; 116 | case 'm': 117 | option_manuf_name = optarg; 118 | break; 119 | case 's': 120 | *option_stdout = true; 121 | break; 122 | case 'V': 123 | printf("%s %s\nCopyright © 2020 solsTice d'Hiver\nLicense GPLv3+: GNU GPL version 3\n", NAME, VERSION); 124 | exit(EXIT_SUCCESS); 125 | break; 126 | case '?': 127 | usage(); 128 | exit(EXIT_FAILURE); 129 | default: 130 | usage(); 131 | exit(EXIT_FAILURE); 132 | } 133 | } 134 | 135 | if (*iface == NULL) { 136 | fprintf(stderr, "Error: no interface selected\n"); 137 | exit(EXIT_FAILURE); 138 | } 139 | //printf("The device you entered: %s\n", iface); 140 | 141 | if (option_channel == NULL) { 142 | fprintf(stderr, "Error: no channel defined\n"); 143 | exit(EXIT_FAILURE); 144 | } else { 145 | *channel = (uint8_t)strtol(option_channel, NULL, 10); 146 | } 147 | 148 | if (option_db_name == NULL) { 149 | *db_name = strdup(DB_NAME); 150 | } else { 151 | *db_name = strdup(option_db_name); 152 | } 153 | 154 | if (option_manuf_name == NULL) { 155 | *manuf_name = strdup(MANUF_NAME); 156 | } else { 157 | *manuf_name = strdup(option_manuf_name); 158 | } 159 | } 160 | 161 | void change_channel(const char *iface, uint8_t channel) 162 | { 163 | // don't do anything for channel 0 164 | if (channel == 0) return; 165 | 166 | // look up for iw in system 167 | char *paths[] = {"/sbin/iw", "/bin/iw", "/usr/sbin/iw", "/usr/bin/iw"}; 168 | char *iw; 169 | bool found = false; 170 | int i = 0; 171 | while (!found && i < sizeof(paths)/sizeof(char *)) { 172 | if (access(paths[i], F_OK) != -1) { 173 | iw = paths[i]; 174 | found = true; 175 | break; 176 | } 177 | i++; 178 | } 179 | if (!found) { 180 | fprintf(stderr, "Error: can't find iw on system (in /{usr/}{s}bin)\n"); 181 | exit(EXIT_FAILURE); 182 | } 183 | // change the channel to listen on 184 | char cmd[128]; 185 | snprintf(cmd, 128, "%s dev %s set channel %d", iw, iface, channel); 186 | if (system(cmd)) { 187 | fprintf(stderr, "Error: can't change to channel %d with iw on interface %s\n", channel, iface); 188 | exit(EXIT_FAILURE); 189 | } 190 | } 191 | 192 | void initiliaze_pcap(pcap_t **handle, const char *iface) 193 | { 194 | char errbuf[PCAP_ERRBUF_SIZE]; 195 | // just check if iface is in the list of known devices 196 | pcap_if_t *devs = NULL; 197 | if (pcap_findalldevs(&devs, errbuf) == 0) { 198 | pcap_if_t *d = devs; 199 | bool found = false; 200 | while (!found && d != NULL) { 201 | if ((strlen(d->name) == strlen(iface)) 202 | && (memcmp(d->name, iface, strlen(iface)) == 0)) { 203 | found = true; 204 | break; 205 | } 206 | d = d->next; 207 | } 208 | pcap_freealldevs(devs); 209 | if (!found) { 210 | fprintf(stderr, "Error: %s is not a known interface.\n", iface); 211 | exit(EXIT_FAILURE); 212 | } 213 | } 214 | 215 | *handle = pcap_create(iface, errbuf); 216 | if (*handle == NULL) { 217 | fprintf(stderr, "Error: unable to create pcap handle: %s\n", errbuf); 218 | exit(EXIT_FAILURE); 219 | } 220 | pcap_set_snaplen(*handle, SNAP_LEN); 221 | pcap_set_timeout(*handle, 1000); 222 | pcap_set_promisc(*handle, 1); 223 | 224 | if (pcap_activate(*handle)) { 225 | pcap_perror(*handle, "Error: "); 226 | exit(EXIT_FAILURE); 227 | } 228 | // only capture packets received by interface 229 | if (pcap_setdirection(*handle, PCAP_D_IN)) { 230 | pcap_perror(*handle, "Error: "); 231 | pcap_close(*handle); 232 | exit(EXIT_FAILURE); 233 | } 234 | 235 | int *dlt_buf, dlt_buf_len; 236 | dlt_buf_len = pcap_list_datalinks(*handle, &dlt_buf); 237 | if (dlt_buf_len < 0) { 238 | pcap_perror(*handle, "Error: "); 239 | pcap_close(*handle); 240 | exit(EXIT_FAILURE); 241 | } 242 | bool found = false; 243 | for (int i=0; i< dlt_buf_len; i++) { 244 | if (dlt_buf[i] == DLT_IEEE802_11_RADIO) { 245 | found = true; 246 | } 247 | } 248 | pcap_free_datalinks(dlt_buf); 249 | 250 | if (found) { 251 | if (pcap_set_datalink(*handle, DLT_IEEE802_11_RADIO)) { 252 | pcap_perror(*handle, "Error: "); 253 | exit(EXIT_FAILURE); 254 | } 255 | } else { 256 | fprintf(stderr, "Error: the interface %s does not support radiotap header or is not in monitor mode\n", iface); 257 | pcap_close(*handle); 258 | exit(EXIT_FAILURE); 259 | } 260 | 261 | // only capture probe request frames 262 | struct bpf_program bfp; 263 | char filter_exp[] = "type mgt subtype probe-req"; 264 | 265 | if (pcap_compile(*handle, &bfp, filter_exp, 1, PCAP_NETMASK_UNKNOWN) == -1) { 266 | fprintf(stderr, "Error: can't compile (bpf) filter '%s': %s\n", 267 | filter_exp, pcap_geterr(*handle)); 268 | pcap_close(*handle); 269 | exit(EXIT_FAILURE); 270 | } 271 | if (pcap_setfilter(*handle, &bfp) == -1) { 272 | fprintf(stderr, "Error: can't install (bpf) filter '%s': %s\n", 273 | filter_exp, pcap_geterr(*handle)); 274 | pcap_close(*handle); 275 | exit(EXIT_FAILURE); 276 | } 277 | pcap_freecode(&bfp); 278 | } 279 | 280 | int main(int argc, char *argv[]) 281 | { 282 | char *iface = NULL; 283 | char *db_name = NULL; 284 | char *manuf_name = NULL; 285 | uint8_t channel; 286 | 287 | parse_args(argc, argv, &iface, &channel, &manuf_name, &db_name, &option_stdout); 288 | 289 | // look for manuf file and parse it into memory 290 | if (access(manuf_name, F_OK) == -1 ) { 291 | fprintf(stderr, "Error: can't find manuf file %s\n", manuf_name); 292 | exit(EXIT_FAILURE); 293 | } 294 | printf(":: Parsing manuf file...\n"); 295 | fflush(stdout); 296 | ouidb = parse_manuf_file(manuf_name, &ouidb_size); 297 | if (ouidb == NULL) { 298 | fprintf(stderr, "Error: can't parse manuf file\n"); 299 | exit(EXIT_FAILURE); 300 | } 301 | 302 | // parse config.yaml file to populate ignored entries 303 | char **entries = parse_config_yaml(CONFIG_NAME, "ignored", &ignored_count); 304 | ignored = parse_ignored_entries(entries, ignored_count); 305 | for (int i=0; isize; 391 | probereq_t *pr; 392 | for (int i = 0; i < qs; i++) { 393 | pr = (probereq_t *) dequeue(queue); 394 | free_probereq(pr); 395 | } 396 | free(queue); 397 | 398 | pthread_mutex_destroy(&mutex_queue); 399 | 400 | pcap_close(handle); 401 | 402 | free(db_name); 403 | 404 | return ret; 405 | } 406 | -------------------------------------------------------------------------------- /src/probemon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- encoding: utf-8 -*- 3 | 4 | import time 5 | import argparse 6 | import subprocess 7 | import sys 8 | import sqlite3 9 | from manuf import manuf 10 | import base64 11 | from lru import LRU 12 | import signal 13 | import struct 14 | import threading 15 | import queue 16 | import os.path 17 | from yaml import load as yaml_load 18 | try: 19 | from yaml import CLoader as Loader 20 | except ImportError: 21 | from yaml import Loader 22 | 23 | NAME = 'probemon' 24 | DESCRIPTION = "a command line tool for logging 802.11 probe requests" 25 | VERSION = '0.7.2' 26 | 27 | MANUF_FILE = './manuf' 28 | MAX_QUEUE_LENGTH = 1024 # just in case 29 | MAX_ELAPSED_TIME = 60 # seconds 30 | MAX_VENDOR_LENGTH = 25 31 | MAX_SSID_LENGTH = 15 32 | 33 | # read config variable from config.yaml file 34 | with open('config.yaml', 'r') as f: 35 | cfg_doc = f.read() 36 | config = yaml_load(cfg_doc, Loader=Loader) 37 | 38 | class Colors: 39 | red = '\033[31m' 40 | green = '\033[32m' 41 | yellow = '\033[33m' 42 | blue = '\033[34m' 43 | magenta = '\033[35m' 44 | cyan = '\033[36m' 45 | endc = '\033[0m' 46 | bold = '\033[1m' 47 | underline = '\033[4m' 48 | 49 | class MyCache: 50 | def __init__(self, size): 51 | self.mac = LRU(size) 52 | self.ssid = LRU(size) 53 | self.vendor = LRU(size) 54 | 55 | def print_fields(fields): 56 | if fields[1] in config['knownmac']: 57 | fields[1] = '%s%s%s%s' % (Colors.bold, Colors.red, fields[1], Colors.endc) 58 | # convert time to iso 59 | fields[0] = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(fields[0 ])) 60 | # strip mac vendor string to MAX_VENDOR_LENGTH chars, left padded with space 61 | vendor = fields[2] 62 | if len(vendor) > MAX_VENDOR_LENGTH: 63 | vendor = vendor[:MAX_VENDOR_LENGTH-3]+ '...' 64 | else: 65 | vendor = vendor.ljust(MAX_VENDOR_LENGTH) 66 | # do the same for ssid 67 | ssid = fields[3] 68 | if len(ssid) > MAX_SSID_LENGTH: 69 | ssid = ssid[:MAX_SSID_LENGTH-3]+ '...' 70 | else: 71 | ssid = ssid.ljust(MAX_SSID_LENGTH) 72 | fields[2] = vendor 73 | fields[3] = ssid 74 | print('%s\t%s\t%s\t%s\t%d' % tuple(fields)) 75 | 76 | class MyQueue: 77 | def __init__(self, max_size=0): 78 | self.queue = queue.Queue(max_size) 79 | 80 | def append(self, fields): 81 | self.queue.put(fields) 82 | 83 | def commit(self, stdout, conn, c): 84 | while not self.queue.empty(): 85 | fields = self.queue.get() 86 | date, mac, ssid, rssi = fields 87 | # look up vendor from OUI value in MAC address 88 | vendor = vendor_db.get_manuf_long(mac) 89 | if vendor is None: 90 | vendor = 'UNKNOWN' 91 | fields.insert(2, vendor) 92 | try: 93 | insert_into_db(fields, conn, c) 94 | except sqlite3.OperationalError as e: 95 | del fields[2] 96 | # re-insert in queue to process it later 97 | self.queue.put(fields) 98 | print(f'Error: {e}') 99 | if stdout: 100 | print_fields(fields) 101 | 102 | # globals 103 | cache = MyCache(128) 104 | queue = MyQueue(MAX_QUEUE_LENGTH) 105 | vendor_db = None 106 | start_ts = time.monotonic() 107 | lock = threading.Lock() 108 | event = threading.Event() 109 | 110 | def sig_handler(signum, frame): 111 | event.set() 112 | 113 | def process_queue(queue, args): 114 | global start_ts 115 | 116 | conn = sqlite3.connect(args.db) 117 | c = conn.cursor() 118 | init_db(conn, c) 119 | 120 | while True: 121 | with lock: 122 | queue.commit(args.stdout, conn, c) 123 | now = time.monotonic() 124 | if now - start_ts > MAX_ELAPSED_TIME or event.is_set(): 125 | start_ts = now 126 | try: 127 | conn.commit() 128 | except sqlite3.OperationalError as e: 129 | print(f'Error: {e}') 130 | if event.is_set(): 131 | conn.close() 132 | break 133 | time.sleep(1) 134 | 135 | def parse_rssi(packet): 136 | # parse dbm_antsignal from radiotap header 137 | # borrowed from python-radiotap module 138 | radiotap_header_fmt = ' 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "logger_thread.h" 9 | #include "manuf.h" 10 | #include "parsers.h" 11 | #include "base64.h" 12 | #include "lruc.h" 13 | 14 | int init_probemon_db(const char *db_file, sqlite3 **db) 15 | { 16 | int ret; 17 | if ((ret = sqlite3_open(db_file, db)) != SQLITE_OK) { 18 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 19 | sqlite3_close(*db); 20 | return ret; 21 | } 22 | 23 | char *sql; 24 | sql = "create table if not exists vendor(" 25 | "id integer not null primary key," 26 | "name text" 27 | ");"; 28 | if ((ret = sqlite3_exec(*db, sql, NULL, 0, NULL)) != SQLITE_OK) { 29 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 30 | sqlite3_close(*db); 31 | return ret; 32 | } 33 | sql = "create table if not exists mac(" 34 | "id integer not null primary key," 35 | "address text," 36 | "vendor integer," 37 | "foreign key(vendor) references vendor(id)" 38 | ");"; 39 | if ((ret = sqlite3_exec(*db, sql, NULL, 0, NULL)) != SQLITE_OK) { 40 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 41 | sqlite3_close(*db); 42 | return ret; 43 | } 44 | sql = "create table if not exists ssid(" 45 | "id integer not null primary key," 46 | "name text" 47 | ");"; 48 | if ((ret = sqlite3_exec(*db, sql, NULL, 0, NULL)) != SQLITE_OK) { 49 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 50 | sqlite3_close(*db); 51 | return ret; 52 | } 53 | sql = "create table if not exists probemon(" 54 | "date float," 55 | "mac integer," 56 | "ssid integer," 57 | "rssi integer," 58 | "foreign key(mac) references mac(id)," 59 | "foreign key(ssid) references ssid(id)" 60 | ");"; 61 | if ((ret = sqlite3_exec(*db, sql, NULL, 0, NULL)) != SQLITE_OK) { 62 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 63 | sqlite3_close(*db); 64 | return ret; 65 | } 66 | sql = "create index if not exists idx_probemon_date on probemon(date);"; 67 | if ((ret = sqlite3_exec(*db, sql, NULL, 0, NULL)) != SQLITE_OK) { 68 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 69 | sqlite3_close(*db); 70 | return ret; 71 | } 72 | sql = "pragma synchronous = normal;"; 73 | if ((ret = sqlite3_exec(*db, sql, NULL, 0, NULL)) != SQLITE_OK) { 74 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 75 | sqlite3_close(*db); 76 | return ret; 77 | } 78 | sql = "pragma temp_store = 2;"; // to store temp table and indices in memory 79 | if ((ret = sqlite3_exec(*db, sql, NULL, 0, NULL)) != SQLITE_OK) { 80 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 81 | sqlite3_close(*db); 82 | return ret; 83 | } 84 | sql = "pragma journal_mode = off;"; // disable journal for rollback (we don't use this) 85 | if ((ret = sqlite3_exec(*db, sql, NULL, 0, NULL)) != SQLITE_OK) { 86 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 87 | sqlite3_close(*db); 88 | return ret; 89 | } 90 | sql = "pragma foreign_keys = on;"; // turn that on to enforce foreign keys 91 | if ((ret = sqlite3_exec(*db, sql, NULL, 0, NULL)) != SQLITE_OK) { 92 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(*db), basename(__FILE__), __LINE__, __func__); 93 | sqlite3_close(*db); 94 | return ret; 95 | } 96 | 97 | return 0; 98 | } 99 | 100 | int64_t search_ssid(const char *ssid, sqlite3 *db) 101 | { 102 | char *sql; 103 | sqlite3_stmt *stmt; 104 | int64_t ssid_id = 0, ret; 105 | 106 | // look for an existing ssid in the db 107 | sql = "select id from ssid where name=?;"; 108 | if ((ret = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL)) != SQLITE_OK) { 109 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 110 | return ret * -1; 111 | } else { 112 | if ((ret = sqlite3_bind_text(stmt, 1, ssid, -1, NULL)) != SQLITE_OK) { 113 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 114 | return ret * -1; 115 | } 116 | 117 | while ((ret = sqlite3_step(stmt)) != SQLITE_DONE) { 118 | if (ret == SQLITE_ERROR) { 119 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 120 | break; 121 | } else if (ret == SQLITE_ROW) { 122 | ssid_id = sqlite3_column_int64(stmt, 0); 123 | } else { 124 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 125 | break; 126 | } 127 | } 128 | sqlite3_finalize(stmt); 129 | } 130 | return ssid_id; 131 | } 132 | 133 | int64_t insert_ssid(const char *ssid, sqlite3 *db) 134 | { 135 | // insert the ssid into the db 136 | int64_t ret, ssid_id = 0; 137 | char sql[128]; 138 | 139 | ssid_id = search_ssid(ssid, db); 140 | if (!ssid_id) { 141 | // we need to escape quote by doubling them 142 | char *nssid = str_replace(ssid, "'", "''"); 143 | snprintf(sql, 128, "insert into ssid (name) values ('%s');", nssid); 144 | free(nssid); 145 | if ((ret = sqlite3_exec(db, sql, NULL, 0, NULL)) != SQLITE_OK) { 146 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 147 | return ret * -1; 148 | } 149 | ssid_id = search_ssid(ssid, db); 150 | } 151 | 152 | return ssid_id; 153 | } 154 | 155 | int64_t search_vendor(const char *vendor, sqlite3 *db) 156 | { 157 | char *sql; 158 | sqlite3_stmt *stmt; 159 | int64_t vendor_id = 0, ret; 160 | 161 | // look for an existing vendor in the db 162 | sql = "select id from vendor where name=?;"; 163 | if ((ret = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL)) != SQLITE_OK) { 164 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 165 | return ret * -1; 166 | } else { 167 | if ((ret = sqlite3_bind_text(stmt, 1, vendor, -1, NULL)) != SQLITE_OK) { 168 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 169 | return ret * -1; 170 | } 171 | 172 | while ((ret = sqlite3_step(stmt)) != SQLITE_DONE) { 173 | if (ret == SQLITE_ERROR) { 174 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 175 | break; 176 | } else if (ret == SQLITE_ROW) { 177 | vendor_id = sqlite3_column_int64(stmt, 0); 178 | } else { 179 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 180 | break; 181 | } 182 | } 183 | sqlite3_finalize(stmt); 184 | } 185 | return vendor_id; 186 | } 187 | 188 | int64_t insert_vendor(const char *vendor, sqlite3 *db) 189 | { 190 | // insert the vendor into the db 191 | int64_t ret, vendor_id = 0; 192 | char sql[128]; 193 | 194 | vendor_id = search_vendor(vendor, db); 195 | if (!vendor_id) { 196 | // we need to escape quote by doubling them 197 | char *nvendor = str_replace(vendor, "'", "''"); 198 | snprintf(sql, 128, "insert into vendor (name) values ('%s');", nvendor); 199 | free(nvendor); 200 | if ((ret = sqlite3_exec(db, sql, NULL, 0, NULL)) != SQLITE_OK) { 201 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 202 | return ret * -1; 203 | } 204 | vendor_id = search_vendor(vendor, db); 205 | } 206 | 207 | return vendor_id; 208 | } 209 | 210 | int64_t search_mac(const char *mac, sqlite3 *db) 211 | { 212 | char *sql; 213 | sqlite3_stmt *stmt; 214 | int64_t mac_id = 0, ret; 215 | 216 | // look for an existing mac in the db 217 | sql = "select id from mac where address=?;"; 218 | if ((ret = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL)) != SQLITE_OK) { 219 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 220 | return ret * -1; 221 | } else { 222 | if ((ret = sqlite3_bind_text(stmt, 1, mac, -1, NULL)) != SQLITE_OK) { 223 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 224 | return ret * -1; 225 | } 226 | 227 | while ((ret = sqlite3_step(stmt)) != SQLITE_DONE) { 228 | if (ret == SQLITE_ERROR) { 229 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 230 | break; 231 | } else if (ret == SQLITE_ROW) { 232 | mac_id = sqlite3_column_int64(stmt, 0); 233 | } else { 234 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 235 | break; 236 | } 237 | } 238 | sqlite3_finalize(stmt); 239 | } 240 | return mac_id; 241 | } 242 | 243 | int64_t insert_mac(const char *mac, int64_t vendor_id, sqlite3 *db) 244 | { 245 | // insert the mac into the db 246 | int64_t ret, mac_id = 0; 247 | char sql[128]; 248 | 249 | mac_id = search_mac(mac, db); 250 | if (!mac_id) { 251 | snprintf(sql, 128, "insert into mac (address, vendor) values ('%s', '%"PRId64"');", mac, vendor_id); 252 | if ((ret = sqlite3_exec(db, sql, NULL, 0, NULL)) != SQLITE_OK) { 253 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 254 | return ret * -1; 255 | } 256 | mac_id = search_mac(mac, db); 257 | } 258 | 259 | return mac_id; 260 | } 261 | 262 | int insert_probereq(probereq_t pr, sqlite3 *db, lruc *mac_pk_cache, lruc *ssid_pk_cache) 263 | { 264 | int64_t vendor_id, ssid_id, mac_id; 265 | int ret; 266 | 267 | // is ssid a valid utf-8 string 268 | char tmp[64]; 269 | memcpy(tmp, pr.ssid, pr.ssid_len); 270 | tmp[pr.ssid_len] = '\0'; 271 | 272 | if (!is_utf8(tmp)) { 273 | // base64 encode the ssid 274 | size_t length; 275 | char *b64tmp = base64_encode((unsigned char *)tmp, pr.ssid_len, &length); 276 | snprintf(tmp, length+4+1, "b64_%s", b64tmp); 277 | free(b64tmp); 278 | } 279 | 280 | void *value = NULL; 281 | // look up ssid (tmp) in ssid_pk_cache 282 | lruc_get(ssid_pk_cache, tmp, strlen(tmp)+1, &value); 283 | if (value == NULL) { 284 | ssid_id = insert_ssid(tmp, db); 285 | // add the ssid_id to the cache 286 | int64_t *new_value = malloc(sizeof(int64_t)); 287 | *new_value = ssid_id; 288 | lruc_set(ssid_pk_cache, strdup(tmp), strlen(tmp)+1, new_value, sizeof(int64_t)); 289 | } else { 290 | ssid_id = *(int64_t *)value; 291 | } 292 | 293 | // look up mac in mac_pk_cache 294 | value = NULL; 295 | lruc_get(mac_pk_cache, pr.mac, 18, &value); 296 | if (value == NULL) { 297 | vendor_id = insert_vendor(pr.vendor, db); 298 | mac_id = insert_mac(pr.mac, vendor_id, db); 299 | // add the mac_id to the cache 300 | int64_t *new_value = malloc(sizeof(int64_t)); 301 | *new_value = mac_id; 302 | lruc_set(mac_pk_cache, strdup(pr.mac), 18, new_value, sizeof(int64_t)); 303 | } else { 304 | mac_id = *(int64_t *)value; 305 | } 306 | 307 | // convert timeval to double 308 | double ts; 309 | char tstmp[32]; 310 | sprintf(tstmp, "%lu.%06lu", pr.tv.tv_sec, pr.tv.tv_usec); 311 | ts = strtod(tstmp, NULL); 312 | 313 | char sql[256]; 314 | snprintf(sql, 256, "insert into probemon (date, mac, ssid, rssi)" 315 | "values ('%f', '%"PRId64"', '%"PRId64"', '%d');", ts, mac_id, ssid_id, pr.rssi); 316 | if ((ret = sqlite3_exec(db, sql, NULL, 0, NULL)) != SQLITE_OK) { 317 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 318 | return ret * -1; 319 | } 320 | 321 | return 0; 322 | } 323 | 324 | int begin_txn(sqlite3 *db) 325 | { 326 | int ret; 327 | char sql[32]; 328 | 329 | snprintf(sql, 32, "begin transaction;"); 330 | if ((ret = sqlite3_exec(db, sql, NULL, 0, NULL)) != SQLITE_OK) { 331 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 332 | return ret * -1; 333 | } 334 | 335 | return 0; 336 | } 337 | 338 | int commit_txn(sqlite3 *db) 339 | { 340 | int ret; 341 | char sql[32]; 342 | 343 | snprintf(sql, 32, "commit transaction;"); 344 | if ((ret = sqlite3_exec(db, sql, NULL, 0, NULL)) != SQLITE_OK) { 345 | fprintf(stderr, "Error: %s (%s:%d in %s)\n", sqlite3_errmsg(db), basename(__FILE__), __LINE__, __func__); 346 | return ret * -1; 347 | } 348 | 349 | return 0; 350 | } 351 | -------------------------------------------------------------------------------- /src/c.d/radiotap.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Radiotap parser 3 | * 4 | * Copyright 2007 Andy Green 5 | * Copyright 2009 Johannes Berg 6 | * 7 | * This program is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License version 2 as 9 | * published by the Free Software Foundation. 10 | * 11 | * Alternatively, this software may be distributed under the terms of ISC 12 | * license, see COPYING for more details. 13 | */ 14 | #include "radiotap_iter.h" 15 | #include "platform.h" 16 | 17 | /* function prototypes and related defs are in radiotap_iter.h */ 18 | 19 | static const struct radiotap_align_size rtap_namespace_sizes[] = { 20 | [IEEE80211_RADIOTAP_TSFT] = { .align = 8, .size = 8, }, 21 | [IEEE80211_RADIOTAP_FLAGS] = { .align = 1, .size = 1, }, 22 | [IEEE80211_RADIOTAP_RATE] = { .align = 1, .size = 1, }, 23 | [IEEE80211_RADIOTAP_CHANNEL] = { .align = 2, .size = 4, }, 24 | [IEEE80211_RADIOTAP_FHSS] = { .align = 2, .size = 2, }, 25 | [IEEE80211_RADIOTAP_DBM_ANTSIGNAL] = { .align = 1, .size = 1, }, 26 | [IEEE80211_RADIOTAP_DBM_ANTNOISE] = { .align = 1, .size = 1, }, 27 | [IEEE80211_RADIOTAP_LOCK_QUALITY] = { .align = 2, .size = 2, }, 28 | [IEEE80211_RADIOTAP_TX_ATTENUATION] = { .align = 2, .size = 2, }, 29 | [IEEE80211_RADIOTAP_DB_TX_ATTENUATION] = { .align = 2, .size = 2, }, 30 | [IEEE80211_RADIOTAP_DBM_TX_POWER] = { .align = 1, .size = 1, }, 31 | [IEEE80211_RADIOTAP_ANTENNA] = { .align = 1, .size = 1, }, 32 | [IEEE80211_RADIOTAP_DB_ANTSIGNAL] = { .align = 1, .size = 1, }, 33 | [IEEE80211_RADIOTAP_DB_ANTNOISE] = { .align = 1, .size = 1, }, 34 | [IEEE80211_RADIOTAP_RX_FLAGS] = { .align = 2, .size = 2, }, 35 | [IEEE80211_RADIOTAP_TX_FLAGS] = { .align = 2, .size = 2, }, 36 | [IEEE80211_RADIOTAP_RTS_RETRIES] = { .align = 1, .size = 1, }, 37 | [IEEE80211_RADIOTAP_DATA_RETRIES] = { .align = 1, .size = 1, }, 38 | [IEEE80211_RADIOTAP_MCS] = { .align = 1, .size = 3, }, 39 | [IEEE80211_RADIOTAP_AMPDU_STATUS] = { .align = 4, .size = 8, }, 40 | [IEEE80211_RADIOTAP_VHT] = { .align = 2, .size = 12, }, 41 | [IEEE80211_RADIOTAP_TIMESTAMP] = { .align = 8, .size = 12, }, 42 | /* 43 | * add more here as they are defined in radiotap.h 44 | */ 45 | }; 46 | 47 | static const struct ieee80211_radiotap_namespace radiotap_ns = { 48 | .n_bits = sizeof(rtap_namespace_sizes) / sizeof(rtap_namespace_sizes[0]), 49 | .align_size = rtap_namespace_sizes, 50 | }; 51 | 52 | /** 53 | * ieee80211_radiotap_iterator_init - radiotap parser iterator initialization 54 | * @iterator: radiotap_iterator to initialize 55 | * @radiotap_header: radiotap header to parse 56 | * @max_length: total length we can parse into (eg, whole packet length) 57 | * 58 | * Returns: 0 or a negative error code if there is a problem. 59 | * 60 | * This function initializes an opaque iterator struct which can then 61 | * be passed to ieee80211_radiotap_iterator_next() to visit every radiotap 62 | * argument which is present in the header. It knows about extended 63 | * present headers and handles them. 64 | * 65 | * How to use: 66 | * call __ieee80211_radiotap_iterator_init() to init a semi-opaque iterator 67 | * struct ieee80211_radiotap_iterator (no need to init the struct beforehand) 68 | * checking for a good 0 return code. Then loop calling 69 | * __ieee80211_radiotap_iterator_next()... it returns either 0, 70 | * -ENOENT if there are no more args to parse, or -EINVAL if there is a problem. 71 | * The iterator's @this_arg member points to the start of the argument 72 | * associated with the current argument index that is present, which can be 73 | * found in the iterator's @this_arg_index member. This arg index corresponds 74 | * to the IEEE80211_RADIOTAP_... defines. 75 | * 76 | * Radiotap header length: 77 | * You can find the CPU-endian total radiotap header length in 78 | * iterator->max_length after executing ieee80211_radiotap_iterator_init() 79 | * successfully. 80 | * 81 | * Alignment Gotcha: 82 | * You must take care when dereferencing iterator.this_arg 83 | * for multibyte types... the pointer is not aligned. Use 84 | * get_unaligned((type *)iterator.this_arg) to dereference 85 | * iterator.this_arg for type "type" safely on all arches. 86 | * 87 | * Example code: parse.c 88 | */ 89 | 90 | int ieee80211_radiotap_iterator_init( 91 | struct ieee80211_radiotap_iterator *iterator, 92 | struct ieee80211_radiotap_header *radiotap_header, 93 | int max_length, const struct ieee80211_radiotap_vendor_namespaces *vns) 94 | { 95 | /* must at least have the radiotap header */ 96 | if (max_length < (int)sizeof(struct ieee80211_radiotap_header)) 97 | return -EINVAL; 98 | 99 | /* Linux only supports version 0 radiotap format */ 100 | if (radiotap_header->it_version) 101 | return -EINVAL; 102 | 103 | /* sanity check for allowed length and radiotap length field */ 104 | if (max_length < get_unaligned_le16(&radiotap_header->it_len)) 105 | return -EINVAL; 106 | 107 | iterator->_rtheader = radiotap_header; 108 | iterator->_max_length = get_unaligned_le16(&radiotap_header->it_len); 109 | iterator->_arg_index = 0; 110 | iterator->_bitmap_shifter = get_unaligned_le32(&radiotap_header->it_present); 111 | iterator->_arg = (uint8_t *)radiotap_header + sizeof(*radiotap_header); 112 | iterator->_reset_on_ext = 0; 113 | iterator->_next_bitmap = &radiotap_header->it_present; 114 | iterator->_next_bitmap++; 115 | iterator->_vns = vns; 116 | iterator->current_namespace = &radiotap_ns; 117 | iterator->is_radiotap_ns = 1; 118 | #ifdef RADIOTAP_SUPPORT_OVERRIDES 119 | iterator->n_overrides = 0; 120 | iterator->overrides = NULL; 121 | #endif 122 | 123 | /* find payload start allowing for extended bitmap(s) */ 124 | 125 | if (iterator->_bitmap_shifter & (1<_arg - 127 | (unsigned long)iterator->_rtheader + sizeof(uint32_t) > 128 | (unsigned long)iterator->_max_length) 129 | return -EINVAL; 130 | while (get_unaligned_le32(iterator->_arg) & 131 | (1 << IEEE80211_RADIOTAP_EXT)) { 132 | iterator->_arg += sizeof(uint32_t); 133 | 134 | /* 135 | * check for insanity where the present bitmaps 136 | * keep claiming to extend up to or even beyond the 137 | * stated radiotap header length 138 | */ 139 | 140 | if ((unsigned long)iterator->_arg - 141 | (unsigned long)iterator->_rtheader + 142 | sizeof(uint32_t) > 143 | (unsigned long)iterator->_max_length) 144 | return -EINVAL; 145 | } 146 | 147 | iterator->_arg += sizeof(uint32_t); 148 | 149 | /* 150 | * no need to check again for blowing past stated radiotap 151 | * header length, because ieee80211_radiotap_iterator_next 152 | * checks it before it is dereferenced 153 | */ 154 | } 155 | 156 | iterator->this_arg = iterator->_arg; 157 | 158 | /* we are all initialized happily */ 159 | 160 | return 0; 161 | } 162 | 163 | static void find_ns(struct ieee80211_radiotap_iterator *iterator, 164 | uint32_t oui, uint8_t subns) 165 | { 166 | int i; 167 | 168 | iterator->current_namespace = NULL; 169 | 170 | if (!iterator->_vns) 171 | return; 172 | 173 | for (i = 0; i < iterator->_vns->n_ns; i++) { 174 | if (iterator->_vns->ns[i].oui != oui) 175 | continue; 176 | if (iterator->_vns->ns[i].subns != subns) 177 | continue; 178 | 179 | iterator->current_namespace = &iterator->_vns->ns[i]; 180 | break; 181 | } 182 | } 183 | 184 | #ifdef RADIOTAP_SUPPORT_OVERRIDES 185 | static int find_override(struct ieee80211_radiotap_iterator *iterator, 186 | int *align, int *size) 187 | { 188 | int i; 189 | 190 | if (!iterator->overrides) 191 | return 0; 192 | 193 | for (i = 0; i < iterator->n_overrides; i++) { 194 | if (iterator->_arg_index == iterator->overrides[i].field) { 195 | *align = iterator->overrides[i].align; 196 | *size = iterator->overrides[i].size; 197 | if (!*align) /* erroneous override */ 198 | return 0; 199 | return 1; 200 | } 201 | } 202 | 203 | return 0; 204 | } 205 | #endif 206 | 207 | 208 | /** 209 | * ieee80211_radiotap_iterator_next - return next radiotap parser iterator arg 210 | * @iterator: radiotap_iterator to move to next arg (if any) 211 | * 212 | * Returns: 0 if there is an argument to handle, 213 | * -ENOENT if there are no more args or -EINVAL 214 | * if there is something else wrong. 215 | * 216 | * This function provides the next radiotap arg index (IEEE80211_RADIOTAP_*) 217 | * in @this_arg_index and sets @this_arg to point to the 218 | * payload for the field. It takes care of alignment handling and extended 219 | * present fields. @this_arg can be changed by the caller (eg, 220 | * incremented to move inside a compound argument like 221 | * IEEE80211_RADIOTAP_CHANNEL). The args pointed to are in 222 | * little-endian format whatever the endianness of your CPU. 223 | * 224 | * Alignment Gotcha: 225 | * You must take care when dereferencing iterator.this_arg 226 | * for multibyte types... the pointer is not aligned. Use 227 | * get_unaligned((type *)iterator.this_arg) to dereference 228 | * iterator.this_arg for type "type" safely on all arches. 229 | */ 230 | 231 | int ieee80211_radiotap_iterator_next( 232 | struct ieee80211_radiotap_iterator *iterator) 233 | { 234 | while (1) { 235 | int hit = 0; 236 | int pad, align, size, subns; 237 | uint32_t oui; 238 | 239 | /* if no more EXT bits, that's it */ 240 | if ((iterator->_arg_index % 32) == IEEE80211_RADIOTAP_EXT && 241 | !(iterator->_bitmap_shifter & 1)) 242 | return -ENOENT; 243 | 244 | if (!(iterator->_bitmap_shifter & 1)) 245 | goto next_entry; /* arg not present */ 246 | 247 | /* get alignment/size of data */ 248 | switch (iterator->_arg_index % 32) { 249 | case IEEE80211_RADIOTAP_RADIOTAP_NAMESPACE: 250 | case IEEE80211_RADIOTAP_EXT: 251 | align = 1; 252 | size = 0; 253 | break; 254 | case IEEE80211_RADIOTAP_VENDOR_NAMESPACE: 255 | align = 2; 256 | size = 6; 257 | break; 258 | default: 259 | #ifdef RADIOTAP_SUPPORT_OVERRIDES 260 | if (find_override(iterator, &align, &size)) { 261 | /* all set */ 262 | } else 263 | #endif 264 | if (!iterator->current_namespace || 265 | iterator->_arg_index >= iterator->current_namespace->n_bits) { 266 | if (iterator->current_namespace == &radiotap_ns) 267 | return -ENOENT; 268 | align = 0; 269 | } else { 270 | align = iterator->current_namespace->align_size[iterator->_arg_index].align; 271 | size = iterator->current_namespace->align_size[iterator->_arg_index].size; 272 | } 273 | if (!align) { 274 | /* skip all subsequent data */ 275 | iterator->_arg = iterator->_next_ns_data; 276 | /* give up on this namespace */ 277 | iterator->current_namespace = NULL; 278 | goto next_entry; 279 | } 280 | break; 281 | } 282 | 283 | /* 284 | * arg is present, account for alignment padding 285 | * 286 | * Note that these alignments are relative to the start 287 | * of the radiotap header. There is no guarantee 288 | * that the radiotap header itself is aligned on any 289 | * kind of boundary. 290 | * 291 | * The above is why get_unaligned() is used to dereference 292 | * multibyte elements from the radiotap area. 293 | */ 294 | 295 | pad = ((unsigned long)iterator->_arg - 296 | (unsigned long)iterator->_rtheader) & (align - 1); 297 | 298 | if (pad) 299 | iterator->_arg += align - pad; 300 | 301 | if (iterator->_arg_index % 32 == IEEE80211_RADIOTAP_VENDOR_NAMESPACE) { 302 | int vnslen; 303 | 304 | if ((unsigned long)iterator->_arg + size - 305 | (unsigned long)iterator->_rtheader > 306 | (unsigned long)iterator->_max_length) 307 | return -EINVAL; 308 | 309 | oui = (*iterator->_arg << 16) | 310 | (*(iterator->_arg + 1) << 8) | 311 | *(iterator->_arg + 2); 312 | subns = *(iterator->_arg + 3); 313 | 314 | find_ns(iterator, oui, subns); 315 | 316 | vnslen = get_unaligned_le16(iterator->_arg + 4); 317 | iterator->_next_ns_data = iterator->_arg + size + vnslen; 318 | if (!iterator->current_namespace) 319 | size += vnslen; 320 | } 321 | 322 | /* 323 | * this is what we will return to user, but we need to 324 | * move on first so next call has something fresh to test 325 | */ 326 | iterator->this_arg_index = iterator->_arg_index; 327 | iterator->this_arg = iterator->_arg; 328 | iterator->this_arg_size = size; 329 | 330 | /* internally move on the size of this arg */ 331 | iterator->_arg += size; 332 | 333 | /* 334 | * check for insanity where we are given a bitmap that 335 | * claims to have more arg content than the length of the 336 | * radiotap section. We will normally end up equalling this 337 | * max_length on the last arg, never exceeding it. 338 | */ 339 | 340 | if ((unsigned long)iterator->_arg - 341 | (unsigned long)iterator->_rtheader > 342 | (unsigned long)iterator->_max_length) 343 | return -EINVAL; 344 | 345 | /* these special ones are valid in each bitmap word */ 346 | switch (iterator->_arg_index % 32) { 347 | case IEEE80211_RADIOTAP_VENDOR_NAMESPACE: 348 | iterator->_reset_on_ext = 1; 349 | 350 | iterator->is_radiotap_ns = 0; 351 | /* 352 | * If parser didn't register this vendor 353 | * namespace with us, allow it to show it 354 | * as 'raw. Do do that, set argument index 355 | * to vendor namespace. 356 | */ 357 | iterator->this_arg_index = 358 | IEEE80211_RADIOTAP_VENDOR_NAMESPACE; 359 | if (!iterator->current_namespace) 360 | hit = 1; 361 | goto next_entry; 362 | case IEEE80211_RADIOTAP_RADIOTAP_NAMESPACE: 363 | iterator->_reset_on_ext = 1; 364 | iterator->current_namespace = &radiotap_ns; 365 | iterator->is_radiotap_ns = 1; 366 | goto next_entry; 367 | case IEEE80211_RADIOTAP_EXT: 368 | /* 369 | * bit 31 was set, there is more 370 | * -- move to next u32 bitmap 371 | */ 372 | iterator->_bitmap_shifter = 373 | get_unaligned_le32(iterator->_next_bitmap); 374 | iterator->_next_bitmap++; 375 | if (iterator->_reset_on_ext) 376 | iterator->_arg_index = 0; 377 | else 378 | iterator->_arg_index++; 379 | iterator->_reset_on_ext = 0; 380 | break; 381 | default: 382 | /* we've got a hit! */ 383 | hit = 1; 384 | next_entry: 385 | iterator->_bitmap_shifter >>= 1; 386 | iterator->_arg_index++; 387 | } 388 | 389 | /* if we found a valid arg earlier, return it now */ 390 | if (hit) 391 | return 0; 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sqlite3 4 | import argparse 5 | import time 6 | import sys 7 | import os.path 8 | from yaml import load as yaml_load 9 | try: 10 | from yaml import CLoader as Loader 11 | except ImportError: 12 | from yaml import Loader 13 | 14 | # avoid IOError when quitting less 15 | from signal import signal, SIGPIPE, SIG_DFL 16 | signal(SIGPIPE, SIG_DFL) 17 | 18 | NUMOFSECSINADAY = 60*60*24 19 | MAX_VENDOR_LENGTH = 25 20 | MAX_SSID_LENGTH = 15 21 | 22 | # read config variable from config.yaml file 23 | try: 24 | with open('config.yaml', 'r') as f: 25 | cfg_doc = f.read() 26 | config = yaml_load(cfg_doc, Loader=Loader) 27 | except FileNotFoundError as e: 28 | print('Warning: config.yaml not found, skipping', file=sys.stderr) 29 | config = {'ignored': tuple(), 'knownmac': tuple(), 'merged': tuple()} 30 | 31 | def is_local_bit_set(mac): 32 | byte = mac.split(':') 33 | try: 34 | bi = int(byte[0], 16) 35 | return bi & 0b00000010 == 0b00000010 36 | except ValueError as e: 37 | return False 38 | 39 | def median(lst): 40 | n = len(lst) 41 | if n < 1: 42 | return None 43 | if n % 2 == 1: 44 | return sorted(lst)[n//2] 45 | else: 46 | return sum(sorted(lst)[n//2-1:n//2+1])//2 47 | 48 | def parse_ts(ts): 49 | try: 50 | date = time.strptime(ts, '%Y-%m-%dT%H:%M') 51 | except ValueError: 52 | try: 53 | date = time.strptime(ts, '%Y-%m-%d') 54 | date = time.strptime(f'{ts}T12:00', '%Y-%m-%dT%H:%M') 55 | except ValueError: 56 | print("Error: can't parse date timestamp", file=sys.stderr) 57 | sys.exit(-1) 58 | t = time.mktime(date) 59 | return t 60 | 61 | def build_sql_query(after, before, macs, rssi, zero, day): 62 | sql_head = '''select date,mac.address,vendor.name,ssid.name,rssi from probemon 63 | inner join mac on mac.id=probemon.mac 64 | inner join vendor on vendor.id=mac.vendor 65 | inner join ssid on ssid.id=probemon.ssid''' 66 | sql_tail = 'order by date' 67 | 68 | sql_where_clause = '' 69 | sql_args = [] 70 | 71 | def add_arg(clause, op, new_arg): 72 | if clause == '': 73 | clause = f'{new_arg}' 74 | else: 75 | clause = f'{clause} {op} {new_arg}' 76 | return clause 77 | 78 | if macs: 79 | for mac in macs: 80 | if len(mac) != 17: 81 | mac = f'{mac}%' 82 | sql_where_clause = add_arg(sql_where_clause, 'or', 'mac.address like ?') 83 | sql_args.append(mac) 84 | if len(macs) > 1: 85 | sql_where_clause = f'({sql_where_clause})' 86 | if rssi: 87 | sql_where_clause = add_arg(sql_where_clause, 'and', 'rssi>?') 88 | sql_args.append(rssi) 89 | 90 | if day: 91 | before = time.time() # now 92 | after = before - NUMOFSECSINADAY # since one day in the past 93 | 94 | if after is not None: 95 | sql_where_clause = add_arg(sql_where_clause, 'and', 'date>?') 96 | sql_args.append(after) 97 | if before is not None: 98 | sql_where_clause = add_arg(sql_where_clause, 'and', 'date 0: 105 | arg_list = ','.join(['?']*len(config['ignored'])) 106 | sql_where_clause = add_arg(sql_where_clause, 'and', f'mac.address not in ({arg_list})') 107 | sql_args.extend(config['ignored']) 108 | 109 | if sql_where_clause != '': 110 | sql = f'{sql_head} where {sql_where_clause} {sql_tail}' 111 | else: 112 | sql = f'{sql_head} {sql_tail}' 113 | 114 | return sql, sql_args 115 | 116 | def main(): 117 | parser = argparse.ArgumentParser(description='Display various stats about mac addresses/probe requests in the database') 118 | parser.add_argument('-a', '--after', help='filter before this timestamp') 119 | parser.add_argument('-b', '--before', help='filter after this timestamp') 120 | parser.add_argument('-d', '--day', action='store_true', help='filter only for the past day') 121 | parser.add_argument('--dont-use-stats-table', action='store_true', default=False, help="don't use the stats table even if it's present") 122 | parser.add_argument('--day-by-day', action='store_true', help='day by day stats for given mac') 123 | parser.add_argument('--db', default='probemon.db', help='file name of database') 124 | parser.add_argument('--list-mac-ssids', action='store_true', help='list ssid with mac that probed for it') 125 | parser.add_argument('-l', '--log', action='store_true', help='log all entries instead of showing stats') 126 | parser.add_argument('-m', '--mac', action='append', help='filter for that mac address') 127 | parser.add_argument('-p', '--privacy', action='store_true', help='merge all LAA mac into one') 128 | parser.add_argument('-r', '--rssi', type=int, help='filter for that minimal RSSI value') 129 | parser.add_argument('-s', '--ssid', help='look up for mac that have probed for that ssid') 130 | parser.add_argument('-z', '--zero', action='store_true', help='filter rssi value of 0') 131 | args = parser.parse_args() 132 | 133 | if args.list_mac_ssids: 134 | args.mac = None 135 | args.day_by_day = False 136 | 137 | if args.day and (args.before or args.after): 138 | print('Error: --day conflicts with --after or --before', file=sys.stderr) 139 | sys.exit(-1) 140 | 141 | if args.day_by_day and not args.mac: 142 | print('Error: --day-by-day needs a --mac switch', file=sys.stderr) 143 | sys.exit(-1) 144 | 145 | before = None 146 | after = None 147 | if args.after: 148 | after = parse_ts(args.after) 149 | if args.before: 150 | before = parse_ts(args.before) 151 | 152 | if not os.path.exists(args.db): 153 | print(f'Error: file not found {args.db}', file=sys.stderr) 154 | sys.exit(-1) 155 | 156 | if args.ssid and args.mac: 157 | print(':: Ignoring --mac switch') 158 | args.mac = None 159 | 160 | conn = sqlite3.connect(f'file:{args.db}?mode=ro', uri=True) 161 | c = conn.cursor() 162 | sql = 'pragma query_only = on;' 163 | c.execute(sql) 164 | sql = 'pragma temp_store = 2;' # to store temp table and indices in memory 165 | c.execute(sql) 166 | sql = 'pragma journal_mode = off;' # disable journal for rollback (we don't use this) 167 | c.execute(sql) 168 | conn.commit() 169 | 170 | try: 171 | c.execute('select count(*) from sqlite_master where type=? and name=?', ('table', 'stats')) 172 | except sqlite3.OperationalError as e: 173 | print(f'Error: {e}', file=sys.stderr) 174 | sys.exit(-1) 175 | is_stats_table_available = c.fetchone()[0] == 1 176 | 177 | if args.ssid: 178 | c.execute('select id from ssid where name=?', (args.ssid,)) 179 | ssid = c.fetchone() 180 | if ssid is None: 181 | print('Error: ssid not found', file=sys.stderr) 182 | conn.close() 183 | sys.exit(-1) 184 | c.execute('select distinct mac from probemon where ssid=?', (ssid[0],)) 185 | 186 | macs = [] 187 | # search for mac that have probed that ssid 188 | for row in c.fetchall(): 189 | c.execute('select address from mac where id=?', (row[0],)) 190 | mac = c.fetchone()[0] 191 | if args.privacy and is_local_bit_set(mac): 192 | continue 193 | macs.append(mac) 194 | 195 | print(f'{args.ssid} : {", ".join(macs)}') 196 | conn.close() 197 | return 198 | 199 | if is_stats_table_available and not args.dont_use_stats_table and not args.log and not args.list_mac_ssids: 200 | print(':: Using the stats table') 201 | args.mac = [f'{m}%' for m in args.mac] 202 | sql = '''select mac.id, address, vendor.name from mac 203 | inner join vendor on vendor.id=mac.vendor 204 | where address like ?''' 205 | if len(args.mac) > 1: 206 | sql += ' or ' + 'or '.join((len(args.mac)-1)*['address like ?']) 207 | c.execute(sql, tuple(args.mac)) 208 | macs = {} 209 | for i, a, v in c.fetchall(): 210 | macs[i] = {'address': a, 'vendor': v, 'ssids': set(), 'count': [], 211 | 'min': 99, 'max': -999, 'avg': [], 'med': [], 'first':'9999', 'last': ''} 212 | if args.day_by_day: 213 | for i in list(macs.keys()): 214 | sql = 'select * from stats where mac_id=?' 215 | c.execute(sql, (i,)) 216 | print(f'MAC: {macs[i]["address"]}, VENDOR: {macs[i]["vendor"]}') 217 | for _, d, first, last, count, rmin, rmax, ravg, rmed, ssid in c.fetchall(): 218 | print(f' {d}: [{first}-{last}]', end=' ') 219 | print(f' RSSI: #: {count:4d}, min: {rmin:3d}, max: {rmax:3d}, avg: {ravg:3d}, median: {rmed:3d}') 220 | conn.close() 221 | return 222 | 223 | if not args.log and not args.list_mac_ssids: 224 | for i in list(macs.keys()): 225 | sql = 'select * from stats where mac_id=?' 226 | c.execute(sql, (i,)) 227 | m = macs[i] 228 | for _, d, first, last, count, rmin, rmax, ravg, rmed, ssid in c.fetchall(): 229 | first = f'{d}T{first}' 230 | last = f'{d}T{last}' 231 | if first < m['first']: 232 | m['first'] = first 233 | if last > m['last']: 234 | m['last'] = last 235 | m['count'].append(count) 236 | m['ssids'] = m['ssids'].union(ssid.split(',')) 237 | if rmin < m['min']: 238 | m['min'] = rmin 239 | if rmax > m['max'] and rmax != 0: 240 | m['max'] = rmax 241 | m['avg'].append(ravg) 242 | m['med'].append(rmed) 243 | if len(m['count']) > 0: 244 | laa = ' (LAA)' if is_local_bit_set(m['address']) else '' 245 | print(f'MAC: {m["address"]}{laa}, VENDOR: {m["vendor"]}') 246 | if '' in m['ssids']: 247 | m['ssids'].remove('') 248 | print(f' SSIDs: {",".join(sorted(list(m["ssids"])))}') 249 | avg = sum([a*b for a,b in zip(m['avg'], m['count'])])//sum(m['count']) 250 | med = sum([a*b for a,b in zip(m['med'], m['count'])])//sum(m['count']) 251 | print(f' RSSI: #: {sum(m["count"]):4d}, min: {m["min"]:3d}, max: {m["max"]:3d}, avg: {avg:3d}, median: {med:3d}') 252 | print(f' First seen at {m["first"]} and last seen at {m["last"]}') 253 | conn.close() 254 | return 255 | 256 | sql, sql_args = build_sql_query(after, before, args.mac, args.rssi, args.zero, args.day) 257 | try: 258 | c.execute(sql, sql_args) 259 | except sqlite3.OperationalError as e: 260 | print(f'Error: {e}', file=sys.stderr) 261 | sys.exit(-1) 262 | 263 | if args.log: 264 | # simply output each log entry to stdout 265 | for t, m, mc, ssid, rssi in c.fetchall(): 266 | t = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(t)) 267 | if is_local_bit_set(m): 268 | m = f'{m} (LAA)' 269 | # strip mac vendor string to MAX_VENDOR_LENGTH chars, left padded with space 270 | if len(mc) > MAX_VENDOR_LENGTH: 271 | mc = mc[:MAX_VENDOR_LENGTH-3]+ '...' 272 | else: 273 | mc = mc.ljust(MAX_VENDOR_LENGTH) 274 | # do the same for ssid 275 | if len(ssid) > MAX_SSID_LENGTH: 276 | ssid = ssid[:MAX_SSID_LENGTH-3]+ '...' 277 | else: 278 | ssid = ssid.ljust(MAX_SSID_LENGTH) 279 | print('\t'.join([t, m, mc, ssid, str(rssi)])) 280 | 281 | conn.close() 282 | return 283 | 284 | if args.day_by_day: 285 | if not args.dont_use_stats_table: 286 | print(':: You can speed up day-by-day look-up by using consolidate-stats.py') 287 | # gather stats day by day for args.mac 288 | stats = {} 289 | for row in c.fetchall(): 290 | if row[1] not in list(stats.keys()): 291 | stats[row[1]] = {'vendor': row[2]} 292 | day = time.strftime('%Y-%m-%d', time.localtime(row[0])) 293 | if day in stats[row[1]]: 294 | smd = stats[row[1]][day] 295 | smd['rssi'].append(row[4]) 296 | if row[0] > smd['last']: 297 | smd['last'] = row[0] 298 | if row[0] < smd['first']: 299 | smd['first'] = row[0] 300 | else: 301 | stats[row[1]][day] = {'rssi': [row[4]], 'first': row[0], 'last': row[0]} 302 | conn.close() 303 | 304 | for mac in list(stats.keys()): 305 | vendor = stats[mac]['vendor'] 306 | del stats[mac]['vendor'] 307 | days = sorted(stats[mac].keys()) 308 | print(f'MAC: {mac}, VENDOR: {vendor}') 309 | for d in days: 310 | rssi = stats[mac][d]['rssi'] 311 | first = time.strftime('%H:%M:%S', time.localtime(stats[mac][d]['first'])) 312 | last = time.strftime('%H:%M:%S', time.localtime(stats[mac][d]['last'])) 313 | print(f' {d}: [{first}-{last}]', end=' ') 314 | print(f' RSSI: #: {len(rssi):4d}, min: {min(rssi):3d}, max: {max(rssi):3d}, avg: {sum(rssi)//len(rssi):3d}, median: {median(rssi):3d}') 315 | return 316 | 317 | if args.list_mac_ssids: 318 | ssids = {} 319 | for row in c.fetchall(): 320 | ssid = row[3] 321 | if ssid == '' or is_local_bit_set(row[1]): 322 | continue 323 | if ssid not in ssids: 324 | ssids[ssid] = set([row[1]]) 325 | else: 326 | ssids[ssid].add(row[1]) 327 | si = sorted(list(ssids.items()), key=lambda x:len(x[1])) 328 | si.reverse() 329 | for k,v in si: 330 | if len(v) > 1: 331 | print(f'{k}: {", ".join(v)}') 332 | 333 | conn.close() 334 | return 335 | 336 | if not args.dont_use_stats_table: 337 | print(':: You can speed up query by using consolidate-stats.py') 338 | # gather stats about each mac 339 | macs = {} 340 | for row in c.fetchall(): 341 | mac = row[1] 342 | if args.privacy and is_local_bit_set(mac): 343 | # create virtual mac for LAA mac address 344 | mac = 'LAA' 345 | if mac not in macs: 346 | macs[mac] = {'vendor': row[2], 'ssid': [], 'rssi': [], 'last': row[0], 'first':row[0]} 347 | d = macs[mac] 348 | if row[3] != '' and row[3] not in d['ssid']: 349 | d['ssid'].append(row[3]) 350 | if row[0] > d['last']: 351 | d['last'] = row[0] 352 | if row[0] < d['first']: 353 | d['first'] = row[0] 354 | if row[4] != 0: 355 | d['rssi'].append(row[4]) 356 | 357 | conn.close() 358 | 359 | # sort on frequency of appearence of a mac 360 | tmp = [(k,len(v['rssi'])) for k,v in list(macs.items())] 361 | tmp = reversed(sorted(tmp, key=lambda k:k[1])) 362 | 363 | # print our stats 364 | for k,_ in tmp: 365 | v = macs[k] 366 | laa = ' (LAA)' if is_local_bit_set(k) else '' 367 | print(f'MAC: {k}{laa}, VENDOR: {v["vendor"]}') 368 | print(f' SSIDs: {",".join(sorted(v["ssid"]))}') 369 | rssi = v['rssi'] 370 | if rssi != []: 371 | print(f' RSSI: #: {len(rssi):4d}, min: {min(rssi):3d}, max: {max(rssi):3d}, avg: {sum(rssi)//len(rssi):3d}, median: {median(rssi):3d}') 372 | else: 373 | print(' RSSI: Nothing found.') 374 | 375 | first = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(v['first'])) 376 | last = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(v['last'])) 377 | print(f' First seen at {first} and last seen at {last}') 378 | 379 | if __name__ == '__main__': 380 | try: 381 | main() 382 | except KeyboardInterrupt: 383 | pass 384 | -------------------------------------------------------------------------------- /src/plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from datetime import datetime 4 | import time 5 | from cycler import cycler 6 | import matplotlib 7 | #matplotlib.use('Agg') 8 | import matplotlib.pyplot as plt 9 | import matplotlib.ticker as ticker 10 | import matplotlib.patches as mpatches 11 | import argparse 12 | import sqlite3 13 | import sys 14 | import os.path 15 | import os 16 | import re 17 | from scapy.all import sniff 18 | from scapy.layers import dot11 19 | from yaml import load as yaml_load 20 | try: 21 | from yaml import CLoader as Loader 22 | except ImportError: 23 | from yaml import Loader 24 | 25 | IS_WINDOW_OPENED = False 26 | NUMOFSECSINADAY = 60*60*24 27 | # standard colors without red and gray 28 | COLORS = ['tab:blue', 'tab:orange', 'tab:green', 'tab:purple', 'tab:brown', 'tab:pink', 'tab:olive', 'tab:cyan'] 29 | REFRESH_TIME = 60 # in seconds 30 | 31 | # read config variable from config.yaml file 32 | try: 33 | with open('config.yaml', 'r') as f: 34 | cfg_doc = f.read() 35 | config = yaml_load(cfg_doc, Loader=Loader) 36 | except FileNotFoundError as e: 37 | print('Warning: config.yaml not found, skipping', file=sys.stderr) 38 | config = {'ignored': tuple(), 'knownmac': tuple(), 'merged': tuple(), 'height': 1366, 'width': 768, 'dpi': 100} 39 | 40 | # draws a rectangle as custom legend handler 41 | class MyLine2DHandler(object): 42 | def legend_artist(self, legend, orig_handle, fontsize, handlebox): 43 | x0, y0 = handlebox.xdescent, handlebox.ydescent 44 | width, height = handlebox.width, handlebox.height 45 | patch = mpatches.Rectangle([x0, y0], width, height, facecolor=orig_handle.get_color()) 46 | handlebox.add_artist(patch) 47 | return patch 48 | 49 | def is_local_bit_set(mac): 50 | byte = mac.split(':') 51 | return int(byte[0], 16) & 0b00000010 == 0b00000010 52 | 53 | def get_data(args): 54 | ts = {} 55 | if args.pcap: 56 | if args.verbose: 57 | print(f':: Processing pcap file {args.pcap}') 58 | if args.only_pr: 59 | packets = sniff(offline=args.pcap, verbose=0, filter='wlan type mgt subtype probe-req') 60 | else: 61 | packets = sniff(offline=args.pcap, verbose=0) 62 | for packet in packets: 63 | if packet.addr2 in config['ignored']: 64 | continue 65 | 66 | if packet.addr2 in ts: 67 | ts[packet.addr2].append(packet.time) 68 | else: 69 | ts[packet.addr2] = [packet.time] 70 | elif args.kismet: 71 | if args.verbose: 72 | print(f':: Processing kismet file {args.kismet}') 73 | # sqlite3 74 | conn = sqlite3.connect(f'file:{args.kismet}?mode=ro', uri=True) 75 | c = conn.cursor() 76 | sql = 'pragma query_only = on;' 77 | c.execute(sql) 78 | sql = 'pragma temp_store = 2;' # to store temp table and indices in memory 79 | c.execute(sql) 80 | sql = 'pragma journal_mode = off;' # disable journal for rollback (we don't use this) 81 | c.execute(sql) 82 | conn.commit() 83 | 84 | sql = 'select ts_sec, ts_usec, lower(sourcemac), lower(destmac), packet from packets where phyname="IEEE802.11";' 85 | c.execute(sql) 86 | for row in c.fetchall(): 87 | if args.only_pr: 88 | # keep only the probe request 89 | packet = dot11.RadioTap(row[4]) 90 | if packet.type != 0 or packet.subtype != 4: 91 | continue 92 | if row[2] in ts: 93 | ts[row[2]].append(row[0]) 94 | else: 95 | ts[row[2]] = [row[0]] 96 | # filter out multicast mac addresses. TODO: use the exact range 00:00:00-7f:ff:ff 97 | if row[3] in ts and not row[3].startswith('01:00:5e'): 98 | ts[row[3]].append(row[0]) 99 | else: 100 | ts[row[3]] = [row[0]] 101 | try: 102 | del ts['ff:ff:ff:ff:ff:ff'] 103 | del ts['00:00:00:00:00:00'] 104 | except KeyError as k: 105 | pass 106 | # filter to keep only wifi client and device 107 | sql = 'select lower(devmac),type from devices' 108 | c.execute(sql) 109 | for row in c.fetchall(): 110 | if row[1] not in ['Wi-Fi Device','Wi-Fi Client']: 111 | try: 112 | del ts[row[0]] 113 | except KeyError as k: 114 | pass 115 | conn.close() 116 | else: 117 | # sqlite3 118 | conn = sqlite3.connect(f'file:{args.db}?mode=ro', uri=True) 119 | c = conn.cursor() 120 | sql = 'pragma query_only = on;' 121 | c.execute(sql) 122 | sql = 'pragma temp_store = 2;' # to store temp table and indices in memory 123 | c.execute(sql) 124 | sql = 'pragma journal_mode = off;' # disable journal for rollback (we don't use this) 125 | c.execute(sql) 126 | conn.commit() 127 | 128 | # keep only the data between 2 timestamps ignoring IGNORED macs with rssi 129 | # greater than the min value 130 | arg_list = ','.join(['?']*len(config['ignored'])) 131 | sql = '''select date,mac.address,rssi from probemon 132 | inner join mac on mac.id=probemon.mac 133 | where date <= ? and date >= ? 134 | and mac.address not in (%s) 135 | and rssi > ? 136 | order by date''' % (arg_list,) 137 | try: 138 | c.execute(sql, (args.end_time, args.start_time) + tuple(config['ignored']) + (args.rssi,)) 139 | except sqlite3.OperationalError as e: 140 | time.sleep(2) 141 | c.execute(sql, (args.end_time, args.start_time) + tuple(config['ignored']) + (args.rssi,)) 142 | for row in c.fetchall(): 143 | if row[1] in ts: 144 | ts[row[1]].append(row[0]) 145 | else: 146 | ts[row[1]] = [row[0]] 147 | conn.close() 148 | 149 | def match(m, s): 150 | # match on start of mac address and use % as wild-card like in SQL syntax 151 | if '%' in m: 152 | m = m.replace('%', '.*') 153 | else: 154 | m = m+'.*' 155 | m = '^'+m 156 | return re.search(m, s) is not None 157 | 158 | if None in ts.keys(): 159 | del ts[None] 160 | macs = list(ts.keys()) 161 | if args.mac : 162 | # keep mac with args.mac as substring 163 | macs = [m for m in macs if any(match(am.lower(), m) for am in args.mac)] 164 | 165 | # filter our data set based on min probe request or mac appearence 166 | for k,v in list(ts.items()): 167 | if (len(v) <= args.min and k not in args.knownmac) or k not in macs: 168 | del ts[k] 169 | 170 | # sort the data on frequency of appearence 171 | data = sorted(list(ts.items()), key=lambda x:len(x[1])) 172 | data.reverse() 173 | macs = [x for x,_ in data] 174 | times = [x for _,x in data] 175 | 176 | # merge all same vendor mac into one plot for a virtual MAC called 'OUI' 177 | for mv in args.merged: 178 | indx = [i for i,m in enumerate(macs) if m[:8] == mv] 179 | if len(indx) > 0: 180 | t = [] 181 | # merge all times for vendor macs 182 | for i in indx: 183 | t.extend(times[i]) 184 | macs = [m for i,m in enumerate(macs) if i not in indx] 185 | times = [x for i,x in enumerate(times) if i not in indx] 186 | macs.append(mv) 187 | times.append(sorted(t)) 188 | 189 | # merge all LAA mac into one plot for a virtual MAC called 'LAA' 190 | if args.privacy: 191 | indx = [i for i,m in enumerate(macs) if is_local_bit_set(m) and m not in args.knownmac and m[:8] not in args.merged] 192 | if len(indx) > 0: 193 | t = [] 194 | # merge all times for LAA macs 195 | for i in indx: 196 | t.extend(times[i]) 197 | macs = [m for i,m in enumerate(macs) if i not in indx] 198 | times = [x for i,x in enumerate(times) if i not in indx] 199 | macs.append('LAA') 200 | times.append(sorted(t)) 201 | 202 | return (macs, times) 203 | 204 | def plot_data(macs, times, args): 205 | global IS_WINDOW_OPENED 206 | 207 | # initialize plot 208 | if IS_WINDOW_OPENED: 209 | plt.close('all') 210 | fig, ax = plt.subplots() 211 | # change margin around axis to the border 212 | fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.07) 213 | # set our custom color cycler (without red and gray) 214 | ax.set_prop_cycle(cycler('color', COLORS)) 215 | 216 | # calculate size of marker given the number of macs to display and convert from inch to point 217 | markersize = (fig.get_figheight()/len(macs))*72 218 | # set default line style for the plot 219 | matplotlib.rc('lines', linestyle=':', linewidth=0.3, marker='|', markersize=markersize) 220 | # plot 221 | lines = [] 222 | for i,p in enumerate(times): 223 | # reverse order to get most frequent at top 224 | n = len(times)-i-1 225 | # constant value 226 | q = [n]*len(p) 227 | label = macs[i] 228 | if macs[i] in args.knownmac: 229 | line, = ax.plot(p, q, color='tab:red', label=label) 230 | elif macs[i] == 'LAA' or is_local_bit_set(macs[i]): 231 | if macs[i] != 'LAA': 232 | label = '%s (LAA)' % macs[i] 233 | line, = ax.plot(p, q, color='tab:gray', label=label) 234 | else: 235 | line, = ax.plot(p, q, label=label) 236 | if args.label: 237 | ax.text(args.end_time, q[-1], label, fontsize=8, color='black', horizontalalignment='right', verticalalignment='center', family='monospace') 238 | lines.append(line) 239 | 240 | # add a grey background on period greater than 15 minutes without data 241 | alltimes = [] 242 | for t in times: 243 | alltimes.extend(t) 244 | alltimes.sort() 245 | diff = [i for i,j in enumerate(zip(alltimes[:-1], alltimes[1:])) if (j[1]-j[0])>60*15] 246 | for i in diff: 247 | ax.axvspan(alltimes[i], alltimes[i+1], facecolor='#bbbbbb', alpha=0.5) 248 | 249 | # define helper function for labels and ticks 250 | def showdate(tick, pos): 251 | return time.strftime('%Y-%m-%d', time.localtime(tick)) 252 | def showtime(tick, pos): 253 | return time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(tick)) 254 | def showhourminute(tick, pos): 255 | return time.strftime('%H:%M', time.localtime(tick)) 256 | def showhour(tick, pos): 257 | return time.strftime('%Hh', time.localtime(tick)) 258 | def showmac(tick, pos): 259 | try: 260 | m = macs[len(times)-int(round(tick))-1] 261 | if m != 'LAA' and is_local_bit_set(m): 262 | m = '%s (LAA)' % m 263 | return m 264 | except IndexError: 265 | pass 266 | 267 | ## customize the appearence of our figure/plot 268 | ax.xaxis.set_remove_overlapping_locs(False) 269 | # customize label of major/minor ticks 270 | ax.xaxis.set_major_formatter(ticker.FuncFormatter(showdate)) 271 | if args.span == 'd': 272 | # show minor tick every hour 273 | ax.xaxis.set_minor_formatter(ticker.FuncFormatter(showhour)) 274 | ax.xaxis.set_minor_locator(ticker.MultipleLocator(60*60)) 275 | elif args.span == 'h': 276 | # show minor tick every x minutes 277 | ax.xaxis.set_minor_formatter(ticker.FuncFormatter(showhourminute)) 278 | h = args.span_time//3600 279 | sm = 10*60 280 | if h > 2: 281 | sm = 15*60 282 | if h > 6: 283 | sm = 30*60 284 | if h > 12: 285 | sm = 60*60 286 | ax.xaxis.set_minor_locator(ticker.MultipleLocator(sm)) 287 | elif args.span == 'm': 288 | # show minor tick every 5 minutes 289 | ax.xaxis.set_minor_formatter(ticker.FuncFormatter(showhourminute)) 290 | ax.xaxis.set_minor_locator(ticker.MultipleLocator(5*60)) 291 | 292 | # show only integer evenly spaced on y axis 293 | #ax.yaxis.set_major_locator(ticker.MaxNLocator(integer=True, steps=[1,2,4,5,10])) 294 | # don't draw y axis 295 | ax.yaxis.set_visible(False) 296 | # move down major tick labels not to overwrite minor tick labels and do not show major ticks 297 | ax.xaxis.set_tick_params(which='major', pad=15, length=0) 298 | # customize the label shown on mouse over 299 | ax.format_xdata = ticker.FuncFormatter(showtime) 300 | ax.format_ydata = ticker.FuncFormatter(showmac) 301 | # show vertical bars matching minor ticks 302 | ax.grid(True, axis='x', which='minor') 303 | # add a legend 304 | if args.legend: 305 | # add a custom label handler to draw rectangle instead of default line style 306 | ax.legend(lines, macs, loc='lower left', ncol=len(macs)//30+1, 307 | handler_map={matplotlib.lines.Line2D: MyLine2DHandler()}, prop={'family':'monospace', 'size':8}) 308 | # avoid too much space around our data by defining set 309 | space = 5*60 # 5 minutes 310 | ax.set_xlim(args.start_time-space, args.end_time+space) 311 | ax.set_ylim(-1, len(macs)) 312 | # add a title to the image 313 | if args.title is not None: 314 | if args.title == '': 315 | ts = time.localtime(os.stat(args.db).st_mtime) 316 | title = time.strftime('%Y-%m-%d %H:%M:%S', ts) 317 | else: 318 | title = args.title 319 | fig.text(0.49, 0.97, title, fontsize=8, alpha=0.2) 320 | 321 | # and tada ! 322 | if args.image: 323 | fig.set_size_inches(config['height']/config['dpi'], config['width']/config['dpi']) 324 | fig.savefig(args.image, dpi=config['dpi']) 325 | #fig.savefig('test.svg', format='svg') 326 | else: 327 | if IS_WINDOW_OPENED: 328 | fig.canvas.draw_idle() 329 | else: 330 | plt.show() 331 | IS_WINDOW_OPENED = True 332 | 333 | def main(): 334 | parser = argparse.ArgumentParser(description='Plot MAC presence from probe requests in the database') 335 | parser.add_argument('-b', '--db', default='probemon.db', help='file name of the db') 336 | parser.add_argument('-c', '--continuous', action='store_true', default=False, help='continously update the plot/image (every minute)') 337 | parser.add_argument('-i', '--image', default=None, const='plot.png', nargs='?', help='output an image') 338 | parser.add_argument('-l', '--legend', action='store_true', default=False, help='add a legend') 339 | parser.add_argument('--label', action='store_true', default=False, help='add a mac label for each plot') 340 | parser.add_argument('-k', '--knownmac', action='append', help='known mac to highlight in red') 341 | parser.add_argument('-g', '--merged', action='append', help='OUI to merge') 342 | parser.add_argument('-M', '--min', type=int, default=3, help='minimum number of probe requests to consider') 343 | parser.add_argument('-m', '--mac', action='append', help='only display that mac') 344 | parser.add_argument('-p', '--privacy', action='store_true', default=False, help='merge LAA MAC address') 345 | parser.add_argument('--pcap', help='pcap file to process instead of the db') 346 | parser.add_argument('--kismet', help='kismet db file to process instead of the db') 347 | parser.add_argument('--only-pr', default=False, action='store_true', help='when processing pcap file/kismet db, keep only probe requests') 348 | parser.add_argument('-r', '--rssi', type=int, default=-99, help='minimal value for RSSI') 349 | parser.add_argument('-s', '--start', help='start timestamp') 350 | parser.add_argument('--span-time', default='1d', help='span of time (coud be #d or ##h or ###m)') 351 | parser.add_argument('-t', '--title', nargs='?', const='', default=None, help='add a title to the top of image (if none specified, use a timestamp)') 352 | parser.add_argument('-v', '--verbose', action='store_true', default=False, help='be verbose') 353 | # RESERVED: span, start_time, end_time 354 | args = parser.parse_args() 355 | 356 | # TODO: fix that 357 | if args.continuous: 358 | if not args.image: 359 | print('Error: --continuous does not currently work without using an image. Please use -i/--image') 360 | sys.exit(-1) 361 | if args.pcap: 362 | print('Error: --continuous does not work with --pcap') 363 | sys.exit(-1) 364 | 365 | # parse span_time 366 | args.span = args.span_time[-1:] 367 | try: 368 | sp = int(args.span_time[:-1]) 369 | except ValueError: 370 | print('Error: --span-time argument should be of the form [digit]...[d|h|m]') 371 | sys.exit(-1) 372 | if args.span == 'd': 373 | args.span_time = sp*NUMOFSECSINADAY 374 | elif args.span == 'h': 375 | args.span_time = sp*60*60 376 | elif args.span == 'm': 377 | args.span_time = sp*60 378 | else: 379 | print('Error: --span-time postfix could only be d or h or m') 380 | sys.exit(-1) 381 | 382 | if args.knownmac is None: 383 | args.knownmac = config['knownmac'] 384 | if args.merged is None: 385 | args.merged = config['merged'] 386 | args.merged = (m[:8] for m in args.merged) 387 | 388 | if args.pcap: 389 | args.db = None 390 | if not os.path.exists(args.pcap): 391 | print(f'Error: pcap file not found {args.pcap}', file=sys.stderr) 392 | sys.exit(-1) 393 | if args.db and not os.path.exists(args.db): 394 | print(f'Error: file not found {args.db}', file=sys.stderr) 395 | sys.exit(-1) 396 | 397 | if args.start: 398 | try: 399 | start_time = time.mktime(time.strptime(args.start, '%Y-%m-%dT%H:%M')) 400 | except ValueError: 401 | try: 402 | date = time.strptime(args.start, '%Y-%m-%d') 403 | date = time.strptime('%sT12:00' % args.start, '%Y-%m-%dT%H:%M') 404 | start_time = time.mktime(date) 405 | except ValueError: 406 | print(f"Error: can't parse date timestamp", file=sys.stderr) 407 | sys.exit(-1) 408 | end_time = start_time + args.span_time 409 | else: 410 | end_time = time.time() 411 | start_time = end_time - args.span_time 412 | args.start_time = start_time 413 | args.end_time = end_time 414 | 415 | fig = None 416 | while True: 417 | if args.verbose: 418 | print(':: Gathering data') 419 | macs, times = get_data(args) 420 | if len(times) == 0 or len(macs) == 0 and not args.continuous: 421 | print(f'Error: nothing to plot', file=sys.stderr) 422 | sys.exit(-1) 423 | 424 | if args.verbose: 425 | print(':: Plotting data') 426 | plot_data(macs, times, args) 427 | 428 | if not args.continuous: 429 | # don't continue the loop if not asked to continue 430 | break 431 | time.sleep(REFRESH_TIME) 432 | 433 | if __name__ == '__main__': 434 | try: 435 | main() 436 | except KeyboardInterrupt as k: 437 | pass 438 | -------------------------------------------------------------------------------- /src/www/static/css/bootstrap-datepicker3.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Datepicker for Bootstrap v1.9.0 (https://github.com/uxsolutions/bootstrap-datepicker) 3 | * 4 | * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0) 5 | */ 6 | 7 | .datepicker{border-radius:4px;direction:ltr}.datepicker-inline{width:220px}.datepicker-rtl{direction:rtl}.datepicker-rtl.dropdown-menu{left:auto}.datepicker-rtl table tr td span{float:right}.datepicker-dropdown{top:0;left:0;padding:4px}.datepicker-dropdown:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(0,0,0,.15);border-top:0;border-bottom-color:rgba(0,0,0,.2);position:absolute}.datepicker-dropdown:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;border-top:0;position:absolute}.datepicker-dropdown.datepicker-orient-left:before{left:6px}.datepicker-dropdown.datepicker-orient-left:after{left:7px}.datepicker-dropdown.datepicker-orient-right:before{right:6px}.datepicker-dropdown.datepicker-orient-right:after{right:7px}.datepicker-dropdown.datepicker-orient-bottom:before{top:-7px}.datepicker-dropdown.datepicker-orient-bottom:after{top:-6px}.datepicker-dropdown.datepicker-orient-top:before{bottom:-7px;border-bottom:0;border-top:7px solid rgba(0,0,0,.15)}.datepicker-dropdown.datepicker-orient-top:after{bottom:-6px;border-bottom:0;border-top:6px solid #fff}.datepicker table{margin:0;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.datepicker table tr td,.datepicker table tr th{text-align:center;width:30px;height:30px;border-radius:4px;border:none}.table-striped .datepicker table tr td,.table-striped .datepicker table tr th{background-color:transparent}.datepicker table tr td.new,.datepicker table tr td.old{color:#777}.datepicker table tr td.day:hover,.datepicker table tr td.focused{background:#eee;cursor:pointer}.datepicker table tr td.disabled,.datepicker table tr td.disabled:hover{background:0 0;color:#777;cursor:default}.datepicker table tr td.highlighted{color:#000;background-color:#d9edf7;border-color:#85c5e5;border-radius:0}.datepicker table tr td.highlighted.focus,.datepicker table tr td.highlighted:focus{color:#000;background-color:#afd9ee;border-color:#298fc2}.datepicker table tr td.highlighted:hover{color:#000;background-color:#afd9ee;border-color:#52addb}.datepicker table tr td.highlighted.active,.datepicker table tr td.highlighted:active{color:#000;background-color:#afd9ee;border-color:#52addb}.datepicker table tr td.highlighted.active.focus,.datepicker table tr td.highlighted.active:focus,.datepicker table tr td.highlighted.active:hover,.datepicker table tr td.highlighted:active.focus,.datepicker table tr td.highlighted:active:focus,.datepicker table tr td.highlighted:active:hover{color:#000;background-color:#91cbe8;border-color:#298fc2}.datepicker table tr td.highlighted.disabled.focus,.datepicker table tr td.highlighted.disabled:focus,.datepicker table tr td.highlighted.disabled:hover,.datepicker table tr td.highlighted[disabled].focus,.datepicker table tr td.highlighted[disabled]:focus,.datepicker table tr td.highlighted[disabled]:hover,fieldset[disabled] .datepicker table tr td.highlighted.focus,fieldset[disabled] .datepicker table tr td.highlighted:focus,fieldset[disabled] .datepicker table tr td.highlighted:hover{background-color:#d9edf7;border-color:#85c5e5}.datepicker table tr td.highlighted.focused{background:#afd9ee}.datepicker table tr td.highlighted.disabled,.datepicker table tr td.highlighted.disabled:active{background:#d9edf7;color:#777}.datepicker table tr td.today{color:#000;background-color:#ffdb99;border-color:#ffb733}.datepicker table tr td.today.focus,.datepicker table tr td.today:focus{color:#000;background-color:#ffc966;border-color:#b37400}.datepicker table tr td.today:hover{color:#000;background-color:#ffc966;border-color:#f59e00}.datepicker table tr td.today.active,.datepicker table tr td.today:active{color:#000;background-color:#ffc966;border-color:#f59e00}.datepicker table tr td.today.active.focus,.datepicker table tr td.today.active:focus,.datepicker table tr td.today.active:hover,.datepicker table tr td.today:active.focus,.datepicker table tr td.today:active:focus,.datepicker table tr td.today:active:hover{color:#000;background-color:#ffbc42;border-color:#b37400}.datepicker table tr td.today.disabled.focus,.datepicker table tr td.today.disabled:focus,.datepicker table tr td.today.disabled:hover,.datepicker table tr td.today[disabled].focus,.datepicker table tr td.today[disabled]:focus,.datepicker table tr td.today[disabled]:hover,fieldset[disabled] .datepicker table tr td.today.focus,fieldset[disabled] .datepicker table tr td.today:focus,fieldset[disabled] .datepicker table tr td.today:hover{background-color:#ffdb99;border-color:#ffb733}.datepicker table tr td.today.focused{background:#ffc966}.datepicker table tr td.today.disabled,.datepicker table tr td.today.disabled:active{background:#ffdb99;color:#777}.datepicker table tr td.range{color:#000;background-color:#eee;border-color:#bbb;border-radius:0}.datepicker table tr td.range.focus,.datepicker table tr td.range:focus{color:#000;background-color:#d5d5d5;border-color:#7c7c7c}.datepicker table tr td.range:hover{color:#000;background-color:#d5d5d5;border-color:#9d9d9d}.datepicker table tr td.range.active,.datepicker table tr td.range:active{color:#000;background-color:#d5d5d5;border-color:#9d9d9d}.datepicker table tr td.range.active.focus,.datepicker table tr td.range.active:focus,.datepicker table tr td.range.active:hover,.datepicker table tr td.range:active.focus,.datepicker table tr td.range:active:focus,.datepicker table tr td.range:active:hover{color:#000;background-color:#c3c3c3;border-color:#7c7c7c}.datepicker table tr td.range.disabled.focus,.datepicker table tr td.range.disabled:focus,.datepicker table tr td.range.disabled:hover,.datepicker table tr td.range[disabled].focus,.datepicker table tr td.range[disabled]:focus,.datepicker table tr td.range[disabled]:hover,fieldset[disabled] .datepicker table tr td.range.focus,fieldset[disabled] .datepicker table tr td.range:focus,fieldset[disabled] .datepicker table tr td.range:hover{background-color:#eee;border-color:#bbb}.datepicker table tr td.range.focused{background:#d5d5d5}.datepicker table tr td.range.disabled,.datepicker table tr td.range.disabled:active{background:#eee;color:#777}.datepicker table tr td.range.highlighted{color:#000;background-color:#e4eef3;border-color:#9dc1d3}.datepicker table tr td.range.highlighted.focus,.datepicker table tr td.range.highlighted:focus{color:#000;background-color:#c1d7e3;border-color:#4b88a6}.datepicker table tr td.range.highlighted:hover{color:#000;background-color:#c1d7e3;border-color:#73a6c0}.datepicker table tr td.range.highlighted.active,.datepicker table tr td.range.highlighted:active{color:#000;background-color:#c1d7e3;border-color:#73a6c0}.datepicker table tr td.range.highlighted.active.focus,.datepicker table tr td.range.highlighted.active:focus,.datepicker table tr td.range.highlighted.active:hover,.datepicker table tr td.range.highlighted:active.focus,.datepicker table tr td.range.highlighted:active:focus,.datepicker table tr td.range.highlighted:active:hover{color:#000;background-color:#a8c8d8;border-color:#4b88a6}.datepicker table tr td.range.highlighted.disabled.focus,.datepicker table tr td.range.highlighted.disabled:focus,.datepicker table tr td.range.highlighted.disabled:hover,.datepicker table tr td.range.highlighted[disabled].focus,.datepicker table tr td.range.highlighted[disabled]:focus,.datepicker table tr td.range.highlighted[disabled]:hover,fieldset[disabled] .datepicker table tr td.range.highlighted.focus,fieldset[disabled] .datepicker table tr td.range.highlighted:focus,fieldset[disabled] .datepicker table tr td.range.highlighted:hover{background-color:#e4eef3;border-color:#9dc1d3}.datepicker table tr td.range.highlighted.focused{background:#c1d7e3}.datepicker table tr td.range.highlighted.disabled,.datepicker table tr td.range.highlighted.disabled:active{background:#e4eef3;color:#777}.datepicker table tr td.range.today{color:#000;background-color:#f7ca77;border-color:#f1a417}.datepicker table tr td.range.today.focus,.datepicker table tr td.range.today:focus{color:#000;background-color:#f4b747;border-color:#815608}.datepicker table tr td.range.today:hover{color:#000;background-color:#f4b747;border-color:#bf800c}.datepicker table tr td.range.today.active,.datepicker table tr td.range.today:active{color:#000;background-color:#f4b747;border-color:#bf800c}.datepicker table tr td.range.today.active.focus,.datepicker table tr td.range.today.active:focus,.datepicker table tr td.range.today.active:hover,.datepicker table tr td.range.today:active.focus,.datepicker table tr td.range.today:active:focus,.datepicker table tr td.range.today:active:hover{color:#000;background-color:#f2aa25;border-color:#815608}.datepicker table tr td.range.today.disabled.focus,.datepicker table tr td.range.today.disabled:focus,.datepicker table tr td.range.today.disabled:hover,.datepicker table tr td.range.today[disabled].focus,.datepicker table tr td.range.today[disabled]:focus,.datepicker table tr td.range.today[disabled]:hover,fieldset[disabled] .datepicker table tr td.range.today.focus,fieldset[disabled] .datepicker table tr td.range.today:focus,fieldset[disabled] .datepicker table tr td.range.today:hover{background-color:#f7ca77;border-color:#f1a417}.datepicker table tr td.range.today.disabled,.datepicker table tr td.range.today.disabled:active{background:#f7ca77;color:#777}.datepicker table tr td.selected,.datepicker table tr td.selected.highlighted{color:#fff;background-color:#777;border-color:#555;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td.selected.focus,.datepicker table tr td.selected.highlighted.focus,.datepicker table tr td.selected.highlighted:focus,.datepicker table tr td.selected:focus{color:#fff;background-color:#5e5e5e;border-color:#161616}.datepicker table tr td.selected.highlighted:hover,.datepicker table tr td.selected:hover{color:#fff;background-color:#5e5e5e;border-color:#373737}.datepicker table tr td.selected.active,.datepicker table tr td.selected.highlighted.active,.datepicker table tr td.selected.highlighted:active,.datepicker table tr td.selected:active{color:#fff;background-color:#5e5e5e;border-color:#373737}.datepicker table tr td.selected.active.focus,.datepicker table tr td.selected.active:focus,.datepicker table tr td.selected.active:hover,.datepicker table tr td.selected.highlighted.active.focus,.datepicker table tr td.selected.highlighted.active:focus,.datepicker table tr td.selected.highlighted.active:hover,.datepicker table tr td.selected.highlighted:active.focus,.datepicker table tr td.selected.highlighted:active:focus,.datepicker table tr td.selected.highlighted:active:hover,.datepicker table tr td.selected:active.focus,.datepicker table tr td.selected:active:focus,.datepicker table tr td.selected:active:hover{color:#fff;background-color:#4c4c4c;border-color:#161616}.datepicker table tr td.selected.disabled.focus,.datepicker table tr td.selected.disabled:focus,.datepicker table tr td.selected.disabled:hover,.datepicker table tr td.selected.highlighted.disabled.focus,.datepicker table tr td.selected.highlighted.disabled:focus,.datepicker table tr td.selected.highlighted.disabled:hover,.datepicker table tr td.selected.highlighted[disabled].focus,.datepicker table tr td.selected.highlighted[disabled]:focus,.datepicker table tr td.selected.highlighted[disabled]:hover,.datepicker table tr td.selected[disabled].focus,.datepicker table tr td.selected[disabled]:focus,.datepicker table tr td.selected[disabled]:hover,fieldset[disabled] .datepicker table tr td.selected.focus,fieldset[disabled] .datepicker table tr td.selected.highlighted.focus,fieldset[disabled] .datepicker table tr td.selected.highlighted:focus,fieldset[disabled] .datepicker table tr td.selected.highlighted:hover,fieldset[disabled] .datepicker table tr td.selected:focus,fieldset[disabled] .datepicker table tr td.selected:hover{background-color:#777;border-color:#555}.datepicker table tr td.active,.datepicker table tr td.active.highlighted{color:#fff;background-color:#337ab7;border-color:#2e6da4;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td.active.focus,.datepicker table tr td.active.highlighted.focus,.datepicker table tr td.active.highlighted:focus,.datepicker table tr td.active:focus{color:#fff;background-color:#286090;border-color:#122b40}.datepicker table tr td.active.highlighted:hover,.datepicker table tr td.active:hover{color:#fff;background-color:#286090;border-color:#204d74}.datepicker table tr td.active.active,.datepicker table tr td.active.highlighted.active,.datepicker table tr td.active.highlighted:active,.datepicker table tr td.active:active{color:#fff;background-color:#286090;border-color:#204d74}.datepicker table tr td.active.active.focus,.datepicker table tr td.active.active:focus,.datepicker table tr td.active.active:hover,.datepicker table tr td.active.highlighted.active.focus,.datepicker table tr td.active.highlighted.active:focus,.datepicker table tr td.active.highlighted.active:hover,.datepicker table tr td.active.highlighted:active.focus,.datepicker table tr td.active.highlighted:active:focus,.datepicker table tr td.active.highlighted:active:hover,.datepicker table tr td.active:active.focus,.datepicker table tr td.active:active:focus,.datepicker table tr td.active:active:hover{color:#fff;background-color:#204d74;border-color:#122b40}.datepicker table tr td.active.disabled.focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.highlighted.disabled.focus,.datepicker table tr td.active.highlighted.disabled:focus,.datepicker table tr td.active.highlighted.disabled:hover,.datepicker table tr td.active.highlighted[disabled].focus,.datepicker table tr td.active.highlighted[disabled]:focus,.datepicker table tr td.active.highlighted[disabled]:hover,.datepicker table tr td.active[disabled].focus,.datepicker table tr td.active[disabled]:focus,.datepicker table tr td.active[disabled]:hover,fieldset[disabled] .datepicker table tr td.active.focus,fieldset[disabled] .datepicker table tr td.active.highlighted.focus,fieldset[disabled] .datepicker table tr td.active.highlighted:focus,fieldset[disabled] .datepicker table tr td.active.highlighted:hover,fieldset[disabled] .datepicker table tr td.active:focus,fieldset[disabled] .datepicker table tr td.active:hover{background-color:#337ab7;border-color:#2e6da4}.datepicker table tr td span{display:block;width:23%;height:54px;line-height:54px;float:left;margin:1%;cursor:pointer;border-radius:4px}.datepicker table tr td span.focused,.datepicker table tr td span:hover{background:#eee}.datepicker table tr td span.disabled,.datepicker table tr td span.disabled:hover{background:0 0;color:#777;cursor:default}.datepicker table tr td span.active,.datepicker table tr td span.active.disabled,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active:hover{color:#fff;background-color:#337ab7;border-color:#2e6da4;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td span.active.disabled.focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover.focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active.focus,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover.focus,.datepicker table tr td span.active:hover:focus{color:#fff;background-color:#286090;border-color:#122b40}.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover{color:#fff;background-color:#286090;border-color:#204d74}.datepicker table tr td span.active.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover.active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active:hover:active{color:#fff;background-color:#286090;border-color:#204d74}.datepicker table tr td span.active.active.focus,.datepicker table tr td span.active.active:focus,.datepicker table tr td span.active.active:hover,.datepicker table tr td span.active.disabled.active.focus,.datepicker table tr td span.active.disabled.active:focus,.datepicker table tr td span.active.disabled.active:hover,.datepicker table tr td span.active.disabled:active.focus,.datepicker table tr td span.active.disabled:active:focus,.datepicker table tr td span.active.disabled:active:hover,.datepicker table tr td span.active.disabled:hover.active.focus,.datepicker table tr td span.active.disabled:hover.active:focus,.datepicker table tr td span.active.disabled:hover.active:hover,.datepicker table tr td span.active.disabled:hover:active.focus,.datepicker table tr td span.active.disabled:hover:active:focus,.datepicker table tr td span.active.disabled:hover:active:hover,.datepicker table tr td span.active:active.focus,.datepicker table tr td span.active:active:focus,.datepicker table tr td span.active:active:hover,.datepicker table tr td span.active:hover.active.focus,.datepicker table tr td span.active:hover.active:focus,.datepicker table tr td span.active:hover.active:hover,.datepicker table tr td span.active:hover:active.focus,.datepicker table tr td span.active:hover:active:focus,.datepicker table tr td span.active:hover:active:hover{color:#fff;background-color:#204d74;border-color:#122b40}.datepicker table tr td span.active.disabled.disabled.focus,.datepicker table tr td span.active.disabled.disabled:focus,.datepicker table tr td span.active.disabled.disabled:hover,.datepicker table tr td span.active.disabled.focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover.disabled.focus,.datepicker table tr td span.active.disabled:hover.disabled:focus,.datepicker table tr td span.active.disabled:hover.disabled:hover,.datepicker table tr td span.active.disabled:hover[disabled].focus,.datepicker table tr td span.active.disabled:hover[disabled]:focus,.datepicker table tr td span.active.disabled:hover[disabled]:hover,.datepicker table tr td span.active.disabled[disabled].focus,.datepicker table tr td span.active.disabled[disabled]:focus,.datepicker table tr td span.active.disabled[disabled]:hover,.datepicker table tr td span.active:hover.disabled.focus,.datepicker table tr td span.active:hover.disabled:focus,.datepicker table tr td span.active:hover.disabled:hover,.datepicker table tr td span.active:hover[disabled].focus,.datepicker table tr td span.active:hover[disabled]:focus,.datepicker table tr td span.active:hover[disabled]:hover,.datepicker table tr td span.active[disabled].focus,.datepicker table tr td span.active[disabled]:focus,.datepicker table tr td span.active[disabled]:hover,fieldset[disabled] .datepicker table tr td span.active.disabled.focus,fieldset[disabled] .datepicker table tr td span.active.disabled:focus,fieldset[disabled] .datepicker table tr td span.active.disabled:hover,fieldset[disabled] .datepicker table tr td span.active.disabled:hover.focus,fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,fieldset[disabled] .datepicker table tr td span.active.focus,fieldset[disabled] .datepicker table tr td span.active:focus,fieldset[disabled] .datepicker table tr td span.active:hover,fieldset[disabled] .datepicker table tr td span.active:hover.focus,fieldset[disabled] .datepicker table tr td span.active:hover:focus,fieldset[disabled] .datepicker table tr td span.active:hover:hover{background-color:#337ab7;border-color:#2e6da4}.datepicker table tr td span.new,.datepicker table tr td span.old{color:#777}.datepicker .datepicker-switch{width:145px}.datepicker .datepicker-switch,.datepicker .next,.datepicker .prev,.datepicker tfoot tr th{cursor:pointer}.datepicker .datepicker-switch:hover,.datepicker .next:hover,.datepicker .prev:hover,.datepicker tfoot tr th:hover{background:#eee}.datepicker .next.disabled,.datepicker .prev.disabled{visibility:hidden}.datepicker .cw{font-size:10px;width:12px;padding:0 2px 0 5px;vertical-align:middle}.input-group.date .input-group-addon{cursor:pointer}.input-daterange{width:100%}.input-daterange input{text-align:center}.input-daterange input:first-child{border-radius:3px 0 0 3px}.input-daterange input:last-child{border-radius:0 3px 3px 0}.input-daterange .input-group-addon{width:auto;min-width:16px;padding:4px 5px;line-height:1.42857143;border-width:1px 0;margin-left:-5px;margin-right:-5px} -------------------------------------------------------------------------------- /src/www/mapot.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response, jsonify, g, render_template 2 | from flask_caching import Cache 3 | from datetime import datetime, timedelta 4 | import sqlite3 5 | import time 6 | import sys 7 | from pathlib import Path 8 | import tempfile 9 | import atexit 10 | import probe_pb2 11 | from yaml import load as yaml_load 12 | try: 13 | from yaml import CLoader as Loader 14 | except ImportError: 15 | from yaml import Loader 16 | 17 | sys.path.insert(0, '..') 18 | from stats import is_local_bit_set, build_sql_query, median 19 | # read config variable from config.yaml file 20 | try: 21 | with open('config.yaml', 'r') as f: 22 | cfg_doc = f.read() 23 | config = yaml_load(cfg_doc, Loader=Loader) 24 | config['merged'] = tuple(m[:8] for m in config['merged']) 25 | except FileNotFoundError as e: 26 | print('Warning: config.yaml not found, skipping', file=sys.stderr) 27 | config = {'knownmac': tuple(), 'merged': tuple()} 28 | 29 | CWD = Path(__file__).resolve().parent 30 | DATABASE = Path.joinpath(CWD, 'probemon.db') 31 | 32 | # temp dir for flask cache files 33 | TMPDIR = tempfile.TemporaryDirectory(prefix='mapot-cache-').name 34 | 35 | # cleanup temp cache dir on exit 36 | def cleanup(): 37 | for f in Path(TMPDIR).glob('*'): 38 | f.unlink() 39 | Path(TMPDIR).rmdir() 40 | 41 | atexit.register(cleanup) 42 | 43 | class InvalidUsage(Exception): 44 | status_code = 400 45 | 46 | def __init__(self, message, status_code=None, payload=None): 47 | Exception.__init__(self) 48 | self.message = message 49 | if status_code is not None: 50 | self.status_code = status_code 51 | self.payload = payload 52 | 53 | def to_dict(self): 54 | rv = dict(self.payload or ()) 55 | rv['message'] = self.message 56 | return rv 57 | 58 | def create_app(): 59 | app = Flask(__name__) 60 | 61 | cache = Cache(config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': TMPDIR}) 62 | cache.init_app(app) 63 | 64 | def get_db(): 65 | db = getattr(g, '_database', None) 66 | if db is None: 67 | db = g._database = sqlite3.connect(f'file:{DATABASE}?mode=ro', uri=True) 68 | return db 69 | 70 | @app.teardown_appcontext 71 | def close_connection(exception): 72 | db = getattr(g, '_database', None) 73 | if db is not None: 74 | db.close() 75 | 76 | @app.route('/') 77 | @app.route('/index.html') 78 | def index(): 79 | return render_template('index.html.j2') 80 | 81 | @app.route('/api/stats/days') 82 | @cache.cached(timeout=43200, query_string=True) # 12 hours 83 | def days(): 84 | cur = get_db().cursor() 85 | # to store temp table and indices in memory 86 | sql = 'pragma temp_store = 2;' 87 | cur.execute(sql) 88 | macs = request.args.getlist('macs') 89 | 90 | if macs is None: 91 | # return list of days with probes in db 92 | try: 93 | sql = 'select date from probemon' 94 | sql_args = () 95 | cur.execute(sql, sql_args) 96 | except sqlite3.OperationalError as e: 97 | return jsonify({'status': 'error', 'message': 'sqlite3 db is not accessible'}), 500 98 | 99 | days = set() 100 | for row in cur.fetchall(): 101 | t = time.strftime('%Y-%m-%d', time.localtime(row[0])) 102 | days.add(t) 103 | days = sorted(list(days)) 104 | missing = [] 105 | last = datetime.strptime(days[-1], '%Y-%m-%d') 106 | day = datetime.strptime(days[0], '%Y-%m-%d') 107 | while day != last: 108 | d = day.strftime('%Y-%m-%d') 109 | if d not in days: 110 | missing.append(d) 111 | day += timedelta(days=1) 112 | data = {'first': days[0], 'last': days[-1], 'missing': missing} 113 | return jsonify(data) 114 | else: 115 | # check if stats table is available 116 | try: 117 | cur.execute('select count(*) from sqlite_master where type=? and name=?', ('table', 'stats')) 118 | except sqlite3.OperationalError as e: 119 | return jsonify({'status': 'error', 'message': 'sqlite3 db is not accessible'}), 500 120 | if cur.fetchone()[0] == 1: 121 | # return day-by-day stats for macs 122 | params = ','.join(['?']*len(macs)) 123 | sql = f'''select mac.id, address from mac 124 | inner join vendor on vendor.id=mac.vendor 125 | where address in ({params});''' 126 | cur.execute(sql, macs) 127 | mac_ids = {} 128 | for row in cur.fetchall(): 129 | mac_ids[row[0]] = row[1] 130 | data = [] 131 | for m in list(mac_ids.keys()): 132 | md = [] 133 | ssids = set() 134 | sql = 'select date, first_seen, last_seen, count, min, max, avg, med, ssids from stats where mac_id=? order by date;' 135 | cur.execute(sql, (m,)) 136 | for d, first, last, count, rmin, rmax, ravg, rmed, ssid in cur.fetchall(): 137 | first = time.mktime(time.strptime(f'{d}T{first}', '%Y-%m-%dT%H:%M:%S'))*1000 138 | last = time.mktime(time.strptime(f'{d}T{last}', '%Y-%m-%dT%H:%M:%S'))*1000 139 | md.append({'day':d.replace('-', ''), 'count':count, 140 | 'last': last, 'first': first, 'min': rmin, 'max': rmax, 'avg': ravg, 'median': rmed}) 141 | ssids = ssids.union(ssid.split(',')) 142 | ssids = sorted(list(ssids)) 143 | if '' in ssids: 144 | ssids.remove('') 145 | data.append({'mac': mac_ids[m], 'days': md, 'ssids': ssids}) 146 | return jsonify(data) 147 | else: 148 | params = ','.join(['?']*len(macs)) 149 | sql = f'''select date,mac.address,rssi,ssid.name from probemon 150 | inner join ssid on ssid.id=probemon.ssid 151 | inner join mac on mac.id=probemon.mac 152 | where mac.address in ({params})''' 153 | sql_args = macs 154 | cur.execute(sql, sql_args) 155 | # WARNING: this is copy-pasted from stats.py 156 | stats = {} 157 | for row in cur.fetchall(): 158 | if row[1] not in list(stats.keys()): 159 | stats[row[1]] = {'ssids': set()} 160 | stats[row[1]]['ssids'].add(row[3]) 161 | day = time.strftime('%Y%m%d', time.localtime(row[0])) 162 | if day in stats[row[1]]: 163 | smd = stats[row[1]][day] 164 | smd['rssi'].append(row[2]) 165 | if row[0] > smd['last']: 166 | smd['last'] = row[0] 167 | if row[0] < smd['first']: 168 | smd['first'] = row[0] 169 | else: 170 | stats[row[1]][day] = {'rssi': [row[2]], 'first': row[0], 'last': row[0]} 171 | 172 | data = [] 173 | for mac in list(stats.keys()): 174 | md = [] 175 | for d in sorted(stats[mac].keys()): 176 | if d == 'ssids': 177 | continue 178 | rssi = stats[mac][d]['rssi'] 179 | md.append({'day':d, 'count':len(rssi), 180 | 'last': int(stats[mac][d]['last']*1000), 'first': int(stats[mac][d]['first']*1000), 181 | 'min': min(rssi), 'max': max(rssi), 'avg': sum(rssi)//len(rssi), 'median': median(rssi)}) 182 | ssids = list(stats[mac]['ssids']) 183 | if '' in ssids: 184 | ssids.remove('') 185 | data.append({'mac': mac, 'days': md, 'ssids': ssids}) 186 | return jsonify(data) 187 | 188 | @app.route('/api/stats/timestamp') 189 | @cache.cached(timeout=60) 190 | def timestamp(): 191 | '''returns latest modification time of the db''' 192 | ts = Path(DATABASE).stat().st_mtime 193 | timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) 194 | return jsonify({'timestamp': timestamp}) 195 | 196 | @app.route('/api/stats') 197 | @cache.cached(timeout=60, query_string=True) 198 | def stats(): 199 | '''returns stats for given macs between timestamp''' 200 | after = request.args.get('after') 201 | if after is not None: 202 | try: 203 | after = time.mktime(time.strptime(after, '%Y-%m-%dT%H:%M:%S')) 204 | except ValueError as v: 205 | raise InvalidUsage('Invalid after parameter') 206 | before = request.args.get('before') 207 | if before is not None: 208 | try: 209 | before = time.mktime(time.strptime(before, '%Y-%m-%dT%H:%M:%S')) 210 | except ValueError as v: 211 | raise InvalidUsage('Invalid before parameter') 212 | macs = request.args.getlist('macs') 213 | rssi, zero, day = None, False, False 214 | 215 | cur = get_db().cursor() 216 | # to store temp table and indices in memory 217 | sql = 'pragma temp_store = 2;' 218 | cur.execute(sql) 219 | 220 | sql, sql_args = build_sql_query(after, before, macs, rssi, zero, day) 221 | try: 222 | cur.execute(sql, sql_args) 223 | except sqlite3.OperationalError as e: 224 | return jsonify({'status': 'error', 'message': 'sqlite3 db is not accessible'}), 500 225 | 226 | # gather stats about each mac, same code as in stats.py 227 | # TODO: just import that 228 | macs = {} 229 | for row in cur.fetchall(): 230 | mac = row[1] 231 | if is_local_bit_set(mac): 232 | # create virtual mac for LAA mac address 233 | mac = 'LAA' 234 | if mac not in macs: 235 | macs[mac] = {'vendor': row[2], 'ssid': [], 'rssi': [], 'last': row[0], 'first':row[0]} 236 | d = macs[mac] 237 | if row[3] != '' and row[3] not in d['ssid']: 238 | d['ssid'].append(row[3]) 239 | if row[0] > d['last']: 240 | d['last'] = row[0] 241 | if row[0] < d['first']: 242 | d['first'] = row[0] 243 | if row[4] != 0: 244 | d['rssi'].append(row[4]) 245 | 246 | # sort on frequency of appearence of a mac 247 | tmp = [(k,len(v['rssi'])) for k,v in macs.items()] 248 | tmp = [m for m,_ in reversed(sorted(tmp, key=lambda k:k[1]))] 249 | 250 | data = [] 251 | # dump our stats 252 | for m in tmp: 253 | v = macs[m] 254 | first = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(v['first'])) 255 | last = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(v['last'])) 256 | t = {'mac': m, 'vendor': v['vendor'], 'ssids': sorted(v['ssid']), 'first': first, 'last': last} 257 | rssi = v['rssi'] 258 | if rssi != []: 259 | t.update({'rssi': {'count': len(rssi), 'min': min(rssi), 'max': max(rssi), 260 | 'avg': sum(rssi)/len(rssi), 'median': int(median(rssi))}}) 261 | data.append(t) 262 | 263 | return jsonify(data) 264 | 265 | @app.route('/api/probes') 266 | @cache.cached(timeout=60, query_string=True) 267 | def probes(): 268 | '''returns list of probe requests for given macs between timestamps''' 269 | after = request.args.get('after') 270 | if after is not None: 271 | try: 272 | after = time.mktime(time.strptime(after, '%Y-%m-%dT%H:%M:%S')) 273 | except ValueError as v: 274 | raise InvalidUsage('Invalid after parameter') 275 | before = request.args.get('before') 276 | if before is not None: 277 | try: 278 | before = time.mktime(time.strptime(before, '%Y-%m-%dT%H:%M:%S')) 279 | except ValueError as v: 280 | raise InvalidUsage('Invalid before parameter') 281 | rssi = request.args.get('rssi') 282 | if rssi is not None: 283 | try: 284 | rssi = int(rssi) 285 | except ValueError as v: 286 | raise InvalidUsage('Invalid rssi value') 287 | 288 | macs = request.args.get('macs') 289 | zero = request.args.get('zero') 290 | today = request.args.get('today') 291 | output = request.args.get('output', default='json') 292 | 293 | cur = get_db().cursor() 294 | # to store temp table and indices in memory 295 | sql = 'pragma temp_store = 2;' 296 | cur.execute(sql) 297 | 298 | now = time.time() 299 | sql, sql_args = build_sql_query(after, before, macs, rssi, zero, today) 300 | try: 301 | cur.execute(sql, sql_args) 302 | except sqlite3.OperationalError as e: 303 | return jsonify({'status': 'error', 'message': 'sqlite3 db is not accessible'}), 500 304 | 305 | vendor = {} 306 | ts = {} 307 | if after is not None: 308 | starting_ts = int(after*1000) 309 | elif today: 310 | starting_ts = int((now-24*60*60)*1000) 311 | else: 312 | sql = 'select date from probemon order by date limit 1;' 313 | c.execute(sql) 314 | starting_ts = int(c.fetchone()[0]*1000) 315 | # extract data from db 316 | for t, mac, vs, ssid, rssi in cur.fetchall(): 317 | #t = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(t)) 318 | d = (int(t*1000-starting_ts), int(rssi), ssid) 319 | if is_local_bit_set(mac) and mac not in config['knownmac'] and mac[:8] not in config['merged']: 320 | mac = 'LAA' 321 | if mac not in ts.keys(): 322 | ts[mac] = [d] 323 | vendor[mac] = vs 324 | else: 325 | ts[mac].append(d) 326 | 327 | data = [] 328 | # recollection 329 | for m in ts.keys(): 330 | if m == 'LAA' or m.startswith(config['merged']): 331 | continue # will deal with them later 332 | known = m in config['knownmac'] 333 | ssids = list(set(f[2] for f in ts[m])) 334 | t = {'mac':m, 'known': known, 'vendor': vendor[m], 'ssids': ssids, 'starting_ts': starting_ts, 335 | 'probereq': [{'ts': f[0], 'rssi':f[1], 'ssid': ssids.index(f[2])} for f in ts[m]]} 336 | if len(t['probereq']) > 3: 337 | data.append(t) 338 | data.sort(key=lambda x:len(x['probereq']), reverse=True) 339 | # LAA 340 | if 'LAA' in ts.keys(): 341 | ssids = list(set(f[2] for f in ts['LAA'])) 342 | t = {'mac':'LAA', 'vendor': u'UNKNOWN', 'ssids': ssids, 'starting_ts': starting_ts, 343 | 'probereq': [{'ts': f[0], 'rssi':f[1], 'ssid': ssids.index(f[2])} for f in ts['LAA']]} 344 | data.append(t) 345 | # MERGED 346 | for m in config['merged']: 347 | mm = [ma for ma in ts.keys() if ma.startswith(m)] 348 | p = [] 349 | for n in mm: 350 | p.extend(ts[n]) 351 | ssids = list(set(f[2] for f in p)) 352 | if len(p) == 0: 353 | continue 354 | p.sort(key=lambda x: x[0]) 355 | t = {'mac':m, 'vendor': u'UNKNOWN', 'ssids': ssids, 'starting_ts': starting_ts, 356 | 'probereq': [{'ts': f[0], 'rssi':f[1], 'ssid': ssids.index(f[2])} for f in p]} 357 | data.append(t) 358 | 359 | if output == 'json': 360 | resp = make_response(jsonify(data)) 361 | elif output == 'protobuf': 362 | res = probe_pb2.MyData() 363 | for t in data: 364 | p = res.probes.add() 365 | p.mac = t['mac'] 366 | p.vendor = t['vendor'] 367 | p.starting_ts = t['starting_ts'] 368 | for s in t['ssids']: 369 | sl = p.ssids.add() 370 | sl.name = s 371 | try: 372 | p.known = t['known'] 373 | except KeyError as k: 374 | p.known = False 375 | for f in t['probereq']: 376 | pr = p.probereq.add() 377 | pr.timestamp = f['ts'] 378 | pr.rssi = f['rssi'] 379 | pr.ssid = f['ssid'] 380 | resp = make_response(res.SerializeToString()) 381 | resp.headers['Content-Type'] = 'application/x-protobuf' 382 | 383 | if not today: 384 | resp.headers['Cache-Control'] = 'max-age=21600' 385 | return resp 386 | 387 | @app.route('/api/probes/latest') 388 | @cache.cached(timeout=60, query_string=True) 389 | def latest(): 390 | '''returns latest probe requests''' 391 | format = request.args.get('format') 392 | if format is None: 393 | format = 'text' 394 | 395 | cur = get_db().cursor() 396 | # to store temp table and indices in memory 397 | sql = 'pragma temp_store = 2;' 398 | cur.execute(sql) 399 | 400 | sql = '''select date, mac.address, vendor.name, ssid.name, rssi from probemon 401 | inner join mac on probemon.mac=mac.id 402 | inner join ssid on probemon.ssid=ssid.id 403 | inner join vendor on mac.vendor=vendor.id 404 | order by date desc limit 100''' 405 | sql_args = None 406 | try: 407 | cur.execute(sql) 408 | except sqlite3.OperationalError as e: 409 | return jsonify({'status': 'error', 'message': 'sqlite3 db is not accessible'}), 500 410 | 411 | # extract data from db 412 | text = '' 413 | for t, mac, vs, ssid, rssi in cur.fetchall(): 414 | t = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(t)) 415 | d = (t, int(rssi), ssid) 416 | if is_local_bit_set(mac): 417 | mac += ' (LAA)' 418 | text += f'{t}\t{mac}\t{int(rssi)}\t{vs}\n' 419 | 420 | resp = make_response('\n'.join(reversed(text.strip().split('\n')))) 421 | resp.headers['Content-Type'] = 'text/plain' 422 | return resp 423 | 424 | @app.route('/api/logs/raw') 425 | def raw(): 426 | day = request.args.get('day') 427 | hour = request.args.get('hour') 428 | if day is None or hour is None: 429 | return 'missing parameter', 422 430 | try: 431 | hour = int(hour) 432 | except ValueError as v: 433 | return 'invalid parameter', 400 434 | 435 | c = get_db().cursor() 436 | # return the raw log with id to avoid too much repeated strings 437 | start = time.mktime(time.strptime(f'{day}T{hour:02d}:00:00', '%Y-%m-%dT%H:%M:%S')) 438 | eday = f'{day}T23:59:59' if hour+1 == 24 else f'{day}T{hour+1}:00:00' 439 | end = time.mktime(time.strptime(eday, '%Y-%m-%dT%H:%M:%S')) 440 | sql = 'select * from probemon where date >= ? and date <= ? order by date asc;' 441 | c.execute(sql, (start, end)) 442 | rawlogs = [] 443 | for row in c.fetchall(): 444 | rawlogs.append(row) 445 | 446 | macs = set(m[1] for m in rawlogs) 447 | mac_ids = {} 448 | args_list = ','.join(["?"]*len(macs)) 449 | sql = f'''select mac.id,address,vendor.name from mac 450 | inner join vendor on vendor.id=mac.vendor 451 | where mac.id in ({args_list});''' 452 | c.execute(sql, tuple(macs)) 453 | del macs 454 | for row in c.fetchall(): 455 | mac_ids[row[0]] = (row[1], row[2]) 456 | 457 | ssids = set(m[2] for m in rawlogs) 458 | ssid_ids = {} 459 | args_list = ','.join(["?"]*len(ssids)) 460 | sql = f'select id,name from ssid where id in ({args_list});' 461 | c.execute(sql, tuple(ssids)) 462 | del ssids 463 | for row in c.fetchall(): 464 | ssid_ids[row[0]] = row[1] 465 | 466 | data = {'ssids': ssid_ids, 'macs':mac_ids, 'logs':rawlogs} 467 | return jsonify(data) 468 | 469 | @app.route('/robots.txt') 470 | def robot(): 471 | return app.send_static_file('robots.txt') 472 | 473 | @app.errorhandler(InvalidUsage) 474 | def handle_invalid_usage(error): 475 | response = jsonify(error.to_dict()) 476 | response.status_code = error.status_code 477 | return response 478 | @app.errorhandler(404) 479 | def error_404(e): 480 | return render_template('error.html.j2', error=e), 404 481 | @app.errorhandler(500) 482 | def error_500(e): 483 | return render_template('error.html.j2', error=e), 500 484 | 485 | return app 486 | 487 | if __name__ == '__main__': 488 | app = create_app() 489 | app.run(host='0.0.0.0', port=5556, threaded=True, debug=True) 490 | else: 491 | app = create_app() 492 | --------------------------------------------------------------------------------