Skip to main content

iterm2_client/
transport.rs

1//! WebSocket transport for connecting to iTerm2.
2//!
3//! Supports Unix socket (default) and legacy TCP (`ws://localhost:1912`) connections.
4//! Use `connect` for the default Unix socket transport, or `connect_tcp` for
5//! legacy TCP connections.
6
7use crate::auth::Credentials;
8use crate::error::{Error, Result};
9use futures_util::stream::{SplitSink, SplitStream};
10use futures_util::StreamExt;
11use tokio::io::{AsyncRead, AsyncWrite};
12use tokio_tungstenite::tungstenite::client::IntoClientRequest;
13use tokio_tungstenite::tungstenite::http::header::HeaderValue;
14use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
15use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
16
17pub type WsStream<S> = WebSocketStream<S>;
18pub type WsSink<S> = SplitSink<WsStream<S>, tokio_tungstenite::tungstenite::Message>;
19pub type WsSource<S> = SplitStream<WsStream<S>>;
20
21const SUBPROTOCOL: &str = "api.iterm2.com";
22const TCP_URL: &str = "ws://localhost:1912";
23fn unix_socket_path() -> std::path::PathBuf {
24    let home = std::env::var("HOME").unwrap_or_default();
25    std::path::PathBuf::from(home)
26        .join("Library/Application Support/iTerm2/private/socket")
27}
28
29/// Connect to iTerm2 over TCP WebSocket at `ws://localhost:1912`.
30pub async fn connect_tcp(
31    credentials: &Credentials,
32    app_name: &str,
33) -> Result<(WsSink<MaybeTlsStream<tokio::net::TcpStream>>, WsSource<MaybeTlsStream<tokio::net::TcpStream>>)> {
34    let mut request = TCP_URL.into_client_request()?;
35    apply_headers(request.headers_mut(), credentials, app_name)?;
36    let config = ws_config();
37    let (ws_stream, _response) =
38        tokio_tungstenite::connect_async_with_config(request, Some(config), false).await?;
39    Ok(ws_stream.split())
40}
41
42/// Connect to iTerm2 over a Unix domain socket.
43pub async fn connect_unix(
44    credentials: &Credentials,
45    app_name: &str,
46) -> Result<(WsSink<tokio::net::UnixStream>, WsSource<tokio::net::UnixStream>)> {
47    let path = unix_socket_path();
48    let stream = tokio::net::UnixStream::connect(&path).await?;
49    connect_with_stream(stream, credentials, app_name).await
50}
51
52/// Upgrade an existing `AsyncRead + AsyncWrite` stream to a WebSocket connection.
53///
54/// Useful for testing with mock streams or custom transports.
55pub async fn connect_with_stream<S>(
56    stream: S,
57    credentials: &Credentials,
58    app_name: &str,
59) -> Result<(WsSink<S>, WsSource<S>)>
60where
61    S: AsyncRead + AsyncWrite + Unpin,
62{
63    let mut request = TCP_URL.into_client_request()?;
64    apply_headers(request.headers_mut(), credentials, app_name)?;
65    let config = ws_config();
66    let (ws_stream, _response) =
67        tokio_tungstenite::client_async_with_config(request, stream, Some(config)).await?;
68    Ok(ws_stream.split())
69}
70
71/// Connect to iTerm2 using the default Unix socket transport.
72///
73/// iTerm2 only serves its API over a Unix domain socket at
74/// `~/Library/Application Support/iTerm2/private/socket`.
75/// TCP on port 1912 is legacy and no longer served.
76pub async fn connect(
77    credentials: &Credentials,
78    app_name: &str,
79) -> Result<(WsSink<tokio::net::UnixStream>, WsSource<tokio::net::UnixStream>)> {
80    connect_unix(credentials, app_name).await
81}
82
83fn make_header_value(value: &str, field_name: &str) -> Result<HeaderValue> {
84    HeaderValue::from_str(value).map_err(|_| {
85        Error::Auth(format!(
86            "Invalid characters in {field_name} (must be visible ASCII)"
87        ))
88    })
89}
90
91fn apply_headers(
92    headers: &mut tokio_tungstenite::tungstenite::http::HeaderMap,
93    credentials: &Credentials,
94    app_name: &str,
95) -> Result<()> {
96    headers.insert(
97        "Sec-WebSocket-Protocol",
98        HeaderValue::from_static(SUBPROTOCOL),
99    );
100    headers.insert(
101        "Origin",
102        HeaderValue::from_static("ws://localhost"),
103    );
104    headers.insert(
105        "x-iterm2-library-version",
106        HeaderValue::from_static(concat!("rust ", "0.2.1")),
107    );
108    headers.insert(
109        "x-iterm2-cookie",
110        make_header_value(&credentials.cookie, "cookie")?,
111    );
112    headers.insert(
113        "x-iterm2-key",
114        make_header_value(&credentials.key, "key")?,
115    );
116    headers.insert(
117        "x-iterm2-advisory-name",
118        make_header_value(app_name, "app_name")?,
119    );
120    Ok(())
121}
122
123fn ws_config() -> WebSocketConfig {
124    let mut config = WebSocketConfig::default();
125    config.max_frame_size = Some(4 * 1024 * 1024); // 4MB per frame
126    config.max_message_size = Some(8 * 1024 * 1024); // 8MB total
127    config
128}