Skip to content

Commit

Permalink
Merge branch 'add-settings-patching' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
dlon committed Nov 17, 2023
2 parents 0a82036 + b83735c commit d798fd5
Show file tree
Hide file tree
Showing 10 changed files with 677 additions and 84 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Line wrap the file at 100 chars. Th
`SOCKS5`.
- Add social media content blocker.
- Add ability to override server IPs to the CLI.
- Add CLI support for applying patches to the settings with `mullvad import-settings`.

### Changed
- Update Electron from 25.2.0 to 26.3.0.
Expand Down
101 changes: 101 additions & 0 deletions mullvad-cli/src/cmds/import_settings.rs
Original file line number Diff line number Diff line change
@@ -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 handle(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())
}
1 change: 1 addition & 0 deletions mullvad-cli/src/cmds/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod beta_program;
pub mod bridge;
pub mod custom_list;
pub mod dns;
pub mod import_settings;
pub mod lan;
pub mod lockdown;
pub mod obfuscation;
Expand Down
7 changes: 7 additions & 0 deletions mullvad-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ enum Cli {
/// Manage custom lists
#[clap(subcommand)]
CustomList(custom_list::CustomList),

/// Apply a JSON patch
ImportSettings {
/// File to read from. If this is "-", read from standard input
file: String,
},
}

#[tokio::main]
Expand Down Expand Up @@ -160,6 +166,7 @@ async fn main() -> Result<()> {
Cli::SplitTunnel(cmd) => cmd.handle().await,
Cli::Status { cmd, args } => status::handle(cmd, args).await,
Cli::CustomList(cmd) => cmd.handle().await,
Cli::ImportSettings { file } => import_settings::handle(file).await,

#[cfg(all(unix, not(target_os = "android")))]
Cli::ShellCompletions { shell, dir } => {
Expand Down
15 changes: 15 additions & 0 deletions mullvad-daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ pub enum DaemonCommand {
/// Verify that a google play payment was successful through the API.
#[cfg(target_os = "android")]
VerifyPlayPurchase(ResponseTx<(), Error>, PlayPurchase),
/// Patch the settings using a blob of JSON settings
ApplyJsonSettings(ResponseTx<(), settings::patch::Error>, String),
}

/// All events that can happen in the daemon. Sent from various threads and exposed interfaces.
Expand Down Expand Up @@ -1171,6 +1173,7 @@ where
VerifyPlayPurchase(tx, play_purchase) => {
self.on_verify_play_purchase(tx, play_purchase)
}
ApplyJsonSettings(tx, blob) => self.on_apply_json_settings(tx, blob).await,
}
}

Expand Down Expand Up @@ -2439,6 +2442,18 @@ where
});
}

async fn on_apply_json_settings(
&mut self,
tx: ResponseTx<(), settings::patch::Error>,
blob: String,
) {
let result = settings::patch::merge_validate_patch(&mut self.settings, &blob).await;
if result.is_ok() {
self.reconnect_tunnel();
}
Self::oneshot_send(tx, result, "apply_json_settings response");
}

/// Set the target state of the client. If it changed trigger the operations needed to
/// progress towards that state.
/// Returns a bool representing whether or not a state change was initiated.
Expand Down
Loading

0 comments on commit d798fd5

Please sign in to comment.