summaryrefslogtreecommitdiffhomepage
path: root/mullvad-cli/src/cmds/patch.rs
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2024-01-04 17:33:21 +0100
committerDavid Lönnhager <david.l@mullvad.net>2024-01-10 10:42:49 +0100
commite162d0b94c6e013e964ef9dff4bde80ccae4d58f (patch)
tree820758d4b8dd1a1d1209e75b6fe08999b041f6e3 /mullvad-cli/src/cmds/patch.rs
parentedbd1f52c44ba6ee9a290146f714453fc87e689d (diff)
downloadmullvadvpn-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.rs101
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())
+}