summaryrefslogtreecommitdiffhomepage
path: root/mullvad-daemon/src/target_state.rs
blob: cc3094b9863c750e581de46ca1e1bfafaf1d9952 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
use mullvad_types::states::TargetState;
use std::{
    ops::Deref,
    path::{Path, PathBuf},
};
use talpid_types::ErrorExt;
use tokio::{fs, io};

/// State to use by default if there is no cache.
const DEFAULT_TARGET_STATE: TargetState = TargetState::Unsecured;
const TARGET_START_STATE_FILE: &str = "target-start-state.json";

/// Persists the target state to a file, which is only removed if the instance is dropped cleanly.
pub struct PersistentTargetState {
    state: TargetState,
    cache_path: PathBuf,
    locked: bool,
}

impl PersistentTargetState {
    /// Initialize using the current target state (if there is one)
    pub async fn new(cache_dir: &Path) -> Self {
        let cache_path = cache_dir.join(TARGET_START_STATE_FILE);
        let mut update_cache = false;
        let state = match fs::read_to_string(&cache_path).await {
            Ok(content) => serde_json::from_str(&content)
                .map(|state| {
                    log::info!(
                        "Loaded cached target state \"{}\" from {}",
                        state,
                        cache_path.display()
                    );
                    state
                })
                .unwrap_or_else(|error| {
                    log::error!(
                        "{}",
                        error.display_chain_with_msg("Failed to parse cached target tunnel state")
                    );
                    update_cache = true;
                    TargetState::Secured
                }),
            Err(error) => {
                if error.kind() == io::ErrorKind::NotFound {
                    log::debug!("No cached target state to load");
                    DEFAULT_TARGET_STATE
                } else {
                    log::error!(
                        "{}",
                        error.display_chain_with_msg("Failed to read cached target tunnel state")
                    );
                    update_cache = true;
                    TargetState::Secured
                }
            }
        };
        let state = PersistentTargetState {
            state,
            cache_path,
            locked: false,
        };
        if update_cache {
            state.save().await;
        }
        state
    }

    /// Override the current target state, if there is one
    pub async fn force(cache_dir: &Path, state: TargetState) -> Self {
        let cache_path = cache_dir.join(TARGET_START_STATE_FILE);
        let state = PersistentTargetState {
            state,
            cache_path,
            locked: false,
        };
        state.save().await;
        state
    }

    pub async fn set(&mut self, new_state: TargetState) {
        if new_state != self.state {
            self.state = new_state;
            self.save().await;
        }
    }

    /// Prevent the file from being removed when the instance is dropped.
    pub fn lock(&mut self) {
        self.locked = true;
    }

    /// Async destructor
    pub async fn finalize(mut self) {
        if self.locked {
            return;
        }
        let _ = fs::remove_file(&self.cache_path).await.map_err(|error| {
            if error.kind() != io::ErrorKind::NotFound {
                log::error!(
                    "{}",
                    error.display_chain_with_msg("Cannot delete target tunnel state cache")
                );
            }
        });
        // prevent the sync destructor from running
        self.locked = true;
    }

    async fn save(&self) {
        log::trace!(
            "Saving tunnel target state to {}",
            self.cache_path.display()
        );
        match serde_json::to_string(&self.state) {
            Ok(data) => {
                if let Err(error) = fs::write(&self.cache_path, data).await {
                    log::error!(
                        "{}",
                        error.display_chain_with_msg("Failed to write cache target state")
                    );
                }
            }
            Err(error) => {
                log::error!(
                    "{}",
                    error.display_chain_with_msg("Failed to serialize cache target state")
                )
            }
        }
    }
}

impl Drop for PersistentTargetState {
    fn drop(&mut self) {
        if self.locked {
            return;
        }
        let _ = std::fs::remove_file(&self.cache_path).map_err(|error| {
            if error.kind() != io::ErrorKind::NotFound {
                log::error!(
                    "{}",
                    error.display_chain_with_msg("Cannot delete target tunnel state cache")
                );
            }
        });
    }
}

impl Deref for PersistentTargetState {
    type Target = TargetState;

    fn deref(&self) -> &Self::Target {
        &self.state
    }
}