diff options
Diffstat (limited to '')
| -rw-r--r-- | src/client/transport.rs | 171 |
1 files changed, 109 insertions, 62 deletions
diff --git a/src/client/transport.rs b/src/client/transport.rs index 97c9227..60e7ab6 100644 --- a/src/client/transport.rs +++ b/src/client/transport.rs | |||
| @@ -7,20 +7,27 @@ | |||
| 7 | use std::fmt; | 7 | use std::fmt; |
| 8 | use std::io::{Read, Write}; | 8 | use std::io::{Read, Write}; |
| 9 | use std::net::TcpStream; | 9 | use std::net::TcpStream; |
| 10 | use std::sync::mpsc; | ||
| 11 | use std::sync::Arc; | 10 | use std::sync::Arc; |
| 11 | use std::sync::mpsc; | ||
| 12 | use std::thread; | 12 | use std::thread; |
| 13 | use std::time::{Duration, Instant}; | 13 | use std::time::{Duration, Instant}; |
| 14 | 14 | ||
| 15 | use parking_lot::Mutex; | 15 | use parking_lot::Mutex; |
| 16 | use rustls::client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; | ||
| 17 | use rustls::DigitallySignedStruct; | ||
| 18 | use rusqlite::Connection; | 16 | use rusqlite::Connection; |
| 17 | use rustls::DigitallySignedStruct; | ||
| 18 | use rustls::client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; | ||
| 19 | 19 | ||
| 20 | use crate::client::database; | 20 | use crate::client::database; |
| 21 | use crate::protocol::LineBuffer; | 21 | use crate::protocol::{ |
| 22 | use crate::tofu::{decide, spki_fingerprint, TrustDecision}; | 22 | FrameBuffer, Packet, parse_packet, request_line, write_frame as write_protocol_frame, |
| 23 | use crate::version::{parse_hello, HelloReply, VERSION}; | 23 | }; |
| 24 | use crate::tofu::{TrustDecision, decide, spki_fingerprint}; | ||
| 25 | use crate::version::{HelloReply, VERSION, parse_hello}; | ||
| 26 | |||
| 27 | /// Reserved request ids for the transport handshake. Application requests | ||
| 28 | /// start at 1, so the two ranges cannot collide on one connection. | ||
| 29 | const HELLO_REQUEST_ID: u64 = u64::MAX; | ||
| 30 | const AUTH_REQUEST_ID: u64 = u64::MAX - 1; | ||
| 24 | 31 | ||
| 25 | /// How long to wait for TCP + TLS before giving up. | 32 | /// How long to wait for TCP + TLS before giving up. |
| 26 | const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); | 33 | const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); |
| @@ -43,6 +50,8 @@ pub struct Connected { | |||
| 43 | pub server_version: String, | 50 | pub server_version: String, |
| 44 | pub latest: String, | 51 | pub latest: String, |
| 45 | pub url: String, | 52 | pub url: String, |
| 53 | /// Server Unix time reported by `HELLO`, milliseconds. | ||
| 54 | pub server_time: u64, | ||
| 46 | /// Round trip of the HELLO exchange. | 55 | /// Round trip of the HELLO exchange. |
| 47 | pub elapsed: Duration, | 56 | pub elapsed: Duration, |
| 48 | } | 57 | } |
| @@ -51,10 +60,7 @@ pub struct Connected { | |||
| 51 | pub enum TransportError { | 60 | pub enum TransportError { |
| 52 | Timeout, | 61 | Timeout, |
| 53 | TrustDeclined, | 62 | TrustDeclined, |
| 54 | KeyChanged { | 63 | KeyChanged { expected: String, observed: String }, |
| 55 | expected: String, | ||
| 56 | observed: String, | ||
| 57 | }, | ||
| 58 | VersionMismatch(String), | 64 | VersionMismatch(String), |
| 59 | Closed, | 65 | Closed, |
| 60 | Unexpected(String), | 66 | Unexpected(String), |
| @@ -121,7 +127,7 @@ pub fn connect( | |||
| 121 | return Err(TransportError::KeyChanged { | 127 | return Err(TransportError::KeyChanged { |
| 122 | expected, | 128 | expected, |
| 123 | observed: actual, | 129 | observed: actual, |
| 124 | }) | 130 | }); |
| 125 | } | 131 | } |
| 126 | } | 132 | } |
| 127 | } | 133 | } |
| @@ -131,12 +137,7 @@ pub fn connect( | |||
| 131 | .map_err(io_failure)?; | 137 | .map_err(io_failure)?; |
| 132 | 138 | ||
| 133 | let start = Instant::now(); | 139 | let start = Instant::now(); |
| 134 | tls.write_line(&format!("HELLO {VERSION}")) | 140 | let response = exchange(&mut tls, HELLO_REQUEST_ID, &format!("HELLO {VERSION}"))?; |
| 135 | .map_err(io_failure)?; | ||
| 136 | let mut response = String::new(); | ||
| 137 | if tls.read_line(&mut response).map_err(io_failure)? == 0 { | ||
| 138 | return Err(TransportError::Closed); | ||
| 139 | } | ||
| 140 | let elapsed = start.elapsed(); | 141 | let elapsed = start.elapsed(); |
| 141 | 142 | ||
| 142 | let response = response.trim(); | 143 | let response = response.trim(); |
| @@ -151,10 +152,11 @@ pub fn connect( | |||
| 151 | let mut user = None; | 152 | let mut user = None; |
| 152 | let mut auth_error = None; | 153 | let mut auth_error = None; |
| 153 | if let Some((username, password)) = database::get_credentials(conn, &observed) { | 154 | if let Some((username, password)) = database::get_credentials(conn, &observed) { |
| 154 | tls.write_line(&format!("AUTH {username} {password}")) | 155 | let auth = exchange( |
| 155 | .map_err(io_failure)?; | 156 | &mut tls, |
| 156 | let mut auth = String::new(); | 157 | AUTH_REQUEST_ID, |
| 157 | tls.read_line(&mut auth).map_err(io_failure)?; | 158 | &format!("AUTH {username} {password}"), |
| 159 | )?; | ||
| 158 | if auth.trim().starts_with("OK") { | 160 | if auth.trim().starts_with("OK") { |
| 159 | user = Some(username); | 161 | user = Some(username); |
| 160 | } else { | 162 | } else { |
| @@ -169,19 +171,31 @@ pub fn connect( | |||
| 169 | server_version: hello.version, | 171 | server_version: hello.version, |
| 170 | latest: hello.latest, | 172 | latest: hello.latest, |
| 171 | url: hello.url, | 173 | url: hello.url, |
| 174 | server_time: hello.time, | ||
| 172 | elapsed, | 175 | elapsed, |
| 173 | }) | 176 | }) |
| 174 | } | 177 | } |
| 175 | 178 | ||
| 179 | /// Send one request and wait for the reply with the same id, ignoring events | ||
| 180 | /// that arrive in between. Used only for the handshake. | ||
| 181 | fn exchange(stream: &mut TlsStream, id: u64, payload: &str) -> Result<String, TransportError> { | ||
| 182 | stream | ||
| 183 | .write_request(id, None, payload) | ||
| 184 | .map_err(io_failure)?; | ||
| 185 | loop { | ||
| 186 | match stream.read_packet().map_err(io_failure)? { | ||
| 187 | None => return Err(TransportError::Closed), | ||
| 188 | Some(Packet::Reply { id: reply_id, body }) if reply_id == id => return Ok(body), | ||
| 189 | Some(Packet::Reply { .. }) | Some(Packet::Event { .. }) => continue, | ||
| 190 | } | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 176 | /// TCP + TLS on a thread so a blocked handshake can time out. | 194 | /// TCP + TLS on a thread so a blocked handshake can time out. |
| 177 | fn tls_connect(addr: &str) -> Result<(TlsStream, Option<String>), TransportError> { | 195 | fn tls_connect(addr: &str) -> Result<(TlsStream, Option<String>), TransportError> { |
| 178 | let (sender, receiver) = mpsc::channel(); | 196 | let (sender, receiver) = mpsc::channel(); |
| 179 | let addr = addr.to_string(); | 197 | let addr = addr.to_string(); |
| 180 | let server_name = addr | 198 | let server_name = addr.split(':').next().unwrap_or("127.0.0.1").to_string(); |
| 181 | .split(':') | ||
| 182 | .next() | ||
| 183 | .unwrap_or("127.0.0.1") | ||
| 184 | .to_string(); | ||
| 185 | 199 | ||
| 186 | thread::spawn(move || { | 200 | thread::spawn(move || { |
| 187 | let outcome = (|| -> Result<(TlsStream, Option<String>), TransportError> { | 201 | let outcome = (|| -> Result<(TlsStream, Option<String>), TransportError> { |
| @@ -241,7 +255,7 @@ impl TlsConnector { | |||
| 241 | session: Arc::new(Mutex::new(Session { | 255 | session: Arc::new(Mutex::new(Session { |
| 242 | sock, | 256 | sock, |
| 243 | tls: connection, | 257 | tls: connection, |
| 244 | lines: LineBuffer::new(), | 258 | frames: FrameBuffer::new(), |
| 245 | read_timeout: Some(READ_TIMEOUT), | 259 | read_timeout: Some(READ_TIMEOUT), |
| 246 | })), | 260 | })), |
| 247 | }) | 261 | }) |
| @@ -268,8 +282,9 @@ impl ServerCertVerifier for RecordingVerifier { | |||
| 268 | _ocsp_response: &[u8], | 282 | _ocsp_response: &[u8], |
| 269 | _now: std::time::SystemTime, | 283 | _now: std::time::SystemTime, |
| 270 | ) -> Result<ServerCertVerified, rustls::Error> { | 284 | ) -> Result<ServerCertVerified, rustls::Error> { |
| 271 | let fingerprint = spki_fingerprint(&end_entity.0) | 285 | let fingerprint = spki_fingerprint(&end_entity.0).map_err(|error| { |
| 272 | .map_err(|error| rustls::Error::General(format!("certificate parse failed: {error}")))?; | 286 | rustls::Error::General(format!("certificate parse failed: {error}")) |
| 287 | })?; | ||
| 273 | *self.observed.lock() = Some(fingerprint); | 288 | *self.observed.lock() = Some(fingerprint); |
| 274 | Ok(ServerCertVerified::assertion()) | 289 | Ok(ServerCertVerified::assertion()) |
| 275 | } | 290 | } |
| @@ -313,7 +328,7 @@ impl ServerCertVerifier for RecordingVerifier { | |||
| 313 | struct Session { | 328 | struct Session { |
| 314 | sock: TcpStream, | 329 | sock: TcpStream, |
| 315 | tls: rustls::ClientConnection, | 330 | tls: rustls::ClientConnection, |
| 316 | lines: LineBuffer, | 331 | frames: FrameBuffer, |
| 317 | read_timeout: Option<Duration>, | 332 | read_timeout: Option<Duration>, |
| 318 | } | 333 | } |
| 319 | 334 | ||
| @@ -329,37 +344,58 @@ pub struct TlsStream { | |||
| 329 | } | 344 | } |
| 330 | 345 | ||
| 331 | impl TlsStream { | 346 | impl TlsStream { |
| 332 | pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> { | 347 | /// Write one enveloped request. Application code allocates `id`. |
| 348 | pub fn write_request( | ||
| 349 | &mut self, | ||
| 350 | id: u64, | ||
| 351 | idem: Option<&str>, | ||
| 352 | payload: &str, | ||
| 353 | ) -> std::io::Result<()> { | ||
| 354 | self.write_frame(&request_line(id, idem, payload)) | ||
| 355 | } | ||
| 356 | |||
| 357 | /// Write one raw frame payload. | ||
| 358 | pub fn write_frame(&mut self, payload: &str) -> std::io::Result<()> { | ||
| 359 | let mut s = self.session.lock(); | ||
| 360 | write_protocol_frame(&mut s.tls.writer(), payload)?; | ||
| 361 | s.drive() | ||
| 362 | } | ||
| 363 | |||
| 364 | /// Block until one whole frame arrives. | ||
| 365 | pub fn read_frame(&mut self) -> std::io::Result<Option<String>> { | ||
| 333 | let mut s = self.session.lock(); | 366 | let mut s = self.session.lock(); |
| 334 | loop { | 367 | loop { |
| 335 | if let Some(line) = s.lines.next_line() { | 368 | match s.frames.next_frame() { |
| 336 | buf.push_str(&line); | 369 | Some(Ok(frame)) => return Ok(Some(frame)), |
| 337 | buf.push('\n'); | 370 | Some(Err(error)) => { |
| 338 | return Ok(line.len() + 1); | 371 | return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, error)); |
| 372 | } | ||
| 373 | None => {} | ||
| 339 | } | 374 | } |
| 340 | 375 | ||
| 341 | let mut chunk = [0u8; 4096]; | 376 | let mut chunk = [0u8; 4096]; |
| 342 | match s.tls.reader().read(&mut chunk) { | 377 | match s.tls.reader().read(&mut chunk) { |
| 343 | Ok(0) => { | 378 | Ok(0) => return Ok(None), |
| 344 | if let Some(rest) = s.lines.take_rest() { | 379 | Ok(n) => s.frames.push(&chunk[..n]), |
| 345 | let read = rest.len(); | ||
| 346 | buf.push_str(&rest); | ||
| 347 | return Ok(read); | ||
| 348 | } | ||
| 349 | return Ok(0); | ||
| 350 | } | ||
| 351 | Ok(n) => s.lines.push(&chunk[..n]), | ||
| 352 | Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => s.drive()?, | 380 | Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => s.drive()?, |
| 353 | Err(error) => return Err(error), | 381 | Err(error) => return Err(error), |
| 354 | } | 382 | } |
| 355 | } | 383 | } |
| 356 | } | 384 | } |
| 357 | 385 | ||
| 358 | pub fn write_line(&mut self, message: &str) -> std::io::Result<()> { | 386 | /// Block until one `RES` or `EVT` arrives. |
| 359 | let mut s = self.session.lock(); | 387 | pub fn read_packet(&mut self) -> std::io::Result<Option<Packet>> { |
| 360 | s.tls.writer().write_all(message.as_bytes())?; | 388 | loop { |
| 361 | s.tls.writer().write_all(b"\n")?; | 389 | let Some(frame) = self.read_frame()? else { |
| 362 | s.drive() | 390 | return Ok(None); |
| 391 | }; | ||
| 392 | match parse_packet(&frame) { | ||
| 393 | Ok(packet) => return Ok(Some(packet)), | ||
| 394 | Err(error) => { | ||
| 395 | return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, error)); | ||
| 396 | } | ||
| 397 | } | ||
| 398 | } | ||
| 363 | } | 399 | } |
| 364 | 400 | ||
| 365 | pub fn set_read_timeout(&self, duration: Option<Duration>) -> std::io::Result<()> { | 401 | pub fn set_read_timeout(&self, duration: Option<Duration>) -> std::io::Result<()> { |
| @@ -368,24 +404,36 @@ impl TlsStream { | |||
| 368 | s.sock.set_read_timeout(duration) | 404 | s.sock.set_read_timeout(duration) |
| 369 | } | 405 | } |
| 370 | 406 | ||
| 371 | /// Return one complete line if the socket already has one, without | 407 | /// Return one packet if the socket already has one, without blocking. |
| 372 | /// blocking. Used to pick up server pushes between requests. | 408 | /// Used to pick up server pushes between requests. |
| 373 | pub fn read_available_line(&mut self) -> std::io::Result<Option<String>> { | 409 | pub fn read_available_packet(&mut self) -> std::io::Result<Option<Packet>> { |
| 374 | let mut s = self.session.lock(); | 410 | let mut s = self.session.lock(); |
| 375 | if let Some(line) = s.lines.next_line() { | ||
| 376 | return Ok(Some(line)); | ||
| 377 | } | ||
| 378 | 411 | ||
| 379 | // `set_read_timeout(Some(0))` is rejected by std, so poll with the | 412 | // `set_read_timeout(Some(0))` is rejected by std, so poll with the |
| 380 | // shortest timeout the socket accepts instead of a nonblocking mode. | 413 | // shortest timeout the socket accepts instead of a nonblocking mode. |
| 381 | s.sock | 414 | s.sock |
| 382 | .set_read_timeout(Some(std::time::Duration::from_millis(1)))?; | 415 | .set_read_timeout(Some(std::time::Duration::from_millis(1)))?; |
| 383 | let drained = (|| -> std::io::Result<()> { | 416 | let drained = (|| -> std::io::Result<Option<Packet>> { |
| 384 | loop { | 417 | loop { |
| 418 | match s.frames.next_frame() { | ||
| 419 | Some(Ok(frame)) => { | ||
| 420 | return match parse_packet(&frame) { | ||
| 421 | Ok(packet) => Ok(Some(packet)), | ||
| 422 | Err(error) => { | ||
| 423 | Err(std::io::Error::new(std::io::ErrorKind::InvalidData, error)) | ||
| 424 | } | ||
| 425 | }; | ||
| 426 | } | ||
| 427 | Some(Err(error)) => { | ||
| 428 | return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, error)); | ||
| 429 | } | ||
| 430 | None => {} | ||
| 431 | } | ||
| 432 | |||
| 385 | let mut buf = [0u8; 4096]; | 433 | let mut buf = [0u8; 4096]; |
| 386 | match s.tls.reader().read(&mut buf) { | 434 | match s.tls.reader().read(&mut buf) { |
| 387 | Ok(0) => return Ok(()), | 435 | Ok(0) => return Ok(None), |
| 388 | Ok(n) => s.lines.push(&buf[..n]), | 436 | Ok(n) => s.frames.push(&buf[..n]), |
| 389 | // The reader only drains plaintext rustls has already | 437 | // The reader only drains plaintext rustls has already |
| 390 | // decrypted; `drive` moves bytes off the socket. | 438 | // decrypted; `drive` moves bytes off the socket. |
| 391 | Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { | 439 | Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { |
| @@ -397,7 +445,7 @@ impl TlsStream { | |||
| 397 | std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut | 445 | std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut |
| 398 | ) => | 446 | ) => |
| 399 | { | 447 | { |
| 400 | return Ok(()); | 448 | return Ok(None); |
| 401 | } | 449 | } |
| 402 | Err(error) => return Err(error), | 450 | Err(error) => return Err(error), |
| 403 | } | 451 | } |
| @@ -408,15 +456,14 @@ impl TlsStream { | |||
| 408 | std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut | 456 | std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut |
| 409 | ) => | 457 | ) => |
| 410 | { | 458 | { |
| 411 | return Ok(()); | 459 | return Ok(None); |
| 412 | } | 460 | } |
| 413 | Err(error) => return Err(error), | 461 | Err(error) => return Err(error), |
| 414 | } | 462 | } |
| 415 | } | 463 | } |
| 416 | })(); | 464 | })(); |
| 417 | s.sock.set_read_timeout(s.read_timeout)?; | 465 | s.sock.set_read_timeout(s.read_timeout)?; |
| 418 | drained?; | 466 | drained |
| 419 | Ok(s.lines.next_line()) | ||
| 420 | } | 467 | } |
| 421 | } | 468 | } |
| 422 | 469 | ||
