tor_interface/
arti_process.rs

1// standard
2use std::fs;
3use std::fs::File;
4use std::io::{BufRead, BufReader, Write};
5use std::ops::Drop;
6#[cfg(unix)]
7use std::os::unix::fs::PermissionsExt;
8use std::path::Path;
9use std::process::{Child, ChildStdout, Command, Stdio};
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(
58        arti_bin_path: &Path,
59        data_directory: &Path,
60        stdout_lines: Weak<Mutex<Vec<String>>>,
61    ) -> Result<Self, Error> {
62        // verify provided paths are absolute
63        if arti_bin_path.is_relative() {
64            return Err(Error::ArtiBinPathNotAbsolute(format!(
65                "{}",
66                arti_bin_path.display()
67            )));
68        }
69        if data_directory.is_relative() {
70            return Err(Error::ArtiDataDirectoryPathNotAbsolute(format!(
71                "{}",
72                data_directory.display()
73            )));
74        }
75
76        // create data directory if it doesn't exist
77        if !data_directory.exists() {
78            fs::create_dir_all(data_directory).map_err(Error::ArtiDataDirectoryCreationFailed)?;
79        } else if data_directory.is_file() {
80            return Err(Error::ArtiDataDirectoryPathExistsAsFile(format!(
81                "{}",
82                data_directory.display()
83            )));
84        }
85
86        // arti data directory must not be world-writable on unix platforms when using a unix domain socket endpoint
87        #[cfg(unix)]
88        fs::set_permissions(data_directory, PermissionsExt::from_mode(0o700))
89            .map_err(Error::ArtiDataDirectorySetPermissionsFailed)?;
90
91        // construct paths to arti files file
92        let arti_toml = data_directory.join("arti.toml");
93        let cache_dir_string = data_directory
94            .join("cache")
95            .display()
96            .to_string()
97            .escape_default()
98            .to_string();
99        let state_dir_string = data_directory
100            .join("state")
101            .display()
102            .to_string()
103            .escape_default()
104            .to_string();
105
106        let mut arti_toml_content = format!(
107            "\
108        [rpc]\n\
109        enable = true\n\n\
110        [rpc.listen.user-default]\n\
111        enable = false\n\n\
112        [rpc.listen.system-default]\n\
113        enable = false\n\n\
114        [storage]\n\
115        cache_dir = \"{cache_dir_string}\"\n\
116        state_dir = \"{state_dir_string}\"\n\n\
117        [storage.keystore]\n\
118        enabled = true\n\n\
119        [storage.keystore.primary]\n\
120        kind = \"ephemeral\"\n\n\
121        [storage.permissions]\n\
122        dangerously_trust_everyone = true\n\n\
123        "
124        );
125
126        let connect_string = if cfg!(unix) {
127            // use domain socket for unix
128            let unix_rpc_toml_path = data_directory.join("rpc.toml");
129            let unix_rpc_toml_path_string = unix_rpc_toml_path
130                .display()
131                .to_string()
132                .escape_default()
133                .to_string();
134
135            arti_toml_content.push_str(
136                format!(
137                    "\
138            [rpc.listen.unix-point]\n\
139            enable = true\n\
140            file = \"{unix_rpc_toml_path_string}\"\n\n\
141            "
142                )
143                .as_str(),
144            );
145
146            let socket_path = data_directory
147                .join("rpc.socket")
148                .display()
149                .to_string()
150                .escape_default()
151                .to_string();
152
153            let unix_rpc_toml_content = format!(
154                "\
155            [connect]\n\
156            socket = \"unix:{socket_path}\"\n\
157            auth = \"none\"\n\
158            "
159            );
160
161            let mut unix_rpc_toml_file =
162                File::create(&unix_rpc_toml_path).map_err(Error::RpcTomlFileCreationFailed)?;
163            unix_rpc_toml_file
164                .write_all(unix_rpc_toml_content.as_bytes())
165                .map_err(Error::RpcTomlFileWriteFailed)?;
166
167            unix_rpc_toml_path_string
168        } else {
169            // use tcp socket everywhere else
170            let tcp_rpc_toml_path = data_directory.join("rpc.toml");
171            let tcp_rpc_toml_path_string = tcp_rpc_toml_path
172                .display()
173                .to_string()
174                .escape_default()
175                .to_string();
176
177            arti_toml_content.push_str(
178                format!(
179                    "\
180            [rpc.listen.tcp-point]\n\
181            enable = true\n\
182            file = \"{tcp_rpc_toml_path_string}\"\n\n\
183            "
184                )
185                .as_str(),
186            );
187
188            let cookie_path_string = data_directory
189                .join("rpc.cookie")
190                .display()
191                .to_string()
192                .escape_default()
193                .to_string();
194
195            const RPC_PORT: u16 = 18929;
196
197            let tcp_rpc_toml_content = format!(
198                "\
199            [connect]\n\
200            socket = \"inet:127.0.0.1:{RPC_PORT}\"\n\
201            auth = {{ cookie = {{ path = \"{cookie_path_string}\" }} }}\n\
202            "
203            );
204
205            let mut tcp_rpc_toml_file =
206                File::create(&tcp_rpc_toml_path).map_err(Error::RpcTomlFileCreationFailed)?;
207            tcp_rpc_toml_file
208                .write_all(tcp_rpc_toml_content.as_bytes())
209                .map_err(Error::RpcTomlFileWriteFailed)?;
210
211            tcp_rpc_toml_path_string
212        };
213
214        let mut arti_toml_file =
215            File::create(&arti_toml).map_err(Error::ArtiTomlFileCreationFailed)?;
216        arti_toml_file
217            .write_all(arti_toml_content.as_bytes())
218            .map_err(Error::ArtiTomlFileWriteFailed)?;
219
220        let mut process = Command::new(arti_bin_path.as_os_str())
221            .stdout(Stdio::piped())
222            .stdin(Stdio::null())
223            .stderr(Stdio::null())
224            // set working directory to data directory
225            .current_dir(data_directory)
226            // proxy subcommand
227            .arg("proxy")
228            // point to our above written arti.toml file
229            .arg("--config")
230            .arg(arti_toml)
231            .spawn()
232            .map_err(Error::ArtiProcessStartFailed)?;
233
234        // spawn a task to read stdout lines and forward to list
235        let stdout = BufReader::new(match process.stdout.take() {
236            Some(stdout) => stdout,
237            None => return Err(Error::ArtiProcessStdoutTakeFailed()),
238        });
239        std::thread::Builder::new()
240            .name("arti_stdout_reader".to_string())
241            .spawn(move || {
242                ArtiProcess::read_stdout_task(&stdout_lines, stdout);
243            })
244            .map_err(Error::ArtiStdoutReadThreadSpawnFailed)?;
245
246        Ok(ArtiProcess {
247            process,
248            connect_string,
249        })
250    }
251
252    pub fn connect_string(&self) -> &str {
253        self.connect_string.as_str()
254    }
255
256    fn read_stdout_task(
257        stdout_lines: &std::sync::Weak<Mutex<Vec<String>>>,
258        mut stdout: BufReader<ChildStdout>,
259    ) {
260        while let Some(stdout_lines) = stdout_lines.upgrade() {
261            let mut line = String::default();
262            // read line
263            if stdout.read_line(&mut line).is_ok() {
264                // remove trailing '\n'
265                line.pop();
266                // then acquire the lock on the line buffer
267                let mut stdout_lines = match stdout_lines.lock() {
268                    Ok(stdout_lines) => stdout_lines,
269                    Err(_) => unreachable!(),
270                };
271                stdout_lines.push(line);
272            }
273        }
274    }
275}
276
277impl Drop for ArtiProcess {
278    fn drop(&mut self) {
279        let _ = self.process.kill();
280    }
281}