diff options
| author | David Lönnhager <david.l@mullvad.net> | 2024-01-04 17:33:21 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2024-01-10 10:42:49 +0100 |
| commit | e162d0b94c6e013e964ef9dff4bde80ccae4d58f (patch) | |
| tree | 820758d4b8dd1a1d1209e75b6fe08999b041f6e3 /mullvad-cli/src/cmds/patch.rs | |
| parent | edbd1f52c44ba6ee9a290146f714453fc87e689d (diff) | |
| download | mullvadvpn-e162d0b94c6e013e964ef9dff4bde80ccae4d58f.tar.xz mullvadvpn-e162d0b94c6e013e964ef9dff4bde80ccae4d58f.zip | |
Add patch export to the management interface
Diffstat (limited to 'mullvad-cli/src/cmds/patch.rs')
| -rw-r--r-- | mullvad-cli/src/cmds/patch.rs | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/mullvad-cli/src/cmds/patch.rs b/mullvad-cli/src/cmds/patch.rs new file mode 100644 index 0000000000..d5a79aaef9 --- /dev/null +++ b/mullvad-cli/src/cmds/patch.rs @@ -0,0 +1,101 @@ +use anyhow::{anyhow, Context, Result}; +use mullvad_management_interface::MullvadProxyClient; +use std::{ + fs::File, + io::{stdin, BufRead, BufReader}, + path::Path, +}; + +/// Maximum size of a settings patch. Bigger files/streams cause the read to fail. +const MAX_PATCH_BYTES: usize = 10 * 1024; + +/// If source is specified, read from the provided file and send it as a settings patch to the daemon. +/// Otherwise, read the patch from standard input. +pub async fn import(source: String) -> Result<()> { + let json_blob = tokio::task::spawn_blocking(|| get_blob(source)) + .await + .unwrap()?; + + let mut rpc = MullvadProxyClient::new().await?; + rpc.apply_json_settings(json_blob) + .await + .context("Error applying patch")?; + + println!("Settings applied"); + + Ok(()) +} + +fn get_blob(source: String) -> Result<String> { + match source.as_str() { + "-" => read_settings_from_stdin().context("Failed to read from stdin"), + _ => read_settings_from_file(source).context("Failed to read from path: {source}"), + } +} + +/// Read settings from standard input +fn read_settings_from_stdin() -> Result<String> { + read_settings_from_reader(BufReader::new(stdin())) +} + +/// Read settings from a path +fn read_settings_from_file(path: impl AsRef<Path>) -> Result<String> { + read_settings_from_reader(BufReader::new(File::open(path)?)) +} + +/// Read until EOF or until newline when the last pair of braces has been closed +fn read_settings_from_reader(mut reader: impl BufRead) -> Result<String> { + let mut buf = [0u8; MAX_PATCH_BYTES]; + + let mut was_open = false; + let mut close_after_newline = false; + let mut brace_count: usize = 0; + let mut cursor_pos = 0; + + loop { + let Some(cursor) = buf.get_mut(cursor_pos..) else { + return Err(anyhow!( + "Patch too long: maximum length is {MAX_PATCH_BYTES} bytes" + )); + }; + + let prev_cursor_pos = cursor_pos; + let read_n = reader.read(cursor)?; + if read_n == 0 { + // EOF + break; + } + cursor_pos += read_n; + + let additional_bytes = &buf[prev_cursor_pos..cursor_pos]; + + if !close_after_newline { + for next in additional_bytes { + match next { + b'{' => brace_count += 1, + b'}' => { + brace_count = brace_count.checked_sub(1).with_context(|| { + // exit: too many closing braces + "syntax error: unexpected '}'" + })? + } + _ => (), + } + was_open |= brace_count > 0; + } + if brace_count == 0 && was_open { + // complete settings + close_after_newline = true; + } + } + + if close_after_newline && additional_bytes.contains(&b'\n') { + // done + break; + } + } + + Ok(std::str::from_utf8(&buf[0..cursor_pos]) + .context("settings must be utf8 encoded")? + .to_owned()) +} |
