mod auth;
mod config;
pub mod http_client;
pub mod websockets;
#[cfg(test)]
mod unit;
pub use websockets::WebSocketConnections;
use crate::os_input_output::ClientOsApi;
use crate::RemoteClientError;
use tokio::runtime::Handle;
use zellij_utils::remote_session_tokens;
#[cfg(test)]
const MAX_AUTH_ATTEMPTS: u32 = 1;
#[cfg(not(test))]
const MAX_AUTH_ATTEMPTS: u32 = 3;
pub fn attach_to_remote_session(
runtime: Handle,
_os_input: Box<dyn ClientOsApi>,
remote_session_url: &str,
token: Option<String>,
remember: bool,
forget: bool,
ca_cert: Option<&std::path::Path>,
insecure: bool,
) -> Result<WebSocketConnections, RemoteClientError> {
let server_url = extract_server_url(remote_session_url)?;
if forget {
let _ = remote_session_tokens::delete_session_token(&server_url);
}
if token.is_some() {
let _ = remote_session_tokens::delete_session_token(&server_url);
}
if token.is_none() {
if let Some(connections) = try_to_connect_with_saved_session_token(
runtime.clone(),
remote_session_url,
&server_url,
ca_cert,
insecure,
)? {
return Ok(connections);
}
}
authenticate_with_retry(
runtime,
remote_session_url,
token,
remember,
ca_cert,
insecure,
)
}
fn try_to_connect_with_saved_session_token(
runtime: Handle,
remote_session_url: &str,
server_url: &str,
ca_cert: Option<&std::path::Path>,
insecure: bool,
) -> Result<Option<WebSocketConnections>, RemoteClientError> {
if let Ok(Some(saved_session_token)) = remote_session_tokens::get_session_token(server_url) {
let ca_cert_owned = ca_cert.map(|p| p.to_path_buf());
match runtime.block_on(async move {
remote_attach_with_session_token(
remote_session_url,
&saved_session_token,
ca_cert_owned.as_deref(),
insecure,
)
.await
}) {
Ok(connections) => {
return Ok(Some(connections));
},
Err(RemoteClientError::SessionTokenExpired) => {
let _ = remote_session_tokens::delete_session_token(server_url);
eprintln!("Session expired, please re-authenticate");
return Ok(None);
},
Err(e) => {
return Err(e);
},
}
}
Ok(None)
}
fn authenticate_with_retry(
runtime: Handle,
remote_session_url: &str,
initial_token: Option<String>,
remember: bool,
ca_cert: Option<&std::path::Path>,
insecure: bool,
) -> Result<WebSocketConnections, RemoteClientError> {
use dialoguer::{Confirm, Password};
let mut attempt = 0;
let mut current_token = initial_token;
loop {
attempt += 1;
let auth_token = match ¤t_token {
Some(t) => t.clone(),
None => Password::new()
.with_prompt("Enter authentication token")
.interact()
.map_err(|e| RemoteClientError::IoError(e))?,
};
let ca_cert_owned = ca_cert.map(|p| p.to_path_buf());
match runtime.block_on(async move {
remote_attach(
remote_session_url,
&auth_token,
remember,
ca_cert_owned.as_deref(),
insecure,
)
.await
}) {
Ok((connections, session_token_opt)) => {
if let Some(session_token) = session_token_opt {
let server_url = extract_server_url(remote_session_url)?;
let _ = remote_session_tokens::save_session_token(&server_url, &session_token);
}
return Ok(connections);
},
Err(RemoteClientError::InvalidAuthToken) => {
eprintln!("Invalid authentication token");
if attempt >= MAX_AUTH_ATTEMPTS {
eprintln!(
"Maximum authentication attempts ({}) exceeded.",
MAX_AUTH_ATTEMPTS
);
return Err(RemoteClientError::InvalidAuthToken);
}
match Confirm::new()
.with_prompt("Try again?")
.default(true)
.interact()
{
Ok(true) => {
current_token = None;
continue;
},
Ok(false) => {
return Err(RemoteClientError::InvalidAuthToken);
},
Err(e) => {
return Err(RemoteClientError::IoError(e));
},
}
},
Err(e) => {
return Err(e);
},
}
}
}
async fn remote_attach(
server_url: &str,
auth_token: &str,
remember_me: bool,
ca_cert: Option<&std::path::Path>,
insecure: bool,
) -> Result<(websockets::WebSocketConnections, Option<String>), RemoteClientError> {
let server_base_url = extract_server_url(server_url)?;
let session_name = extract_session_name(server_url)?;
let (web_client_id, http_client, session_token) =
auth::authenticate(&server_base_url, auth_token, remember_me, ca_cert, insecure).await?;
let connections = websockets::establish_websocket_connections(
&web_client_id,
&http_client,
&server_base_url,
&session_name,
ca_cert,
insecure,
)
.await
.map_err(|e| RemoteClientError::ConnectionFailed(e.to_string()))?;
Ok((connections, session_token))
}
async fn remote_attach_with_session_token(
server_url: &str,
session_token: &str,
ca_cert: Option<&std::path::Path>,
insecure: bool,
) -> Result<websockets::WebSocketConnections, RemoteClientError> {
let server_base_url = extract_server_url(server_url)?;
let session_name = extract_session_name(server_url)?;
let (web_client_id, http_client) =
auth::validate_session_token(&server_base_url, session_token, ca_cert, insecure).await?;
let connections = websockets::establish_websocket_connections(
&web_client_id,
&http_client,
&server_base_url,
&session_name,
ca_cert,
insecure,
)
.await
.map_err(|e| RemoteClientError::ConnectionFailed(e.to_string()))?;
Ok(connections)
}
pub fn extract_server_url(full_url: &str) -> Result<String, RemoteClientError> {
let parsed = url::Url::parse(full_url)?;
let mut base_url = parsed.clone();
base_url.set_path("");
base_url.set_query(None);
base_url.set_fragment(None);
Ok(base_url.to_string().trim_end_matches('/').to_string())
}
fn extract_session_name(server_url: &str) -> Result<String, RemoteClientError> {
let parsed_url = url::Url::parse(server_url)?;
let path = parsed_url.path();
if path.len() > 1 && path.starts_with('/') {
Ok(path[1..].trim_end_matches('/').to_string())
} else {
Ok(String::new())
}
}