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 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    state: &mut ConnState,
119    header: FrameHeader,
120    payload: &[u8],
121    ctx: &HandlerContext,
122) -> Result<(), String> {
123    // Decode the client message
124    let msg = protocol::decode_client_message(header.msg_type, payload)
125        .map_err(|e| format!("Decode: {}", e))?;
126
127    match (&state, msg) {
128        // --- Handshake (only valid before auth) ---
129        (ConnState::AwaitingHandshake, ClientMessage::Handshake(hs)) => {
130            handle_handshake(stream, state, &hs, ctx)
131        }
132
133        // --- Archive flow ---
134        (ConnState::Authenticated, ClientMessage::ArchiveRequest(req)) => {
135            handle_archive_start(stream, state, &req, ctx)?;
136            // After accepting, receive raw chunk data for the archive blob
137            handle_archive_chunks(stream, state, ctx)
138        }
139        (ConnState::Archiving(_), ClientMessage::ArchiveComplete(complete)) => {
140            handle_archive_finish(stream, state, &complete, ctx)
141        }
142
143        // --- Restore flow ---
144        (ConnState::Authenticated, ClientMessage::RestoreRequest(req)) => {
145            handle_restore_start(stream, state, &req, ctx)?;
146            // After accepting, stream raw chunk data back to client
147            handle_restore_chunks(stream, state, ctx)
148        }
149
150        // --- Status query ---
151        (ConnState::Authenticated, ClientMessage::StatusRequest(req)) => {
152            handle_status(stream, &req, ctx)
153        }
154
155        // --- Protocol violations ---
156        (ConnState::AwaitingHandshake, _) => {
157            Err("Handshake required before any other message".into())
158        }
159        (ConnState::Closed, _) => Err("Connection is closed".into()),
160        (_, _msg) => Err(format!(
161            "Unexpected message in state {:?}",
162            std::mem::discriminant(state)
163        )),
164    }
165}
166
167// ---------------------------------------------------------------------------
168// Handshake handler
169// ---------------------------------------------------------------------------
170
171fn handle_handshake(
172    stream: &mut (impl Read + Write),
173    state: &mut ConnState,
174    hs: &Handshake,
175    ctx: &HandlerContext,
176) -> Result<(), String> {
177    // Rate limiting check
178    let peer_ip = ctx.peer_addr.ip();
179    {
180        let mut limiter = ctx.rate_limiter.lock().unwrap();
181        if !limiter.check_attempt(peer_ip) {
182            *state = ConnState::Closed;
183            send_server_msg(
184                stream,
185                &protocol::build_handshake_ack_failed("Too many authentication attempts — try again later"),
186            )?;
187            return Err("Rate limited".into());
188        }
189    }
190
191    let expected_proof = crypto::compute_client_proof(&ctx.psk, &hs.nonce);
192
193    if expected_proof != hs.proof {
194        *state = ConnState::Closed;
195        send_server_msg(
196            stream,
197            &protocol::build_handshake_ack_failed("Authentication failed: invalid proof"),
198        )?;
199        return Err("Authentication failed".into());
200    }
201
202    // Record successful auth for rate limiting
203    ctx.rate_limiter.lock().unwrap().record_success(peer_ip);
204
205    // Generate server nonce and proof
206    let mut server_nonce = [0u8; 32];
207    ring::rand::SystemRandom::new()
208        .fill(&mut server_nonce)
209        .map_err(|_| "CSPRNG failure".to_string())?;
210
211    let server_proof =
212        crypto::compute_server_proof(&ctx.psk, &hs.nonce, &server_nonce);
213
214    *state = ConnState::Authenticated;
215    send_server_msg(
216        stream,
217        &protocol::build_handshake_ack_success(
218            env!("CARGO_PKG_VERSION"),
219            server_nonce,
220            server_proof,
221        ),
222    )?;
223
224    log::info!("Client '{}' authenticated from {}", hs.client_id, ctx.peer_addr);
225    Ok(())
226}
227
228// ---------------------------------------------------------------------------
229// Archive handlers
230// ---------------------------------------------------------------------------
231
232fn handle_archive_start(
233    stream: &mut (impl Read + Write),
234    state: &mut ConnState,
235    req: &ArchiveRequest,
236    ctx: &HandlerContext,
237) -> Result<(), String> {
238    let session_id = Uuid::new_v4();
239
240    // Check storage limit
241    {
242        let storage = ctx.storage.read().unwrap();
243        storage.check_size_limit(req.total_size)
244            .map_err(|e| format!("Archive rejected: {}", e))?;
245    }
246
247    // Create project entry
248    let project = StoredProject {
249        uuid: req.project_uuid,
250        name: req.project_name.clone(),
251        size_bytes: req.total_size,
252        file_count: req.file_count,
253        encrypted: true,
254        created_at: chrono::Utc::now(),
255        updated_at: chrono::Utc::now(),
256    };
257
258    {
259        let mut storage = ctx.storage.write().unwrap();
260        storage.add_project(project.clone())
261            .map_err(|e| format!("Cannot add project: {}", e))?;
262        storage.write_meta(&req.project_uuid, &project)
263            .map_err(|e| format!("Cannot write meta: {}", e))?;
264    }
265
266    *state = ConnState::Archiving(ArchiveSession {
267        session_id,
268        project_uuid: req.project_uuid,
269        project_name: req.project_name.clone(),
270        total_size: req.total_size,
271        file_count: req.file_count,
272        compression: req.compression,
273        bytes_received: 0,
274    });
275
276    send_server_msg(stream, &protocol::build_archive_accept(session_id))?;
277
278    log::info!(
279        "Archive started: {} ({} bytes, {} files)",
280        req.project_name, req.total_size, req.file_count
281    );
282    Ok(())
283}
284
285fn handle_archive_finish(
286    stream: &mut (impl Read + Write),
287    state: &mut ConnState,
288    complete: &ArchiveComplete,
289    ctx: &HandlerContext,
290) -> Result<(), String> {
291    // Extract session data before modifying state (avoids borrow conflict)
292    let (project_uuid, project_name, _total_size) = match state {
293        ConnState::Archiving(s) => (s.project_uuid, s.project_name.clone(), s.total_size),
294        _ => return Err("Not in archiving state".into()),
295    };
296
297    if !complete.success {
298        let _ = ctx.storage.write().unwrap().remove_project(&project_uuid);
299        *state = ConnState::Authenticated;
300        send_server_msg(stream, &protocol::build_error(0, "Archive cancelled by client"))?;
301        return Ok(());
302    }
303
304    // Update project with final size
305    {
306        let mut storage = ctx.storage.write().unwrap();
307        if let Some(mut project) = storage.get_project(&project_uuid).cloned() {
308            project.size_bytes = complete.total_size;
309            project.updated_at = chrono::Utc::now();
310            storage.update_project(project)
311                .map_err(|e| format!("Cannot update: {}", e))?;
312        }
313    }
314
315    *state = ConnState::Authenticated;
316    log::info!(
317        "Archive complete: {} ({} bytes, hash: {})",
318        project_name, complete.total_size,
319        &complete.archive_hash[..16.min(complete.archive_hash.len())]
320    );
321    Ok(())
322}
323
324// ---------------------------------------------------------------------------
325// Archive chunk streaming
326// ---------------------------------------------------------------------------
327
328/// Receive raw chunk data from the client and write it to the project's
329/// archive file on disk. Chunks use the 4-byte length-prefixed format
330/// with a zero-length chunk indicating EOF.
331fn handle_archive_chunks(
332    stream: &mut (impl Read + Write),
333    state: &ConnState,
334    ctx: &HandlerContext,
335) -> Result<(), String> {
336    let (project_uuid, _total_size) = match state {
337        ConnState::Archiving(s) => (s.project_uuid, s.total_size),
338        _ => return Err("Not in archiving state".into()),
339    };
340
341    let mut total_bytes = 0u64;
342    let mut header_buf = [0u8; 4];
343
344    loop {
345        // Read 4-byte chunk length
346        stream
347            .read_exact(&mut header_buf)
348            .map_err(|e| format!("chunk header read: {}", e))?;
349
350        let chunk_len = u32::from_be_bytes(header_buf) as usize;
351
352        if chunk_len == 0 {
353            break; // EOF
354        }
355
356        // Read chunk data into a buffer and write to archive
357        let mut chunk = vec![0u8; chunk_len];
358        stream
359            .read_exact(&mut chunk)
360            .map_err(|e| format!("chunk data read: {}", e))?;
361
362        total_bytes += chunk_len as u64;
363
364        // Write chunk to the archive file
365        {
366            let storage = ctx.storage.read().unwrap();
367            let archive_path = storage.archive_path(&project_uuid);
368
369            use std::io::Write;
370            let mut file = std::fs::OpenOptions::new()
371                .create(true)
372                .append(true)
373                .open(&archive_path)
374                .map_err(|e| format!("Cannot open archive: {}", e))?;
375            file.write_all(&chunk)
376                .map_err(|e| format!("Cannot write chunk: {}", e))?;
377        }
378
379        log::debug!(
380            "Received chunk: {} bytes (total: {})",
381            chunk_len,
382            total_bytes
383        );
384    }
385
386    log::info!(
387        "Archive data received: {} bytes for project {}",
388        total_bytes,
389        project_uuid
390    );
391
392    Ok(())
393}
394
395// ---------------------------------------------------------------------------
396// Restore chunk streaming
397// ---------------------------------------------------------------------------
398
399/// Stream the project's archive file back to the client in chunked format.
400fn handle_restore_chunks(
401    stream: &mut (impl Read + Write),
402    state: &ConnState,
403    ctx: &HandlerContext,
404) -> Result<(), String> {
405    let project_uuid = match state {
406        ConnState::Restoring(s) => s.project_uuid,
407        _ => return Err("Not in restoring state".into()),
408    };
409
410    // Read the archive file from disk
411    let archive_data = {
412        let storage = ctx.storage.read().unwrap();
413        let mut reader = storage
414            .read_archive(&project_uuid)
415            .map_err(|e| format!("Cannot read archive: {}", e))?;
416        let mut data = Vec::new();
417        std::io::Read::read_to_end(&mut reader, &mut data)
418            .map_err(|e| format!("Cannot read archive data: {}", e))?;
419        data
420    };
421
422    // Stream in chunks
423    let chunk_size: usize = 1024 * 1024; // 1 MiB
424    let mut total_sent = 0u64;
425
426    for chunk in archive_data.chunks(chunk_size) {
427        // Write 4-byte length prefix + chunk data
428        stream
429            .write_all(&(chunk.len() as u32).to_be_bytes())
430            .map_err(|e| format!("chunk header write: {}", e))?;
431        stream
432            .write_all(chunk)
433            .map_err(|e| format!("chunk data write: {}", e))?;
434
435        total_sent += chunk.len() as u64;
436    }
437
438    // Send EOF marker
439    stream
440        .write_all(&0u32.to_be_bytes())
441        .map_err(|e| format!("eof write: {}", e))?;
442    stream.flush().map_err(|e| format!("flush: {}", e))?;
443
444    log::info!(
445        "Restore data sent: {} bytes for project {}",
446        total_sent,
447        project_uuid
448    );
449
450    Ok(())
451}
452
453// ---------------------------------------------------------------------------
454// Restore handler
455// ---------------------------------------------------------------------------
456
457fn handle_restore_start(
458    stream: &mut (impl Read + Write),
459    state: &mut ConnState,
460    req: &RestoreRequest,
461    ctx: &HandlerContext,
462) -> Result<(), String> {
463    let storage = ctx.storage.read().unwrap();
464    let project = storage
465        .get_project(&req.project_uuid)
466        .cloned()
467        .ok_or_else(|| format!("Project not found: {}", req.project_uuid))?;
468
469    if !storage.archive_exists(&req.project_uuid) {
470        return Err(format!("Archive data missing for {}", req.project_uuid));
471    }
472    drop(storage);
473
474    let session_id = Uuid::new_v4();
475    let total_size = project.size_bytes;
476    let file_count = project.file_count;
477
478    *state = ConnState::Restoring(RestoreSession {
479        session_id,
480        project_uuid: req.project_uuid,
481        total_size,
482        file_count,
483        bytes_sent: 0,
484    });
485
486    send_server_msg(
487        stream,
488        &protocol::build_restore_accept(session_id, total_size, file_count, ""),
489    )?;
490
491    log::info!("Restore started: {} ({} bytes)", project.name, total_size);
492    Ok(())
493}
494
495// ---------------------------------------------------------------------------
496// Status handler
497// ---------------------------------------------------------------------------
498
499fn handle_status(
500    stream: &mut (impl Read + Write),
501    req: &StatusRequest,
502    ctx: &HandlerContext,
503) -> Result<(), String> {
504    let storage = ctx.storage.read().unwrap();
505    let projects: Vec<ProjectInfo> = if let Some(uuid) = &req.project_uuid {
506        storage
507            .get_project(uuid)
508            .map(|p| vec![ProjectInfo {
509                uuid: p.uuid,
510                name: p.name.clone(),
511                size_bytes: p.size_bytes,
512                file_count: p.file_count,
513                created_at: p.created_at,
514                last_modified: p.updated_at,
515            }])
516            .unwrap_or_default()
517    } else {
518        storage
519            .list_projects()
520            .iter()
521            .map(|p| ProjectInfo {
522                uuid: p.uuid,
523                name: p.name.clone(),
524                size_bytes: p.size_bytes,
525                file_count: p.file_count,
526                created_at: p.created_at,
527                last_modified: p.updated_at,
528            })
529            .collect()
530    };
531
532    send_server_msg(stream, &protocol::build_status_response(projects))?;
533    Ok(())
534}
535
536// ---------------------------------------------------------------------------
537// I/O helpers
538// ---------------------------------------------------------------------------
539
540fn send_server_msg(
541    stream: &mut (impl Write),
542    msg: &ServerMessage,
543) -> Result<(), String> {
544    let frame = pwr_core::frame::encode_frame(msg, msg.message_type())
545        .map_err(|e| format!("encode: {}", e))?;
546    stream.write_all(&frame).map_err(|e| format!("write: {}", e))?;
547    stream.flush().map_err(|e| format!("flush: {}", e))?;
548    Ok(())
549}