Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,10 @@ src-tauri/vendor/**/target/**.exe
# Tauri
target
/target

# Nix
shell.nix

# Dev tools
gen_pcap.py
test.pcap
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,27 @@
```bash
sudo apt install libpcap-dev
```
- **Non-root Execution:** Grant required capabilities using:

- **Non-root Execution:** Grant the required network capabilities to the binary:

```bash
sudo setcap cap_net_raw,cap_net_admin=eip path/to/binary
sudo setcap cap_net_raw,cap_net_admin=eip src-tauri/target/debug/sonar
```

Example:
> **Note:** `setcap` must be re-applied each time the binary is recompiled.

### NixOS

Add `libpcap` and `libcap` to your `buildInputs` (a `shell.nix` is provided at the root of the repository).

- **Non-root Execution:** Grant capabilities after building:

```bash
sudo setcap cap_net_raw,cap_net_admin=eip src-tauri/target/debug/sonar-desktop-app
sudo setcap cap_net_raw,cap_net_admin=eip src-tauri/target/debug/sonar
```

> On NixOS the binary path may differ if using a Nix-managed Rust toolchain — adjust the path accordingly.

### macOS

- **libpcap:** Already included by default on macOS systems. No additional setup
Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ sysinfo = "0.38.3"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-cli = "2"
tauri-plugin-global-shortcut = "2"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_Storage_FileSystem"] }
5 changes: 5 additions & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
],
"permissions": [
"core:default",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"core:window:allow-start-dragging",
"core:window:allow-is-maximized",
"shell:allow-open",
"dialog:default",
"log:allow-log",
Expand Down
79 changes: 67 additions & 12 deletions src-tauri/src/commandes/import/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use log::{error, info};
use packet_parser::PacketFlow;
use pcap::Capture;
use std::fs::File;
#[cfg(unix)]
use std::os::unix::io::IntoRawFd;
use std::sync::{Arc, Mutex};
use tauri::{State, ipc::Channel};

Expand All @@ -13,13 +16,70 @@ use crate::{
},
};

fn open_capture(file_path: &str) -> Result<Capture<pcap::Offline>, CaptureStateError> {
#[cfg(unix)]
{
let file = File::open(file_path).map_err(|e| {
CaptureStateError::Import(PcapImportError::OpenFileError(
file_path.to_string(),
e.to_string(),
))
})?;
let fd = file.into_raw_fd();
// SAFETY: fd est valide et nous en sommes propriétaires. `from_raw_fd` en prend possession.
unsafe { Capture::from_raw_fd(fd) }.map_err(|e| {
CaptureStateError::Import(PcapImportError::OpenFileError(
file_path.to_string(),
e.to_string(),
))
})
Comment on lines +30 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Ce bloc de mappage d'erreur est répété à l'identique dans les autres branches de compilation conditionnelle (lignes 62-67 et 72-77). Pour améliorer la lisibilité et la maintenabilité, vous pourriez extraire cette logique dans une closure locale et la réutiliser. Cela éviterait la duplication de code.

}
#[cfg(windows)]
{
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::GetShortPathNameW;

// Convertit le chemin en UTF-16 pour l'API Windows
let wide: Vec<u16> = OsStr::new(file_path)
.encode_wide()
.chain(std::iter::once(0))
.collect();

// Récupère la taille du buffer nécessaire pour le chemin court (8.3)
let len = unsafe { GetShortPathNameW(wide.as_ptr(), std::ptr::null_mut(), 0) };

let short_path = if len > 0 {
let mut buf = vec![0u16; len as usize];
unsafe { GetShortPathNameW(wide.as_ptr(), buf.as_mut_ptr(), len) };
// Retire le null-terminator
String::from_utf16_lossy(&buf[..len as usize - 1])
Comment on lines +54 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

L'implémentation actuelle pour Windows ignore la valeur de retour de GetShortPathNameW (ligne 54), qui indique le nombre de caractères réellement écrits dans le buffer. À la place, elle utilise len - 1 (ligne 56), où len est la capacité du buffer. C'est potentiellement dangereux. Si GetShortPathNameW échoue (retourne 0) ou écrit moins de caractères que prévu, vous pourriez soit paniquer, soit lire des données invalides. Il est plus sûr d'utiliser la longueur retournée par l'appel unsafe pour découper le buffer.

Suggested change
unsafe { GetShortPathNameW(wide.as_ptr(), buf.as_mut_ptr(), len) };
// Retire le null-terminator
String::from_utf16_lossy(&buf[..len as usize - 1])
let n_written = unsafe { GetShortPathNameW(wide.as_ptr(), buf.as_mut_ptr(), len) };
if n_written > 0 && n_written < len {
String::from_utf16_lossy(&buf[..n_written as usize])
} else {
file_path.to_string()
}

} else {
// Fallback si GetShortPathNameW échoue (ex: short names désactivés)
file_path.to_string()
};

Capture::from_file(&short_path).map_err(|e| {
CaptureStateError::Import(PcapImportError::OpenFileError(
file_path.to_string(),
e.to_string(),
))
})
}

#[cfg(not(any(unix, windows)))]
{
Capture::from_file(file_path).map_err(|e| {
CaptureStateError::Import(PcapImportError::OpenFileError(
file_path.to_string(),
e.to_string(),
))
})
}
}

fn count_packets_in_pcap(file_path: &str) -> Result<usize, CaptureStateError> {
let mut cap = Capture::from_file(file_path).map_err(|e| {
CaptureStateError::Import(PcapImportError::OpenFileError(
file_path.to_string(),
e.to_string(),
))
})?;
let mut cap = open_capture(file_path)?;

let mut count: usize = 0;
while cap.next_packet().is_ok() {
Expand Down Expand Up @@ -89,12 +149,7 @@ fn handle_pcap_file(
file_path, total
);

let mut cap = Capture::from_file(file_path).map_err(|e| {
CaptureStateError::Import(PcapImportError::OpenFileError(
file_path.to_string(),
e.to_string(),
))
})?;
let mut cap = open_capture(file_path)?;

let mut packet_count: usize = 0;

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/commandes/net_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,6 @@ pub fn set_filter(
) -> Result<(), CaptureStateError> {
info!("[set_filter] filter: {}", filter);
let mut app = state.lock()?;
app.filter = Some(filter);
app.filter = if filter.is_empty() { None } else { Some(filter) };
Ok(())
}
11 changes: 2 additions & 9 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use commandes::{
use log::info;

use std::sync::{Arc, Mutex};
use tauri::{Manager, menu::MenuBuilder};
use tauri::Manager;
use tauri_plugin_cli::CliExt;
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState};

Expand Down Expand Up @@ -111,21 +111,14 @@ pub fn run() -> Result<(), tauri::Error> {
if !headless_enabled {
start_cpu_monitor(app.handle().clone());

let menu = MenuBuilder::new(app)
.text("fichier", "Fichier")
.text("apropos", "A propos")
.text("fermer", "Fermer")
.build()?;

app.set_menu(menu)?;

tauri::WebviewWindowBuilder::new(
app,
"main",
tauri::WebviewUrl::App("index.html".into()),
)
.title("SONAR")
.inner_size(1800.0, 950.0)
.decorations(false)
.build()?;
} else {
let capture_state = app.state::<Arc<Mutex<CaptureState>>>();
Expand Down
63 changes: 42 additions & 21 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
<template>
<div class="bg"></div>
<router-view></router-view>
<QuitDialog
:visible="showQuitDialog"
@confirm="onQuitConfirm"
@cancel="onQuitCancel"
/>
<ErrorDialog />
</template>

<style>
:root {
height: 100vh;
--ease-out: cubic-bezier(0.25, 1, 0.5, 1);
--ease-out-fast: cubic-bezier(0.22, 1, 0.36, 1);
}

html, body {
Expand All @@ -24,53 +32,66 @@ html, body {
left: 0;
width: 100%;
height: 100%;
background-color: #1a1a1a;
background-color: #282838;
z-index: -1;
}

@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>

<script>
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { confirm } from '@tauri-apps/plugin-dialog';
import { exit } from '@tauri-apps/plugin-process';
import { info } from '@tauri-apps/plugin-log';
import QuitDialog from './components/QuitDialog.vue';
import ErrorDialog from './components/ErrorDialog.vue';

const appWindow = getCurrentWebviewWindow()

export default {
components: { QuitDialog, ErrorDialog },

data() {
return {
// Add a data property for the unlisten function
unlistenCloseEvent: null,
showQuitDialog: false,
pendingCloseEvent: null,
};
},

async mounted() {
console.log("mounted");

// Set up the close event listener
this.unlistenCloseEvent = await appWindow.onCloseRequested(async (event) => {
info("close resquested")
const confirmed = await confirm('Etes-vous sûr de vouloir quitter ?');
if (!confirmed) {
// User did not confirm closing the window; let's prevent it
info("anule exit")
event.preventDefault();
}
else {
info("exit")
await exit(1);
}
info("close requested")
event.preventDefault();
this.pendingCloseEvent = event;
this.showQuitDialog = true;
});
},

beforeUnmount() {
// Call the unlisten function when the component is unmounted
if (this.unlistenCloseEvent) {
this.unlistenCloseEvent();
}
}
},

methods: {
async onQuitConfirm() {
info("exit confirmed")
this.showQuitDialog = false;
this.pendingCloseEvent = null;
await exit(0);
},
onQuitCancel() {
info("exit cancelled")
this.showQuitDialog = false;
this.pendingCloseEvent = null;
},
},
};
</script>


21 changes: 17 additions & 4 deletions src/components/AnalyseView/BottomLong.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,27 +116,40 @@ export default defineComponent({
display: block;
height: 190px;
flex-shrink: 0;
background-color: #000;
background-color: #2c2c3a;
font-family: 'Courier New', Courier, monospace;
border-top: 1px solid #383850;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
td, th {
padding: 8px;
padding: 6px 8px;
text-align: center;
color: rgb(132, 195, 247);
background-color: #000;
color: #7090a8;
background-color: #2c2c3a;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 11px;
}
th {
color: #687888;
font-weight: 500;
}
tbody {
display: block;
overflow-y: auto;
}
tbody tr {
animation: row-in 0.18s cubic-bezier(0.25, 1, 0.5, 1);
}
@keyframes row-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
thead, tbody tr {
display: table;
width: 100%;
Expand Down
Loading