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