use crate::cli::OpenUrlHelperArgs;
use nono::supervisor::types::{SupervisorMessage, SupervisorResponse};
use nono::supervisor::{SupervisorSocket, UrlOpenRequest};
use nono::{NonoError, Result};
use std::path::Path;
pub(crate) fn run_open_url_helper(args: OpenUrlHelperArgs) -> Result<()> {
run_open_url_helper_inner(&args.url)
}
fn run_open_url_helper_inner(url: &str) -> Result<()> {
let socket_path = std::env::var("NONO_SUPERVISOR_PATH").map_err(|_| {
NonoError::SandboxInit(
"NONO_SUPERVISOR_PATH not set. open-url-helper must be invoked inside a nono sandbox."
.to_string(),
)
})?;
let mut socket = SupervisorSocket::connect(Path::new(&socket_path))?;
socket.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
let request = UrlOpenRequest {
request_id: format!("url-{}", std::process::id()),
url: url.to_string(),
child_pid: std::process::id(),
session_id: String::new(),
};
socket.send_message(&SupervisorMessage::OpenUrl(request))?;
let response = socket.recv_response()?;
match response {
SupervisorResponse::UrlOpened { success: true, .. } => Ok(()),
SupervisorResponse::UrlOpened {
success: false,
error,
..
} => {
let msg = error.unwrap_or_else(|| "Unknown error".to_string());
Err(NonoError::SandboxInit(format!(
"Supervisor denied URL open: {msg}"
)))
}
other => Err(NonoError::SandboxInit(format!(
"Unexpected supervisor response: {other:?}"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use nono::supervisor::SupervisorListener;
use std::path::PathBuf;
fn socket_test_dir() -> tempfile::TempDir {
let target = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target");
std::fs::create_dir_all(&target).ok();
tempfile::Builder::new()
.prefix("url-open-test-")
.tempdir_in(&target)
.expect("create test tmpdir in target/")
}
fn can_use_unix_sockets(dir: &std::path::Path) -> bool {
let probe_path = dir.join("probe.sock");
let listener = match std::os::unix::net::UnixListener::bind(&probe_path) {
Ok(l) => l,
Err(_) => return false,
};
let connect_result = std::os::unix::net::UnixStream::connect(&probe_path);
drop(listener);
let _ = std::fs::remove_file(&probe_path);
connect_result.is_ok()
}
#[test]
fn test_open_url_helper_ipc_succeeds_when_supervisor_approves() {
let dir = socket_test_dir();
if !can_use_unix_sockets(dir.path()) {
eprintln!("Skipping: Unix socket connect() blocked by sandbox");
return;
}
let sock_path = dir.path().join("approve.sock");
let listener = SupervisorListener::bind(&sock_path).expect("bind listener");
let sock_path_clone = sock_path.clone();
let handle = std::thread::spawn(move || {
let mut socket = SupervisorSocket::connect(&sock_path_clone).expect("connect");
socket
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.expect("set timeout");
let request = UrlOpenRequest {
request_id: format!("url-{}", std::process::id()),
url: "https://example.com/oauth".to_string(),
child_pid: std::process::id(),
session_id: String::new(),
};
socket
.send_message(&SupervisorMessage::OpenUrl(request))
.expect("send");
socket.recv_response()
});
std::thread::sleep(std::time::Duration::from_millis(50));
let mut server_sock = listener
.accept()
.expect("accept should not error")
.expect("accept should return a connection");
let msg = server_sock.recv_message().expect("recv message");
match msg {
SupervisorMessage::OpenUrl(req) => {
assert_eq!(req.url, "https://example.com/oauth");
}
other => panic!("Expected OpenUrl, got {:?}", other),
}
let response = SupervisorResponse::UrlOpened {
request_id: "test".to_string(),
success: true,
error: None,
};
server_sock.send_response(&response).expect("send response");
let result = handle
.join()
.expect("client thread")
.expect("recv response");
match result {
SupervisorResponse::UrlOpened { success, .. } => assert!(success),
other => panic!("Expected UrlOpened, got: {other:?}"),
}
}
#[test]
fn test_open_url_helper_ipc_returns_denial_from_supervisor() {
let dir = socket_test_dir();
if !can_use_unix_sockets(dir.path()) {
eprintln!("Skipping: Unix socket connect() blocked by sandbox");
return;
}
let sock_path = dir.path().join("deny.sock");
let listener = SupervisorListener::bind(&sock_path).expect("bind listener");
let sock_path_clone = sock_path.clone();
let handle = std::thread::spawn(move || {
let mut socket = SupervisorSocket::connect(&sock_path_clone).expect("connect");
socket
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.expect("set timeout");
let request = UrlOpenRequest {
request_id: "url-deny-test".to_string(),
url: "https://evil.example.com".to_string(),
child_pid: std::process::id(),
session_id: String::new(),
};
socket
.send_message(&SupervisorMessage::OpenUrl(request))
.expect("send");
socket.recv_response()
});
std::thread::sleep(std::time::Duration::from_millis(50));
let mut server_sock = listener
.accept()
.expect("accept should not error")
.expect("accept should return a connection");
let _msg = server_sock.recv_message().expect("recv message");
let response = SupervisorResponse::UrlOpened {
request_id: "url-deny-test".to_string(),
success: false,
error: Some("Origin not in allowed list".to_string()),
};
server_sock.send_response(&response).expect("send response");
let result = handle
.join()
.expect("client thread")
.expect("recv response");
match result {
SupervisorResponse::UrlOpened { success, error, .. } => {
assert!(!success);
assert_eq!(error.as_deref(), Some("Origin not in allowed list"));
}
other => panic!("Expected UrlOpened, got: {other:?}"),
}
}
#[test]
fn test_open_url_helper_fails_when_socket_path_does_not_exist() {
let bad_path = PathBuf::from("/tmp/nonexistent-nono-socket-12345.sock");
let result = SupervisorSocket::connect(&bad_path);
assert!(result.is_err());
let err_msg = format!("{}", result.err().expect("should have error"));
assert!(
err_msg.contains("Failed to connect"),
"Error should mention connection failure, got: {err_msg}"
);
}
}