Skip to content

Commit

Permalink
Merge branch 'add-settings-json-export'
Browse files Browse the repository at this point in the history
  • Loading branch information
dlon committed Jan 10, 2024
2 parents edbd1f5 + 01bcf9e commit 75eb89c
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 106 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Line wrap the file at 100 chars. Th
### Added
- Add account UUID to verbose 'mullvad account get -v' output.
- Respect OS prefer-reduced-motion setting
- Add CLI command for exporting settings patches: `mullvad export-settings`. Currently, it generates
a patch containing all patchable settings, which only includes relay IP overrides.

#### Android
- Add support for all screen orientations.
Expand Down
101 changes: 0 additions & 101 deletions mullvad-cli/src/cmds/import_settings.rs

This file was deleted.

2 changes: 1 addition & 1 deletion mullvad-cli/src/cmds/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ pub mod bridge;
pub mod custom_list;
pub mod debug;
pub mod dns;
pub mod import_settings;
pub mod lan;
pub mod lockdown;
pub mod obfuscation;
pub mod patch;
pub mod proxies;
pub mod relay;
pub mod relay_constraints;
Expand Down
60 changes: 60 additions & 0 deletions mullvad-cli/src/cmds/patch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use anyhow::{Context, Result};
use mullvad_management_interface::MullvadProxyClient;
use std::{
fs::File,
io::{stdin, BufReader, Read},
};

/// 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(())
}

/// If source is specified, write a patch to the file. Otherwise, write the patch to standard
/// output.
pub async fn export(dest: String) -> Result<()> {
let mut rpc = MullvadProxyClient::new().await?;
let blob = rpc
.export_json_settings()
.await
.context("Error exporting patch")?;

match dest.as_str() {
"-" => {
println!("{blob}");
Ok(())
}
_ => tokio::fs::write(&dest, blob)
.await
.context(format!("Failed to write to path {dest}")),
}
}

fn get_blob(source: String) -> Result<String> {
match source.as_str() {
"-" => {
read_settings_from_reader(BufReader::new(stdin())).context("Failed to read from stdin")
}
_ => read_settings_from_reader(File::open(&source)?)
.context(format!("Failed to read from path: {source}")),
}
}

/// Read until EOF or until newline when the last pair of braces has been closed
fn read_settings_from_reader(mut reader: impl Read) -> Result<String> {
let mut s = String::new();
reader.read_to_string(&mut s)?;
Ok(s)
}
13 changes: 11 additions & 2 deletions mullvad-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,19 @@ enum Cli {
#[clap(subcommand)]
CustomList(custom_list::CustomList),

/// Apply a JSON patch
/// Apply a JSON patch generated by 'export-settings'
#[clap(arg_required_else_help = true)]
ImportSettings {
/// File to read from. If this is "-", read from standard input
file: String,
},

/// Export a JSON patch based on the current settings
#[clap(arg_required_else_help = true)]
ExportSettings {
/// File to write to. If this is "-", write to standard output
file: String,
},
}

#[tokio::main]
Expand All @@ -169,7 +177,8 @@ 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,
Cli::ImportSettings { file } => patch::import(file).await,
Cli::ExportSettings { file } => patch::export(file).await,

#[cfg(all(unix, not(target_os = "android")))]
Cli::ShellCompletions { shell, dir } => {
Expand Down
10 changes: 9 additions & 1 deletion mullvad-daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,10 @@ 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
/// Patch the settings using a JSON patch
ApplyJsonSettings(ResponseTx<(), settings::patch::Error>, String),
/// Return a JSON blob containing all overridable settings, if there are any
ExportJsonSettings(ResponseTx<String, settings::patch::Error>),
}

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

Expand Down Expand Up @@ -2626,6 +2629,11 @@ where
Self::oneshot_send(tx, result, "apply_json_settings response");
}

fn on_export_json_settings(&mut self, tx: ResponseTx<String, settings::patch::Error>) {
let result = settings::patch::export_settings(&self.settings);
Self::oneshot_send(tx, result, "export_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
8 changes: 8 additions & 0 deletions mullvad-daemon/src/management_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,14 @@ impl ManagementService for ManagementServiceImpl {
self.wait_for_result(rx).await??;
Ok(Response::new(()))
}

async fn export_json_settings(&self, _: Request<()>) -> ServiceResult<String> {
log::debug!("export_json_settings");
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::ExportJsonSettings(tx))?;
let blob = self.wait_for_result(rx).await??;
Ok(Response::new(blob))
}
}

