289 lines
9.1 KiB
Rust
289 lines
9.1 KiB
Rust
use crate::crypto::Crypto;
|
|
use crate::database::Database;
|
|
use crate::models::{ClipboardData, ClipboardEntry};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fs;
|
|
use std::io::{Read, Write};
|
|
use std::os::unix::net::UnixListener;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
const IPC_READ_TIMEOUT: Duration = Duration::from_secs(5);
|
|
const IPC_MAX_REQUEST_BYTES: usize = 4 * 1024 * 1024;
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct HistoryItem {
|
|
pub content: String,
|
|
pub timestamp: i64,
|
|
#[serde(default)]
|
|
pub pinned: bool,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
pub enum IpcRequest {
|
|
GetHistory {
|
|
limit: usize,
|
|
},
|
|
SetClipboard {
|
|
content: String,
|
|
},
|
|
DeleteEntry {
|
|
content: String,
|
|
},
|
|
UpdateEntry {
|
|
old_content: String,
|
|
new_content: String,
|
|
},
|
|
AddEntry {
|
|
content: String,
|
|
},
|
|
PinEntry {
|
|
content: String,
|
|
pinned: bool,
|
|
},
|
|
ClearHistory,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
pub enum IpcResponse {
|
|
History(Vec<HistoryItem>),
|
|
Ok,
|
|
Error(String),
|
|
}
|
|
|
|
fn reply(stream: &mut std::os::unix::net::UnixStream, resp: IpcResponse) {
|
|
if let Ok(json) = serde_json::to_string(&resp) {
|
|
let _ = stream.write_all(json.as_bytes());
|
|
}
|
|
}
|
|
|
|
pub fn start_server(
|
|
db: Arc<Mutex<Database>>,
|
|
crypto: Arc<Crypto>,
|
|
socket_path: &Path,
|
|
data_dir: Arc<PathBuf>,
|
|
) {
|
|
if socket_path.exists() {
|
|
let _ = fs::remove_file(socket_path);
|
|
}
|
|
|
|
let listener = match UnixListener::bind(socket_path) {
|
|
Ok(l) => l,
|
|
Err(e) => {
|
|
eprintln!("Erreur socket IPC : {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
if let Err(e) = fs::set_permissions(socket_path, fs::Permissions::from_mode(0o600)) {
|
|
eprintln!("Impossible de restreindre les permissions du socket : {e}");
|
|
}
|
|
}
|
|
|
|
println!("IPC server en écoute sur {:?}", socket_path);
|
|
|
|
for stream in listener.incoming() {
|
|
match stream {
|
|
Ok(mut stream) => {
|
|
if let Err(e) = stream.set_read_timeout(Some(IPC_READ_TIMEOUT)) {
|
|
eprintln!("Impossible de définir le timeout IPC : {e}");
|
|
}
|
|
|
|
let db_clone = Arc::clone(&db);
|
|
let crypto_clone = Arc::clone(&crypto);
|
|
let data_dir_clone = Arc::clone(&data_dir);
|
|
|
|
std::thread::spawn(move || {
|
|
handle_connection(&mut stream, db_clone, crypto_clone, data_dir_clone);
|
|
});
|
|
}
|
|
Err(e) => eprintln!("Erreur connexion IPC : {e}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_connection(
|
|
stream: &mut std::os::unix::net::UnixStream,
|
|
db: Arc<Mutex<Database>>,
|
|
crypto: Arc<Crypto>,
|
|
data_dir: Arc<PathBuf>,
|
|
) {
|
|
let mut buf = Vec::new();
|
|
let mut tmp = [0u8; 4096];
|
|
|
|
loop {
|
|
match stream.read(&mut tmp) {
|
|
Ok(0) => break,
|
|
Ok(n) => {
|
|
buf.extend_from_slice(&tmp[..n]);
|
|
if buf.len() > IPC_MAX_REQUEST_BYTES {
|
|
eprintln!("IPC : requête trop grande, abandon");
|
|
return;
|
|
}
|
|
}
|
|
Err(e)
|
|
if e.kind() == std::io::ErrorKind::WouldBlock
|
|
|| e.kind() == std::io::ErrorKind::TimedOut =>
|
|
{
|
|
eprintln!("IPC : timeout de lecture");
|
|
return;
|
|
}
|
|
Err(e) => {
|
|
eprintln!("IPC read error : {e}");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let buf_str = match String::from_utf8(buf) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!("IPC : requête non-UTF8 : {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let req = match serde_json::from_str::<IpcRequest>(&buf_str) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
eprintln!("IPC parse error : {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
match req {
|
|
IpcRequest::GetHistory { limit } => {
|
|
let limit = limit.min(1000);
|
|
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
|
let history = lock.read_history(limit).unwrap_or_default();
|
|
let items: Vec<HistoryItem> = history
|
|
.into_iter()
|
|
.map(|e| {
|
|
let content = match e.content {
|
|
ClipboardData::Text(t) => t,
|
|
ClipboardData::Image(img) => format!("{}.jpg", img.id),
|
|
};
|
|
let ts = e
|
|
.timestamp
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis() as i64;
|
|
HistoryItem {
|
|
content,
|
|
timestamp: ts,
|
|
pinned: e.pinned,
|
|
}
|
|
})
|
|
.collect();
|
|
reply(stream, IpcResponse::History(items));
|
|
}
|
|
|
|
IpcRequest::SetClipboard { content } => {
|
|
let actual = if Crypto::is_legacy_encrypted(&content) {
|
|
crypto.decrypt(&content).unwrap_or_else(|e| {
|
|
eprintln!("Impossible de déchiffrer l'entrée enc: : {e}");
|
|
content.clone()
|
|
})
|
|
} else if Crypto::is_password_encrypted(&content) {
|
|
reply(
|
|
stream,
|
|
IpcResponse::Error(
|
|
"Entrée chiffrée par mot de passe : déchiffrez côté client avant de coller"
|
|
.to_string(),
|
|
),
|
|
);
|
|
return;
|
|
} else {
|
|
content
|
|
};
|
|
|
|
match arboard::Clipboard::new() {
|
|
Ok(mut cb) => {
|
|
if actual.ends_with(".jpg") || actual.ends_with(".png") {
|
|
let path = data_dir.join("images").join(&actual);
|
|
if let Ok(img) = image::open(&path) {
|
|
let rgba = img.into_rgba8();
|
|
let (w, h) = (rgba.width() as usize, rgba.height() as usize);
|
|
let _ = cb.set_image(arboard::ImageData {
|
|
width: w,
|
|
height: h,
|
|
bytes: std::borrow::Cow::Owned(rgba.into_raw()),
|
|
});
|
|
reply(stream, IpcResponse::Ok);
|
|
} else {
|
|
reply(
|
|
stream,
|
|
IpcResponse::Error(format!("Image introuvable : {actual}")),
|
|
);
|
|
}
|
|
} else {
|
|
let _ = cb.set_text(actual);
|
|
reply(stream, IpcResponse::Ok);
|
|
}
|
|
}
|
|
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
|
}
|
|
}
|
|
|
|
IpcRequest::DeleteEntry { content } => {
|
|
{
|
|
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
|
let _ = lock.delete_entry_by_content(&content);
|
|
}
|
|
if !Crypto::is_any_encrypted(&content)
|
|
&& (content.ends_with(".jpg") || content.ends_with(".png"))
|
|
{
|
|
let p = data_dir.join("images").join(&content);
|
|
if p.exists() {
|
|
let _ = fs::remove_file(p);
|
|
}
|
|
}
|
|
reply(stream, IpcResponse::Ok);
|
|
}
|
|
|
|
IpcRequest::UpdateEntry {
|
|
old_content,
|
|
new_content,
|
|
} => {
|
|
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
|
match lock.update_entry_content(&old_content, &new_content) {
|
|
Ok(_) => reply(stream, IpcResponse::Ok),
|
|
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
|
}
|
|
}
|
|
|
|
IpcRequest::AddEntry { content } => {
|
|
let entry = ClipboardEntry {
|
|
content: ClipboardData::Text(content),
|
|
timestamp: SystemTime::now(),
|
|
pinned: false,
|
|
};
|
|
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
|
match lock.append(entry) {
|
|
Ok(_) => reply(stream, IpcResponse::Ok),
|
|
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
|
}
|
|
}
|
|
|
|
IpcRequest::PinEntry { content, pinned } => {
|
|
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
|
match lock.set_pin(&content, pinned) {
|
|
Ok(_) => reply(stream, IpcResponse::Ok),
|
|
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
|
}
|
|
}
|
|
|
|
IpcRequest::ClearHistory => {
|
|
let lock = db.lock().unwrap_or_else(|p| p.into_inner());
|
|
match lock.clear_history() {
|
|
Ok(_) => reply(stream, IpcResponse::Ok),
|
|
Err(e) => reply(stream, IpcResponse::Error(e.to_string())),
|
|
}
|
|
}
|
|
}
|
|
}
|