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. Returns both IPv6 and IPv4 addresses — we try
78        // them in order (typically v6 first) and use the first that connects.
79        let addrs: Vec<_> = addr
80            .to_socket_addrs()
81            .map_err(|e| format!("Cannot resolve {}: {}", addr, e))?
82            .collect();
83
84        if addrs.is_empty() {
85            return Err(format!("No addresses found for {}", addr));
86        }
87
88        // Try each resolved address until one connects
89        let mut last_err = String::new();
90        for a in &addrs {
91            match TcpStream::connect_timeout(a, timeout) {
92                Ok(stream) => {
93                    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(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 &stream, &mut decoder, &psk, "pwr-cli")?;
108                        (Box::new(stream) as Box<dyn ReadWrite>, decoder)
109                    };
110
111                    return Ok(Self { stream, decoder });
112                }
113                Err(e) => {
114                    log::debug!("  {}: {}", a, e);
115                    last_err = format!("{}: {}", a, e);
116                }
117            }
118        }
119
120        Err(format!("Connection to {} failed (tried {} addresses): {}",
121            addr, addrs.len(), last_err))
122    }
123
124    /// Archive a project: send the encrypted archive blob to the server.
125    ///
126    /// `archive_data` is the fully encrypted tar.gz.age blob.
127    /// `archive_hash` is its SHA-256 hex hash for server-side verification.
128    /// An optional progress callback receives bytes sent so far and total.
129    pub fn archive_project(
130        &mut self,
131        project_uuid: &uuid::Uuid,
132        project_name: &str,
133        archive_data: &[u8],
134        archive_hash: &str,
135    ) -> ClientResult<()> {
136        self.archive_project_with_progress(
137            project_uuid, project_name, archive_data, archive_hash, None,
138        )
139    }
140
141    /// Archive with progress callback: fn(bytes_sent, total_bytes).
142    pub fn archive_project_with_progress(
143        &mut self,
144        project_uuid: &uuid::Uuid,
145        project_name: &str,
146        archive_data: &[u8],
147        archive_hash: &str,
148        progress: Option<&dyn Fn(u64, u64)>,
149    ) -> ClientResult<()> {
150        // Send ArchiveRequest using protocol builder
151        let req = protocol::build_archive_request(
152            *project_uuid,
153            project_name,
154            archive_data.len() as u64,
155            1, // Single archive blob
156            true,
157        );
158        send_client_msg(&mut self.stream, &req)?;
159
160        // Receive ArchiveAccept
161        match recv_server_msg(&mut self.stream, &mut self.decoder)? {
162            ServerMessage::ArchiveAccept(accept) => {
163                log::debug!("Archive accepted, session {}", accept.session_id);
164            }
165            ServerMessage::Error(e) => return Err(format!("Server rejected: {}", e.message)),
166            other => return Err(format!("Unexpected response: {:?}", other.message_type())),
167        }
168
169        // Stream the archive data in chunks
170        let chunk_size = 1024 * 1024; // 1 MiB
171        let mut bytes_sent = 0u64;
172        let total = archive_data.len() as u64;
173        for (i, chunk) in archive_data.chunks(chunk_size).enumerate() {
174            self.stream
175                .write_all(&(chunk.len() as u32).to_be_bytes())
176                .map_err(|e| format!("chunk write: {}", e))?;
177            self.stream
178                .write_all(chunk)
179                .map_err(|e| format!("chunk data write: {}", e))?;
180            bytes_sent += chunk.len() as u64;
181            if let Some(ref cb) = progress {
182                cb(bytes_sent, total);
183            }
184            log::debug!("Sent chunk {} ({} bytes)", i, chunk.len());
185        }
186
187        // Send EOF marker
188        self.stream
189            .write_all(&0u32.to_be_bytes())
190            .map_err(|e| format!("eof write: {}", e))?;
191        self.stream.flush().map_err(|e| format!("flush: {}", e))?;
192
193        // Send ArchiveComplete
194        let complete = protocol::build_archive_complete(
195            archive_data.len() as u64,
196            archive_hash,
197        );
198        send_client_msg(&mut self.stream, &complete)?;
199
200        log::info!("Archive sent: {} bytes, hash: {}", archive_data.len(), archive_hash);
201        Ok(())
202    }
203
204    /// Restore a project: request the server send back the archive blob.
205    ///
206    /// Returns the encrypted archive data that can then be decrypted
207    /// and extracted locally.
208    pub fn restore_project(
209        &mut self,
210        project_uuid: &uuid::Uuid,
211    ) -> ClientResult<Vec<u8>> {
212        self.restore_project_with_progress(project_uuid, None)
213    }
214
215    /// Restore with progress callback: fn(bytes_received, total_bytes).
216    pub fn restore_project_with_progress(
217        &mut self,
218        project_uuid: &uuid::Uuid,
219        progress: Option<&dyn Fn(u64, u64)>,
220    ) -> ClientResult<Vec<u8>> {
221        // Send RestoreRequest
222        let req = protocol::build_restore_request(*project_uuid);
223        send_client_msg(&mut self.stream, &req)?;
224
225        // Receive RestoreAccept
226        let (total_size, _file_count) = match recv_server_msg(&mut self.stream, &mut self.decoder)? {
227            ServerMessage::RestoreAccept(accept) => {
228                log::info!("Restoring {} bytes ({} files)", accept.total_size, accept.file_count);
229                (accept.total_size, accept.file_count)
230            }
231            ServerMessage::Error(e) => return Err(format!("Server rejected: {}", e.message)),
232            other => return Err(format!("Unexpected response: {:?}", other.message_type())),
233        };
234
235        // Drain the frame decoder: it may have buffered chunk bytes
236        // that were read from the transport alongside the RestoreAccept
237        // frame. Consume those first, then read remaining chunks from
238        // the stream.
239        let prefix = self.decoder.drain_bytes();
240
241        // Receive raw chunk data until EOF
242        let mut data = Vec::with_capacity(total_size as usize);
243        let mut buf = Vec::with_capacity(prefix.len() + 8192);
244        buf.extend_from_slice(&prefix);
245        let mut pos: usize = 0;
246
247        loop {
248            // Ensure we have at least 4 bytes for the chunk header
249            while buf.len() - pos < 4 {
250                let start = buf.len();
251                buf.resize(start + 8192, 0);
252                let n = self.stream
253                    .read(&mut buf[start..])
254                    .map_err(|e| format!("chunk read: {}", e))?;
255                if n == 0 {
256                    return Err("Connection closed during chunk transfer".into());
257                }
258                buf.truncate(start + n);
259            }
260
261            let chunk_len = u32::from_be_bytes([
262                buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3],
263            ]) as usize;
264            pos += 4;
265
266            if chunk_len == 0 {
267                break; // EOF
268            }
269
270            // Ensure we have the full chunk data
271            while buf.len() - pos < chunk_len {
272                let start = buf.len();
273                buf.resize(start + (chunk_len - (buf.len() - pos)).max(8192), 0);
274                let n = self.stream
275                    .read(&mut buf[start..])
276                    .map_err(|e| format!("chunk read: {}", e))?;
277                if n == 0 {
278                    return Err("Connection closed during chunk data".into());
279                }
280                buf.truncate(start + n);
281            }
282
283            data.extend_from_slice(&buf[pos..pos + chunk_len]);
284            pos += chunk_len;
285
286            if let Some(ref cb) = progress {
287                cb(data.len() as u64, total_size);
288            }
289
290            // Compact the buffer periodically
291            if pos > 65536 {
292                buf.drain(..pos);
293                pos = 0;
294            }
295        }
296
297        log::info!("Restored {} bytes", data.len());
298        Ok(data)
299    }
300
301    /// Query the server for project status.
302    pub fn get_status(
303        &mut self,
304        project_uuid: Option<&uuid::Uuid>,
305    ) -> ClientResult<Vec<ProjectInfo>> {
306        let req = protocol::ClientMessage::StatusRequest(
307            protocol::StatusRequest { project_uuid: project_uuid.copied() },
308        );
309        send_client_msg(&mut self.stream, &req)?;
310
311        match recv_server_msg(&mut self.stream, &mut self.decoder)? {
312            ServerMessage::StatusResponse(response) => Ok(response.projects),
313            ServerMessage::Error(e) => Err(format!("Server error: {}", e.message)),
314            other => Err(format!("Unexpected response: {:?}", other.message_type())),
315        }
316    }
317}
318
319// ---------------------------------------------------------------------------
320// Internal helpers
321// ---------------------------------------------------------------------------
322
323/// Perform the PSK handshake on a raw (non-TLS) stream.
324fn perform_handshake(
325    stream: &mut impl ReadWrite,
326    decoder: &mut FrameDecoder,
327    psk: &[u8; 32],
328    client_id: &str,
329) -> ClientResult<()> {
330    let mut nonce = [0u8; 32];
331    ring::rand::SystemRandom::new()
332        .fill(&mut nonce)
333        .map_err(|_| "CSPRNG failure".to_string())?;
334
335    let proof = crypto::compute_client_proof(psk, &nonce);
336
337    // Send Handshake
338    let hs = ClientMessage::Handshake(Handshake {
339        version: frame::PROTOCOL_VERSION,
340        client_id: client_id.to_string(),
341        nonce,
342        proof,
343    });
344    send_client_msg(stream, &hs)?;
345
346    // Receive HandshakeAck
347    match recv_server_msg(stream, decoder)? {
348        ServerMessage::HandshakeAck(ack) => {
349            if !ack.success {
350                return Err(format!(
351                    "Authentication failed: {}",
352                    ack.reason.unwrap_or_default()
353                ));
354            }
355
356            // Verify server proof (mutual auth)
357            let expected = crypto::compute_server_proof(psk, &nonce, &ack.server_nonce);
358            if expected != ack.server_proof {
359                return Err("Server authentication failed: invalid proof".into());
360            }
361
362            log::info!("Authenticated to server v{}", ack.server_version);
363            Ok(())
364        }
365        ServerMessage::Error(e) => Err(format!("Server rejected handshake: {}", e.message)),
366        other => Err(format!("Unexpected handshake response: {:?}", other.message_type())),
367    }
368}
369
370/// Send a ClientMessage as a framed message on the stream.
371fn send_client_msg(
372    stream: &mut impl Write,
373    msg: &ClientMessage,
374) -> ClientResult<()> {
375    let frame = frame::encode_frame(msg, msg.message_type())
376        .map_err(|e| format!("encode: {}", e))?;
377    stream.write_all(&frame).map_err(|e| format!("write: {}", e))?;
378    stream.flush().map_err(|e| format!("flush: {}", e))?;
379    Ok(())
380}
381
382/// Receive one ServerMessage from the stream.
383///
384/// Reads from the stream until a complete frame is decoded, then
385/// deserializes the payload using the typed server message decoder.
386fn recv_server_msg(
387    stream: &mut impl Read,
388    decoder: &mut FrameDecoder,
389) -> ClientResult<ServerMessage> {
390    let mut buf = [0u8; 8192];
391
392    loop {
393        if let Some((header, payload)) = decoder.try_decode()
394            .map_err(|e| format!("decode: {}", e))?
395        {
396            return protocol::decode_server_message(header.msg_type, &payload)
397                .map_err(|e| format!("deserialize: {}", e));
398        }
399
400        let n = stream.read(&mut buf).map_err(|e| format!("read: {}", e))?;
401        if n == 0 {
402            return Err("Connection closed by server".into());
403        }
404        decoder.push_bytes(&buf[..n]);
405    }
406}
407
408// ---------------------------------------------------------------------------
409// TLS connection
410// ---------------------------------------------------------------------------
411
412/// Establish a TLS-encrypted connection to the server.
413///
414/// Uses a custom certificate verifier that accepts self-signed
415/// certificates while optionally checking the SHA-256 fingerprint
416/// against a pinned value for MITM protection. The PSK handshake
417/// provides authentication even without CA-signed certificates.
418fn connect_tls(
419    tcp_stream: TcpStream,
420    config: &PwrConfig,
421) -> ClientResult<rustls::StreamOwned<rustls::ClientConnection, TcpStream>> {
422    let pinned = config.server_fingerprint.clone();
423    let verifier = Arc::new(CertVerifier::new(pinned));
424
425    let tls_config = rustls::ClientConfig::builder()
426        .dangerous()
427        .with_custom_certificate_verifier(verifier)
428        .with_no_client_auth();
429
430    let server_name: ServerName = config
431        .server_host
432        .clone()
433        .try_into()
434        .map_err(|_| format!("Invalid server hostname: {}", config.server_host))?;
435
436    let client_conn = rustls::ClientConnection::new(
437        Arc::new(tls_config),
438        server_name,
439    )
440    .map_err(|e| format!("TLS client setup: {}", e))?;
441
442    let mut tls_stream = rustls::StreamOwned::new(client_conn, tcp_stream);
443
444    // Trigger TLS handshake
445    tls_stream.flush().map_err(|e| format!("TLS handshake: {}", e))?;
446
447    log::info!(
448        "TLS connection established to {}:{}",
449        config.server_host,
450        config.server_port
451    );
452
453    Ok(tls_stream)
454}
455
456/// Custom TLS certificate verifier for pwr.
457///
458/// Accepts self-signed certificates (the server generates its own)
459/// and optionally verifies the SHA-256 fingerprint against a pinned
460/// value for defense-in-depth beyond the PSK authentication layer.
461#[derive(Debug)]
462struct CertVerifier {
463    pinned_fingerprint: Option<String>,
464}
465
466impl CertVerifier {
467    fn new(pinned_fingerprint: Option<String>) -> Self {
468        Self { pinned_fingerprint }
469    }
470}
471
472impl ServerCertVerifier for CertVerifier {
473    fn verify_server_cert(
474        &self,
475        end_entity: &rustls::pki_types::CertificateDer<'_>,
476        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
477        _server_name: &ServerName<'_>,
478        _ocsp_response: &[u8],
479        _now: UnixTime,
480    ) -> Result<ServerCertVerified, rustls::Error> {
481        // If a fingerprint is pinned, verify it matches
482        if let Some(ref pinned) = self.pinned_fingerprint {
483            let actual = crypto::sha256_hex(end_entity.as_ref());
484            if actual != *pinned {
485                return Err(rustls::Error::General(format!(
486                    "Certificate fingerprint mismatch: expected {}",
487                    pinned
488                )));
489            }
490            log::info!("Server certificate fingerprint verified against pinned value");
491        } else {
492            log::warn!(
493                "No certificate fingerprint pinned. Server cert SHA-256: {}",
494                crypto::sha256_hex(end_entity.as_ref())
495            );
496        }
497        Ok(ServerCertVerified::assertion())
498    }
499
500    fn verify_tls12_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 verify_tls13_signature(
511        &self,
512        _message: &[u8],
513        _cert: &rustls::pki_types::CertificateDer<'_>,
514        _dss: &DigitallySignedStruct,
515    ) -> Result<HandshakeSignatureValid, rustls::Error> {
516        // Accept any signature: authentication is via PSK, not PKI
517        Ok(HandshakeSignatureValid::assertion())
518    }
519
520    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
521        vec![
522            rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
523            rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
524            rustls::SignatureScheme::RSA_PSS_SHA512,
525            rustls::SignatureScheme::RSA_PSS_SHA256,
526            rustls::SignatureScheme::RSA_PKCS1_SHA512,
527            rustls::SignatureScheme::RSA_PKCS1_SHA256,
528        ]
529    }
530}