Skip to main content

pwr_server/
handler.rs

1//! Per-connection request handler for pwr-server.
2//!
3//! Each TCP connection progresses through a state machine:
4//! AwaitingHandshake → Authenticated → (Archiving | Restoring | Idle).
5//! After authentication, the handler loops reading frames and dispatching
6//! to the appropriate operation handler until the client disconnects.
7//!
8//! Note: parentheses around `impl Read + Write` in argument position are
9//! required by Rust 2024 edition to disambiguate `&mut (impl A + B)` from
10//! `(&mut impl A) + B`. The `unused_parens` warning is suppressed.
11
12use 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// ---------------------------------------------------------------------------
30// Connection state machine
31// ---------------------------------------------------------------------------
32
33#[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
72// ---------------------------------------------------------------------------
73// Main dispatch loop
74// ---------------------------------------------------------------------------
75
76pub 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
112// ---------------------------------------------------------------------------
113// Message dispatch
114// ---------------------------------------------------------------------------
115
116fn 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    // Decode the client message
125    let msg = protocol::decode_client_message(header.msg_type, payload)
126        .map_err(|e| format!("Decode: {}", e))?;
127
128    match (&state, msg) {
129        // --- Handshake (only valid before auth) ---
130        (ConnState::AwaitingHandshake, ClientMessage::Handshake(hs)) => {
131            handle_handshake(stream, state, &hs, ctx)
132        }
133
134        // --- Archive flow ---
135        (ConnState::Authenticated, ClientMessage::ArchiveRequest(req)) => {
136            handle_archive_start(stream, state, &req, ctx)?;
137            // Drain any bytes already buffered in the decoder — they
138            // belong to the chunk stream, not to the frame protocol.
139            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        // --- Restore flow ---
147        (ConnState::Authenticated, ClientMessage::RestoreRequest(req)) => {
148            handle_restore_start(stream, state, &req, ctx)?;
149            // After accepting, stream raw chunk data back to client
150            handle_restore_chunks(stream, state, ctx)
151        }
152
153        // --- Status query ---
154        (ConnState::Authenticated, ClientMessage::StatusRequest(req)) => {
155            handle_status(stream, &req, ctx)
156        }
157
158        // --- Protocol violations ---
159        (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
170// ---------------------------------------------------------------------------
171// Handshake handler
172// ---------------------------------------------------------------------------
173
174fn handle_handshake(
175    stream: &mut (impl Read + Write),
176    state: &mut ConnState,
177    hs: &Handshake,
178    ctx: &HandlerContext,
179) -> Result<(), String> {
180    // Rate limiting check
181    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    // Record successful auth for rate limiting
206    ctx.rate_limiter.lock().unwrap().record_success(peer_ip);
207
208    // Generate server nonce and proof
209    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
231// ---------------------------------------------------------------------------
232// Archive handlers
233// ---------------------------------------------------------------------------
234
235fn 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    // Check storage limit
244    {
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    // Create project entry
251    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    // Extract session data before modifying state (avoids borrow conflict)
295    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    // Update project with final size
308    {
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
327// ---------------------------------------------------------------------------
328// Archive chunk streaming
329// ---------------------------------------------------------------------------
330
331/// Receive raw chunk data from the client and write it to the project's
332/// archive file on disk. Chunks use the 4-byte length-prefixed format
333/// with a zero-length chunk indicating EOF.
334///
335/// `prefix` contains any bytes already read from the transport by the
336/// frame decoder that belong to the chunk stream rather than a frame.
337/// These are consumed first before reading more from `stream`.
338fn 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    // Combine prefix bytes (drained from the frame decoder) with
352    // subsequent reads from the stream so we don't miss any data
353    // that was already buffered.
354    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        // Ensure we have at least 4 bytes for the chunk header
360        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; // EOF
380        }
381
382        // Ensure we have the full chunk data
383        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        // Write chunk to the archive file
401        {
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        // Compact the buffer: discard consumed bytes
422        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
437// ---------------------------------------------------------------------------
438// Restore chunk streaming
439// ---------------------------------------------------------------------------
440
441/// Stream the project's archive file back to the client in chunked format.
442fn 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    // Read the archive file from disk
453    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    // Stream in chunks
465    let chunk_size: usize = 1024 * 1024; // 1 MiB
466    let mut total_sent = 0u64;
467
468    for chunk in archive_data.chunks(chunk_size) {
469        // Write 4-byte length prefix + chunk data
470        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    // Send EOF marker
481    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
495// ---------------------------------------------------------------------------
496// Restore handler
497// ---------------------------------------------------------------------------
498
499fn 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
537// ---------------------------------------------------------------------------
538// Status handler
539// ---------------------------------------------------------------------------
540
541fn 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
578// ---------------------------------------------------------------------------
579// I/O helpers
580// ---------------------------------------------------------------------------
581
582fn 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}