From 54b2a9cb16201c35f6f1155e0d6239129ac585f8 Mon Sep 17 00:00:00 2001 From: Nathaniel Wesley Filardo Date: Sat, 5 Sep 2026 11:45:24 -0400 Subject: [PATCH] Pass around listening addresses - SocketBindError now wraps a SocketAddr - ButtplugWebsocketServerTransport now wraps a SocketAddr - ButtplugWebsocketServerTransportBuilder now takes a SocketAddr - intiface_engine sprouts a new CLI option and either passes in or computes the right string for EngineOptions and later parses that to hand to the ButtplugWebsocketServerTransportBuilder. --- .../src/connector/transport/mod.rs | 6 +-- .../src/websocket_server.rs | 42 +++++------------ crates/intiface_engine/src/bin/main.rs | 47 +++++++++++++++++-- crates/intiface_engine/src/buttplug_server.rs | 13 +++-- crates/intiface_engine/src/engine.rs | 5 +- crates/intiface_engine/src/options.rs | 21 +++------ 6 files changed, 76 insertions(+), 58 deletions(-) diff --git a/crates/buttplug_core/src/connector/transport/mod.rs b/crates/buttplug_core/src/connector/transport/mod.rs index 1033838c3..65c5cf9b8 100644 --- a/crates/buttplug_core/src/connector/transport/mod.rs +++ b/crates/buttplug_core/src/connector/transport/mod.rs @@ -15,6 +15,7 @@ use crate::connector::{ use displaydoc::Display; use futures::future::BoxFuture; use thiserror::Error; +use std::net::SocketAddr; use tokio::sync::mpsc::{Receiver, Sender}; /// Messages we can receive from a connector. @@ -44,10 +45,9 @@ pub trait ButtplugConnectorTransport: Send + Sync { pub enum ButtplugConnectorTransportSpecificError { #[error("Network error: {0}")] GenericNetworkError(String), - #[error("Socket bind error on {address}:{port}: {kind:?}: {message}")] + #[error("Socket bind error on {address}: {kind:?}: {message}")] SocketBindError { - address: String, - port: u16, + address: SocketAddr, kind: std::io::ErrorKind, message: String, }, diff --git a/crates/buttplug_transport_websocket_tungstenite/src/websocket_server.rs b/crates/buttplug_transport_websocket_tungstenite/src/websocket_server.rs index aca38df61..ae9571cad 100644 --- a/crates/buttplug_transport_websocket_tungstenite/src/websocket_server.rs +++ b/crates/buttplug_transport_websocket_tungstenite/src/websocket_server.rs @@ -18,7 +18,7 @@ use buttplug_core::{ message::serializer::ButtplugSerializedMessage, }; use futures::{FutureExt, SinkExt, StreamExt, future::BoxFuture}; -use std::{fmt, sync::Arc, time::Duration}; +use std::{fmt, net::SocketAddr, sync::Arc, time::Duration}; use tokio::{ net::{TcpListener, TcpStream}, select, @@ -51,10 +51,8 @@ impl fmt::Debug for ListenerBoundCallback { #[derive(Clone, Debug)] pub struct ButtplugWebsocketServerTransportBuilder { - /// If true, listens all on available interfaces. Otherwise, only listens on 127.0.0.1. - listen_on_all_interfaces: bool, - /// Insecure port for listening for websocket connections. - port: u16, + /// TCP/IP address for listening for insecure websocket connections; defaults to localhost:12345 + listen_address: SocketAddr, /// Optional callback fired after the listener is bound and the actual local port is known. listener_bound_callback: Option, } @@ -62,21 +60,18 @@ pub struct ButtplugWebsocketServerTransportBuilder { impl Default for ButtplugWebsocketServerTransportBuilder { fn default() -> Self { Self { - listen_on_all_interfaces: false, - port: 12345, + listen_address: SocketAddr::new( + std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + 12345, + ), listener_bound_callback: None, } } } impl ButtplugWebsocketServerTransportBuilder { - pub fn listen_on_all_interfaces(&mut self, listen_on_all_interfaces: bool) -> &mut Self { - self.listen_on_all_interfaces = listen_on_all_interfaces; - self - } - - pub fn port(&mut self, port: u16) -> &mut Self { - self.port = port; + pub fn listen_address(&mut self, listen_address: SocketAddr) -> &mut Self { + self.listen_address = listen_address; self } @@ -87,8 +82,7 @@ impl ButtplugWebsocketServerTransportBuilder { pub fn finish(&self) -> ButtplugWebsocketServerTransport { ButtplugWebsocketServerTransport { - port: self.port, - listen_on_all_interfaces: self.listen_on_all_interfaces, + listen_address: self.listen_address.clone(), listener_bound_callback: self.listener_bound_callback.clone(), disconnect_notifier: Arc::new(Notify::new()), } @@ -222,8 +216,7 @@ async fn run_connection_loop( /// Websocket connector for ButtplugClients, using [tokio_tungstenite] pub struct ButtplugWebsocketServerTransport { - port: u16, - listen_on_all_interfaces: bool, + listen_address: SocketAddr, listener_bound_callback: Option, disconnect_notifier: Arc, } @@ -237,15 +230,7 @@ impl ButtplugConnectorTransport for ButtplugWebsocketServerTransport { let disconnect_notifier = self.disconnect_notifier.clone(); let listener_bound_callback = self.listener_bound_callback.clone(); - let base_addr = if self.listen_on_all_interfaces { - "0.0.0.0" - } else { - "127.0.0.1" - }; - - let address = base_addr.to_owned(); - let port = self.port; - let addr = format!("{}:{}", address, port); + let addr = self.listen_address.clone(); debug!("Websocket: Trying to listen on {}", addr); let response_sender_clone = incoming_sender; let disconnect_notifier_clone = disconnect_notifier; @@ -256,8 +241,7 @@ impl ButtplugConnectorTransport for ButtplugWebsocketServerTransport { let listener = try_socket.map_err(|e| { ButtplugConnectorError::TransportSpecificError( ButtplugConnectorTransportSpecificError::SocketBindError { - address, - port, + address: addr, kind: e.kind(), message: e.to_string(), }, diff --git a/crates/intiface_engine/src/bin/main.rs b/crates/intiface_engine/src/bin/main.rs index c7de7e420..7dd773297 100644 --- a/crates/intiface_engine/src/bin/main.rs +++ b/crates/intiface_engine/src/bin/main.rs @@ -45,13 +45,18 @@ pub struct IntifaceCLIArguments { /// listen on 127.0.0.1. #[argh(switch)] #[getset(get_copy = "pub")] - websocket_use_all_interfaces: bool, + websocket_use_all_interfaces: Option, /// insecure port for websocket servers. #[argh(option)] #[getset(get_copy = "pub")] websocket_port: Option, + /// address on which the websocket server listens for insecure connections + #[argh(option)] + #[getset(get = "pub")] + websocket_listen_address: Option, + /// insecure address for connecting to websocket servers. #[argh(option)] #[getset(get = "pub")] @@ -239,7 +244,6 @@ impl TryFrom for EngineOptions { } builder - .websocket_use_all_interfaces(args.websocket_use_all_interfaces()) .use_bluetooth_le(args.use_bluetooth_le()) .use_serial_port(args.use_serial()) .use_hid(args.use_hid()) @@ -259,9 +263,44 @@ impl TryFrom for EngineOptions { .crash_task_thread(args.crash_task_thread()); } - if let Some(value) = args.websocket_port() { - builder.websocket_port(value); + /* + * websocket_listen_address supplants websocket_use_all_interfaces and + * websocket_port, but we want to keep the latter two for backwards + * compatibility. Ensure that, if the former is given, neither of the + * latter two have been. + */ + let maybe_listen_address = match args.websocket_listen_address() { + None => { + match args.websocket_port() { + None => Ok(None), // no listen address & no port: don't listen + Some(port) => { + let base_addr = + if args.websocket_use_all_interfaces().unwrap_or(false) { + "0.0.0.0" + } else { + "127.0.0.1" + }; + Ok(Some(format!("{base_addr}:{port}"))) + } + } + } + Some(address) => match (args.websocket_use_all_interfaces(), args.websocket_port()) { + (None, None) => Ok(Some(address.to_owned())), + (Some(_), None) => Err(IntifaceError::new( + "websocket-use-all-interfaces conflicts with websocket-listen-address", + )), + (None, Some(_)) => Err(IntifaceError::new( + "websocket-use-all-interfaces conflicts with websocket-port", + )), + (Some(_), Some(_)) => Err(IntifaceError::new( + "websocket-use-all-interfaces conflicts with both websocket-port and websocket-use-all-interfaces", + )), + }, + }; + if let Some(listen_address) = maybe_listen_address? { + builder.websocket_listen_address(&listen_address); } + if let Some(value) = args.websocket_client_address() { builder.websocket_client_address(value); } diff --git a/crates/intiface_engine/src/buttplug_server.rs b/crates/intiface_engine/src/buttplug_server.rs index 73ba239a5..eb56d5dac 100644 --- a/crates/intiface_engine/src/buttplug_server.rs +++ b/crates/intiface_engine/src/buttplug_server.rs @@ -166,11 +166,16 @@ pub async fn run_server( options: &EngineOptions, on_listener_bound: Option>, ) -> Result<(), ButtplugServerConnectorError> { - if let Some(port) = options.websocket_port() { + if let Some(listen_address) = options.websocket_listen_address() { let mut transport_builder = ButtplugWebsocketServerTransportBuilder::default(); - transport_builder - .port(port) - .listen_on_all_interfaces(options.websocket_use_all_interfaces()); + + let parsed_listen_address = listen_address.parse().map_err(|pe| { + ButtplugServerConnectorError::ConnectorError( + buttplug_core::connector::ButtplugConnectorError::ConnectorGenericError( + format!("Could not parse provided websocket-listen-address: {pe}") + ))})?; + + transport_builder.listen_address(parsed_listen_address); if let Some(on_listener_bound) = on_listener_bound { transport_builder.on_listener_bound(move |bound_port| { on_listener_bound(bound_port); diff --git a/crates/intiface_engine/src/engine.rs b/crates/intiface_engine/src/engine.rs index a4a81e44c..50829d095 100644 --- a/crates/intiface_engine/src/engine.rs +++ b/crates/intiface_engine/src/engine.rs @@ -88,12 +88,11 @@ fn websocket_port_in_use_error(err: &ButtplugServerConnectorError) -> Option<(St ButtplugConnectorError::TransportSpecificError( ButtplugConnectorTransportSpecificError::SocketBindError { address, - port, kind, message: _, }, ), - ) if *kind == ErrorKind::AddrInUse => Some((address.clone(), *port)), + ) if *kind == ErrorKind::AddrInUse => Some((address.ip().to_string(), address.port())), _ => None, } } @@ -154,7 +153,7 @@ impl IntifaceEngine { } let mdns_service_metadata = - if options.broadcast_server_mdns() && options.websocket_port().is_some() { + if options.broadcast_server_mdns() && options.websocket_listen_address().is_some() { Some(Arc::new(IntifaceMdnsServiceMetadata::new( options.mdns_suffix().as_deref(), ))) diff --git a/crates/intiface_engine/src/options.rs b/crates/intiface_engine/src/options.rs index f76e1adaa..f55250702 100644 --- a/crates/intiface_engine/src/options.rs +++ b/crates/intiface_engine/src/options.rs @@ -17,10 +17,8 @@ pub struct EngineOptions { user_device_config_path: Option, #[getset(get = "pub")] server_name: String, - #[getset(get_copy = "pub")] - websocket_use_all_interfaces: bool, - #[getset(get_copy = "pub")] - websocket_port: Option, + #[getset(get = "pub")] + websocket_listen_address: Option, #[getset(get = "pub")] websocket_client_address: Option, #[getset(get_copy = "pub")] @@ -75,8 +73,7 @@ pub struct EngineOptionsExternal { pub user_device_config_json: Option, pub user_device_config_path: Option, pub server_name: String, - pub websocket_use_all_interfaces: bool, - pub websocket_port: Option, + pub websocket_listen_address: Option, pub websocket_client_address: Option, pub frontend_websocket_port: Option, pub frontend_in_process_channel: bool, @@ -109,8 +106,7 @@ impl From for EngineOptions { user_device_config_json: other.user_device_config_json, user_device_config_path: other.user_device_config_path, server_name: other.server_name, - websocket_use_all_interfaces: other.websocket_use_all_interfaces, - websocket_port: other.websocket_port, + websocket_listen_address: other.websocket_listen_address, websocket_client_address: other.websocket_client_address, frontend_websocket_port: other.frontend_websocket_port, frontend_in_process_channel: other.frontend_in_process_channel, @@ -182,8 +178,8 @@ impl EngineOptionsBuilder { self } - pub fn websocket_use_all_interfaces(&mut self, value: bool) -> &mut Self { - self.options.websocket_use_all_interfaces = value; + pub fn websocket_listen_address(&mut self, address: &str) -> &mut Self { + self.options.websocket_listen_address = Some(address.to_owned()); self } @@ -232,11 +228,6 @@ impl EngineOptionsBuilder { self } - pub fn websocket_port(&mut self, port: u16) -> &mut Self { - self.options.websocket_port = Some(port); - self - } - pub fn websocket_client_address(&mut self, address: &str) -> &mut Self { self.options.websocket_client_address = Some(address.to_owned()); self