tor_interface/
arti_process.rs

1// standard
2use std::fs;
3#[cfg(unix)]
4use std::os::unix::fs::PermissionsExt;
5use std::fs::File;
6use std::io::{BufRead, BufReader, Write};
7use std::ops::Drop;
8use std::process::{Child, ChildStdout, Command, Stdio};
9use std::path::Path;
10use std::sync::{Mutex, Weak};
11
12#[derive(thiserror::Error, Debug)]
13pub enum Error {
14    #[error("provided arti bin path '{0}' must be an absolute path")]
15    ArtiBinPathNotAbsolute(String),
16
17    #[error("provided data directory '{0}' must be an absolute path")]
18    ArtiDataDirectoryPathNotAbsolute(String),
19
20    #[error("failed to create data directory: {0}")]
21    ArtiDataDirectoryCreationFailed(#[source] std::io::Error),
22
23    #[error("file exists in provided data directory path '{0}'")]
24    ArtiDataDirectoryPathExistsAsFile(String),
25
26    #[error("unable to set permissions for data directory: {0}")]
27    ArtiDataDirectorySetPermissionsFailed(#[source] std::io::Error),
28
29    #[error("failed to create arti.toml file: {0}")]
30    ArtiTomlFileCreationFailed(#[source] std::io::Error),
31
32    #[error("failed to write arti.toml file: {0}")]
33    ArtiTomlFileWriteFailed(#[source] std::io::Error),
34
35    #[error("failed to create rpc.toml file: {0}")]
36    RpcTomlFileCreationFailed(#[source] std::io::Error),
37
38    #[error("failed to write rpc.toml file: {0}")]
39    RpcTomlFileWriteFailed(#[source] std::io::Error),
40
41    #[error("failed to start arti process: {0}")]
42    ArtiProcessStartFailed(#[source] std::io::Error),
43
44    #[error("unable to take arti process stdout")]
45    ArtiProcessStdoutTakeFailed(),
46
47    #[error("failed to spawn arti process stdout read thread: {0}")]
48    ArtiStdoutReadThreadSpawnFailed(#[source] std::io::Error),
49}
50
51pub(crate) struct ArtiProcess {
52    process: Child,
53    connect_string: String,
54}
55
56impl ArtiProcess {
57    pub fn new(arti_bin_path: &Path, data_directory: &Path, stdout_lines: Weak<Mutex<Vec<String>>>) -> Result<Self, Error> {
58        // verify provided paths are absolute
59        if arti_bin_path.is_relative() {
60            return Err(Error::ArtiBinPathNotAbsolute(format!(
61                "{}",
62                arti_bin_path.display()
63            )));
64        }
65        if data_directory.is_relative() {
66            return Err(Error::ArtiDataDirectoryPathNotAbsolute(format!(
67                "{}",
68                data_directory.display()
69            )));
70        }
71
72        // create data directory if it doesn't exist
73        if !data_directory.exists() {
74            fs::create_dir_all(data_directory).map_err(Error::ArtiDataDirectoryCreationFailed)?;
75        } else if data_directory.is_file() {
76            return Err(Error::ArtiDataDirectoryPathExistsAsFile(format!(
77                "{}",
78                data_directory.display()
79            )));
80        }
81
82        // arti data directory must not be world-writable on unix platforms when using a unix domain socket endpoint
83        #[cfg(unix)]
84        fs::set_permissions(
85            data_directory,
86            PermissionsExt::from_mode(0o700))
87        .map_err(Error::ArtiDataDirectorySetPermissionsFailed)?;
88
89        // construct paths to arti files file
90        let arti_toml = data_directory.join("arti.toml");
91        let cache_dir_string = data_directory.join("cache").display().to_string().escape_default().to_string();
92        let state_dir_string = data_directory.join("state").display().to_string().escape_default().to_string();
93
94        let mut arti_toml_content = format!("\
95        [rpc]\n\
96        enable = true\n\n\
97        [rpc.listen.user-default]\n\
98        enable = false\n\n\
99        [rpc.listen.system-default]\n\
100        enable = false\n\n\
101        [storage]\n\
102        cache_dir = \"{cache_dir_string}\"\n\
103        state_dir = \"{state_dir_string}\"\n\n\
104        [storage.keystore]\n\
105        enabled = true\n\n\
106        [storage.keystore.primary]\n\
107        kind = \"ephemeral\"\n\n\
108        [storage.permissions]\n\
109        dangerously_trust_everyone = true\n\n\
110        ");
111
112        let connect_string = if cfg!(unix) {
113            // use domain socket for unix
114            let unix_rpc_toml_path = data_directory.join("rpc.toml");
115            let unix_rpc_toml_path_string = unix_rpc_toml_path.display().to_string().escape_default().to_string();
116
117            arti_toml_content.push_str(format!("\
118            [rpc.listen.unix-point]\n\
119            enable = true\n\
120            file = \"{unix_rpc_toml_path_string}\"\n\n\
121            ").as_str());
122
123            let socket_path = data_directory.join("rpc.socket").display().to_string().escape_default().to_string();
124
125            let unix_rpc_toml_content = format!("\
126            [connect]\n\
127            socket = \"unix:{socket_path}\"\n\
128            auth = \"none\"\n\
129            ");
130
131            let mut unix_rpc_toml_file =
132                File::create(&unix_rpc_toml_path).map_err(Error::RpcTomlFileCreationFailed)?;
133            unix_rpc_toml_file
134                .write_all(unix_rpc_toml_content.as_bytes())
135                .map_err(Error::RpcTomlFileWriteFailed)?;
136
137            unix_rpc_toml_path_string
138        } else {
139            // use tcp socket everywhere else
140            let tcp_rpc_toml_path = data_directory.join("rpc.toml");
141            let tcp_rpc_toml_path_string = tcp_rpc_toml_path.display().to_string().escape_default().to_string();
142
143            arti_toml_content.push_str(format!("\
144            [rpc.listen.tcp-point]\n\
145            enable = true\n\
146            file = \"{tcp_rpc_toml_path_string}\"\n\n\
147            ").as_str());
148
149            let cookie_path_string = data_directory.join("rpc.cookie").display().to_string().escape_default().to_string();
150
151            const RPC_PORT: u16 = 18929;
152
153            let tcp_rpc_toml_content = format!("\
154            [connect]\n\
155            socket = \"inet:127.0.0.1:{RPC_PORT}\"\n\
156            auth = {{ cookie = {{ path = \"{cookie_path_string}\" }} }}\n\
157            ");
158
159            let mut tcp_rpc_toml_file =
160                File::create(&tcp_rpc_toml_path).map_err(Error::RpcTomlFileCreationFailed)?;
161            tcp_rpc_toml_file
162                .write_all(tcp_rpc_toml_content.as_bytes())
163                .map_err(Error::RpcTomlFileWriteFailed)?;
164
165            tcp_rpc_toml_path_string
166        };
167
168        let mut arti_toml_file =
169            File::create(&arti_toml).map_err(Error::ArtiTomlFileCreationFailed)?;
170        arti_toml_file
171            .write_all(arti_toml_content.as_bytes())
172            .map_err(Error::ArtiTomlFileWriteFailed)?;
173
174        let mut process = Command::new(arti_bin_path.as_os_str())
175            .stdout(Stdio::piped())
176            .stdin(Stdio::null())
177            .stderr(Stdio::null())
178            // set working directory to data directory
179            .current_dir(data_directory)
180            // proxy subcommand
181            .arg("proxy")
182            // point to our above written arti.toml file
183            .arg("--config")
184            .arg(arti_toml)
185            .spawn()
186            .map_err(Error::ArtiProcessStartFailed)?;
187
188        // spawn a task to read stdout lines and forward to list
189        let stdout = BufReader::new(match process.stdout.take() {
190            Some(stdout) => stdout,
191            None => return Err(Error::ArtiProcessStdoutTakeFailed()),
192        });
193        std::thread::Builder::new()
194            .name("arti_stdout_reader".to_string())
195            .spawn(move || {
196                ArtiProcess::read_stdout_task(&stdout_lines, stdout);
197            })
198            .map_err(Error::ArtiStdoutReadThreadSpawnFailed)?;
199
200        Ok(ArtiProcess { process, connect_string })
201    }
202
203    pub fn connect_string(&self) -> &str {
204        self.connect_string.as_str()
205    }
206
207    fn read_stdout_task(
208        stdout_lines: &std::sync::Weak<Mutex<Vec<String>>>,
209        mut stdout: BufReader<ChildStdout>,
210    ) {
211        while let Some(stdout_lines) = stdout_lines.upgrade() {
212            let mut line = String::default();
213            // read line
214            if stdout.read_line(&mut line).is_ok() {
215                // remove trailing '\n'
216                line.pop();
217                // then acquire the lock on the line buffer
218                let mut stdout_lines = match stdout_lines.lock() {
219                    Ok(stdout_lines) => stdout_lines,
220                    Err(_) => unreachable!(),
221                };
222                stdout_lines.push(line);
223            }
224        }
225    }
226}
227
228impl Drop for ArtiProcess {
229    fn drop(&mut self) {
230        let _ = self.process.kill();
231    }
232}