Init repo
318
README.md
Normal file
@ -0,0 +1,318 @@
|
||||
# Projekt magisterski - skaner ruchu bezprzewodowego sieci 802.11
|
||||
|
||||
# Autor - Wojciech Pakulski - s426211
|
||||
|
||||
# Podprojekty - netvu i Netview
|
||||
|
||||
1. netvu - skaner sieciowy, analizator plików PCAP, serwer HTTP
|
||||
2. Netview - nakładka graficzna umożliwiająca interakcję z modułem serwera HTTP netvu
|
||||
|
||||
## Dokumentacja - netvu
|
||||
|
||||
### Opis
|
||||
|
||||
Program netvu jest aplikacją konsolową napisaną pod system Linux. Aplikacja wejściowa służy jako
|
||||
pośrednik, który przekazuje odpowiednie parametry do dostępnych modułów. Przed procesem budowania
|
||||
programu użytkownik może wybrać, które z modułów będą dostępne. Proces budowania i wymagane
|
||||
zależności są zapisane w odpowiednich plikach `CMakeLists.txt`.
|
||||
|
||||
Po uruchomieniu programu bez podania parametrów bądź z flagą `-h`/`--help` ukazane zostaną
|
||||
dostępne opcje i domyślne argumenty:
|
||||
|
||||
```
|
||||
Allowed options:
|
||||
-h [ --help ] print usage information and exit
|
||||
-c [ --config ] arg (=netvu.conf) alternative config file
|
||||
|
||||
Configuration:
|
||||
-m [ --mode ] arg runtime mode - scanner/analyzer/server
|
||||
|
||||
Scanner:
|
||||
-i [ --scanner.interface ] arg wireless interface
|
||||
-C [ --scanner.channels ] arg space separated 802.11 channels numbers
|
||||
-M [ --scanner.max_packets ] arg (=0) packet capture limit
|
||||
-l [ --scanner.log_dir ] arg (=log) where to save captured packets
|
||||
-I [ --scanner.channel_hop_interval ] arg (=300)
|
||||
channel hop interval in milliseconds
|
||||
|
||||
Analyzer:
|
||||
-L [ --analyzer.log_dir ] arg (=log) where captured packets are stored
|
||||
-D [ --analyzer.database_dir ] arg (=db)
|
||||
where to save analisys result
|
||||
|
||||
Server:
|
||||
-a [ --server.address ] arg (=0.0.0.0)
|
||||
listening address
|
||||
-p [ --server.port ] arg (=9000) listening port
|
||||
-d [ --server.database_dir ] arg (=db)
|
||||
where analisys result is stored
|
||||
-o [ --server.cors ] arg (=*) access control allow origin
|
||||
```
|
||||
|
||||
Argumenty nie muszą być podawane wyłącznie z poziomu terminala. Netvu postara się odczytać
|
||||
także wartości z pliku konfiguracyjnego `netvu.conf`, jeśli taki istnieje. Użytkownik może
|
||||
podać inną ścieżkę do pliku konfiguracyjnego przez parametr `-c`/`--config`.
|
||||
|
||||
Przykładowy plik konfiguracyjny wygląda następująco:
|
||||
|
||||
```
|
||||
mode=scanner
|
||||
|
||||
[scanner]
|
||||
max_packets=2000
|
||||
|
||||
[analyzer]
|
||||
log_dir=pcaps
|
||||
database_dir=/home/x/my_db
|
||||
```
|
||||
|
||||
Parametr `-m`/`--mode` ustala tryb w jakim netvu zostanie uruchomiony. W przypadku próby
|
||||
uruchomienia trybu, który nie został dołączony w procesie budowania, zostanie zwrócony błąd.
|
||||
|
||||
Dokładniejszy opis każdego z dostępnych parametrów zostanie opisany w sekcjach poświęconych
|
||||
danemu modułowi.
|
||||
|
||||
### Budowanie
|
||||
|
||||
Do zbudowania wszystkich modułów wymagane są następujące narzędzia i biblioteki:
|
||||
|
||||
* Kompilator obsługujący C++17
|
||||
* CMake 3.16
|
||||
* libtins
|
||||
* Boost >=1.7
|
||||
* SQLite3
|
||||
* nlohmann_json
|
||||
* Biblioteka wątków (Threads)
|
||||
|
||||
Przykładowy proces zbudowania projektu może wyglądać następująco:
|
||||
|
||||
```
|
||||
$ mkdir netvu/bin
|
||||
$ cd netvu/bin
|
||||
$ cmake ../src
|
||||
$ cmake --build .
|
||||
```
|
||||
|
||||
### Moduł skanera sieciowego
|
||||
|
||||
#### Opis
|
||||
|
||||
Moduł skanera składa się z kilku funkcji - nie jest nią jedynie sniffowanie ruchu Wi-Fi. Do jego
|
||||
zadań należy również zapisywanie przechwyconych pakietów do plików i
|
||||
przełączanie kanałów. Ponadto, w jego skład wchodzą również funkcje pomocnicze, są to:
|
||||
|
||||
1. Wyszukiwanie domyślnego interfejsu karty bezprzewodowej.
|
||||
2. Sprawdzanie obecności enkapsulacji RadioTap.
|
||||
3. Włączanie/wyłączanie trybu Monitor na czas działania skanera.
|
||||
4. Odszukanie dostępnych kanałów na danym interfejsie.
|
||||
|
||||
Nie wszystkie zadania muszą być wykonywane po uruchomieniu skanera. Część zadań może być całkowicie
|
||||
pominięta w zależności od parametrów podanych przez użytkownika. Większość zadań jest wykonywana
|
||||
synchronicznie, wyjątkiem są sniffowanie pakietów i przełączanie kanałów, które są podzielone na
|
||||
dwa osobne wątki.
|
||||
|
||||
Spośród pozostałych modułów jest to
|
||||
jedyny, który musi zostać uruchomiony z podwyższonymi uprawnieniami np. poprzez `sudo`.
|
||||
|
||||
#### Parametry
|
||||
|
||||
* `interface` - określa, który interfejs sieciowy zostanie wykorzystany do skanowania .
|
||||
* `channels` - oddzielone spacją numery kanałów po których interfejs będzie się przełączać.
|
||||
W przypadku nie podania wartości lub wartości 0 netvu postara się automatycznie wykryć listę
|
||||
dostępnych kanałów na danym interfejsie. Zostaną wówczas wykorzystane wszystkie wykryte kanały.
|
||||
* `max_packets` - określa, po ilu pakietach skanowanie zostanie przerwane. W przypadku nie podania
|
||||
wartości lub wartości 0 skanowanie będzie trwało do momentu przerwania przez użytkownika.
|
||||
* `log_dir` - katalog do którego zostanie zapisany wynik skanowania w formacie pcap.
|
||||
* `channel_hop_interval` - podawany w milisekundach czas po którym netvu będzie przełączać kanały.
|
||||
|
||||
|
||||
#### Wymagania
|
||||
|
||||
* Bezprzewodowa karta sieciowa i sterownik z obsługą trybu Monitor
|
||||
* Podwyższone uprawnienia systemowe
|
||||
* Obecność następujących programów:
|
||||
1. `ip` - w celu wykrycia obecności enkapsulacji RadioTap na karcie sieciowej
|
||||
2. `ifconfig` - w celu włączania/wyłączania interfejsu karty
|
||||
3. `iwconfig` - w celu przełączania między trybami Managed/Monitor na czas uruchomienia
|
||||
programu, wyszukiwaniu interfejsów bezprzewodowych oraz przełączaniu kanałów
|
||||
4. `iwlist` - w celu wykrycia dostępnych kanałów
|
||||
|
||||
#### Przykład
|
||||
|
||||
Wywołanie bez podania konkretnego interfejsu, skanowanie jedynie kanałów 1,6 i 12 z interwałem
|
||||
przełączania kanału co pół sekundy i limitem 300 pakietów.
|
||||
Przed wywołaniem programu na interfejsie WLAN włączony był tryb Managed.
|
||||
|
||||
|
||||
```
|
||||
% sudo ./netvu --scanner.channels 1 6 12 --scanner.max_packets 300 --scanner.channel_hop_interval 500 --mode scanner
|
||||
Starting scanner
|
||||
lo no wireless extensions.
|
||||
|
||||
enp11s0 no wireless extensions.
|
||||
|
||||
enp12s0 no wireless extensions.
|
||||
|
||||
br-06884e076801 no wireless extensions.
|
||||
|
||||
docker0 no wireless extensions.
|
||||
|
||||
Using wlp9s0
|
||||
Turning wlp9s0 down
|
||||
Enabling Monitor mode
|
||||
Turning wlp9s0 up
|
||||
Packet: 1/300
|
||||
Packet: 2/300
|
||||
Packet: 3/300
|
||||
Packet: 4/300
|
||||
Packet: 5/300
|
||||
|
||||
...
|
||||
|
||||
Packet: 298/300
|
||||
Packet: 299/300
|
||||
Packet: 300/300
|
||||
Turning wlp9s0 down
|
||||
Enabling Managed mode
|
||||
Turning wlp9s0up
|
||||
All done
|
||||
|
||||
```
|
||||
|
||||
|
||||
#### Budowanie
|
||||
|
||||
Do zbudowania modułu skanera wymagane są następujące komponenty:
|
||||
|
||||
* Boost (thread, filesystem, system)
|
||||
* libtins
|
||||
* Threads
|
||||
|
||||
### Moduł analizatora
|
||||
|
||||
#### Opis
|
||||
|
||||
Zadaniem analizatora jest iteracja po wszystkich plikach pcap z danego katalogu utworzonych przez
|
||||
skaner i wyciągnięcie z nich konkretnych danych. Wśród tych danych należą m. in. adresy MAC,
|
||||
połączenia między urządzeniami i ilość przesłanych pakietów między hostami, średnia siła sygnału
|
||||
czy interwały w których wykryto ruch z lub do danego urządzenia. Wszystkie dane wyciągnięte z
|
||||
plików są
|
||||
umieszczane w pamięciowej bazie danych SQLite, która jest zrzutowana do pliku po zakończeniu pracy
|
||||
modułu.
|
||||
|
||||
### Przykład
|
||||
|
||||
Wywołanie modułu z domyślnymi parametrami:
|
||||
|
||||
```
|
||||
% ./netvu --mode analyzer
|
||||
Starting analyzer
|
||||
Adding hosts..Done
|
||||
Adding connections..Done
|
||||
Adding host info..Done
|
||||
Adding packet types..Done
|
||||
Adding timestamps..Done
|
||||
Adding scanning times..Done
|
||||
|
||||
% ls db
|
||||
2021-01-31_13-30-01.sqlite
|
||||
```
|
||||
|
||||
#### Kompilacja
|
||||
|
||||
Do skompilowania modułu analizatora wymagane są następujące komponenty:
|
||||
|
||||
* SQLite3
|
||||
* libtins
|
||||
* Threads
|
||||
|
||||
### Moduł serwera
|
||||
|
||||
#### Opis
|
||||
|
||||
Moduł ten pełni funkcję lekkiego serwera HTTP udostępniającego dane w formacie JSON z najnowszej
|
||||
bazy SQLite wygenerowanej przez moduł analizatora. Serwer asynchronicznie otwiera połączenia po
|
||||
otrzymaniu odpowiedniej metody HTTP. Serwer nie umożliwia modyfikacji bazy, jego zadaniem jest
|
||||
jedynie pobieranie danych na żądanie i przesyłanie ich do klienta. W razie potrzeby istnieje
|
||||
możliwość ustawienia innych parametrów nasłuchiwania i odczytu danych z innego katalogu.
|
||||
|
||||
#### Parametry
|
||||
|
||||
* `address` - adres, pod którym serwer będzie nasłuchiwał nadchodzących połączeń
|
||||
* `port` - port, pod którym serwer będzie nasłuchiwał nadchodzących połączeń
|
||||
* `database_dir` - katalog, w którym zawarte są pliki sqlite wygenerowane przez analizator
|
||||
* `cors` - URI, na które serwer będzie odpowiadał
|
||||
|
||||
#### Budowanie
|
||||
|
||||
Do zbudowania modułu serwera wymagane są następujące komponenty:
|
||||
|
||||
* Boost (thread)
|
||||
* SQLite3
|
||||
* nlohmann_json
|
||||
* Threads
|
||||
|
||||
## Dokumentacja - Netview
|
||||
|
||||
### Opis
|
||||
|
||||
Netview jest aplikacją internetową za pomocą której można interaktywnie obejrzeć wyniki
|
||||
modułu analizatora netvu. Do komunikacji z netvu nie jest potrzebny serwer proxy - Netview można
|
||||
uruchomić lokalnie otwierając plik `index.html` w przeglądarce. Aplikacja postara się automatycznie
|
||||
załadować z serwera dane o hostach po jej uruchomieniu. Domyślnie Netview będzie próbować się
|
||||
połączyć z serwerem na adresie lokalnym i porcie 9000. Adres ten można zmienić w widoku ustawień,
|
||||
a po jego zmianie dane zostaną pobrane automatycznie.
|
||||
|
||||
Aplikacja składa się z:
|
||||
|
||||
1. Interaktywnego grafu w którym widoczne są wykryte urządzenia, połączenia a także natężenie ruchu
|
||||
między urządzeniami. W widoku tym można przełączać wyświetlanie danych elementów
|
||||
|
||||
* Odległość wierzchołków od skanera jest wyliczona na podstawie średniej
|
||||
arytmetycznej wykrytych sił sygnału (dBm).
|
||||
* Grubość krawędzi jest zależna od ilości przesłanych pakietów w daną stronę. Można dzięki temu
|
||||
łatwiej stwierdzić, czy dany host pełni funkcję punktu dostępowego.
|
||||
* Szczegółowe informacje o urządzeniach w kolejnych zakładkach dostępne są po
|
||||
zaznaczeniu wierzchołków.
|
||||
* W celu ułatwienia analizy możliwe jest przemieszczanie wierzchołków.
|
||||
|
||||