impl ManagementServiceImpl {
Expand Down
63 changes: 62 additions & 1 deletion mullvad-daemon/src/settings/patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
//! existing settings.
//!
//! Permitted settings and merge strategies are defined in the [PERMITTED_SUBKEYS] constant.
//!
//! This implementation must be kept in sync with the
//! [spec](../../../docs/settings-patch-format.md).
use super::SettingsPersister;
use mullvad_types::settings::Settings;
Expand All @@ -33,6 +36,9 @@ pub enum Error {
/// Failed to serialize settings
#[error(display = "Failed to serialize current settings")]
SerializeSettings(#[error(source)] serde_json::Error),
/// Failed to serialize field
#[error(display = "Failed to serialize value")]
SerializeValue(#[error(source)] serde_json::Error),
/// Recursion limit reached
#[error(display = "Maximum JSON object depth reached")]
RecursionLimit,
Expand All @@ -54,7 +60,9 @@ impl From<Error> for mullvad_management_interface::Status {
| Error::DeserializePatched(_)
| Error::RecursionLimit => Status::invalid_argument(error.to_string()),
Error::Settings(error) => Status::from(error),
Error::SerializeSettings(error) => Status::internal(error.to_string()),
Error::SerializeSettings(error) | Error::SerializeValue(error) => {
Status::internal(error.to_string())
}
}
}
}
Expand Down Expand Up @@ -125,6 +133,40 @@ const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::object(&[(
/// tail-call optimization can be enforced?
const RECURSE_LIMIT: usize = 15;

/// Export a patch containing all currently supported settings.
pub fn export_settings(settings: &Settings) -> Result<String, Error> {
let patch = export_settings_inner(settings)?;
serde_json::to_string_pretty(&patch).map_err(Error::SerializeValue)
}

fn export_settings_inner(settings: &Settings) -> Result<serde_json::Value, Error> {
let mut out = serde_json::Map::new();
let mut overrides = vec![];

for relay_override in &settings.relay_overrides {
let mut relay_override =
serde_json::to_value(relay_override).map_err(Error::SerializeValue)?;
if let Some(relay_overrides) = relay_override.as_object_mut() {
// prune empty override entries
relay_overrides.retain(|_k, v| !v.is_null());
let has_overrides = relay_overrides.iter().any(|(key, _)| key != "hostname");
if !has_overrides {
continue;
}
}
overrides.push(relay_override);
}

if !overrides.is_empty() {
out.insert(
"relay_overrides".to_owned(),
serde_json::Value::Array(overrides),
);
}

Ok(serde_json::Value::Object(out))
}

/// Update the settings with the supplied patch. Only settings specified in `PERMITTED_SUBKEYS` can
/// be updated. All other changes are rejected
pub async fn merge_validate_patch(
Expand Down Expand Up @@ -417,6 +459,25 @@ fn test_valid_patch_files() {
.expect("failed to apply relay overrides");
}

#[test]
fn test_patch_export() {
use mullvad_types::relay_constraints::RelayOverride;

let mut settings = Settings::default();

let mut relay_override = RelayOverride::empty("test".to_owned());
relay_override.ipv4_addr_in = Some("1.2.3.4".parse().unwrap());
relay_override.ipv6_addr_in = Some("::1".parse().unwrap());
settings.relay_overrides.push(relay_override);

let exported = export_settings_inner(&settings).expect("patch export failed");

let expected = r#"{ "relay_overrides": [ { "hostname": "test", "ipv4_addr_in": "1.2.3.4", "ipv6_addr_in": "::1" } ] }"#;
let expected: serde_json::Value = serde_json::from_str(expected).unwrap();

assert_eq!(exported, expected);
}

#[test]
fn test_patch_relay_override() {
const PERMITTED_SUBKEYS: &PermittedKey = &PermittedKey::object(&[(
Expand Down
2 changes: 2 additions & 0 deletions mullvad-management-interface/proto/management_interface.proto
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ service ManagementService {
// Apply a JSON blob to the settings
// See ../../docs/settings-patch-format.md for a description of the format
rpc ApplyJsonSettings(google.protobuf.StringValue) returns (google.protobuf.Empty) {}
// Return a JSON blob containing all overridable settings, if there are any
rpc ExportJsonSettings(google.protobuf.Empty) returns (google.protobuf.StringValue) {}
}

message UUID { string value = 1; }
Expand Down
5 changes: 5 additions & 0 deletions mullvad-management-interface/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,11 @@ impl MullvadProxyClient {
self.0.apply_json_settings(blob).await.map_err(Error::Rpc)?;
Ok(())
}

pub async fn export_json_settings(&mut self) -> Result<String> {
let blob = self.0.export_json_settings(()).await.map_err(Error::Rpc)?;
Ok(blob.into_inner())
}
}

fn map_device_error(status: Status) -> Error {
Expand Down

0 comments on commit 75eb89c

Please sign in to comment.