1use std::ffi::OsStr;
4#[cfg(all(test, unix))]
5use std::ffi::OsString;
6#[cfg(all(test, unix))]
7use std::fs;
8use std::io::{self, Read, Write};
9#[cfg(all(test, unix))]
10use std::os::unix::ffi::{OsStrExt, OsStringExt};
11use std::path::{Path, PathBuf};
12use std::time::Duration;
13
14use crate::ClientError;
15use rmux_ipc::{connect_blocking, BlockingLocalStream, LocalEndpoint};
16use rmux_proto::{
17 encode_frame, AttachSessionResponse, ControlMode, ControlModeResponse, FrameDecoder,
18 HandshakeRequest, Request, Response, RmuxError, RMUX_FRAME_MAGIC, RMUX_WIRE_VERSION,
19};
20
21const READ_BUFFER_SIZE: usize = 8192;
23const SOCKET_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
25const SOCKET_WRITE_TIMEOUT: Duration = Duration::from_secs(5);
27const SOCKET_RESPONSE_TIMEOUT: Duration = Duration::from_secs(15);
29const LEGACY_SHUTDOWN_WIRE_VERSION: u8 = 1;
31
32#[cfg(all(test, unix))]
33const FALLBACK_SOCKET_ROOT: &str = "/tmp";
34#[cfg(all(test, unix))]
35const SOCKET_DIR_PREFIX: &str = "rmux";
36
37pub fn default_socket_path() -> Result<PathBuf, ClientError> {
42 rmux_ipc::default_endpoint()
43 .map(LocalEndpoint::into_path)
44 .map_err(ClientError::Io)
45}
46
47pub fn socket_path_for_label(label: impl AsRef<OsStr>) -> Result<PathBuf, ClientError> {
49 rmux_ipc::endpoint_for_label(label)
50 .map(LocalEndpoint::into_path)
51 .map_err(ClientError::Io)
52}
53
54pub fn resolve_socket_path(
60 socket_name: Option<&OsStr>,
61 socket_path: Option<&Path>,
62) -> Result<PathBuf, ClientError> {
63 rmux_ipc::resolve_endpoint(socket_name, socket_path)
64 .map(LocalEndpoint::into_path)
65 .map_err(ClientError::Io)
66}
67
68#[allow(clippy::large_enum_variant)]
72#[derive(Debug)]
73pub enum ConnectResult {
74 Connected(Connection),
76 Absent,
78}
79
80pub fn connect_or_absent(socket_path: &Path) -> Result<ConnectResult, ClientError> {
88 connect_or_absent_with_timeout_using(
89 socket_path,
90 SOCKET_CONNECT_TIMEOUT,
91 connect_stream_with_timeout,
92 )
93}
94
95pub fn connect(socket_path: &Path) -> Result<Connection, ClientError> {
97 connect_with_timeout_using(
98 socket_path,
99 SOCKET_CONNECT_TIMEOUT,
100 connect_stream_with_timeout,
101 )
102}
103
104#[derive(Debug)]
106pub struct Connection {
107 stream: BlockingLocalStream,
108 decoder: FrameDecoder,
109 handshake_capabilities: Option<Vec<String>>,
110}
111
112#[allow(clippy::large_enum_variant)]
115#[derive(Debug)]
116pub enum AttachTransition {
117 Upgraded(AttachSessionUpgrade),
119 Rejected(Response),
121}
122
123#[allow(clippy::large_enum_variant)]
126#[derive(Debug)]
127pub enum ControlTransition {
128 Upgraded(ControlModeUpgrade),
130 Rejected(Response),
132}
133
134#[derive(Debug)]
136pub struct AttachSessionUpgrade {
137 response: AttachSessionResponse,
138 stream: BlockingLocalStream,
139 initial_bytes: Vec<u8>,
140}
141
142#[derive(Debug)]
144pub struct ControlModeUpgrade {
145 pub(crate) response: ControlModeResponse,
146 pub(crate) stream: BlockingLocalStream,
147}
148
149impl AttachSessionUpgrade {
150 #[must_use]
152 pub const fn response(&self) -> &AttachSessionResponse {
153 &self.response
154 }
155
156 #[must_use]
158 pub fn into_stream(self) -> BlockingLocalStream {
159 self.stream
160 }
161
162 #[must_use]
165 pub fn into_parts(self) -> (BlockingLocalStream, Vec<u8>) {
166 (self.stream, self.initial_bytes)
167 }
168}
169
170impl ControlModeUpgrade {
171 #[must_use]
173 pub const fn response(&self) -> &ControlModeResponse {
174 &self.response
175 }
176
177 #[must_use]
179 pub const fn mode(&self) -> ControlMode {
180 self.response.mode
181 }
182
183 #[must_use]
185 pub fn into_stream(self) -> BlockingLocalStream {
186 self.stream
187 }
188}
189
190impl Connection {
191 pub(crate) fn new(stream: BlockingLocalStream) -> Result<Self, ClientError> {
192 set_read_timeout(&stream, Some(SOCKET_RESPONSE_TIMEOUT)).map_err(ClientError::Io)?;
193 set_write_timeout(&stream, Some(SOCKET_WRITE_TIMEOUT)).map_err(ClientError::Io)?;
194
195 Ok(Self {
196 stream,
197 decoder: FrameDecoder::new(),
198 handshake_capabilities: None,
199 })
200 }
201
202 pub fn roundtrip(&mut self, request: &Request) -> Result<Response, ClientError> {
208 self.write_request(request)?;
209 self.read_response()
210 }
211
212 pub fn supports_capability(&mut self, capability: &str) -> Result<bool, ClientError> {
219 if let Some(capabilities) = &self.handshake_capabilities {
220 return Ok(capabilities.iter().any(|supported| supported == capability));
221 }
222
223 match self.roundtrip(&Request::Handshake(HandshakeRequest::current()))? {
224 Response::Handshake(response) => {
225 self.handshake_capabilities = Some(response.capabilities);
226 Ok(self
227 .handshake_capabilities
228 .as_ref()
229 .expect("handshake capabilities were just cached")
230 .iter()
231 .any(|supported| supported == capability))
232 }
233 Response::Error(error) => {
234 if matches!(&error.error, RmuxError::UnsupportedWireVersion { .. }) {
235 return Err(ClientError::Protocol(error.error));
236 }
237 self.handshake_capabilities = Some(Vec::new());
238 Ok(false)
239 }
240 _ => {
241 self.handshake_capabilities = Some(Vec::new());
242 Ok(false)
243 }
244 }
245 }
246
247 pub(crate) fn roundtrip_without_read_timeout(
252 &mut self,
253 request: &Request,
254 ) -> Result<Response, ClientError> {
255 let previous_timeout = read_timeout(&self.stream).map_err(ClientError::Io)?;
256 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
257 let result = self.roundtrip(request);
258 set_read_timeout(&self.stream, previous_timeout).map_err(ClientError::Io)?;
259 result
260 }
261
262 pub(crate) fn write_request(&mut self, request: &Request) -> Result<(), ClientError> {
263 let frame = encode_frame(request).map_err(ClientError::Protocol)?;
264 self.stream.write_all(&frame).map_err(ClientError::Io)
265 }
266
267 pub(crate) fn write_legacy_wire_v1_request(
268 &mut self,
269 request: &Request,
270 ) -> Result<(), ClientError> {
271 let frame = encode_legacy_wire_v1_frame(request)?;
272 self.stream.write_all(&frame).map_err(ClientError::Io)
273 }
274
275 pub(crate) fn read_response(&mut self) -> Result<Response, ClientError> {
276 let mut buffer = [0u8; READ_BUFFER_SIZE];
277
278 loop {
279 match self.decoder.next_frame::<Response>() {
280 Ok(Some(response)) => return Ok(response),
281 Ok(None) => {}
282 Err(error) => return Err(ClientError::Protocol(error)),
283 }
284
285 let bytes_read = match self.stream.read(&mut buffer) {
286 Ok(bytes_read) => bytes_read,
287 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
288 Err(error) => return Err(ClientError::Io(error)),
289 };
290
291 if bytes_read == 0 {
292 return Err(ClientError::UnexpectedEof);
293 }
294
295 self.decoder.push_bytes(&buffer[..bytes_read]);
296 }
297 }
298
299 pub(crate) fn stream_mut(&mut self) -> &mut BlockingLocalStream {
300 &mut self.stream
301 }
302
303 pub(crate) fn into_attach_upgrade(
304 self,
305 response: AttachSessionResponse,
306 ) -> Result<AttachSessionUpgrade, ClientError> {
307 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
308 set_write_timeout(&self.stream, None).map_err(ClientError::Io)?;
309 let initial_bytes = self.decoder.remaining_bytes().to_vec();
310
311 Ok(AttachSessionUpgrade {
312 response,
313 stream: self.stream,
314 initial_bytes,
315 })
316 }
317
318 pub(crate) fn into_control_upgrade(
319 self,
320 response: ControlModeResponse,
321 ) -> Result<ControlModeUpgrade, ClientError> {
322 set_read_timeout(&self.stream, None).map_err(ClientError::Io)?;
323 set_write_timeout(&self.stream, None).map_err(ClientError::Io)?;
324
325 Ok(ControlModeUpgrade {
326 response,
327 stream: self.stream,
328 })
329 }
330}
331
332fn encode_legacy_wire_v1_frame(request: &Request) -> Result<Vec<u8>, ClientError> {
333 let mut frame = encode_frame(request).map_err(ClientError::Protocol)?;
334 if frame.first().copied() != Some(RMUX_FRAME_MAGIC) {
335 return Err(ClientError::Protocol(RmuxError::Encode(
336 "current frame encoder produced an invalid RMUX envelope".to_owned(),
337 )));
338 }
339
340 if RMUX_WIRE_VERSION > 0x7f {
341 return Err(ClientError::Protocol(RmuxError::Encode(
342 "legacy shutdown recovery expects a single-byte current wire version".to_owned(),
343 )));
344 }
345
346 match frame.get_mut(1) {
347 Some(version) if *version == RMUX_WIRE_VERSION as u8 => {
348 *version = LEGACY_SHUTDOWN_WIRE_VERSION;
349 Ok(frame)
350 }
351 _ => Err(ClientError::Protocol(RmuxError::Encode(
352 "current frame encoder used an unexpected wire-version envelope".to_owned(),
353 ))),
354 }
355}
356
357pub(crate) fn read_response_frame_exact(
358 stream: &mut BlockingLocalStream,
359) -> Result<Response, ClientError> {
360 let mut decoder = FrameDecoder::new();
361 let mut byte = [0_u8; 1];
362
363 loop {
364 match decoder.next_frame::<Response>() {
365 Ok(Some(response)) => return Ok(response),
366 Ok(None) => {}
367 Err(error) => return Err(ClientError::Protocol(error)),
368 }
369
370 read_exact_or_eof(stream, &mut byte)?;
371 decoder.push_bytes(&byte);
372 }
373}
374
375fn read_exact_or_eof(
376 stream: &mut BlockingLocalStream,
377 buffer: &mut [u8],
378) -> Result<(), ClientError> {
379 match stream.read_exact(buffer) {
380 Ok(()) => Ok(()),
381 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => {
382 Err(ClientError::UnexpectedEof)
383 }
384 Err(error) => Err(ClientError::Io(error)),
385 }
386}
387
388#[cfg(all(test, unix))]
389fn socket_path_from_parts(
390 rmux_tmpdir: Option<&OsStr>,
391 user_id: u32,
392 label: &OsStr,
393) -> io::Result<PathBuf> {
394 let root = socket_root_from_parts(rmux_tmpdir)?;
395 let base = root.join(format!("{SOCKET_DIR_PREFIX}-{user_id}"));
396 let mut path = base.into_os_string().into_vec();
397 path.push(b'/');
398 path.extend_from_slice(label.as_bytes());
399
400 Ok(PathBuf::from(OsString::from_vec(path)))
401}
402
403#[cfg(all(test, unix))]
404fn socket_root_from_parts(rmux_tmpdir: Option<&OsStr>) -> io::Result<PathBuf> {
405 let rmux_tmpdir = rmux_tmpdir
406 .filter(|value| !value.is_empty())
407 .map(PathBuf::from);
408 let candidates = rmux_tmpdir
409 .into_iter()
410 .chain(std::iter::once(PathBuf::from(FALLBACK_SOCKET_ROOT)));
411
412 for candidate in candidates {
413 if let Ok(resolved) = fs::canonicalize(&candidate) {
414 return Ok(resolved);
415 }
416 }
417
418 Err(io::Error::new(
419 io::ErrorKind::NotFound,
420 "no suitable rmux socket directory",
421 ))
422}
423
424fn connect_or_absent_with_timeout_using<F>(
425 socket_path: &Path,
426 timeout: Duration,
427 connect_stream: F,
428) -> Result<ConnectResult, ClientError>
429where
430 F: FnOnce(&Path, Duration) -> io::Result<BlockingLocalStream>,
431{
432 match connect_stream(socket_path, timeout) {
433 Ok(stream) => Ok(ConnectResult::Connected(Connection::new(stream)?)),
434 Err(error) if is_absent_error(&error) => Ok(ConnectResult::Absent),
435 Err(error) => Err(ClientError::Io(error)),
436 }
437}
438
439fn connect_with_timeout_using<F>(
440 socket_path: &Path,
441 timeout: Duration,
442 connect_stream: F,
443) -> Result<Connection, ClientError>
444where
445 F: FnOnce(&Path, Duration) -> io::Result<BlockingLocalStream>,
446{
447 let stream = connect_stream(socket_path, timeout).map_err(ClientError::Io)?;
448 Connection::new(stream)
449}
450
451fn connect_stream_with_timeout(
452 socket_path: &Path,
453 timeout: Duration,
454) -> io::Result<BlockingLocalStream> {
455 connect_blocking(
456 &LocalEndpoint::from_path(socket_path.to_path_buf()),
457 timeout,
458 )
459}
460
461fn read_timeout(stream: &BlockingLocalStream) -> io::Result<Option<Duration>> {
462 stream.read_timeout()
463}
464
465fn set_read_timeout(stream: &BlockingLocalStream, timeout: Option<Duration>) -> io::Result<()> {
466 stream.set_read_timeout(timeout)
467}
468
469fn set_write_timeout(stream: &BlockingLocalStream, timeout: Option<Duration>) -> io::Result<()> {
470 stream.set_write_timeout(timeout)
471}
472
473fn is_absent_error(error: &io::Error) -> bool {
475 matches!(
476 error.kind(),
477 io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused
478 )
479}
480
481#[cfg(all(test, unix))]
482mod tests {
483 include!("connection/tests.rs");
484}