|
||||
|
||||
2. Widok statystyk składający się z wykresów sumy wysłanych, odebranych i transmitowanych pakietów.
|
||||
|
||||

|
||||
|
||||
3. Widok szczegółów urządzeń oraz wykrytych SSID wraz z powiązanymi z nimi punktami dostępowymi.
|
||||
W widoku szczegółów dostępne są informacje takie jak:
|
||||
1. Adres MAC
|
||||
1. Liczba pakietów z/do danego hosta
|
||||
3. Powiązany adres BSSID i SSID
|
||||
4. Partnerzy z którymi host nawiązał połączenie
|
||||
5. Wykres dostępności urządzenia - punkty w czasie, w których wykryto ruch z/do urządzenia
|
||||
|
||||

|
||||

|
||||
|
||||
4. Widoku informacji ogólnych, w którym zawarty jest wykres liczby pakietów danego typu, a także
|
||||
przedziały czasowe w którym rozpoczęto i zakończono skanowanie
|
||||
|
||||

|
||||
|
||||
Jeśli istnieje potrzeba zmiany adresu serwera, można to zrobić klikając w przycisk ustawień
|
||||
dostępnym na pasku nawigacji. Po jego kliknięciu otworzy się okienko zmiany adresu.
|
||||
|
||||

|
||||
|
||||
|
||||
### Wymagania
|
||||
|
||||
* Do pobrania danych konieczny jest uruchomiony moduł serwera netvu.
|
||||
* Przeglądarka internetowa.
|
||||
|
||||
### Budowanie - wymagania
|
||||
|
||||
W celu zbudowania Netview z kodu źródłowego konieczna jest instalacja Vue CLI (Vue.js 3).
|
||||
Do poprawnego zbudowania należy także pobrać zależności projektu zawarte w pliku `package.json`.
|
||||
Następnie, należy wykonać polecenie `npm run build`. Zostanie wówczas wygenerowany katalog dist
|
||||
zawierający gotową aplikację.
|
||||
|
BIN
doc-img/aps.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
doc-img/disconnected.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
doc-img/graph.png
Normal file
After Width: | Height: | Size: 445 KiB |
BIN
doc-img/host_info.png
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
doc-img/overall.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
doc-img/stats.png
Normal file
After Width: | Height: | Size: 80 KiB |
3
netview/.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
7
netview/.editorconfig
Normal file
@ -0,0 +1,7 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
29
netview/.eslintrc.js
Normal file
@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'@vue/airbnb',
|
||||
'@vue/typescript/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)',
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
26
netview/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
/tests/e2e/videos/
|
||||
/tests/e2e/screenshots/
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
34
netview/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# netview
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your unit tests
|
||||
```
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run your end-to-end tests
|
||||
```
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
netview/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
],
|
||||
};
|
3
netview/cypress.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"pluginsFile": "tests/e2e/plugins/index.js"
|
||||
}
|
6
netview/jest.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
|
||||
transform: {
|
||||
'^.+\\.vue$': 'vue-jest',
|
||||
},
|
||||
};
|
17588
netview/package-lock.json
generated
Normal file
53
netview/package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "netview",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"test:e2e": "vue-cli-service test:e2e",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.6.0",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^5.0.0-beta1",
|
||||
"bootstrap-icons": "^1.3.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"core-js": "^3.6.5",
|
||||
"cytoscape": "^3.17.1",
|
||||
"vue": "^3.0.0",
|
||||
"vue-class-component": "^8.0.0-0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vuex": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "^5.0.4",
|
||||
"@types/chart.js": "^2.9.29",
|
||||
"@types/cytoscape": "^3.14.11",
|
||||
"@types/jest": "^24.0.19",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-e2e-cypress": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-plugin-unit-jest": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"@vue/eslint-config-airbnb": "^5.0.2",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"@vue/test-utils": "^2.0.0-0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-unused-imports": "^1.0.1",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "~3.9.3",
|
||||
"vue-jest": "^5.0.0-0"
|
||||
}
|
||||
}
|
BIN
netview/public/favicon.ico
Normal file
After Width: | Height: | Size: 260 KiB |
17
netview/public/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
34
netview/src/App.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<navigation />
|
||||
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive include="Network">
|
||||
<component :is="Component"></component>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import store from './store';
|
||||
import Navigation from './views/Navigation.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
Navigation,
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
await store.dispatch('loadHosts');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import "~bootstrap-icons/font/bootstrap-icons.css";
|
||||
</style>
|
1285
netview/src/assets/font/bootstrap-icons.css
vendored
Normal file
1267
netview/src/assets/font/bootstrap-icons.json
Normal file
BIN
netview/src/assets/font/fonts/bootstrap-icons.woff
Normal file
BIN
netview/src/assets/font/fonts/bootstrap-icons.woff2
Normal file
5107
netview/src/assets/font/index.html
Normal file
BIN
netview/src/assets/logo.png
Normal file
After Width: | Height: | Size: 337 KiB |
17
netview/src/components/Alert.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="alert" :class="`alert-${type}`" role="alert">
|
||||
{{ msg }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Alert',
|
||||
props: {
|
||||
type: String,
|
||||
msg: String,
|
||||
},
|
||||
});
|
||||
</script>
|
93
netview/src/components/ConnectionFormModal.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
|
||||
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">Ustawienia</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="serverUrl" class="form-label">Adres serwera</label>
|
||||
<input
|
||||
type="url"
|
||||
class="form-control"
|
||||
id="serverUrl"
|
||||
placeholder="URI:PORT"
|
||||
required
|
||||
v-model="url"
|
||||
/>
|
||||
</div>
|
||||
<loading-spinner v-if="loading" msg="Łączenie.." />
|
||||
<alert v-if="errorMsg" :msg="errorMessage" type="danger" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button type="button" class="btn btn-primary" @click="setServerURL">Zapisz</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import store from '@/store';
|
||||
|
||||
import axios from 'axios';
|
||||
import { defineComponent } from 'vue';
|
||||
import { Modal } from 'bootstrap';
|
||||
import LoadingSpinner from './LoadingSpinner.vue';
|
||||
import Alert from './Alert.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ConnectionFormModal',
|
||||
|
||||
data() {
|
||||
return {
|
||||
url: store.state.serverURL,
|
||||
loading: false,
|
||||
errorMsg: '',
|
||||
};
|
||||
},
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
Alert,
|
||||
},
|
||||
computed: {
|
||||
errorMessage: {
|
||||
get(): string {
|
||||
return `Błąd: ${this.errorMsg}`;
|
||||
},
|
||||
set(newError: Error) {
|
||||
this.errorMsg = newError.message;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async setServerURL() {
|
||||
this.loading = true;
|
||||
this.errorMsg = '';
|
||||
|
||||
try {
|
||||
const res = await axios.get(`${this.url}/hosts`);
|
||||
// if (res.status) {
|
||||
store.dispatch('loadHosts');
|
||||
// store.commit('setServerURL', this.url);
|
||||
// store.commit('setHosts', res.data);
|
||||
const modal = Modal.getInstance(document.querySelector('#exampleModal') as Element);
|
||||
modal.toggle();
|
||||
// }
|
||||
} catch (err) {
|
||||
this.errorMsg = err.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
60
netview/src/components/HostsInNetwork.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
|
||||
<h4 class="d-flex flex-column align-items-center">AP powiązane z danym SSID:</h4>
|
||||
<div class="accordion" id="accordionExample">
|
||||
<div class="accordion-item" v-for="ssid of ssids" :key="ssid">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" type="button" @click="collapse(ssid)">
|
||||
{{ ssid }}
|
||||
</button>
|
||||
</h2>
|
||||
<div
|
||||
:id="'collapse-' + ssid"
|
||||
class="accordion-collapse collapse"
|
||||
aria-labelledby="headingOne"
|
||||
data-bs-parent="#accordionExample"
|
||||
>
|
||||
<div class="accordion-body">
|
||||
<ul>
|
||||
<li v-for="host of hostsInNetwork(ssid)" :key="host.id">
|
||||
ID: {{ host.id }}, MAC: {{ host.hw }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import store from '@/store';
|
||||
import { defineComponent } from 'vue';
|
||||
import { Collapse } from 'bootstrap';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HostsInNetwork',
|
||||
|
||||
computed: {
|
||||
ssids() {
|
||||
const ssidNames = Array.from(
|
||||
new Set(store.state.hosts.map((host) => host.info.ssid)),
|
||||
);
|
||||
return ssidNames.sort();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hostsInNetwork(name: string) {
|
||||
return store.state.hosts.filter((host) => host.info.ssid === name);
|
||||
},
|
||||
collapse(name: string) {
|
||||
const collapseElementList = [].slice.call(
|
||||
document.querySelectorAll('.collapse'),
|
||||
) as HTMLElement[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _ = collapseElementList
|
||||
.filter((val) => val.id === `collapse-${name}`)
|
||||
.map((collapseEl) => new Collapse(collapseEl));
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
19
netview/src/components/LoadingSpinner.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="d-flex align-items-center">
|
||||
<strong>{{msg}}</strong>
|
||||
<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { Options, Vue } from 'vue-class-component';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LoadingSpinner',
|
||||
|
||||
props: {
|
||||
msg: String,
|
||||
},
|
||||
});
|
||||
</script>
|
6
netview/src/main.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
|
||||
createApp(App).use(store).use(router).mount('#app');
|
33
netview/src/router/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
|
||||
import Network from '../views/Network.vue';
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Graph',
|
||||
component: Network,
|
||||
},
|
||||
{
|
||||
path: '/hosts',
|
||||
name: 'Hosts',
|
||||
component: () => import('../views/Hosts.vue'),
|
||||
},
|
||||
{
|
||||
path: '/overall',
|
||||
name: 'Overall',
|
||||
component: () => import('../views/Overall.vue'),
|
||||
},
|
||||
{
|
||||
path: '/statistics',
|
||||
name: 'Statistics',
|
||||
component: () => import('../views/Statistics.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
// history: createWebHistory(process.env.BASE_URL),
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
6
netview/src/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
120
netview/src/store/index.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import axios from 'axios';
|
||||
import { createStore } from 'vuex';
|
||||
|
||||
export interface Connection {
|
||||
id: number;
|
||||
src: number;
|
||||
dst: number;
|
||||
packet_count: number;
|
||||
}
|
||||
|
||||
export interface Info {
|
||||
id: number;
|
||||
host: number;
|
||||
distance: number;
|
||||
packets_send: number;
|
||||
packets_recv: number;
|
||||
packets_trans: number;
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
avg_signal: number;
|
||||
}
|
||||
|
||||
export interface PacketType {
|
||||
id: number;
|
||||
name: string;
|
||||
packets: number;
|
||||
}
|
||||
|
||||
export interface Availability {
|
||||
id: number;
|
||||
host: number;
|
||||
date: number;
|
||||
}
|
||||
|
||||
export interface ScanDates {
|
||||
id: number;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface Host {
|
||||
id: number;
|
||||
hw: string;
|
||||
partners: Connection[];
|
||||
info: Info;
|
||||
availabilities: Availability[];
|
||||
}
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
serverURL: 'http://localhost:9000',
|
||||
hosts: [] as Host[],
|
||||
selectedHosts: [] as Host[],
|
||||
packetTypes: [] as PacketType[],
|
||||
scanDates: [] as ScanDates[],
|
||||
connections: [] as Connection[],
|
||||
},
|
||||
|
||||
mutations: {
|
||||
setServerURL(state, n) {
|
||||
state.serverURL = n;
|
||||
},
|
||||
setHosts(state, hosts) {
|
||||
state.hosts = hosts;
|
||||
},
|
||||
setSelectedHosts(state, selectedHosts) {
|
||||
state.selectedHosts = selectedHosts;
|
||||
},
|
||||
setPacketTypes(state, packetTypes) {
|
||||
state.packetTypes = packetTypes;
|
||||
},
|
||||
setScanDates(state, scanDates) {
|
||||
state.scanDates = scanDates;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async loadHosts(context) {
|
||||
let hosts = ((await axios.get(`${this.state.serverURL}/hosts`)).data as Host[]);
|
||||
const connections = (await axios.get(`${this.state.serverURL}/connections`)).data as Connection[];
|
||||
const info = (await axios.get(`${this.state.serverURL}/info`)).data as Info[];
|
||||
const avail = (await axios.get(`${this.state.serverURL}/availability`)).data as Availability[];
|
||||
|
||||
hosts = hosts.map((host) => {
|
||||
Object.assign(host, {
|
||||
partners: [],
|
||||
info: {},
|
||||
availabilities: [],
|
||||
});
|
||||
|
||||
const hostConnections = connections
|
||||
.filter((con) => con.src === host.id);
|
||||
|
||||
Object.assign(host.partners, hostConnections);
|
||||
|
||||
const hostInfo = info.find((_info) => _info.host === host.id);
|
||||
Object.assign(host.info, hostInfo);
|
||||
|
||||
const hostAvailabilities = avail.filter((_avail) => _avail.host === host.id);
|
||||
Object.assign(host.availabilities, hostAvailabilities);
|
||||
|
||||
return host;
|
||||
});
|
||||
|
||||
context.commit('setHosts', hosts);
|
||||
},
|
||||
|
||||
async loadPacketTypes(context) {
|
||||
const packetTypes = ((await axios.get(`${this.state.serverURL}/packet-types`)).data as PacketType[]);
|
||||
context.commit('setPacketTypes', packetTypes);
|
||||
},
|
||||
|
||||
async loadScanDates(context) {
|
||||
const scanDates = ((await axios.get(`${this.state.serverURL}/scan-dates`)).data as ScanDates[]);
|
||||
context.commit('setScanDates', scanDates);
|
||||
},
|
||||
},
|
||||
|
||||
modules: {
|
||||
},
|
||||
});
|
163
netview/src/views/Hosts.vue
Normal file
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="d-flex flex-column align-items-center m-3 p-2">
|
||||
<h1 class="display-5">Hosty/AP</h1>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid row border p-4" v-if="loadedHosts.length !== 0">
|
||||
<h4 class="d-flex flex-column align-items-center">Informacje o hostach:</h4>
|
||||
<div class="col-4">
|
||||
<div class="list-group" id="list-tab" role="tablist">
|
||||
<button
|
||||
class="list-group-item list-group-item-action"
|
||||
v-for="host of loadedHosts"
|
||||
:key="host.id"
|
||||
data-bs-toggle="list"
|
||||
:href="'#list-' + host.id"
|
||||
@click="makeGraph(host.id)"
|
||||
>
|
||||
<i class="bi bi-display"></i> ID: {{ host.id }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8 border p-3">
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
<ul
|
||||
class="tab-pane fade"
|
||||
v-for="host of loadedHosts"
|
||||
:key="host.id"
|
||||
:id="'list-' + host.id"
|
||||
role="tabpanel"
|
||||
aria-labelledby="list-home-list"
|
||||
>
|
||||
<li>ID: {{ host.id }}</li>
|
||||
<li>Adres MAC: {{ host.hw }}</li>
|
||||
<li>Wysłane pakiety: {{ host.info.packets_send }}</li>
|
||||
<li>Odebrane pakiety: {{ host.info.packets_recv }}</li>
|
||||
<li>Transmitowane pakiety: {{ host.info.packets_trans }}</li>
|
||||
<li>Średnia siła sygnału: {{ host.info.avg_signal }}dBm</li>
|
||||
<li>BSSID: {{ host.info.bssid }}</li>
|
||||
<li>SSID: {{ host.info.ssid }}</li>
|
||||
|
||||
<li>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseExample"
|
||||
aria-expanded="false"
|
||||
aria-controls="collapseExample"
|
||||
>
|
||||
Partnerzy
|
||||
</button>
|
||||
</li>
|
||||
<p></p>
|
||||
<div class="collapse" id="collapseExample">
|
||||
<div class="card card-body">
|
||||
<ul v-for="partner of hostPartners(host.id)" :key="partner.id">
|
||||
<li>ID: {{ partner.id }}</li>
|
||||
<li>Adres MAC: {{ partner.hw }}</li>
|
||||
<li>Wysłane pakiety: {{ partner.info.packets_send }}</li>
|
||||
<li>Odebrane pakiety: {{ partner.info.packets_recv }}</li>
|
||||
<li>Transmitowane pakiety: {{ partner.info.packets_trans }}</li>
|
||||
<li>Średnia siła sygnału: {{ partner.info.avg_signal }}dBm</li>
|
||||
<li>BSSID: {{ partner.info.bssid }}</li>
|
||||
<li>SSID: {{ partner.info.ssid }}</li>
|
||||
<p></p>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<li v-if="this.avGraph">Dostępność:</li>
|
||||
<canvas
|
||||
v-show="this.avGraph"
|
||||
:class="'availabilitygraph-' + host.id"
|
||||
width="50"
|
||||
height="20"
|
||||
></canvas>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<alert v-else msg="Nie wybrano żadnych hostów" type="warning" />
|
||||
|
||||
<hosts-in-network />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import store, { Host } from '@/store';
|
||||
import { defineComponent } from 'vue';
|
||||
import chart from 'chart.js';
|
||||
import HostsInNetwork from '../components/HostsInNetwork.vue';
|
||||
import Alert from '../components/Alert.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Hosts',
|
||||
components: {
|
||||
HostsInNetwork,
|
||||
Alert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
avGraph: null as unknown,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
loadedHosts() {
|
||||
return store.state.selectedHosts;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
makeGraph(id: number) {
|
||||
const availabilitygraphs = document.getElementsByClassName(
|
||||
`availabilitygraph-${id}`,
|
||||
);
|
||||
|
||||
if (!availabilitygraphs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = availabilitygraphs[0] as HTMLCanvasElement;
|
||||
const host = this.loadedHosts.find((_host) => _host.id === id);
|
||||
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = host.availabilities.map((av) => ({
|
||||
x: new Date(av.date * 1000).toLocaleString(),
|
||||
y: 1,
|
||||
}));
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.avGraph = new chart.Chart(
|
||||
canvas.getContext('2d') as CanvasRenderingContext2D,
|
||||
{
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.map((v) => v.x),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Wykryty pakiet z/do (interwał >= 5m)',
|
||||
data,
|
||||
borderColor: 'blue',
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
hostPartners(id: number): Host[] {
|
||||
const host = store.state.hosts.find((h) => h.id === id) as Host;
|
||||
if (host?.partners.length !== 0) {
|
||||
return host.partners.map((val) =>
|
||||
// eslint-disable-next-line implicit-arrow-linebreak
|
||||
store.state.hosts.find((_host) => _host.id === val.dst)) as Host[];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
142
netview/src/views/Navigation.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light p-3 justify-content-between">
|
||||
<!-- <div class="d-flex justify-content-start container-fluid"> -->
|
||||
<!-- <div> -->
|
||||
<div class="m-3 gap-3 d-flex align-items-center">
|
||||
<img
|
||||
src="@/assets/logo.png"
|
||||
alt="Netview logo"
|
||||
width="100"
|
||||
height="100"
|
||||
/>
|
||||
<h1 class="navbar-brand">Netview</h1>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
|
||||
<div>
|
||||
<ul class="nav nav-pills nav-fill">
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:class="{ active: isGraphRoute }"
|
||||
to="/"
|
||||
>Graf</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:class="{ active: isStatisticsRoute }"
|
||||
to="/statistics"
|
||||
>Statystyki</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
class="nav-link"
|
||||
to="/hosts"
|
||||
:class="{ active: isHostsRoute }"
|
||||
>Hosty/AP</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:class="{ active: isOverallRoute }"
|
||||
to="/overall"
|
||||
>Informacje ogólne</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
<div class="d-flex align-self-">
|
||||
<div class="row">
|
||||
<div class="col nav-opts" >
|
||||
<button
|
||||
class="btn btn-primary m-2"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#exampleModal"
|
||||
>
|
||||
<i class="bi bi-gear-wide-connected"></i>
|
||||
Ustawienia
|
||||
</button>
|
||||
<connection-form-modal />
|
||||
|
||||
<alert v-if="connection" msg="Status: Połączono" type="success" />
|
||||
<alert v-else msg="Status: Brak połączenia" type="danger" />
|
||||
<div class="card" :class="{ 'bg-info': selectedHostsCount > 0 }">
|
||||
<div class="card-body">
|
||||
Wybrano {{ selectedHostsCount }} hostów
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
aria-label="Close"
|
||||
@click="clearSelectedHosts"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { Modal } from 'bootstrap';
|
||||
import Alert from '@/components/Alert.vue';
|
||||
import ConnectionFormModal from '../components/ConnectionFormModal.vue';
|
||||
import store from '../store/index';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Navigation',
|
||||
|
||||
components: {
|
||||
ConnectionFormModal,
|
||||
Alert,
|
||||
},
|
||||
computed: {
|
||||
connection() {
|
||||
return store.state.hosts.length > 0;
|
||||
},
|
||||
selectedHostsCount() {
|
||||
return store.state.selectedHosts.length;
|
||||
},
|
||||
isGraphRoute() {
|
||||
return this.$route.name === 'Graph';
|
||||
},
|
||||
isHostsRoute() {
|
||||
return this.$route.name === 'Hosts';
|
||||
},
|
||||
isOverallRoute() {
|
||||
return this.$route.name === 'Overall';
|
||||
},
|
||||
isStatisticsRoute() {
|
||||
return this.$route.name === 'Statistics';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
_placeholder() {
|
||||
const modal = new Modal(
|
||||
document.getElementById('connectionModal') as HTMLElement,
|
||||
);
|
||||
},
|
||||
clearSelectedHosts() {
|
||||
store.commit('setSelectedHosts', []);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-opts {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
}
|
||||
</style>
|
222
netview/src/views/Network.vue
Normal file
@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="d-flex flex-column align-items-center m-3 p-2">
|
||||
<h1 class="display-5">Graf ruchu bezprzewodowego</h1>
|
||||
<div class="m-3">
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="flexSwitchCheckChecked"
|
||||
@change="switchNodes"
|
||||
:checked="showNodes"
|
||||
v-model="showNodes"
|
||||
/>
|
||||
<label class="form-check-label" for="flexSwitchCheckChecked"
|
||||
>Hosty - wierzchołki</label
|
||||
>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="flexSwitchCheckDefault"
|
||||
@change="switchEdges"
|
||||
v-model="showEdges"
|
||||
:checked="showEdges"
|
||||
/>
|
||||
<label class="form-check-label" for="flexSwitchCheckDefault"
|
||||
>Połączenia - krawędzie</label
|
||||
>
|
||||
</div>
|
||||
<form class="d-flex m-3">
|
||||
<input
|
||||
class="form-control me-2"
|
||||
type="search"
|
||||
placeholder="Znajdź ID"
|
||||
aria-label="Search"
|
||||
v-model="search"
|
||||
@keyup="findIds"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-info border-3">
|
||||
<div id="graph"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import cytoscape, {
|
||||
Core,
|
||||
ElementDefinition,
|
||||
} from 'cytoscape';
|
||||
import store, { Host } from '@/store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Network',
|
||||
data() {
|
||||
return {
|
||||
cy: (null as unknown) as Core,
|
||||
nodes: [] as ElementDefinition[],
|
||||
edges: [] as ElementDefinition[],
|
||||
showNodes: true,
|
||||
showEdges: false,
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (store.state.hosts.length !== 0) {
|
||||
this.updateGraph();
|
||||
}
|
||||
|
||||
store.subscribe((mutation) => {
|
||||
if (mutation.type === 'setHosts') {
|
||||
this.updateGraph();
|
||||
}
|
||||
});
|
||||
},
|
||||
// computed: {
|
||||
// cons() {
|
||||
// return store.state.
|
||||
// }
|
||||
// }
|
||||
methods: {
|
||||
findIds() {
|
||||
this.cy.filter((sel) => +sel.data('id') === +this.search)
|
||||
.forEach((ele) => { ele.addClass('found'); });
|
||||
this.cy.filter((sel) => +sel.data('id') !== +this.search)
|
||||
.forEach((ele) => { ele.removeClass('found'); });
|
||||
},
|
||||
updateGraph() {
|
||||
const nodes = store.state.hosts.map((host) => ({
|
||||
group: 'nodes',
|
||||
classes: 'hosts',
|
||||
data: {
|
||||
id: `${host.id}`,
|
||||
distance: host.info.avg_signal ? host.info.avg_signal : 0,
|
||||
},
|
||||
})) as ElementDefinition[];
|
||||
|
||||
const edges = store.state.hosts.flatMap((host) => host.partners.map((partner) => ({
|
||||
group: 'edges',
|
||||
classes: 'connections hide',
|
||||
data: {
|
||||
source: host.id,
|
||||
target: partner.dst,
|
||||
weight: Math.log10(partner.packet_count),
|
||||
},
|
||||
}))) as ElementDefinition[];
|
||||
|
||||
this.edges = edges;
|
||||
this.nodes = nodes;
|
||||
|
||||
this.cy = cytoscape({
|
||||
container: document.getElementById('graph'),
|
||||
elements: [
|
||||
{
|
||||
group: 'nodes',
|
||||
data: { id: 'S', distance: 99 },
|
||||
classes: 'root',
|
||||
selectable: false,
|
||||
grabbable: false,
|
||||
},
|
||||
...nodes,
|
||||
...edges,
|
||||
],
|
||||
style: [
|
||||
{
|
||||
selector: '.root',
|
||||
style: {
|
||||
shape: 'ellipse',
|
||||
'background-color': '#0077b6',
|
||||
label: 'data(id)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.hosts',
|
||||
style: {
|
||||
shape: 'round-triangle',
|
||||
label: 'data(id)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.connections',
|
||||
style: {
|
||||
width: 'data(weight)',
|
||||
'line-color': '#ccc',
|
||||
'target-arrow-color': '#ccc',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.hide',
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: '.found',
|
||||
style: {
|
||||
'background-color': 'green',
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
name: 'concentric',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
concentric(node: any) {
|
||||
return node.data('distance');
|
||||
},
|
||||
levelWidth() {
|
||||
return 10;
|
||||
},
|
||||
spacingFactor: 1.5,
|
||||
},
|
||||
});
|
||||
|
||||
this.cy.on('select', (evt) => {
|
||||
const node = evt.target;
|
||||
if (node.group() === 'nodes') {
|
||||
store.state.selectedHosts.push(
|
||||
store.state.hosts.find((host) => host.id === +node.data().id) as Host,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.cy.on('unselect', (evt) => {
|
||||
const node = evt.target;
|
||||
if (node.group() === 'nodes') {
|
||||
store.state.selectedHosts = store.state.selectedHosts.filter(
|
||||
(host) => host.id !== +node.data().id,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
switchNodes() {
|
||||
if (this.showNodes) {
|
||||
this.cy.elements('.hosts').removeClass('hide');
|
||||
} else {
|
||||
this.cy.elements('.hosts').addClass('hide');
|
||||
}
|
||||
},
|
||||
switchEdges() {
|
||||
if (this.showEdges) {
|
||||
this.cy.elements('.connections').removeClass('hide');
|
||||
} else {
|
||||
this.cy.elements('.connections').addClass('hide');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#graph {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
132
netview/src/views/Overall.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<alert v-if="err" :msg="err.message" type="danger" />
|
||||
<div class="d-flex flex-column align-items-center m-3 p-2">
|
||||
<h1 class="display-5">Informacje ogólne</h1>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<canvas id="packetTypeChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class=" d-flex flex-column align-items-center m-5" v-if="scanDates.length">
|
||||
<h4>
|
||||
Przedziały czasowe skanowania
|
||||
</h4>
|
||||
<table class="table d-flex flex-column align-items-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Rozpoczęto</th>
|
||||
<th scope="col">Zakończono</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="scanDate of scanDates" :key="scanDate.id">
|
||||
<td>{{scanDate.start}}</td>
|
||||
<td>{{scanDate.end}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import store from '@/store';
|
||||
import { Chart } from 'chart.js';
|
||||
import Alert from '@/components/Alert.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Alert,
|
||||
},
|
||||
name: 'Overall',
|
||||
data() {
|
||||
return {
|
||||
packetTypeGraph: Chart as unknown,
|
||||
err: (null as unknown) as Error,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
packetTypes() {
|
||||
return store.state.packetTypes;
|
||||
},
|
||||
scanDates() {
|
||||
return store.state.scanDates.map((scanDate) => ({
|
||||
id: scanDate.id,
|
||||
start: new Date(scanDate.start * 1000).toLocaleString(),
|
||||
end: new Date(scanDate.end * 1000).toLocaleString(),
|
||||
}));
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
store.subscribe((mutation) => {
|
||||
if (mutation.type === 'setPacketTypes') {
|
||||
this.updatePacketTypeGraph();
|
||||
}
|
||||
});
|
||||
|
||||
if (this.packetTypes.length === 0) {
|
||||
try {
|
||||
await store.dispatch('loadPacketTypes');
|
||||
} catch (err) {
|
||||
this.err = err;
|
||||
}
|
||||
} else {
|
||||
this.updatePacketTypeGraph();
|
||||
}
|
||||
|
||||
if (this.scanDates.length === 0) {
|
||||
try {
|
||||
await store.dispatch('loadScanDates');
|
||||
} catch (err) {
|
||||
this.err = err;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
randomColor() {
|
||||
const letters = '0123456789ABCDEF'.split('');
|
||||
let color = '#';
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
color += letters[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return color;
|
||||
},
|
||||
updatePacketTypeGraph() {
|
||||
const canvas = document.getElementById(
|
||||
'packetTypeChart',
|
||||
) as HTMLCanvasElement;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
this.packetTypeGraph = new Chart(context, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: this.packetTypes.map((pt) => pt.name),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Liczba pakietów',
|
||||
data: this.packetTypes.map((pt) => pt.packets),
|
||||
backgroundColor: this.packetTypes.map(() => this.randomColor()),
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
title: {
|
||||
display: true,
|
||||
fontSize: 20,
|
||||
text: 'Zanotowane rodzaje pakietów',
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
132
netview/src/views/Statistics.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="d-flex flex-column align-items-center m-3 p-2">
|
||||
<h1 class="display-5">Statystyki</h1>
|
||||
</div>
|
||||
|
||||
<alert
|
||||
msg="Nie wybrano żadnych hostów"
|
||||
type="warning"
|
||||
v-if="hosts.length === 0"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div class="container-fluid">
|
||||
<canvas id="receivedChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<canvas id="sendChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<canvas id="transChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import store from '@/store';
|
||||
import { defineComponent } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import Alert from '../components/Alert.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Statistics',
|
||||
|
||||
components: {
|
||||
Alert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
charts: Array(3).fill(null) as Chart[],
|
||||
colors: [] as string[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hosts() {
|
||||
return store.state.selectedHosts;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.hosts.length !== 0) {
|
||||
this.colors = this.hosts.map(() => this.randomColor());
|
||||
this.updateGraph(
|
||||
'receivedChart',
|
||||
'Liczba odebranych pakietów',
|
||||
this.hosts.map((host) => host.info.packets_recv),
|
||||
);
|
||||
this.updateGraph(
|
||||
'sendChart',
|
||||
'Liczba wysłanych pakietów',
|
||||
this.hosts.map((host) => host.info.packets_send),
|
||||
);
|
||||
this.updateGraph(
|
||||
'transChart',
|
||||
'Liczba transmitowanych pakietów',
|
||||
this.hosts.map((host) => host.info.packets_trans),
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
randomColor() {
|
||||
const letters = '0123456789ABCDEF'.split('');
|
||||
let color = '#';
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
color += letters[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return color;
|
||||
},
|
||||
|
||||
updateGraph(elementId: string, label: string, data: number[]) {
|
||||
this.charts.map((thisChart) => {
|
||||
if (thisChart !== null) {
|
||||
thisChart.clear();
|
||||
thisChart.destroy();
|
||||
}
|
||||
|
||||
const canvas = document.getElementById(elementId) as HTMLCanvasElement;
|
||||
if (!canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Chart(canvas.getContext('2d') as CanvasRenderingContext2D, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: this.hosts.map(
|
||||
(host) => `(ID: ${host.id}, MAC: ${host.hw})`,
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
data,
|
||||
backgroundColor: this.colors,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
title: {
|
||||
display: true,
|
||||
fontSize: 20,
|
||||
text: label,
|
||||
},
|
||||
// legend: {
|
||||
// labels: {
|
||||
// fontSize: 5,
|
||||
// },
|
||||
// },
|
||||
// scales: {
|
||||
// xAxes: [
|
||||
// {
|
||||
// fontSize: 3,
|
||||
// scaleLabel: {
|
||||
// fontSize: 2,
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
12
netview/tests/e2e/.eslintrc.js
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'cypress',
|
||||
],
|
||||
env: {
|
||||
mocha: true,
|
||||
'cypress/globals': true,
|
||||
},
|
||||
rules: {
|
||||
strict: 'off',
|
||||
},
|
||||
};
|
26
netview/tests/e2e/plugins/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
/* eslint-disable arrow-body-style */
|
||||
// https://docs.cypress.io/guides/guides/plugins-guide.html
|
||||
|
||||
// if you need a custom webpack configuration you can uncomment the following import
|
||||
// and then use the `file:preprocessor` event
|
||||
// as explained in the cypress docs
|
||||
// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples
|
||||
|
||||
// /* eslint-disable import/no-extraneous-dependencies, global-require */
|
||||
// const webpack = require('@cypress/webpack-preprocessor')
|
||||
|
||||
module.exports = (on, config) => {
|
||||
// on('file:preprocessor', webpack({
|
||||
// webpackOptions: require('@vue/cli-service/webpack.config'),
|
||||
// watchOptions: {}
|
||||
// }))
|
||||
|
||||
return {
|
||||
...config,
|
||||
fixturesFolder: 'tests/e2e/fixtures',
|
||||
integrationFolder: 'tests/e2e/specs',
|
||||
screenshotsFolder: 'tests/e2e/screenshots',
|
||||
videosFolder: 'tests/e2e/videos',
|
||||
supportFile: 'tests/e2e/support/index.js',
|
||||
};
|
||||
};
|
8
netview/tests/e2e/specs/test.js
Normal file
@ -0,0 +1,8 @@
|
||||
// https://docs.cypress.io/api/introduction/api.html
|
||||
|
||||
describe('My First Test', () => {
|
||||
it('Visits the app root url', () => {
|
||||
cy.visit('/');
|
||||
cy.contains('h1', 'Welcome to Your Vue.js + TypeScript App');
|
||||
});
|
||||
});
|
25
netview/tests/e2e/support/commands.js
Normal file
@ -0,0 +1,25 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
20
netview/tests/e2e/support/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
12
netview/tests/unit/example.spec.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import HelloWorld from '@/components/HelloWorld.vue';
|
||||
|
||||
describe('HelloWorld.vue', () => {
|
||||
it('renders props.msg when passed', () => {
|
||||
const msg = 'new message';
|
||||
const wrapper = shallowMount(HelloWorld, {
|
||||
props: { msg },
|
||||
});
|
||||
expect(wrapper.text()).toMatch(msg);
|
||||
});
|
||||
});
|
42
netview/tsconfig.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
// "noImplicitAny": false,
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env",
|
||||
"jest"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
3
netview/vue.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
publicPath: process.env.NODE_ENV === 'production' ? '' : '/',
|
||||
};
|
108
netvu/src/Analyzer/Analyzer.h
Normal file
@ -0,0 +1,108 @@
|
||||
#ifndef ANALYZER_H
|
||||
#define ANALYZER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <tins/packet.h>
|
||||
|
||||
namespace Analyzer {
|
||||
const std::string HOSTS_TABLE = R"EOF(
|
||||
CREATE TABLE hosts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
hw VARCHAR(255)
|
||||
);
|
||||
)EOF";
|
||||
const std::string CONNECTIONS_TABLE = R"EOF(
|
||||
CREATE TABLE connections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
src INTEGER,
|
||||
dst INTEGER,
|
||||
packet_count INTEGER,
|
||||
FOREIGN KEY (src) REFERENCES hosts(id),
|
||||
FOREIGN KEY (dst) REFERENCES hosts(id)
|
||||
);
|
||||
)EOF";
|
||||
const std::string INFO_TABLE = R"EOF(
|
||||
CREATE TABLE info (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
host INTEGER,
|
||||
packets_send INTEGER,
|
||||
packets_recv INTEGER,
|
||||
packets_trans INTEGER,
|
||||
avg_signal INTEGER,
|
||||
ssid VARCHAR(255),
|
||||
bssid VARCHAR(255),
|
||||
FOREIGN KEY (host) REFERENCES hosts(id)
|
||||
);
|
||||
)EOF";
|
||||
const std::string PACKET_TYPES_TABLE = R"EOF(
|
||||
CREATE TABLE packet_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
name VARCHAR(255),
|
||||
packets INTEGER
|
||||
);
|
||||
)EOF";
|
||||
const std::string SCAN_DATES_TABLE = R"EOF(
|
||||
CREATE TABLE scan_dates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
start INTEGER,
|
||||
end INTEGER
|
||||
);
|
||||
)EOF";
|
||||
const std::string AVAILABILITY_TABLE = R"EOF(
|
||||
CREATE TABLE availability (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
host INTEGER,
|
||||
date INTEGER,
|
||||
FOREIGN KEY (host) REFERENCES hosts(id)
|
||||
);
|
||||
)EOF";
|
||||
std::string getNow();
|
||||
|
||||
class PCAPAnalyzer {
|
||||
struct HostInfo {
|
||||
int packetsSend = 0;
|
||||
int packetsReceived = 0;
|
||||
int packetsTransmitted = 0;
|
||||
int distance = 0;
|
||||
std::string ssid = "";
|
||||
std::string bssid = "";
|
||||
std::vector<int> signalStrengths = {};
|
||||
};
|
||||
std::vector<std::string> pcaps;
|
||||
std::string dbPath;
|
||||
std::set<std::string> hosts;
|
||||
std::map<std::string, std::map<std::string, int>> connections;
|
||||
std::map<std::string, std::list<long>> availability;
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> scanDates;
|
||||
std::map<std::string, HostInfo> hostsInfo;
|
||||
std::map<std::string, int> packetTypes;
|
||||
long availInterval = 300;
|
||||
void setupPath();
|
||||
void addHosts(sqlite3* db);
|
||||
void addConnections(sqlite3* db);
|
||||
void addHostInfo(sqlite3* db);
|
||||
void addPacketTypes(sqlite3* db);
|
||||
void addTimestamps(sqlite3* db);
|
||||
void addScanTimes(sqlite3* db);
|
||||
void appendAvailability(const std::string& host, long timestamp);
|
||||
public:
|
||||
PCAPAnalyzer(
|
||||
const std::string& logDir = "log",
|
||||
const std::string& dbDir = "db"
|
||||
);
|
||||
void start();
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
#endif
|
49
netvu/src/Analyzer/CMakeLists.txt
Normal file
@ -0,0 +1,49 @@
|
||||
add_library(Analyzer PCAPAnalyzer.cpp)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
find_package(SQLite3 REQUIRED)
|
||||
|
||||
if(SQLite3_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
SQLite::SQLite3
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
if(Threads_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
Threads::Threads
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(libtins)
|
||||
if(libtins_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
tins
|
||||
)
|
||||
else()
|
||||
find_library(TINS_LIBRARY tins REQUIRED)
|
||||
if(TINS_LIBRARY)
|
||||
list(APPEND EXTRA_LIBS
|
||||
tins
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
target_include_directories(
|
||||
Analyzer
|
||||
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
|
||||
target_link_libraries(netvu PUBLIC ${EXTRA_LIBS})
|
||||
|
||||
install(TARGETS Analyzer DESTINATION lib)
|
||||
install(FILES Analyzer.h DESTINATION include)
|
609
netvu/src/Analyzer/PCAPAnalyzer.cpp
Normal file
@ -0,0 +1,609 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <tins/data_link_type.h>
|
||||
#include <tins/exceptions.h>
|
||||
#include <tins/ip.h>
|
||||
#include <tins/packet.h>
|
||||
#include <tins/pdu.h>
|
||||
#include <tins/rawpdu.h>
|
||||
#include <tins/sniffer.h>
|
||||
#include <tins/tins.h>
|
||||
|
||||
#include "Analyzer.h"
|
||||
|
||||
namespace Analyzer {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::map<std::string, std::string> accessPoints;
|
||||
|
||||
std::string getNow() {
|
||||
auto now = std::chrono::system_clock::to_time_t(
|
||||
std::chrono::system_clock::now()
|
||||
);
|
||||
auto localNow = *std::localtime(&now);
|
||||
std::ostringstream localTimeString;
|
||||
localTimeString << std::put_time(&localNow, "%F %T");
|
||||
|
||||
auto locString = localTimeString.str();
|
||||
std::replace(
|
||||
locString.begin(),
|
||||
locString.end(),
|
||||
' ',
|
||||
'_'
|
||||
);
|
||||
std::replace(
|
||||
locString.begin(),
|
||||
locString.end(),
|
||||
':',
|
||||
'-'
|
||||
);
|
||||
|
||||
return locString;
|
||||
}
|
||||
|
||||
|
||||
PCAPAnalyzer::PCAPAnalyzer(
|
||||
const std::string& logDir,
|
||||
const std::string& dbDir
|
||||
) {
|
||||
|
||||
auto pcapFilesPath = fs::path(logDir);
|
||||
for (const auto& file : fs::directory_iterator(pcapFilesPath)) {
|
||||
if (file.path().extension().string() == ".pcap") {
|
||||
this->pcaps.emplace_back(file.path().string());
|
||||
}
|
||||
}
|
||||
|
||||
this->dbPath = dbDir;
|
||||
if (!fs::exists(this->dbPath)) {
|
||||
auto res = fs::create_directory(this->dbPath);
|
||||
if (!res) {
|
||||
std::cerr << "Failed to create " << this->dbPath << " - using .\n";
|
||||
this->dbPath = ".";
|
||||
}
|
||||
}
|
||||
this->dbPath += "/";
|
||||
|
||||
std::array packetTypes{
|
||||
"Control",
|
||||
"Data",
|
||||
"Management",
|
||||
};
|
||||
for (auto type : packetTypes) {
|
||||
this->packetTypes[type] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PCAPAnalyzer::addHosts(sqlite3* db) {
|
||||
char *errMsg = 0;
|
||||
|
||||
int res = sqlite3_exec(
|
||||
db,
|
||||
Analyzer::HOSTS_TABLE.data(),
|
||||
nullptr,
|
||||
0,
|
||||
&errMsg
|
||||
);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
throw std::runtime_error(errMsg);
|
||||
}
|
||||
|
||||
std::string insert = "INSERT INTO hosts (hw) VALUES ";
|
||||
for (auto host : this->hosts) {
|
||||
insert.append("('" + host + "')").append(",");
|
||||
}
|
||||
insert.pop_back();
|
||||
insert += ';';
|
||||
|
||||
res = sqlite3_exec(
|
||||
db,
|
||||
insert.c_str(),
|
||||
nullptr,
|
||||
0,
|
||||
&errMsg
|
||||
);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
std::cerr << errMsg << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void PCAPAnalyzer::addConnections(sqlite3* db) {
|
||||
char *errMsg = 0;
|
||||
|
||||
int res = sqlite3_exec(
|
||||
db,
|
||||
Analyzer::CONNECTIONS_TABLE.data(),
|
||||
nullptr,
|
||||
0,
|
||||
&errMsg
|
||||
);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
throw std::runtime_error(errMsg);
|
||||
}
|
||||
|
||||
for (auto con : this->connections) {
|
||||
for (auto val : con.second) {
|
||||
std::string insert = "INSERT INTO connections(src, dst, packet_count) ";
|
||||
insert += "SELECT id, (SELECT id FROM hosts WHERE hw = '" + con.first + "'), ";
|
||||
insert += std::to_string(val.second);
|
||||
insert += " FROM hosts WHERE hw = '" + val.first + "';";
|
||||
|
||||
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
std::cerr << errMsg << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PCAPAnalyzer::addHostInfo(sqlite3* db) {
|
||||
char *errMsg = 0;
|
||||
|
||||
int res = sqlite3_exec(
|
||||
db,
|
||||
Analyzer::INFO_TABLE.data(),
|
||||
nullptr,
|
||||
0,
|
||||
&errMsg
|
||||
);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
throw std::runtime_error(errMsg);
|
||||
}
|
||||
|
||||
for (auto host : this->hostsInfo) {
|
||||
|
||||
double avgSignal = 0;
|
||||
|
||||
if (!host.second.signalStrengths.empty()) {
|
||||
avgSignal = std::floor(std::accumulate(
|
||||
host.second.signalStrengths.begin(),
|
||||
host.second.signalStrengths.end(),
|
||||
0
|
||||
) / host.second.signalStrengths.size()) * -1;
|
||||
}
|
||||
|
||||
std::string insert = "INSERT INTO info(host, packets_send, packets_recv, ";
|
||||
insert += "packets_trans, avg_signal, ssid, bssid) SELECT id ";
|
||||
|
||||
if (host.second.packetsSend) {
|
||||
insert += ", ";
|
||||
insert += std::to_string(host.second.packetsSend);
|
||||
} else {
|
||||
insert += ", ";
|
||||
insert += "0";
|
||||
}
|
||||
|
||||
if (host.second.packetsReceived) {
|
||||
insert += ", ";
|
||||
insert += std::to_string(host.second.packetsReceived);
|
||||
} else {
|
||||
insert += ", ";
|
||||
insert += "0";
|
||||
}
|
||||
|
||||
if (host.second.packetsTransmitted) {
|
||||
insert += ", ";
|
||||
insert += std::to_string(host.second.packetsTransmitted);
|
||||
} else {
|
||||
insert += ", ";
|
||||
insert += "0";
|
||||
}
|
||||
|
||||
insert += ", ";
|
||||
insert += std::to_string(avgSignal);
|
||||
|
||||
if (host.second.ssid.empty()) {
|
||||
insert += ", ";
|
||||
insert += "'?'";
|
||||
} else {
|
||||
insert += ", ";
|
||||
insert += "'" + host.second.ssid + "'";
|
||||
}
|
||||
|
||||
if (host.second.bssid.empty()) {
|
||||
insert += ", ";
|
||||
insert += "'?'";
|
||||
} else {
|
||||
insert += ", ";
|
||||
insert += "'" + host.second.bssid + "'";
|
||||
}
|
||||
|
||||
insert += " FROM hosts WHERE hw = '" + host.first + "';";
|
||||
|
||||
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
std::cerr << errMsg << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PCAPAnalyzer::addPacketTypes(sqlite3* db) {
|
||||
char *errMsg = 0;
|
||||
|
||||
int res = sqlite3_exec(
|
||||
db,
|
||||
Analyzer::PACKET_TYPES_TABLE.data(),
|
||||
nullptr,
|
||||
0,
|
||||
&errMsg
|
||||
);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
std::cerr << errMsg << '\n';
|
||||
throw std::runtime_error(errMsg);
|
||||
}
|
||||
|
||||
std::string insert = "INSERT INTO packet_types(name, packets) VALUES ";
|
||||
for (auto type : this->packetTypes) {
|
||||
insert += "('" + type.first + "', " + std::to_string(type.second) + "),";
|
||||
}
|
||||
insert.pop_back();
|
||||
insert += ';';
|
||||
|
||||
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
std::cerr << errMsg << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
void PCAPAnalyzer::addTimestamps(sqlite3* db) {
|
||||
char *errMsg = 0;
|
||||
|
||||
int res = sqlite3_exec(
|
||||
db,
|
||||
Analyzer::AVAILABILITY_TABLE.data(),
|
||||
nullptr,
|
||||
0,
|
||||
&errMsg
|
||||
);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
std::cerr << errMsg << '\n';
|
||||
throw std::runtime_error(errMsg);
|
||||
}
|
||||
|
||||
for (auto host : this->availability) {
|
||||
for (auto date : host.second) {
|
||||
std::string insert = "INSERT INTO availability(host, date) VALUES (";
|
||||
insert += " (SELECT id FROM hosts WHERE hw = '" + host.first + "'), ";
|
||||
insert += std::to_string(date);
|
||||
insert += ");";
|
||||
|
||||
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
|
||||
if (res != SQLITE_OK) {
|
||||
std::cerr << errMsg << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PCAPAnalyzer::addScanTimes(sqlite3* db) {
|
||||
char *errMsg = 0;
|
||||
|
||||
int res = sqlite3_exec(
|
||||
db,
|
||||
Analyzer::SCAN_DATES_TABLE.data(),
|
||||
nullptr,
|
||||
0,
|
||||
&errMsg
|
||||
);
|
||||
|
||||
if (res != SQLITE_OK) {
|
||||
std::cerr << errMsg << '\n';
|
||||
throw std::runtime_error(errMsg);
|
||||
}
|
||||
|
||||
|
||||
for (auto scanDate : this->scanDates) {
|
||||
std::string insert = "INSERT INTO scan_dates(start, end) VALUES (";
|
||||
insert += std::to_string(scanDate.first);
|
||||
insert += ", ";
|
||||
insert += std::to_string(scanDate.second);
|
||||
insert += ");";
|
||||
|
||||
res = sqlite3_exec(db, insert.c_str(), nullptr, 0, &errMsg);
|
||||
if (res != SQLITE_OK) {
|
||||
std::cerr << errMsg << '\n';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PCAPAnalyzer::appendAvailability(const std::string& host, long timestamp) {
|
||||
if (this->availability[host].size()) {
|
||||
auto last = this->availability[host].back();
|
||||
if (std::abs(timestamp - last) > this->availInterval) {
|
||||
this->availability[host].push_back(timestamp);
|
||||
}
|
||||
} else {
|
||||
this->availability[host].push_back(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
void PCAPAnalyzer::start() {
|
||||
if (this->pcaps.empty()) {
|
||||
throw std::runtime_error("No .pcap files found!");
|
||||
}
|
||||
|
||||
for (auto pcapFile : this->pcaps) {
|
||||
Tins::FileSniffer sniffer(pcapFile);
|
||||
|
||||
int firstScanDate = 0;
|
||||
int lastScanDate = 0;
|
||||
|
||||
std::vector<std::uint32_t> packetTimestamps;
|
||||
|
||||
try {
|
||||
for (auto packet : sniffer) {
|
||||
packetTimestamps.emplace_back(packet.timestamp().seconds());
|
||||
|
||||
auto pdu = packet.release_pdu();
|
||||
auto dbm = std::numeric_limits<int>::max();
|
||||
auto radioTap = false;
|
||||
|
||||
while (pdu) {
|
||||
|
||||
if (pdu->matches_flag(Tins::PDU::PDUType::RADIOTAP)) {
|
||||
auto radioTapPDU = dynamic_cast<Tins::RadioTap*>(pdu);
|
||||
radioTap = true;
|
||||
|
||||
auto dot11PDU = radioTapPDU->find_pdu<Tins::Dot11>();
|
||||
if (dot11PDU) {
|
||||
try {
|
||||
dbm = radioTapPDU->dbm_signal();
|
||||
} catch (Tins::field_not_present&) {
|
||||
dbm = 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!radioTap) {
|
||||
if (pdu->matches_flag(Tins::PDU::PDUType::DOT11)) {
|
||||
auto dot11PDU = dynamic_cast<Tins::Dot11*>(pdu);
|
||||
auto dst = dot11PDU->addr1().to_string();
|
||||
this->hostsInfo[dst].signalStrengths.emplace_back(100);
|
||||
}
|
||||
}
|
||||
|
||||
if (pdu->matches_flag(Tins::PDU::PDUType::DOT11_MANAGEMENT)) {
|
||||
this->packetTypes["Management"] += 1;
|
||||
|
||||
auto managementPDU = dynamic_cast<Tins::Dot11ManagementFrame*>(pdu);
|
||||
|
||||
auto dst = managementPDU->addr1().to_string();
|
||||
auto trn = managementPDU->addr2().to_string();
|
||||
auto src = managementPDU->addr3().to_string();
|
||||
|
||||
this->hosts.insert(src);
|
||||
this->hosts.insert(trn);
|
||||
this->hosts.insert(dst);
|
||||
|
||||
this->connections[trn][dst] += 1;
|
||||
|
||||
if (src != dst) {
|
||||
this->connections[src][dst] += 1;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this->hostsInfo[trn].ssid.empty()
|
||||
&& !managementPDU->from_ds()
|
||||
&& !managementPDU->to_ds()) {
|
||||
this->hostsInfo[trn].ssid = managementPDU->ssid();
|
||||
}
|
||||
} catch (Tins::option_not_found&) {}
|
||||
|
||||
if (!managementPDU->addr3().is_broadcast()) {
|
||||
this->hostsInfo[src].packetsSend += 1;
|
||||
}
|
||||
this->hostsInfo[trn].packetsTransmitted += 1;
|
||||
this->hostsInfo[dst].packetsReceived += 1;
|
||||
|
||||
auto timestamp = packet.timestamp().seconds();
|
||||
|
||||
if (!managementPDU->addr3().is_broadcast()) {
|
||||
appendAvailability(src, timestamp);
|
||||
}
|
||||
|
||||
appendAvailability(trn, timestamp);
|
||||
|
||||
if (dbm != std::numeric_limits<int>::max()) {
|
||||
if (!managementPDU->addr3().is_broadcast()) {
|
||||
this->hostsInfo[src].signalStrengths.emplace_back(std::abs(dbm));
|
||||
}
|
||||
this->hostsInfo[trn].signalStrengths.emplace_back(std::abs(dbm));
|
||||
this->hostsInfo[dst].signalStrengths.emplace_back(std::abs(dbm));
|
||||
}
|
||||
} else if (pdu->matches_flag(Tins::PDU::PDUType::DOT11_CONTROL)) {
|
||||
this->packetTypes["Control"] += 1;
|
||||
|
||||
auto controlPDU = dynamic_cast<Tins::Dot11Control*>(pdu);
|
||||
|
||||
auto dst = controlPDU->addr1().to_string();
|
||||
|
||||
this->hosts.insert(dst);
|
||||
|
||||
this->hostsInfo[dst].packetsReceived += 1;
|
||||
|
||||
if (dbm != std::numeric_limits<int>::max()) {
|
||||
this->hostsInfo[dst].signalStrengths.emplace_back(std::abs(dbm));
|
||||
}
|
||||
|
||||
if (pdu->matches_flag(Tins::PDU::PDUType::DOT11_RTS)) {
|
||||
auto rtsPDU = dynamic_cast<Tins::Dot11RTS*>(controlPDU);
|
||||
auto trn = rtsPDU->target_addr().to_string();
|
||||
if (!rtsPDU->target_addr().is_broadcast()) {
|
||||
this->hosts.insert(trn);
|
||||
this->hostsInfo[trn].packetsTransmitted += 1;
|
||||
}
|
||||
this->connections[trn][dst] += 1;
|
||||
}
|
||||
|
||||
|
||||
auto timestamp = packet.timestamp().seconds();
|
||||
appendAvailability(dst, timestamp);
|
||||
|
||||
} else if (pdu->matches_flag(Tins::PDU::PDUType::DOT11_DATA)) {
|
||||
this->packetTypes["Data"] += 1;
|
||||
|
||||
auto dataPDU = dynamic_cast<Tins::Dot11Data*>(pdu);
|
||||
|
||||
auto dst = dataPDU->dst_addr().to_string();
|
||||
auto trn = dataPDU->addr2().to_string();
|
||||
auto src = dataPDU->src_addr().to_string();
|
||||
|
||||
this->hosts.insert(dst);
|
||||
this->hosts.insert(trn);
|
||||
this->hosts.insert(src);
|
||||
|
||||
this->connections[trn][dst] += 1;
|
||||
|
||||
if (!dataPDU->src_addr().is_broadcast()) {
|
||||
this->connections[src][trn] += 1;
|
||||
}
|
||||
|
||||
if (!dataPDU->src_addr().is_broadcast()) {
|
||||
if (this->hostsInfo[src].bssid.empty()) {
|
||||
this->hostsInfo[src].bssid = dataPDU->bssid_addr().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataPDU->dst_addr().is_broadcast()) {
|
||||
if (this->hostsInfo[dst].bssid.empty()) {
|
||||
this->hostsInfo[dst].bssid = dataPDU->bssid_addr().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataPDU->addr2().is_broadcast()) {
|
||||
if (this->hostsInfo[trn].bssid.empty()) {
|
||||
this->hostsInfo[trn].bssid = dataPDU->bssid_addr().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataPDU->src_addr().is_broadcast()) {
|
||||
this->hostsInfo[src].packetsSend += 1;
|
||||
}
|
||||
|
||||
this->hostsInfo[trn].packetsTransmitted += 1;
|
||||
this->hostsInfo[dst].packetsReceived += 1;
|
||||
|
||||
auto timestamp = packet.timestamp().seconds();
|
||||
|
||||
if (!dataPDU->src_addr().is_broadcast()) {
|
||||
appendAvailability(src, timestamp);
|
||||
}
|
||||
|
||||
appendAvailability(trn, timestamp);
|
||||
appendAvailability(dst, timestamp);
|
||||
|
||||
if (dbm != std::numeric_limits<int>::max()) {
|
||||
if (!dataPDU->src_addr().is_broadcast()) {
|
||||
this->hostsInfo[src].signalStrengths.emplace_back(std::abs(dbm));
|
||||
}
|
||||
this->hostsInfo[trn].signalStrengths.emplace_back(std::abs(dbm));
|
||||
this->hostsInfo[dst].signalStrengths.emplace_back(std::abs(dbm));
|
||||
}
|
||||
} else {
|
||||
if (pdu->matches_flag(Tins::PDU::PDUType::DOT11)) {
|
||||
auto dot11PDU = dynamic_cast<Tins::Dot11*>(pdu);
|
||||
auto dst = dot11PDU->addr1().to_string();
|
||||
this->hosts.insert(dst);
|
||||
this->hostsInfo[dst].packetsReceived += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pdu = pdu->release_inner_pdu();
|
||||
}
|
||||
}
|
||||
} catch (std::exception& ) {}
|
||||
|
||||
if (!packetTimestamps.empty()) {
|
||||
this->scanDates.emplace_back(
|
||||
std::make_pair(packetTimestamps.front(), packetTimestamps.back())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& entry : this->availability) {
|
||||
entry.second.sort();
|
||||
entry.second.unique();
|
||||
}
|
||||
|
||||
sqlite3* db;
|
||||
auto memorySqlite = sqlite3_open(":memory:", &db);
|
||||
if (memorySqlite != SQLITE_OK) {
|
||||
throw "Could not open in-memory database";
|
||||
}
|
||||
|
||||
sqlite3* localDb;
|
||||
auto localSqlite = sqlite3_open((this->dbPath + getNow() + ".sqlite").c_str(), &localDb);
|
||||
if (memorySqlite != SQLITE_OK) {
|
||||
throw "Could not open database file";
|
||||
}
|
||||
|
||||
std::cout << "Adding hosts..";
|
||||
this->addHosts(db);
|
||||
std::cout << "Done\n";
|
||||
|
||||
std::cout << "Adding connections..";
|
||||
this->addConnections(db);
|
||||
std::cout << "Done\n";
|
||||
|
||||
std::cout << "Adding host info..";
|
||||
this->addHostInfo(db);
|
||||
std::cout << "Done\n";
|
||||
|
||||
std::cout << "Adding packet types..";
|
||||
this->addPacketTypes(db);
|
||||
std::cout << "Done\n";
|
||||
|
||||
std::cout << "Adding timestamps..";
|
||||
this->addTimestamps(db);
|
||||
std::cout << "Done\n";
|
||||
|
||||
std::cout << "Adding scanning times..";
|
||||
this->addScanTimes(db);
|
||||
std::cout << "Done\n";
|
||||
|
||||
auto dbFile = sqlite3_backup_init(localDb, "main", db, "main");
|
||||
if (dbFile) {
|
||||
sqlite3_backup_step(dbFile, -1);
|
||||
sqlite3_backup_finish(dbFile);
|
||||
}
|
||||
|
||||
sqlite3_close(db);
|
||||
}
|
||||
}
|
62
netvu/src/CMakeLists.txt
Normal file
@ -0,0 +1,62 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
project(netvu VERSION 1.0)
|
||||
|
||||
add_executable(netvu netvu.cpp)
|
||||
|
||||
find_package(Boost REQUIRED COMPONENTS
|
||||
program_options
|
||||
filesystem
|
||||
system
|
||||
)
|
||||
|
||||
if(Boost_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
Boost::program_options
|
||||
Boost::filesystem
|
||||
Boost::system
|
||||
)
|
||||
endif()
|
||||
|
||||
# set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
|
||||
# set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
|
||||
# set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
|
||||
|
||||
# option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
|
||||
|
||||
option(USE_SCANNER "Use 802.11 scanner" ON)
|
||||
option(USE_ANALYZER "Use pcap files analyzer" ON)
|
||||
option(USE_SERVER "Use listen server to interact with database" ON)
|
||||
|
||||
if(USE_SCANNER)
|
||||
add_subdirectory(Scanner)
|
||||
list(APPEND EXTRA_LIBS Scanner)
|
||||
endif()
|
||||
|
||||
if(USE_ANALYZER)
|
||||
add_subdirectory(Analyzer)
|
||||
list(APPEND EXTRA_LIBS Analyzer)
|
||||
endif()
|
||||
|
||||
if(USE_SERVER)
|
||||
add_subdirectory(Server)
|
||||
list(APPEND EXTRA_LIBS Server)
|
||||
endif()
|
||||
|
||||
configure_file(netvuconfig.h.in netvuconfig.h)
|
||||
|
||||
target_link_libraries(netvu PUBLIC ${EXTRA_LIBS} )
|
||||
|
||||
target_include_directories(
|
||||
netvu PUBLIC
|
||||
"${PROJECT_BINARY_DIR}"
|
||||
)
|
||||
|
||||
install(TARGETS netvu DESTINATION bin)
|
||||
install(
|
||||
FILES "${PROJECT_BINARY_DIR}/netvuconfig.h"
|
||||
DESTINATION include
|
||||
)
|
53
netvu/src/Scanner/CMakeLists.txt
Normal file
@ -0,0 +1,53 @@
|
||||
add_library(Scanner WifiScanner.cpp)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
find_package(Boost REQUIRED COMPONENTS
|
||||
thread
|
||||
filesystem
|
||||
system
|
||||
)
|
||||
|
||||
if(Boost_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
Boost::thread
|
||||
Boost::filesystem
|
||||
Boost::system
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(libtins)
|
||||
if(libtins_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
tins
|
||||
)
|
||||
else()
|
||||
find_library(TINS_LIBRARY tins REQUIRED)
|
||||
if(TINS_LIBRARY)
|
||||
list(APPEND EXTRA_LIBS
|
||||
tins
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
if(Threads_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
Threads::Threads
|
||||
)
|
||||
endif()
|
||||
|
||||
target_include_directories(
|
||||
Scanner
|
||||
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(netvu PUBLIC ${EXTRA_LIBS})
|
||||
|
||||
install(TARGETS Scanner DESTINATION lib)
|
||||
install(FILES Scanner.h DESTINATION include)
|
49
netvu/src/Scanner/Scanner.h
Normal file
@ -0,0 +1,49 @@
|
||||
#ifndef SCANNER_H
|
||||
#define SCANNER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <tins/packet_writer.h>
|
||||
#include <tins/sniffer.h>
|
||||
|
||||
namespace Scanner {
|
||||
std::string getPcapPath();
|
||||
class WifiScanner {
|
||||
bool radioTap;
|
||||
bool monitorAlreadyEnabled = false;
|
||||
std::string interface;
|
||||
std::string logDir;
|
||||
std::uint32_t channelHopInterval;
|
||||
std::uint32_t maxPackets;
|
||||
std::uint32_t packetCount = 0;
|
||||
std::vector<std::uint8_t> channels;
|
||||
std::unique_ptr<Tins::Sniffer> sniffer;
|
||||
std::unique_ptr<Tins::PacketWriter> packetWriter;
|
||||
bool sniffCallback(Tins::Packet& packet);
|
||||
bool isRadioTap(const std::string& interfaceName);
|
||||
bool setupMonitorMode();
|
||||
bool teardownMonitorMode();
|
||||
std::string findDefaultInterface();
|
||||
std::vector<std::uint8_t> getInterfaceChannels(const std::string& interfaceName);
|
||||
void channelHopper(
|
||||
const std::string& interface,
|
||||
const std::vector<std::uint8_t>& channels,
|
||||
std::uint32_t channelHopInterval
|
||||
);
|
||||
public:
|
||||
WifiScanner(
|
||||
const std::string& interface = "",
|
||||
const std::vector<std::string>& channels = {},
|
||||
const std::string& maxPackets = "0",
|
||||
const std::string& logDir = "log",
|
||||
const std::string& channelHopInterval = "300"
|
||||
);
|
||||
~WifiScanner();
|
||||
void start();
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
340
netvu/src/Scanner/WifiScanner.cpp
Normal file
@ -0,0 +1,340 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include <boost/process.hpp>
|
||||
#include <boost/process/system.hpp>
|
||||
#include <boost/thread/thread.hpp>
|
||||
|
||||
#include <thread>
|
||||
#include <tins/network_interface.h>
|
||||
#include <tins/packet.h>
|
||||
#include <tins/sniffer.h>
|
||||
#include <tins/tins.h>
|
||||
|
||||
#include "Scanner.h"
|
||||
|
||||
namespace Scanner {
|
||||
namespace bp = boost::process;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::string getPcapPath() {
|
||||
auto now = std::chrono::system_clock::to_time_t(
|
||||
std::chrono::system_clock::now()
|
||||
);
|
||||
auto localNow = *std::localtime(&now);
|
||||
std::ostringstream localTimeString;
|
||||
localTimeString << std::put_time(&localNow, "%F %T");
|
||||
|
||||
auto locString = localTimeString.str();
|
||||
std::replace(
|
||||
locString.begin(),
|
||||
locString.end(),
|
||||
' ',
|
||||
'_'
|
||||
);
|
||||
std::replace(
|
||||
locString.begin(),
|
||||
locString.end(),
|
||||
':',
|
||||
'-'
|
||||
);
|
||||
locString += ".pcap";
|
||||
|
||||
return locString;
|
||||
}
|
||||
|
||||
WifiScanner::WifiScanner(
|
||||
const std::string& interface,
|
||||
const std::vector<std::string>& channels,
|
||||
const std::string& maxPackets,
|
||||
const std::string& logDir,
|
||||
const std::string& channelHopInterval
|
||||
) {
|
||||
this->interface = interface;
|
||||
if (this->interface.empty()) {
|
||||
this->interface = findDefaultInterface();
|
||||
if (this->interface.empty()) {
|
||||
throw std::runtime_error("No wireless interface was found");
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Using " << this->interface << '\n';
|
||||
bool setMonitor = this->setupMonitorMode();
|
||||
if (!setMonitor) {
|
||||
throw std::runtime_error("Failed to set Monitor mode on this interface");
|
||||
}
|
||||
|
||||
if (channels.empty() || channels.at(0) == "0") {
|
||||
this->channels = this->getInterfaceChannels(this->interface);
|
||||
if (this->channels.empty()) {
|
||||
throw std::runtime_error("No channels were found for this interface");
|
||||
}
|
||||
} else {
|
||||
for (auto ch : channels) {
|
||||
this->channels.emplace_back(std::stoul(ch));
|
||||
}
|
||||
}
|
||||
|
||||
this->maxPackets = std::stoul(maxPackets);
|
||||
|
||||
this->logDir = logDir;
|
||||
if (!fs::exists(this->logDir)) {
|
||||
auto res = fs::create_directory(this->logDir);
|
||||
if (!res) {
|
||||
std::cerr << "Failed to create " << this->logDir << " - using .\n";
|
||||
this->logDir = ".";
|
||||
}
|
||||
}
|
||||
this->logDir = (fs::path(this->logDir) / getPcapPath()).string();
|
||||
|
||||
if (isRadioTap(this->interface)) {
|
||||
this->packetWriter = std::make_unique<Tins::PacketWriter>(
|
||||
this->logDir,
|
||||
Tins::DataLinkType<Tins::RadioTap>()
|
||||
);
|
||||
} else {
|
||||
this->packetWriter = std::make_unique<Tins::PacketWriter>(
|
||||
this->logDir,
|
||||
Tins::DataLinkType<Tins::Dot11>()
|
||||
);
|
||||
}
|
||||
|
||||
std::stoul(channelHopInterval) < 300
|
||||
? this->channelHopInterval = 300
|
||||
: this->channelHopInterval = std::stoul(channelHopInterval);
|
||||
|
||||
Tins::SnifferConfiguration snifferConfiguration;
|
||||
snifferConfiguration.set_promisc_mode(true);
|
||||
snifferConfiguration.set_rfmon(true);
|
||||
this->sniffer = std::make_unique<Tins::Sniffer>(
|
||||
this->interface,
|
||||
snifferConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
WifiScanner::~WifiScanner() {
|
||||
if (!this->monitorAlreadyEnabled) {
|
||||
this->teardownMonitorMode();
|
||||
}
|
||||
}
|
||||
|
||||
bool WifiScanner::sniffCallback(Tins::Packet& packet) {
|
||||
this->packetWriter->write(packet);
|
||||
this->packetCount += 1;
|
||||
|
||||
std::cout << "Packet: " << std::to_string(this->packetCount) ;
|
||||
if (this->maxPackets) {
|
||||
std::cout << '/' << std::to_string(this->maxPackets);
|
||||
}
|
||||
std::cout << '\n';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WifiScanner::isRadioTap(const std::string& intName) {
|
||||
bp::ipstream is;
|
||||
bp::child c(bp::search_path("ip"), "link", "show", intName,
|
||||
bp::std_out > is);
|
||||
std::string line;
|
||||
|
||||
std::vector<std::string> strs;
|
||||
bool yes = false;
|
||||
while (c.running() && std::getline(is, line) && !line.empty()) {
|
||||
if (line.find("radiotap") != std::string::npos) {
|
||||
yes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
c.wait();
|
||||
|
||||
return yes;
|
||||
}
|
||||
|
||||
std::string WifiScanner::findDefaultInterface() {
|
||||
bp::ipstream is;
|
||||
bp::child c(bp::search_path("iwconfig"), bp::std_out > is);
|
||||
|
||||
std::string interface;
|
||||
std::string line;
|
||||
while (c.running() && std::getline(is, line) && !line.empty()) {
|
||||
if (line.find("802.11") != std::string::npos) {
|
||||
std::vector<std::string> strs;
|
||||
boost::split(strs, line, boost::is_any_of("\t "));
|
||||
interface = strs.front();
|
||||
break;
|
||||
}
|
||||
}
|
||||
c.wait();
|
||||
|
||||
return interface;
|
||||
}
|
||||
|
||||
bool WifiScanner::setupMonitorMode() {
|
||||
bp::ipstream is;
|
||||
bp::child c(bp::search_path("iwconfig"), this->interface, bp::std_out > is);
|
||||
|
||||
std::string line;
|
||||
auto isEnabled = false;
|
||||
while (c.running() && std::getline(is, line) && !line.empty()) {
|
||||
if (line.find("Mode:Monitor") != std::string::npos) {
|
||||
isEnabled = true;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
c.wait();
|
||||
|
||||
if (isEnabled) {
|
||||
this->monitorAlreadyEnabled = true;
|
||||
std::cout<< "Monitor mode already enabled\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cout << "Turning " << this->interface << " down\n";
|
||||
bp::child ifconfigDown(bp::search_path("ifconfig"), this->interface, "down");
|
||||
ifconfigDown.wait();
|
||||
if (ifconfigDown.exit_code()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Enabling Monitor mode\n";
|
||||
bp::child iwconfigMonitor(bp::search_path("iwconfig"), this->interface, "mode", "Monitor");
|
||||
iwconfigMonitor.wait();
|
||||
if (iwconfigMonitor.exit_code()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Turning " << this->interface << " up\n";
|
||||
bp::child ifconfigUp(bp::search_path("ifconfig"), this->interface, "up");
|
||||
ifconfigUp.wait();
|
||||
if (ifconfigUp.exit_code()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WifiScanner::teardownMonitorMode() {
|
||||
bp::ipstream is;
|
||||
bp::child c(bp::search_path("iwconfig"), this->interface, bp::std_out > is);
|
||||
|
||||
std::cout << "Turning " << this->interface << " down\n";
|
||||
bp::child ifconfigDown(bp::search_path("ifconfig"), this->interface, "down");
|
||||
ifconfigDown.wait();
|
||||
if (ifconfigDown.exit_code()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Enabling Managed mode\n";
|
||||
bp::child iwconfigMonitor(bp::search_path("iwconfig"), this->interface, "mode", "Managed");
|
||||
iwconfigMonitor.wait();
|
||||
if (iwconfigMonitor.exit_code()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Turning " << this->interface << "up\n";
|
||||
bp::child ifconfigUp(bp::search_path("ifconfig"), this->interface, "up");
|
||||
ifconfigUp.wait();
|
||||
if (ifconfigUp.exit_code()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "All done\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> WifiScanner::getInterfaceChannels(const std::string& interfaceName) {
|
||||
bp::ipstream is;
|
||||
bp::child c(bp::search_path("iwlist"), interfaceName, "channel", bp::std_out > is);
|
||||
|
||||
std::string line;
|
||||
std::vector<std::string> strs;
|
||||
while (c.running() && std::getline(is, line) && !line.empty()) {
|
||||
if (line.find(interfaceName) != std::string::npos) {
|
||||
boost::split(strs, line, boost::is_any_of("\t "));
|
||||
break;
|
||||
}
|
||||
}
|
||||
c.wait();
|
||||
|
||||
std::vector<std::uint8_t> channels;
|
||||
if (strs.empty()) {
|
||||
return channels;
|
||||
}
|
||||
|
||||
std::uint8_t channelCount = 0;
|
||||
|
||||
for (auto str : strs) {
|
||||
if (str.length() != 0) {
|
||||
auto isInt = std::all_of(std::begin(str), std::end(str), [](char i) {
|
||||
return std::isdigit(i);
|
||||
});
|
||||
if (isInt) {
|
||||
channelCount = std::stoul(str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channelCount == 0) {
|
||||
return channels;
|
||||
}
|
||||
|
||||
for (std::uint8_t i = 1; i <= channelCount; ++i) {
|
||||
channels.emplace_back(i);
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
void WifiScanner::channelHopper(
|
||||
const std::string& interface,
|
||||
const std::vector<std::uint8_t>& channels,
|
||||
std::uint32_t channelHopInterval
|
||||
) {
|
||||
|
||||
auto currentChannel = 0;
|
||||
|
||||
for (;;) {
|
||||
currentChannel += 1;
|
||||
|
||||
if (currentChannel == channels.size()) {
|
||||
currentChannel = 0;
|
||||
}
|
||||
|
||||
bp::ipstream is;
|
||||
bp::child c(
|
||||
bp::search_path("iwconfig"),
|
||||
interface,
|
||||
"channel",
|
||||
std::to_string(channels.at(currentChannel)),
|
||||
bp::std_out > is
|
||||
);
|
||||
c.wait();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(channelHopInterval));
|
||||
}
|
||||
}
|
||||
|
||||
void WifiScanner::start() {
|
||||
boost::thread channelHoppingThread(
|
||||
std::bind(&WifiScanner::channelHopper, this,
|
||||
this->interface, this->channels, this->channelHopInterval)
|
||||
);
|
||||
|
||||
this->sniffer->sniff_loop(
|
||||
std::bind(&WifiScanner::sniffCallback, this, std::placeholders::_1), this->maxPackets
|
||||
);
|
||||
}
|
||||
|
||||
}
|
46
netvu/src/Server/CMakeLists.txt
Normal file
@ -0,0 +1,46 @@
|
||||
add_library(Server HttpServer.cpp)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
find_package(Boost REQUIRED COMPONENTS
|
||||
thread
|
||||
)
|
||||
if(Boost_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
Boost::thread
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(SQLite3 REQUIRED)
|
||||
if(SQLite3_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
SQLite::SQLite3
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
if(nlohmann_json_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
nlohmann_json
|
||||
)
|
||||
endif()
|
||||
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
if(Threads_FOUND)
|
||||
list(APPEND EXTRA_LIBS
|
||||
Threads::Threads
|
||||
)
|
||||
endif()
|
||||
|
||||
target_include_directories(
|
||||
Server
|
||||
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(netvu PUBLIC ${EXTRA_LIBS})
|
||||
|
||||
install(TARGETS Server DESTINATION lib)
|
||||
install(FILES Server.h DESTINATION include)
|
270
netvu/src/Server/HttpServer.cpp
Normal file
@ -0,0 +1,270 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "Server.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
bool isInteger(const std::string& s) {
|
||||
return std::all_of(
|
||||
std::begin(s),
|
||||
std::end(s),
|
||||
[](char i) {
|
||||
return isdigit(i);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
bool isReal(const std::string& s) {
|
||||
char* end = nullptr;
|
||||
auto val = std::strtod(s.c_str(), &end);
|
||||
return end != s.c_str() && *end == '\0' && val != std::numeric_limits<double>::max();
|
||||
}
|
||||
|
||||
int jsonCustomer(
|
||||
void* resp,
|
||||
int argc,
|
||||
char** argv,
|
||||
char** colname) {
|
||||
|
||||
auto respp = static_cast<json *>(resp);
|
||||
json o;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
auto isInt = isInteger(argv[i]);
|
||||
|
||||
if (isInt) {
|
||||
o[colname[i]] = std::stoi(argv[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto isNumeric = isReal(argv[i]);
|
||||
if (isNumeric) {
|
||||
o[colname[i]] = std::stod(argv[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
o[colname[i]] = argv[i];
|
||||
}
|
||||
|
||||
(*respp).emplace_back(o);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
namespace Server {
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace net = boost::asio;
|
||||
namespace fs = std::filesystem;
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
class HttpServer::HttpConnection : public std::enable_shared_from_this<HttpConnection> {
|
||||
public:
|
||||
HttpConnection(
|
||||
tcp::socket socket,
|
||||
const std::string& dbPath,
|
||||
const std::string& cors
|
||||
) : socket(std::move(socket)) {
|
||||
this->dbPath = dbPath;
|
||||
this->cors = cors;
|
||||
}
|
||||
|
||||
void open() {
|
||||
readRequest();
|
||||
handleDeadline();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string dbPath;
|
||||
std::string cors;
|
||||
tcp::socket socket;
|
||||
beast::flat_buffer buffer{8192};
|
||||
http::request<http::dynamic_body> request;
|
||||
http::response<http::dynamic_body> response;
|
||||
net::steady_timer deadline{socket.get_executor(), std::chrono::seconds(30)};
|
||||
|
||||
|
||||
json getData(std::string sqlQuery) {
|
||||
sqlite3* db;
|
||||
auto sql = sqlQuery.data();
|
||||
char *err_msg = 0;
|
||||
int rc = sqlite3_open(this->dbPath.c_str(), &db);
|
||||
json j;
|
||||
rc = sqlite3_exec(db, sql, jsonCustomer, &j, &err_msg);
|
||||
if (rc != SQLITE_OK) {
|
||||
std::cerr << err_msg;
|
||||
}
|
||||
sqlite3_close(db);
|
||||
return j;
|
||||
}
|
||||
|
||||
void readRequest() {
|
||||
auto self = shared_from_this();
|
||||
|
||||
http::async_read(socket, buffer,
|
||||
request,
|
||||
[self](beast::error_code errorCode,
|
||||
std::size_t bytes_transferred
|
||||
) {
|
||||
boost::ignore_unused(bytes_transferred);
|
||||
if(!errorCode) {
|
||||
self->processRequest();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void processRequest() {
|
||||
response.version(request.version());
|
||||
response.keep_alive(false);
|
||||
|
||||
if (request.method() == http::verb::get) {
|
||||
response.result(http::status::ok);
|
||||
response.set(http::field::access_control_allow_origin, this->cors);
|
||||
createResponse();
|
||||
} else {
|
||||
response.result(http::status::bad_request);
|
||||
}
|
||||
|
||||
writeResponse();
|
||||
}
|
||||
|
||||
void createResponse() {
|
||||
auto target = request.target();
|
||||
json data;
|
||||
|
||||
if (target == "/hosts") {
|
||||
data = getData("SELECT * FROM hosts;");
|
||||
}
|
||||
|
||||
if (target == "/connections") {
|
||||
data = getData("SELECT * FROM connections;");
|
||||
}
|
||||
|
||||
if (target == "/info") {
|
||||
data = getData("SELECT * FROM info;");
|
||||
}
|
||||
|
||||
if (target == "/packet-types") {
|
||||
data = getData("SELECT * FROM packet_types;");
|
||||
}
|
||||
|
||||
if (target == "/availability") {
|
||||
data = getData("SELECT * FROM availability;");
|
||||
}
|
||||
|
||||
if (target == "/scan-dates") {
|
||||
data = getData("SELECT * FROM scan_dates;");
|
||||
}
|
||||
|
||||
if (data.empty()) {
|
||||
response.result(http::status::not_found);
|
||||
response.set(http::field::content_type, "text/plain");
|
||||
beast::ostream(response.body()) << "Not found\r\n";
|
||||
} else {
|
||||
response.result(http::status::ok);
|
||||
response.set(http::field::content_type, "application/json");
|
||||
beast::ostream(response.body()) << data.dump();
|
||||
}
|
||||
}
|
||||
|
||||
void writeResponse() {
|
||||
auto self = shared_from_this();
|
||||
response.content_length(response.body().size());
|
||||
http::async_write(socket, response, [self](beast::error_code ec, std::size_t) {
|
||||
self->socket.shutdown(tcp::socket::shutdown_send, ec);
|
||||
self->deadline.cancel();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void handleDeadline() {
|
||||
auto self = shared_from_this();
|
||||
deadline.async_wait([self](beast::error_code ec) {
|
||||
if(!ec) {
|
||||
self->socket.close(ec);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
HttpServer::HttpServer(
|
||||
const std::string& dbPath,
|
||||
const std::string& addr,
|
||||
const std::string& port,
|
||||
const std::string& cors
|
||||
) {
|
||||
this->dbPath = dbPath;
|
||||
this->addr = net::ip::make_address(addr);
|
||||
this->port = std::stoul(port);
|
||||
this->cors = cors;
|
||||
|
||||
if (!fs::exists(this->dbPath)) {
|
||||
throw std::runtime_error("Specified path does not exist");
|
||||
}
|
||||
|
||||
if (fs::is_directory(this->dbPath)) {
|
||||
auto dir = fs::directory_iterator(this->dbPath);
|
||||
std::vector<std::string> entries;
|
||||
for (auto entry : dir) {
|
||||
if (entry.is_regular_file()) {
|
||||
if (entry.path().extension().string() == ".sqlite") {
|
||||
entries.emplace_back(entry.path().string());
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(entries.begin(), entries.end(), std::greater<std::string>());
|
||||
|
||||
if (entries.empty()) {
|
||||
throw std::runtime_error("No database files found");
|
||||
}
|
||||
|
||||
this->dbPath = entries.front();
|
||||
} else {
|
||||
throw std::runtime_error("Invalid database path");
|
||||
}
|
||||
}
|
||||
|
||||
void HttpServer::setup(tcp::acceptor& acceptor, tcp::socket& socket) {
|
||||
acceptor.async_accept(socket, [&](beast::error_code errorCode) {
|
||||
if(!errorCode) {
|
||||
std::make_shared<HttpConnection>(std::move(socket), this->dbPath, this->cors)->open();
|
||||
}
|
||||
setup(acceptor, socket);
|
||||
});
|
||||
}
|
||||
|
||||
void HttpServer::start() {
|
||||
net::io_context ioc{1};
|
||||
tcp::acceptor acceptor{ioc, {this->addr, this->port}};
|
||||
tcp::socket socket{ioc};
|
||||
setup(acceptor, socket);
|
||||
ioc.run();
|
||||
}
|
||||
}
|
33
netvu/src/Server/Server.h
Normal file
@ -0,0 +1,33 @@
|
||||
#ifndef SERVER_H
|
||||
#define SERVER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
namespace Server {
|
||||
class HttpServer {
|
||||
class HttpConnection;
|
||||
boost::asio::ip::address addr;
|
||||
std::uint16_t port;
|
||||
std::string dbPath;
|
||||
std::string cors;
|
||||
void setup(
|
||||
boost::asio::ip::tcp::acceptor& acceptor,
|
||||
boost::asio::ip::tcp::socket& socket
|
||||
);
|
||||
public:
|
||||
HttpServer(
|
||||
const std::string& dbPath = "db",
|
||||
const std::string& addr = "0.0.0.0",
|
||||
const std::string& port = "9000",
|
||||
const std::string& cors = "*"
|
||||
);
|
||||
void start();
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
231
netvu/src/netvu.cpp
Normal file
@ -0,0 +1,231 @@
|
||||
#include <algorithm>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/program_options.hpp>
|
||||
#include <boost/program_options/options_description.hpp>
|
||||
#include <boost/program_options/parsers.hpp>
|
||||
#include <boost/program_options/value_semantic.hpp>
|
||||
#include <boost/program_options/variables_map.hpp>
|
||||
|
||||
#include "netvuconfig.h"
|
||||
|
||||
#ifdef USE_SCANNER
|
||||
# include "Scanner.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ANALYZER
|
||||
# include "Analyzer.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERVER
|
||||
# include "Server.h"
|
||||
#endif
|
||||
|
||||
std::string launchMode;
|
||||
void signalHandler(int signal);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
namespace po = boost::program_options;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
po::options_description desc("Allowed options");
|
||||
desc.add_options()
|
||||
(
|
||||
"help,h",
|
||||
"print usage information and exit"
|
||||
)
|
||||
(
|
||||
"config,c",
|
||||
po::value<std::string>()->default_value("netvu.conf")->required(),
|
||||
"alternative config file"
|
||||
);
|
||||
|
||||
po::options_description config("Configuration");
|
||||
config.add_options()
|
||||
(
|
||||
"mode,m",
|
||||
po::value<std::string>(),
|
||||
"runtime mode - scanner/analyzer/server"
|
||||
);
|
||||
|
||||
|
||||
po::options_description scannerConfig("Scanner");
|
||||
scannerConfig.add_options()
|
||||
(
|
||||
"scanner.interface,i",
|
||||
po::value<std::string>(),
|
||||
"wireless interface"
|
||||
)
|
||||
(
|
||||
"scanner.channels,C",
|
||||
po::value< std::vector<std::string> >()->composing()->multitoken(),
|
||||
"space separated 802.11 channels numbers"
|
||||
)
|
||||
(
|
||||
"scanner.max_packets,M",
|
||||
po::value< std::string >()->default_value("0"),
|
||||
"packet capture limit"
|
||||
)
|
||||
(
|
||||
"scanner.log_dir,l",
|
||||
po::value<std::string>()->default_value("log")->required(),
|
||||
"where to save captured packets"
|
||||
)
|
||||
(
|
||||
"scanner.channel_hop_interval,I",
|
||||
po::value<std::string>()->default_value("300")->required(),
|
||||
"channel hop interval in milliseconds"
|
||||
);
|
||||
config.add(scannerConfig);
|
||||
|
||||
po::options_description analyzerConfig("Analyzer");
|
||||
analyzerConfig.add_options()
|
||||
(
|
||||
"analyzer.log_dir,L",
|
||||
po::value<std::string>()->default_value("log"),
|
||||
"where captured packets are stored"
|
||||
)
|
||||
(
|
||||
"analyzer.database_dir,D",
|
||||
po::value<std::string>()->default_value("db"),
|
||||
"where to save analisys result"
|
||||
);
|
||||
config.add(analyzerConfig);
|
||||
|
||||
po::options_description serverConfig("Server");
|
||||
serverConfig.add_options()
|
||||
(
|
||||
"server.address,a",
|
||||
po::value<std::string>()->default_value("0.0.0.0"),
|
||||
"listening address"
|
||||
)
|
||||
(
|
||||
"server.port,p",
|
||||
po::value<std::string>()->default_value("9000"),
|
||||
"listening port"
|
||||
)
|
||||
(
|
||||
"server.database_dir,d",
|
||||
po::value<std::string>()->default_value("db"),
|
||||
"where analisys result is stored"
|
||||
)
|
||||
(
|
||||
"server.cors,o",
|
||||
po::value<std::string>()->default_value("*"),
|
||||
"access control allow origin"
|
||||
);
|
||||
config.add(serverConfig);
|
||||
|
||||
desc.add(config);
|
||||
|
||||
try {
|
||||
po::variables_map vm;
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
|
||||
if (fs::exists(vm["config"].as<std::string>())) {
|
||||
po::store(
|
||||
po::parse_config_file(vm["config"].as<std::string>().data(), config),
|
||||
vm
|
||||
);
|
||||
}
|
||||
|
||||
po::notify(vm);
|
||||
|
||||
if (vm.count("help")) {
|
||||
std::cout << desc << "\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
std::signal(SIGINT, signalHandler);
|
||||
|
||||
if (!vm.count("mode")) {
|
||||
std::cout << desc << "\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
auto mode = vm["mode"].as<std::string>();
|
||||
|
||||
if (mode == "scanner") {
|
||||
#ifdef USE_SCANNER
|
||||
std::cout << "Starting scanner\n";
|
||||
launchMode = "scanner";
|
||||
|
||||
std::string interface;
|
||||
if (vm.count("scanner.interface")) {
|
||||
interface = vm["scanner.interface"].as<std::string>();
|
||||
}
|
||||
|
||||
std::vector<std::string> channels;
|
||||
if (vm.count("scanner.channels")) {
|
||||
channels = vm["scanner.channels"].as<std::vector<std::string>>();
|
||||
}
|
||||
|
||||
auto maxPackets = vm["scanner.max_packets"].as<std::string>();
|
||||
auto logDir = vm["scanner.log_dir"].as<std::string>();
|
||||
auto channelHopInterval = vm["scanner.channel_hop_interval"].as<std::string>();
|
||||
|
||||
auto wifiScanner = Scanner::WifiScanner(
|
||||
interface,
|
||||
channels,
|
||||
maxPackets,
|
||||
logDir,
|
||||
channelHopInterval
|
||||
);
|
||||
wifiScanner.start();
|
||||
#else
|
||||
throw std::runtime_error("Netvu was compiled without Scanner support");
|
||||
#endif
|
||||
} else if (mode == "analyzer") {
|
||||
#ifdef USE_ANALYZER
|
||||
std::cout << "Starting analyzer\n";
|
||||
launchMode = "analyzer";
|
||||
|
||||
auto logDir = vm["analyzer.log_dir"].as<std::string>();
|
||||
auto databaseDir = vm["analyzer.database_dir"].as<std::string>();
|
||||
|
||||
auto pcapAnalyzer = Analyzer::PCAPAnalyzer(logDir, databaseDir);
|
||||
pcapAnalyzer.start();
|
||||
#else
|
||||
throw std::runtime_error("Netvu was compiled without Analyzer support");
|
||||
#endif
|
||||
} else if (mode == "server") {
|
||||
#ifdef USE_SERVER
|
||||
std::cout << "Starting server\n";
|
||||
launchMode = "server";
|
||||
|
||||
auto address = vm["server.address"].as<std::string>();
|
||||
auto port = vm["server.port"].as<std::string>();
|
||||
auto databaseDir = vm["server.database_dir"].as<std::string>();
|
||||
auto cors = vm["server.cors"].as<std::string>();
|
||||
|
||||
auto httpServer = Server::HttpServer(databaseDir, address, port, cors);
|
||||
httpServer.start();
|
||||
#else
|
||||
throw std::runtime_error("Netvu was compiled without Server support");
|
||||
#endif
|
||||
} else {
|
||||
std::cerr << "Invalid runtime mode specified\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
} catch (const std::exception& ex) {
|
||||
std::cerr << ex.what() << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
void signalHandler(int signal) {
|
||||
std::cout << "Stopping " + launchMode << '\n';
|
||||
std::exit(2);
|
||||
}
|
||||
|
6
netvu/src/netvuconfig.h.in
Normal file
@ -0,0 +1,6 @@
|
||||
#define netvu_VERSION_MAJOR @netvu_VERSION_MAJOR@
|
||||
#define netvu_VERSION_MINOR @netvu_VERSION_MINOR@
|
||||
|
||||
#cmakedefine USE_SCANNER
|
||||
#cmakedefine USE_ANALYZER
|
||||
#cmakedefine USE_SERVER
|