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        // Drain the frame decoder: it may have buffered chunk bytes
214        // that were read from the transport alongside the RestoreAccept
215        // frame. Consume those first, then read remaining chunks from
216        // the stream.
217        let prefix = self.decoder.drain_bytes();
218
219        // Receive raw chunk data until EOF
220        let mut data = Vec::with_capacity(total_size as usize);
221        let mut buf = Vec::with_capacity(prefix.len() + 8192);
222        buf.extend_from_slice(&prefix);
223        let mut pos: usize = 0;
224
225        loop {
226            // Ensure we have at least 4 bytes for the chunk header
227            while buf.len() - pos < 4 {
228                let start = buf.len();
229                buf.resize(start + 8192, 0);
230                let n = self.stream
231                    .read(&mut buf[start..])
232                    .map_err(|e| format!("chunk read: {}", e))?;
233                if n == 0 {
234                    return Err("Connection closed during chunk transfer".into());
235                }
236                buf.truncate(start + n);
237            }
238
239            let chunk_len = u32::from_be_bytes([
240                buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3],
241            ]) as usize;
242            pos += 4;
243
244            if chunk_len == 0 {
245                break; // EOF
246            }
247
248            // Ensure we have the full chunk data
249            while buf.len() - pos < chunk_len {
250                let start = buf.len();
251                buf.resize(start + (chunk_len - (buf.len() - pos)).max(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 data".into());
257                }
258                buf.truncate(start + n);
259            }
260
261            data.extend_from_slice(&buf[pos..pos + chunk_len]);
262            pos += chunk_len;
263
264            if let Some(ref cb) = progress {
265                cb(data.len() as u64, total_size);
266            }
267
268            // Compact the buffer periodically
269            if pos > 65536 {
270                buf.drain(..pos);
271                pos = 0;
272            }
273        }
274
275        log::info!("Restored {} bytes", data.len());
276        Ok(data)
277    }
278
279    /// Query the server for project status.
280    pub fn get_status(
281        &mut self,
282        project_uuid: Option<&uuid::Uuid>,
283    ) -> ClientResult<Vec<ProjectInfo>> {
284        let req = protocol::ClientMessage::StatusRequest(
285            protocol::StatusRequest { project_uuid: project_uuid.copied() },
286        );
287        send_client_msg(&mut self.stream, &req)?;
288
289        match recv_server_msg(&mut self.stream, &mut self.decoder)? {
290            ServerMessage::StatusResponse(response) => Ok(response.projects),
291            ServerMessage::Error(e) => Err(format!("Server error: {}", e.message)),
292            other => Err(format!("Unexpected response: {:?}", other.message_type())),
293        }
294    }
295}
296
297// ---------------------------------------------------------------------------
298// Internal helpers
299// ---------------------------------------------------------------------------
300
301/// Perform the PSK handshake on a raw (non-TLS) stream.
302fn perform_handshake(
303    stream: &mut impl ReadWrite,
304    decoder: &mut FrameDecoder,
305    psk: &[u8; 32],
306    client_id: &str,
307) -> ClientResult<()> {
308    let mut nonce = [0u8; 32];
309    ring::rand::SystemRandom::new()
310        .fill(&mut nonce)
311        .map_err(|_| "CSPRNG failure".to_string())?;
312
313    let proof = crypto::compute_client_proof(psk, &nonce);
314
315    // Send Handshake
316    let hs = ClientMessage::Handshake(Handshake {
317        version: frame::PROTOCOL_VERSION,
318        client_id: client_id.to_string(),
319        nonce,
320        proof,
321    });
322    send_client_msg(stream, &hs)?;
323
324    // Receive HandshakeAck
325    match recv_server_msg(stream, decoder)? {
326        ServerMessage::HandshakeAck(ack) => {
327            if !ack.success {
328                return Err(format!(
329                    "Authentication failed: {}",
330                    ack.reason.unwrap_or_default()
331                ));
332            }
333
334            // Verify server proof (mutual auth)
335            let expected = crypto::compute_server_proof(psk, &nonce, &ack.server_nonce);
336            if expected != ack.server_proof {
337                return Err("Server authentication failed: invalid proof".into());
338            }
339
340            log::info!("Authenticated to server v{}", ack.server_version);
341            Ok(())
342        }
343        ServerMessage::Error(e) => Err(format!("Server rejected handshake: {}", e.message)),
344        other => Err(format!("Unexpected handshake response: {:?}", other.message_type())),
345    }
346}
347
348/// Send a ClientMessage as a framed message on the stream.
349fn send_client_msg(
350    stream: &mut impl Write,
351    msg: &ClientMessage,
352) -> ClientResult<()> {
353    let frame = frame::encode_frame(msg, msg.message_type())
354        .map_err(|e| format!("encode: {}", e))?;
355    stream.write_all(&frame).map_err(|e| format!("write: {}", e))?;
356    stream.flush().map_err(|e| format!("flush: {}", e))?;
357    Ok(())
358}
359
360/// Receive one ServerMessage from the stream.
361///
362/// Reads from the stream until a complete frame is decoded, then
363/// deserializes the payload using the typed server message decoder.
364fn recv_server_msg(
365    stream: &mut impl Read,
366    decoder: &mut FrameDecoder,
367) -> ClientResult<ServerMessage> {
368    let mut buf = [0u8; 8192];
369
370    loop {
371        if let Some((header, payload)) = decoder.try_decode()
372            .map_err(|e| format!("decode: {}", e))?
373        {
374            return protocol::decode_server_message(header.msg_type, &payload)
375                .map_err(|e| format!("deserialize: {}", e));
376        }
377
378        let n = stream.read(&mut buf).map_err(|e| format!("read: {}", e))?;
379        if n == 0 {
380            return Err("Connection closed by server".into());
381        }
382        decoder.push_bytes(&buf[..n]);
383    }
384}
385
386// ---------------------------------------------------------------------------
387// TLS connection
388// ---------------------------------------------------------------------------
389
390/// Establish a TLS-encrypted connection to the server.
391///
392/// Uses a custom certificate verifier that accepts self-signed
393/// certificates while optionally checking the SHA-256 fingerprint
394/// against a pinned value for MITM protection. The PSK handshake
395/// provides authentication even without CA-signed certificates.
396fn connect_tls(
397    tcp_stream: TcpStream,
398    config: &PwrConfig,
399) -> ClientResult<rustls::StreamOwned<rustls::ClientConnection, TcpStream>> {
400    let pinned = config.server_fingerprint.clone();
401    let verifier = Arc::new(CertVerifier::new(pinned));
402
403    let tls_config = rustls::ClientConfig::builder()
404        .dangerous()
405        .with_custom_certificate_verifier(verifier)
406        .with_no_client_auth();
407
408    let server_name: ServerName = config
409        .server_host
410        .clone()
411        .try_into()
412        .map_err(|_| format!("Invalid server hostname: {}", config.server_host))?;
413
414    let client_conn = rustls::ClientConnection::new(
415        Arc::new(tls_config),
416        server_name,
417    )
418    .map_err(|e| format!("TLS client setup: {}", e))?;
419
420    let mut tls_stream = rustls::StreamOwned::new(client_conn, tcp_stream);
421
422    // Trigger TLS handshake
423    tls_stream.flush().map_err(|e| format!("TLS handshake: {}", e))?;
424
425    log::info!(
426        "TLS connection established to {}:{}",
427        config.server_host,
428        config.server_port
429    );
430
431    Ok(tls_stream)
432}
433
434/// Custom TLS certificate verifier for pwr.
435///
436/// Accepts self-signed certificates (the server generates its own)
437/// and optionally verifies the SHA-256 fingerprint against a pinned
438/// value for defense-in-depth beyond the PSK authentication layer.
439#[derive(Debug)]
440struct CertVerifier {
441    pinned_fingerprint: Option<String>,
442}
443
444impl CertVerifier {
445    fn new(pinned_fingerprint: Option<String>) -> Self {
446        Self { pinned_fingerprint }
447    }
448}
449
450impl ServerCertVerifier for CertVerifier {
451    fn verify_server_cert(
452        &self,
453        end_entity: &rustls::pki_types::CertificateDer<'_>,
454        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
455        _server_name: &ServerName<'_>,
456        _ocsp_response: &[u8],
457        _now: UnixTime,
458    ) -> Result<ServerCertVerified, rustls::Error> {
459        // If a fingerprint is pinned, verify it matches
460        if let Some(ref pinned) = self.pinned_fingerprint {
461            let actual = crypto::sha256_hex(end_entity.as_ref());
462            if actual != *pinned {
463                return Err(rustls::Error::General(format!(
464                    "Certificate fingerprint mismatch: expected {}",
465                    pinned
466                )));
467            }
468            log::info!("Server certificate fingerprint verified against pinned value");
469        } else {
470            log::warn!(
471                "No certificate fingerprint pinned. Server cert SHA-256: {}",
472                crypto::sha256_hex(end_entity.as_ref())
473            );
474        }
475        Ok(ServerCertVerified::assertion())
476    }
477
478    fn verify_tls12_signature(
479        &self,
480        _message: &[u8],
481        _cert: &rustls::pki_types::CertificateDer<'_>,
482        _dss: &DigitallySignedStruct,
483    ) -> Result<HandshakeSignatureValid, rustls::Error> {
484        // Accept any signature: authentication is via PSK, not PKI
485        Ok(HandshakeSignatureValid::assertion())
486    }
487
488    fn verify_tls13_signature(
489        &self,
490        _message: &[u8],
491        _cert: &rustls::pki_types::CertificateDer<'_>,
492        _dss: &DigitallySignedStruct,
493    ) -> Result<HandshakeSignatureValid, rustls::Error> {
494        // Accept any signature: authentication is via PSK, not PKI
495        Ok(HandshakeSignatureValid::assertion())
496    }
497
498    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
499        vec![
500            rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
501            rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
502            rustls::SignatureScheme::RSA_PSS_SHA512,
503            rustls::SignatureScheme::RSA_PSS_SHA256,
504            rustls::SignatureScheme::RSA_PKCS1_SHA512,
505            rustls::SignatureScheme::RSA_PKCS1_SHA256,
506        ]
507    }
508}