Skip to main content

pwr/
client.rs

1//! Protocol client for communicating with pwr-server.
2//!
3//! Manages the TLS connection, authentication handshake, and
4//! archive/restore operations. All network I/O is synchronous,
5//! intended to be called from blocking contexts.
6
7use std::io::{Read, Write};
8use std::net::{TcpStream, ToSocketAddrs};
9use std::sync::Arc;
10use std::time::Duration;
11
12use pwr_core::config::PwrConfig;
13use pwr_core::crypto;
14use pwr_core::frame::{self, FrameDecoder};
15use pwr_core::protocol::{self, ClientMessage, Handshake, ProjectInfo, ServerMessage};
16use ring::rand::SecureRandom;
17use rustls::pki_types::{ServerName, UnixTime};
18use rustls::client::danger::{ServerCertVerified, ServerCertVerifier, HandshakeSignatureValid};
19use rustls::DigitallySignedStruct;
20
21/// Result type for client operations.
22pub type ClientResult<T> = Result<T, String>;
23
24/// A connected and authenticated client session with the pwr server.
25pub struct PwrClient {
26    stream: Box<dyn ReadWrite>,
27    decoder: FrameDecoder,
28}
29
30/// Helper trait to abstract over TLS and plain streams.
31trait ReadWrite: Read + Write {}
32impl<T: Read + Write> ReadWrite for T {}
33
34impl PwrClient {
35    /// Connect to the server with retry logic for transient failures.
36    ///
37    /// Retries up to 3 times with exponential backoff (1s, 2s, 4s).
38    /// TLS handshake failures and authentication errors are not retried
39    /// since they indicate configuration problems rather than transient
40    /// network issues.
41    pub fn connect(config: &PwrConfig, tls: bool) -> ClientResult<Self> {
42        let max_retries = 3;
43        let mut last_err = String::new();
44
45        for attempt in 0..=max_retries {
46            match Self::connect_once(config, tls) {
47                Ok(client) => {
48                    if attempt > 0 {
49                        log::info!("Connected after {} retries", attempt);
50                    }
51                    return Ok(client);
52                }
53                Err(e) => {
54                    last_err = e;
55                    if attempt < max_retries {
56                        let delay = Duration::from_secs(1 << attempt); // 1s, 2s, 4s
57                        log::warn!(
58                            "Connection attempt {} failed: {}. Retrying in {:?}...",
59                            attempt + 1, last_err, delay
60                        );
61                        std::thread::sleep(delay);
62                    }
63                }
64            }
65        }
66
67        Err(format!("Connection failed after {} attempts: {}", max_retries + 1, last_err))
68    }
69
70    /// Single connection attempt without retry.
71    fn connect_once(config: &PwrConfig, tls: bool) -> ClientResult<Self> {
72        let addr = config.server_addr();
73        let timeout = Duration::from_secs(config.connect_timeout_secs);
74
75        // Resolve the hostname (or IP) to socket addresses.
76        // This supports DNS hostnames like "arch" or "nas.local", not just
77        // raw IP addresses.
78        let addrs: Vec<_> = addr
79            .to_socket_addrs()
80            .map_err(|e| format!("Cannot resolve {}: {}", addr, e))?
81            .collect();
82
83        if addrs.is_empty() {
84            return Err(format!("No addresses found for {}", addr));
85        }
86
87        let tcp_stream = TcpStream::connect_timeout(
88            &addrs[0],
89            timeout,
90        )
91        .map_err(|e| format!("Connection to {} failed: {}", addr, e))?;
92
93        tcp_stream
94            .set_read_timeout(Some(Duration::from_secs(30)))
95            .map_err(|e| format!("set_read_timeout: {}", e))?;
96
97        let psk = crypto::psk_from_hex(&config.server_psk)
98            .map_err(|e| format!("Invalid PSK: {}", e))?;
99
100        let (stream, decoder) = if tls {
101            let mut tls_stream = connect_tls(tcp_stream, config)?;
102            let mut decoder = FrameDecoder::new();
103            perform_handshake(&mut tls_stream, &mut decoder, &psk, "pwr-cli")?;
104            (Box::new(tls_stream) as Box<dyn ReadWrite>, decoder)
105        } else {
106            let mut decoder = FrameDecoder::new();
107            perform_handshake(&mut &tcp_stream, &mut decoder, &psk, "pwr-cli")?;
108            (Box::new(tcp_stream) as Box<dyn ReadWrite>, decoder)
109        };
110
111        Ok(Self { stream, decoder })
112    }
113
114    /// Archive a project: send the encrypted archive blob to the server.
115    ///
116    /// `archive_data` is the fully encrypted tar.gz.age blob.
117    /// `archive_hash` is its SHA-256 hex hash for server-side verification.
118    /// An optional progress callback receives bytes sent so far and total.
119    pub fn archive_project(
120        &mut self,
121        project_uuid: &uuid::Uuid,
122        project_name: &str,
123        archive_data: &[u8],
124        archive_hash: &str,
125    ) -> ClientResult<()> {
126        self.archive_project_with_progress(
127            project_uuid, project_name, archive_data, archive_hash, None,
128        )
129    }
130
131    /// Archive with progress callback: fn(bytes_sent, total_bytes).
132    pub fn archive_project_with_progress(
133        &mut self,
134        project_uuid: &uuid::Uuid,
135        project_name: &str,
136        archive_data: &[u8],
137        archive_hash: &str,
138        progress: Option<&dyn Fn(u64, u64)>,
139    ) -> ClientResult<()> {
140        // Send ArchiveRequest using protocol builder
141        let req = protocol::build_archive_request(
142            *project_uuid,
143            project_name,
144            archive_data.len() as u64,
145            1, // Single archive blob
146            true,
147        );
148        send_client_msg(&mut self.stream, &req)?;
149
150        // Receive ArchiveAccept
151        match recv_server_msg(&mut self.stream, &mut self.decoder)? {
152            ServerMessage::ArchiveAccept(accept) => {
153                log::debug!("Archive accepted, session {}", accept.session_id);
154            }
155            ServerMessage::Error(e) => return Err(format!("Server rejected: {}", e.message)),
156            other => return Err(format!("Unexpected response: {:?}", other.message_type())),
157        }
158
159        // Stream the archive data in chunks
160        let chunk_size = 1024 * 1024; // 1 MiB
161        let mut bytes_sent = 0u64;
162        let total = archive_data.len() as u64;
163        for (i, chunk) in archive_data.chunks(chunk_size).enumerate() {
164            self.stream
165                .write_all(&(chunk.len() as u32).to_be_bytes())
166                .map_err(|e| format!("chunk write: {}", e))?;
167            self.stream
168                .write_all(chunk)
169                .map_err(|e| format!("chunk data write: {}", e))?;
170            bytes_sent += chunk.len() as u64;
171            if let Some(ref cb) = progress {
172                cb(bytes_sent, total);
173            }
174            log::debug!("Sent chunk {} ({} bytes)", i, chunk.len());
175        }
176
177        // Send EOF marker
178        self.stream
179            .write_all(&0u32.to_be_bytes())
180            .map_err(|e| format!("eof write: {}", e))?;
181        self.stream.flush().map_err(|e| format!("flush: {}", e))?;
182
183        // Send ArchiveComplete
184        let complete = protocol::build_archive_complete(
185            archive_data.len() as u64,
186            archive_hash,
187        );
188        send_client_msg(&mut self.stream, &complete)?;
189
190        log::info!("Archive sent: {} bytes, hash: {}", archive_data.len(), archive_hash);
191        Ok(())
192    }
193
194    /// Restore a project: request the server send back the archive blob.
195    ///
196    /// Returns the encrypted archive data that can then be decrypted
197    /// and extracted locally.
198    pub fn restore_project(
199        &mut self,
200        project_uuid: &uuid::Uuid,
201    ) -> ClientResult<Vec<u8>> {
202        self.restore_project_with_progress(project_uuid, None)
203    }
204
205    /// Restore with progress callback: fn(bytes_received, total_bytes).
206    pub fn restore_project_with_progress(
207        &mut self,
208        project_uuid: &uuid::Uuid,
209        progress: Option<&dyn Fn(u64, u64)>,
210    ) -> ClientResult<Vec<u8>> {
211        // Send RestoreRequest
212        let req = protocol::build_restore_request(*project_uuid);
213        send_client_msg(&mut self.stream, &req)?;
214
215        // Receive RestoreAccept
216        let (total_size, _file_count) = match recv_server_msg(&mut self.stream, &mut self.decoder)? {
217            ServerMessage::RestoreAccept(accept) => {
218                log::info!("Restoring {} bytes ({} files)", accept.total_size, accept.file_count);
219                (accept.total_size, accept.file_count)
220            }
221            ServerMessage::Error(e) => return Err(format!("Server rejected: {}", e.message)),
222            other => return Err(format!("Unexpected response: {:?}", other.message_type())),
223        };
224
225        // Drain the frame decoder: it may have buffered chunk bytes
226        // that were read from the transport alongside the RestoreAccept
227        // frame. Consume those first, then read remaining chunks from
228        // the stream.
229        let prefix = self.decoder.drain_bytes();
230
231        // Receive raw chunk data until EOF
232        let mut data = Vec::with_capacity(total_size as usize);
233        let mut buf = Vec::with_capacity(prefix.len() + 8192);
234        buf.extend_from_slice(&prefix);
235        let mut pos: usize = 0;
236
237        loop {
238            // Ensure we have at least 4 bytes for the chunk header
239            while buf.len() - pos < 4 {
240                let start = buf.len();
241                buf.resize(start + 8192, 0);
242                let n = self.stream
243                    .read(&mut buf[start..])
244                    .map_err(|e| format!("chunk read: {}", e))?;
245                if n == 0 {
246                    return Err("Connection closed during chunk transfer".into());
247                }
248                buf.truncate(start + n);
249            }
250
251            let chunk_len = u32::from_be_bytes([
252                buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3],
253            ]) as usize;
254            pos += 4;
255
256            if chunk_len == 0 {
257                break; // EOF
258            }
259
260            // Ensure we have the full chunk data
261            while buf.len() - pos < chunk_len {
262                let start = buf.len();
263                buf.resize(start + (chunk_len - (buf.len() - pos)).max(8192), 0);
264                let n = self.stream
265                    .read(&mut buf[start..])
266                    .map_err(|e| format!("chunk read: {}", e))?;
267                if n == 0 {
268                    return Err("Connection closed during chunk data".into());
269                }
270                buf.truncate(start + n);
271            }
272
273            data.extend_from_slice(&buf[pos..pos + chunk_len]);
274            pos += chunk_len;
275
276            if let Some(ref cb) = progress {
277                cb(data.len() as u64, total_size);
278            }
279
280            // Compact the buffer periodically
281            if pos > 65536 {
282                buf.drain(..pos);
283                pos = 0;
284            }
285        }
286
287        log::info!("Restored {} bytes", data.len());
288        Ok(data)
289    }
290
291    /// Query the server for project status.
292    pub fn get_status(
293        &mut self,
294        project_uuid: Option<&uuid::Uuid>,
295    ) -> ClientResult<Vec<ProjectInfo>> {
296        let req = protocol::ClientMessage::StatusRequest(
297            protocol::StatusRequest { project_uuid: project_uuid.copied() },
298        );
299        send_client_msg(&mut self.stream, &req)?;
300
301        match recv_server_msg(&mut self.stream, &mut self.decoder)? {
302            ServerMessage::StatusResponse(response) => Ok(response.projects),
303            ServerMessage::Error(e) => Err(format!("Server error: {}", e.message)),
304            other => Err(format!("Unexpected response: {:?}", other.message_type())),
305        }
306    }
307}
308
309// ---------------------------------------------------------------------------
310// Internal helpers
311// ---------------------------------------------------------------------------
312
313/// Perform the PSK handshake on a raw (non-TLS) stream.
314fn perform_handshake(
315    stream: &mut impl ReadWrite,
316    decoder: &mut FrameDecoder,
317    psk: &[u8; 32],
318    client_id: &str,
319) -> ClientResult<()> {
320    let mut nonce = [0u8; 32];
321    ring::rand::SystemRandom::new()
322        .fill(&mut nonce)
323        .map_err(|_| "CSPRNG failure".to_string())?;
324
325    let proof = crypto::compute_client_proof(psk, &nonce);
326
327    // Send Handshake
328    let hs = ClientMessage::Handshake(Handshake {
329        version: frame::PROTOCOL_VERSION,
330        client_id: client_id.to_string(),
331        nonce,
332        proof,
333    });
334    send_client_msg(stream, &hs)?;
335
336    // Receive HandshakeAck
337    match recv_server_msg(stream, decoder)? {
338        ServerMessage::HandshakeAck(ack) => {
339            if !ack.success {
340                return Err(format!(
341                    "Authentication failed: {}",
342                    ack.reason.unwrap_or_default()
343                ));
344            }
345
346            // Verify server proof (mutual auth)
347            let expected = crypto::compute_server_proof(psk, &nonce, &ack.server_nonce);
348            if expected != ack.server_proof {
349                return Err("Server authentication failed: invalid proof".into());
350            }
351
352            log::info!("Authenticated to server v{}", ack.server_version);
353            Ok(())
354        }
355        ServerMessage::Error(e) => Err(format!("Server rejected handshake: {}", e.message)),
356        other => Err(format!("Unexpected handshake response: {:?}", other.message_type())),
357    }
358}
359
360/// Send a ClientMessage as a framed message on the stream.
361fn send_client_msg(
362    stream: &mut impl Write,
363    msg: &ClientMessage,
364) -> ClientResult<()> {
365    let frame = frame::encode_frame(msg, msg.message_type())
366        .map_err(|e| format!("encode: {}", e))?;
367    stream.write_all(&frame).map_err(|e| format!("write: {}", e))?;
368    stream.flush().map_err(|e| format!("flush: {}", e))?;
369    Ok(())
370}
371
372/// Receive one ServerMessage from the stream.
373///
374/// Reads from the stream until a complete frame is decoded, then
375/// deserializes the payload using the typed server message decoder.
376fn recv_server_msg(
377    stream: &mut impl Read,
378    decoder: &mut FrameDecoder,
379) -> ClientResult<ServerMessage> {
380    let mut buf = [0u8; 8192];
381
382    loop {
383        if let Some((header, payload)) = decoder.try_decode()
384            .map_err(|e| format!("decode: {}", e))?
385        {
386            return protocol::decode_server_message(header.msg_type, &payload)
387                .map_err(|e| format!("deserialize: {}", e));
388        }
389
390        let n = stream.read(&mut buf).map_err(|e| format!("read: {}", e))?;
391        if n == 0 {
392            return Err("Connection closed by server".into());
393        }
394        decoder.push_bytes(&buf[..n]);
395    }
396}
397
398// ---------------------------------------------------------------------------
399// TLS connection
400// ---------------------------------------------------------------------------
401
402/// Establish a TLS-encrypted connection to the server.
403///
404/// Uses a custom certificate verifier that accepts self-signed
405/// certificates while optionally checking the SHA-256 fingerprint
406/// against a pinned value for MITM protection. The PSK handshake
407/// provides authentication even without CA-signed certificates.
408fn connect_tls(
409    tcp_stream: TcpStream,
410    config: &PwrConfig,
411) -> ClientResult<rustls::StreamOwned<rustls::ClientConnection, TcpStream>> {
412    let pinned = config.server_fingerprint.clone();
413    let verifier = Arc::new(CertVerifier::new(pinned));
414
415    let tls_config = rustls::ClientConfig::builder()
416        .dangerous()
417        .with_custom_certificate_verifier(verifier)
418        .with_no_client_auth();
419
420    let server_name: ServerName = config
421        .server_host
422        .clone()
423        .try_into()
424        .map_err(|_| format!("Invalid server hostname: {}", config.server_host))?;
425
426    let client_conn = rustls::ClientConnection::new(
427        Arc::new(tls_config),
428        server_name,
429    )
430    .map_err(|e| format!("TLS client setup: {}", e))?;
431
432    let mut tls_stream = rustls::StreamOwned::new(client_conn, tcp_stream);
433
434    // Trigger TLS handshake
435    tls_stream.flush().map_err(|e| format!("TLS handshake: {}", e))?;
436
437    log::info!(
438        "TLS connection established to {}:{}",
439        config.server_host,
440        config.server_port
441    );
442
443    Ok(tls_stream)
444}
445
446/// Custom TLS certificate verifier for pwr.
447///
448/// Accepts self-signed certificates (the server generates its own)
449/// and optionally verifies the SHA-256 fingerprint against a pinned
450/// value for defense-in-depth beyond the PSK authentication layer.
451#[derive(Debug)]
452struct CertVerifier {
453    pinned_fingerprint: Option<String>,
454}
455
456impl CertVerifier {
457    fn new(pinned_fingerprint: Option<String>) -> Self {
458        Self { pinned_fingerprint }
459    }
460}
461
462impl ServerCertVerifier for CertVerifier {
463    fn verify_server_cert(
464        &self,
465        end_entity: &rustls::pki_types::CertificateDer<'_>,
466        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
467        _server_name: &ServerName<'_>,
468        _ocsp_response: &[u8],
469        _now: UnixTime,
470    ) -> Result<ServerCertVerified, rustls::Error> {
471        // If a fingerprint is pinned, verify it matches
472        if let Some(ref pinned) = self.pinned_fingerprint {
473            let actual = crypto::sha256_hex(end_entity.as_ref());
474            if actual != *pinned {
475                return Err(rustls::Error::General(format!(
476                    "Certificate fingerprint mismatch: expected {}",
477                    pinned
478                )));
479            }
480            log::info!("Server certificate fingerprint verified against pinned value");
481        } else {
482            log::warn!(
483                "No certificate fingerprint pinned. Server cert SHA-256: {}",
484                crypto::sha256_hex(end_entity.as_ref())
485            );
486        }
487        Ok(ServerCertVerified::assertion())
488    }
489
490    fn verify_tls12_signature(
491        &self,
492        _message: &[u8],
493        _cert: &rustls::pki_types::CertificateDer<'_>,
494        _dss: &DigitallySignedStruct,
495    ) -> Result<HandshakeSignatureValid, rustls::Error> {
496        // Accept any signature: authentication is via PSK, not PKI
497        Ok(HandshakeSignatureValid::assertion())
498    }
499
500    fn verify_tls13_signature(
501        &self,
502        _message: &[u8],
503        _cert: &rustls::pki_types::CertificateDer<'_>,
504        _dss: &DigitallySignedStruct,
505    ) -> Result<HandshakeSignatureValid, rustls::Error> {
506        // Accept any signature: authentication is via PSK, not PKI
507        Ok(HandshakeSignatureValid::assertion())
508    }
509
510    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
511        vec![
512            rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
513            rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
514            rustls::SignatureScheme::RSA_PSS_SHA512,
515            rustls::SignatureScheme::RSA_PSS_SHA256,
516            rustls::SignatureScheme::RSA_PKCS1_SHA512,
517            rustls::SignatureScheme::RSA_PKCS1_SHA256,
518        ]
519    }
520}