1use pwr_core::frame::{FrameDecoder, FrameHeader};
13use pwr_core::protocol::{
14 self, ArchiveComplete, ArchiveRequest, ClientMessage,
15 Handshake, ProjectInfo, RestoreRequest, ServerMessage,
16 StatusRequest,
17};
18use pwr_core::crypto;
19use ring::rand::SecureRandom;
20use std::io::{Read, Write};
21use std::net::SocketAddr;
22use std::sync::{Arc, Mutex, RwLock};
23use std::time::Instant;
24use uuid::Uuid;
25
26use crate::auth::RateLimiter;
27use crate::storage::{ProjectStorage, StoredProject};
28
29#[derive(Debug)]
34enum ConnState {
35 AwaitingHandshake,
36 Authenticated,
37 Archiving(ArchiveSession),
38 Restoring(RestoreSession),
39 Closed,
40}
41
42#[derive(Debug)]
43#[allow(dead_code)]
44struct ArchiveSession {
45 session_id: Uuid,
46 project_uuid: Uuid,
47 project_name: String,
48 total_size: u64,
49 file_count: u32,
50 compression: bool,
51 bytes_received: u64,
52}
53
54#[derive(Debug)]
55#[allow(dead_code)]
56struct RestoreSession {
57 session_id: Uuid,
58 project_uuid: Uuid,
59 total_size: u64,
60 file_count: u32,
61 bytes_sent: u64,
62}
63
64pub struct HandlerContext {
65 pub storage: Arc<RwLock<ProjectStorage>>,
66 pub rate_limiter: Arc<Mutex<RateLimiter>>,
67 pub psk: [u8; 32],
68 pub peer_addr: SocketAddr,
69 pub connected_at: Instant,
70}
71
72pub fn handle_connection(
77 mut stream: impl Read + Write,
78 ctx: HandlerContext,
79) -> Result<(), String> {
80 let mut state = ConnState::AwaitingHandshake;
81 let mut decoder = FrameDecoder::new();
82 let mut read_buf = vec![0u8; 8192];
83
84 loop {
85 let n = stream.read(&mut read_buf)
86 .map_err(|e| format!("read error: {}", e))?;
87 if n == 0 {
88 break;
89 }
90
91 decoder.push_bytes(&read_buf[..n]);
92
93 loop {
94 match decoder.try_decode() {
95 Ok(Some((header, payload))) => {
96 if let Err(e) = dispatch(&mut stream, &mut decoder, &mut state, header, &payload, &ctx) {
97 send_server_msg(&mut stream, &protocol::build_error(1, &e))?;
98 return Err(e);
99 }
100 }
101 Ok(None) => break,
102 Err(e) => {
103 send_server_msg(&mut stream, &protocol::build_error(2, &format!("Frame: {}", e)))?;
104 return Err(format!("Frame error: {}", e));
105 }
106 }
107 }
108 }
109 Ok(())
110}
111
112fn dispatch(
117 stream: &mut (impl Read + Write),
118 decoder: &mut FrameDecoder,
119 state: &mut ConnState,
120 header: FrameHeader,
121 payload: &[u8],
122 ctx: &HandlerContext,
123) -> Result<(), String> {
124 let msg = protocol::decode_client_message(header.msg_type, payload)
126 .map_err(|e| format!("Decode: {}", e))?;
127
128 match (&state, msg) {
129 (ConnState::AwaitingHandshake, ClientMessage::Handshake(hs)) => {
131 handle_handshake(stream, state, &hs, ctx)
132 }
133
134 (ConnState::Authenticated, ClientMessage::ArchiveRequest(req)) => {
136 handle_archive_start(stream, state, &req, ctx)?;
137 let prefix = decoder.drain_bytes();
140 handle_archive_chunks(stream, state, ctx, &prefix)
141 }
142 (ConnState::Archiving(_), ClientMessage::ArchiveComplete(complete)) => {
143 handle_archive_finish(stream, state, &complete, ctx)
144 }
145
146 (ConnState::Authenticated, ClientMessage::RestoreRequest(req)) => {
148 handle_restore_start(stream, state, &req, ctx)?;
149 handle_restore_chunks(stream, state, ctx)
151 }
152
153 (ConnState::Authenticated, ClientMessage::StatusRequest(req)) => {
155 handle_status(stream, &req, ctx)
156 }
157
158 (ConnState::AwaitingHandshake, _) => {
160 Err("Handshake required before any other message".into())
161 }
162 (ConnState::Closed, _) => Err("Connection is closed".into()),
163 (_, _msg) => Err(format!(
164 "Unexpected message in state {:?}",
165 std::mem::discriminant(state)
166 )),
167 }
168}
169
170fn handle_handshake(
175 stream: &mut (impl Read + Write),
176 state: &mut ConnState,
177 hs: &Handshake,
178 ctx: &HandlerContext,
179) -> Result<(), String> {
180 let peer_ip = ctx.peer_addr.ip();
182 {
183 let mut limiter = ctx.rate_limiter.lock().unwrap();
184 if !limiter.check_attempt(peer_ip) {
185 *state = ConnState::Closed;
186 send_server_msg(
187 stream,
188 &protocol::build_handshake_ack_failed("Too many authentication attempts — try again later"),
189 )?;
190 return Err("Rate limited".into());
191 }
192 }
193
194 let expected_proof = crypto::compute_client_proof(&ctx.psk, &hs.nonce);
195
196 if expected_proof != hs.proof {
197 *state = ConnState::Closed;
198 send_server_msg(
199 stream,
200 &protocol::build_handshake_ack_failed("Authentication failed: invalid proof"),
201 )?;
202 return Err("Authentication failed".into());
203 }
204
205 ctx.rate_limiter.lock().unwrap().record_success(peer_ip);
207
208 let mut server_nonce = [0u8; 32];
210 ring::rand::SystemRandom::new()
211 .fill(&mut server_nonce)
212 .map_err(|_| "CSPRNG failure".to_string())?;
213
214 let server_proof =
215 crypto::compute_server_proof(&ctx.psk, &hs.nonce, &server_nonce);
216
217 *state = ConnState::Authenticated;
218 send_server_msg(
219 stream,
220 &protocol::build_handshake_ack_success(
221 env!("CARGO_PKG_VERSION"),
222 server_nonce,
223 server_proof,
224 ),
225 )?;
226
227 log::info!("Client '{}' authenticated from {}", hs.client_id, ctx.peer_addr);
228 Ok(())
229}
230
231fn handle_archive_start(
236 stream: &mut (impl Read + Write),
237 state: &mut ConnState,
238 req: &ArchiveRequest,
239 ctx: &HandlerContext,
240) -> Result<(), String> {
241 let session_id = Uuid::new_v4();
242
243 {
245 let storage = ctx.storage.read().unwrap();
246 storage.check_size_limit(req.total_size)
247 .map_err(|e| format!("Archive rejected: {}", e))?;
248 }
249
250 let project = StoredProject {
252 uuid: req.project_uuid,
253 name: req.project_name.clone(),
254 size_bytes: req.total_size,
255 file_count: req.file_count,
256 encrypted: true,
257 created_at: chrono::Utc::now(),
258 updated_at: chrono::Utc::now(),
259 };
260
261 {
262 let mut storage = ctx.storage.write().unwrap();
263 storage.add_project(project.clone())
264 .map_err(|e| format!("Cannot add project: {}", e))?;
265 storage.write_meta(&req.project_uuid, &project)
266 .map_err(|e| format!("Cannot write meta: {}", e))?;
267 }
268
269 *state = ConnState::Archiving(ArchiveSession {
270 session_id,
271 project_uuid: req.project_uuid,
272 project_name: req.project_name.clone(),
273 total_size: req.total_size,
274 file_count: req.file_count,
275 compression: req.compression,
276 bytes_received: 0,
277 });
278
279 send_server_msg(stream, &protocol::build_archive_accept(session_id))?;
280
281 log::info!(
282 "Archive started: {} ({} bytes, {} files)",
283 req.project_name, req.total_size, req.file_count
284 );
285 Ok(())
286}
287
288fn handle_archive_finish(
289 stream: &mut (impl Read + Write),
290 state: &mut ConnState,
291 complete: &ArchiveComplete,
292 ctx: &HandlerContext,
293) -> Result<(), String> {
294 let (project_uuid, project_name, _total_size) = match state {
296 ConnState::Archiving(s) => (s.project_uuid, s.project_name.clone(), s.total_size),
297 _ => return Err("Not in archiving state".into()),
298 };
299
300 if !complete.success {
301 let _ = ctx.storage.write().unwrap().remove_project(&project_uuid);
302 *state = ConnState::Authenticated;
303 send_server_msg(stream, &protocol::build_error(0, "Archive cancelled by client"))?;
304 return Ok(());
305 }
306
307 {
309 let mut storage = ctx.storage.write().unwrap();
310 if let Some(mut project) = storage.get_project(&project_uuid).cloned() {
311 project.size_bytes = complete.total_size;
312 project.updated_at = chrono::Utc::now();
313 storage.update_project(project)
314 .map_err(|e| format!("Cannot update: {}", e))?;
315 }
316 }
317
318 *state = ConnState::Authenticated;
319 log::info!(
320 "Archive complete: {} ({} bytes, hash: {})",
321 project_name, complete.total_size,
322 &complete.archive_hash[..16.min(complete.archive_hash.len())]
323 );
324 Ok(())
325}
326
327fn handle_archive_chunks(
339 stream: &mut (impl Read + Write),
340 state: &ConnState,
341 ctx: &HandlerContext,
342 prefix: &[u8],
343) -> Result<(), String> {
344 let (project_uuid, _total_size) = match state {
345 ConnState::Archiving(s) => (s.project_uuid, s.total_size),
346 _ => return Err("Not in archiving state".into()),
347 };
348
349 let mut total_bytes = 0u64;
350
351 let mut buf = Vec::with_capacity(prefix.len() + 8192);
355 buf.extend_from_slice(prefix);
356 let mut pos: usize = 0;
357
358 loop {
359 while buf.len() - pos < 4 {
361 let needed = 4 - (buf.len() - pos);
362 let start = buf.len();
363 buf.resize(start + needed.max(8192), 0);
364 let n = stream
365 .read(&mut buf[start..])
366 .map_err(|e| format!("chunk read: {}", e))?;
367 if n == 0 {
368 return Err("Connection closed during chunk transfer".into());
369 }
370 buf.truncate(start + n);
371 }
372
373 let chunk_len = u32::from_be_bytes([
374 buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3],
375 ]) as usize;
376 pos += 4;
377
378 if chunk_len == 0 {
379 break; }
381
382 while buf.len() - pos < chunk_len {
384 let needed = chunk_len - (buf.len() - pos);
385 let start = buf.len();
386 buf.resize(start + needed.max(8192), 0);
387 let n = stream
388 .read(&mut buf[start..])
389 .map_err(|e| format!("chunk read: {}", e))?;
390 if n == 0 {
391 return Err("Connection closed during chunk data".into());
392 }
393 buf.truncate(start + n);
394 }
395
396 let chunk = &buf[pos..pos + chunk_len];
397 pos += chunk_len;
398 total_bytes += chunk_len as u64;
399
400 {
402 let storage = ctx.storage.read().unwrap();
403 let archive_path = storage.archive_path(&project_uuid);
404
405 use std::io::Write;
406 let mut file = std::fs::OpenOptions::new()
407 .create(true)
408 .append(true)
409 .open(&archive_path)
410 .map_err(|e| format!("Cannot open archive: {}", e))?;
411 file.write_all(chunk)
412 .map_err(|e| format!("Cannot write chunk: {}", e))?;
413 }
414
415 log::debug!(
416 "Received chunk: {} bytes (total: {})",
417 chunk_len,
418 total_bytes
419 );
420
421 if pos > 65536 {
423 buf.drain(..pos);
424 pos = 0;
425 }
426 }
427
428 log::info!(
429 "Archive data received: {} bytes for project {}",
430 total_bytes,
431 project_uuid
432 );
433
434 Ok(())
435}
436
437fn handle_restore_chunks(
443 stream: &mut (impl Read + Write),
444 state: &ConnState,
445 ctx: &HandlerContext,
446) -> Result<(), String> {
447 let project_uuid = match state {
448 ConnState::Restoring(s) => s.project_uuid,
449 _ => return Err("Not in restoring state".into()),
450 };
451
452 let archive_data = {
454 let storage = ctx.storage.read().unwrap();
455 let mut reader = storage
456 .read_archive(&project_uuid)
457 .map_err(|e| format!("Cannot read archive: {}", e))?;
458 let mut data = Vec::new();
459 std::io::Read::read_to_end(&mut reader, &mut data)
460 .map_err(|e| format!("Cannot read archive data: {}", e))?;
461 data
462 };
463
464 let chunk_size: usize = 1024 * 1024; let mut total_sent = 0u64;
467
468 for chunk in archive_data.chunks(chunk_size) {
469 stream
471 .write_all(&(chunk.len() as u32).to_be_bytes())
472 .map_err(|e| format!("chunk header write: {}", e))?;
473 stream
474 .write_all(chunk)
475 .map_err(|e| format!("chunk data write: {}", e))?;
476
477 total_sent += chunk.len() as u64;
478 }
479
480 stream
482 .write_all(&0u32.to_be_bytes())
483 .map_err(|e| format!("eof write: {}", e))?;
484 stream.flush().map_err(|e| format!("flush: {}", e))?;
485
486 log::info!(
487 "Restore data sent: {} bytes for project {}",
488 total_sent,
489 project_uuid
490 );
491
492 Ok(())
493}
494
495fn handle_restore_start(
500 stream: &mut (impl Read + Write),
501 state: &mut ConnState,
502 req: &RestoreRequest,
503 ctx: &HandlerContext,
504) -> Result<(), String> {
505 let storage = ctx.storage.read().unwrap();
506 let project = storage
507 .get_project(&req.project_uuid)
508 .cloned()
509 .ok_or_else(|| format!("Project not found: {}", req.project_uuid))?;
510
511 if !storage.archive_exists(&req.project_uuid) {
512 return Err(format!("Archive data missing for {}", req.project_uuid));
513 }
514 drop(storage);
515
516 let session_id = Uuid::new_v4();
517 let total_size = project.size_bytes;
518 let file_count = project.file_count;
519
520 *state = ConnState::Restoring(RestoreSession {
521 session_id,
522 project_uuid: req.project_uuid,
523 total_size,
524 file_count,
525 bytes_sent: 0,
526 });
527
528 send_server_msg(
529 stream,
530 &protocol::build_restore_accept(session_id, total_size, file_count, ""),
531 )?;
532
533 log::info!("Restore started: {} ({} bytes)", project.name, total_size);
534 Ok(())
535}
536
537fn handle_status(
542 stream: &mut (impl Read + Write),
543 req: &StatusRequest,
544 ctx: &HandlerContext,
545) -> Result<(), String> {
546 let storage = ctx.storage.read().unwrap();
547 let projects: Vec<ProjectInfo> = if let Some(uuid) = &req.project_uuid {
548 storage
549 .get_project(uuid)
550 .map(|p| vec![ProjectInfo {
551 uuid: p.uuid,
552 name: p.name.clone(),
553 size_bytes: p.size_bytes,
554 file_count: p.file_count,
555 created_at: p.created_at,
556 last_modified: p.updated_at,
557 }])
558 .unwrap_or_default()
559 } else {
560 storage
561 .list_projects()
562 .iter()
563 .map(|p| ProjectInfo {
564 uuid: p.uuid,
565 name: p.name.clone(),
566 size_bytes: p.size_bytes,
567 file_count: p.file_count,
568 created_at: p.created_at,
569 last_modified: p.updated_at,
570 })
571 .collect()
572 };
573
574 send_server_msg(stream, &protocol::build_status_response(projects))?;
575 Ok(())
576}
577
578fn send_server_msg(
583 stream: &mut (impl Write),
584 msg: &ServerMessage,
585) -> Result<(), String> {
586 let frame = pwr_core::frame::encode_frame(msg, msg.message_type())
587 .map_err(|e| format!("encode: {}", e))?;
588 stream.write_all(&frame).map_err(|e| format!("write: {}", e))?;
589 stream.flush().map_err(|e| format!("flush: {}", e))?;
590 Ok(())
591}