adguardian/
welcome.rs

1use std::{
2    io:: {self, Write},
3    env,
4    time::Duration
5};
6use reqwest::{Client, Error};
7use colored::*;
8
9use serde_json::Value;
10use serde::Deserialize;
11use semver::{Version};
12
13/// Reusable function that just prints success messages to the console
14fn print_info(text: &str, is_secondary: bool) {
15    if is_secondary {
16        println!("{}", text.green().italic().dimmed());
17    } else {
18        println!("{}", text.green());
19    };
20}
21
22/// Prints the AdGuardian ASCII art to console
23fn print_ascii_art() {
24    let art = r"
25 █████╗ ██████╗  ██████╗ ██╗   ██╗ █████╗ ██████╗ ██████╗ ██╗ █████╗ ███╗   ██╗
26██╔══██╗██╔══██╗██╔════╝ ██║   ██║██╔══██╗██╔══██╗██╔══██╗██║██╔══██╗████╗  ██║
27███████║██║  ██║██║  ███╗██║   ██║███████║██████╔╝██║  ██║██║███████║██╔██╗ ██║
28██╔══██║██║  ██║██║   ██║██║   ██║██╔══██║██╔══██╗██║  ██║██║██╔══██║██║╚██╗██║
29██║  ██║██████╔╝╚██████╔╝╚██████╔╝██║  ██║██║  ██║██████╔╝██║██║  ██║██║ ╚████║
30╚═╝  ╚═╝╚═════╝  ╚═════╝  ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝ ╚═╝╚═╝  ╚═╝╚═╝  ╚═══╝
31";
32    print_info(art, false);
33    print_info("\nWelcome to AdGuardian Terminal Edition!", false);
34    print_info("Terminal-based, real-time traffic monitoring and statistics for your AdGuard Home instance", true);
35    print_info("For documentation and support, please visit: https://github.com/lissy93/adguardian-term", true);
36}
37
38/// Print error message, along with (optional) stack trace, then exit
39fn print_error(message: &str, sub_message: &str, error: Option<&Error>) {
40    eprintln!(
41        "{}{}{}",
42        format!("{}", message).red(),
43        match error {
44            Some(err) => format!("\n{}", err).red().dimmed(),
45            None => "".red().dimmed(),
46        },
47        format!("\n{}", sub_message).yellow(),
48    );
49
50    std::process::exit(1);
51}
52
53/// Given a key, get the value from the environmental variables, and print it to the console
54fn get_env(key: &str) -> Result<String, env::VarError> {
55    env::var(key).map(|v| {
56        println!(
57            "{}",
58            format!(
59                "{} is set to {}",
60                key.bold(),
61                if key.contains("PASSWORD") { "******" } else { &v }
62            )
63            .green()
64        );
65        v
66    })
67}
68
69/// Given a possibly undefined version number, check if it's present and supported
70fn check_version(version: Option<&str>) {
71    let min_version = Version::parse("0.107.29").unwrap();
72    
73    match version {
74        Some(version_str) => {
75            let adguard_version = Version::parse(&version_str[1..]).unwrap();
76            
77            if adguard_version < min_version {
78                print_error(
79                    "AdGuard Home version is too old, and is now unsupported",
80                    format!("You're running AdGuard {}. Please upgrade to v{} or later.", version_str, min_version.to_string()).as_str(),
81                    None,
82                );
83            }
84        },
85        None => {
86            print_error(
87                "Unsupported AdGuard Home version",
88                format!(
89                    concat!(
90                        "Failed to get the version number of your AdGuard Home instance.\n",
91                        "This usually means you're running an old, and unsupported version.\n",
92                        "Please upgrade to v{} or later."
93                    ), min_version.to_string()
94                ).as_str(),
95                None,
96            );
97        }
98    }
99}
100
101/// With the users specified AdGuard details, verify the connection (exit on fail)
102async fn verify_connection(
103    client: &Client,
104    ip: String,
105    port: String,
106    protocol: String,
107    username: String,
108    password: String,
109) -> Result<(), Box<dyn std::error::Error>> {
110    println!("{}", "\nVerifying connection to your AdGuard instance...".blue());
111
112    let auth_string = format!("{}:{}", username, password);
113    let auth_header_value = format!("Basic {}", base64::encode(&auth_string));
114    let mut headers = reqwest::header::HeaderMap::new();
115    headers.insert("Authorization", auth_header_value.parse()?);
116
117    let url = format!("{}://{}:{}/control/status", protocol, ip, port);
118
119    match client
120        .get(&url)
121        .headers(headers)
122        .timeout(Duration::from_secs(2))
123        .send()
124        .await {
125        Ok(res) if res.status().is_success() => {
126            // Get version string (if present), and check if valid - exit if not
127            let body: Value = res.json().await?;
128            check_version(body["version"].as_str());
129            // All good! Print success message :)
130            let safe_version = body["version"].as_str().unwrap_or("mystery version");
131            println!("{}", format!("AdGuard ({}) connection successful!\n", safe_version).green());
132            Ok(())
133        }
134        // Connection failed to authenticate. Print error and exit
135        Ok(_) => {
136            print_error(
137                &format!("Authentication with AdGuard at {}:{} failed", ip, port),
138                "Please check your environmental variables and try again.",
139                None,
140            );
141            Ok(())
142        },
143        // Connection failed to establish. Print error and exit
144        Err(e) => {
145            print_error(
146                &format!("Failed to connect to AdGuard at: {}:{}", ip, port),
147                "Please check your environmental variables and try again.",
148                Some(&e),
149            );
150            Ok(())
151        }
152    }
153}
154
155#[derive(Deserialize)]
156struct CratesIoResponse {
157    #[serde(rename = "crate")]
158    krate: Crate,
159}
160
161#[derive(Deserialize)]
162struct Crate {
163    max_version: String,
164}
165
166/// Gets the latest version of the crate from crates.io
167async fn get_latest_version(crate_name: &str) -> Result<String, Box<dyn std::error::Error>> {
168    let url = format!("https://crates.io/api/v1/crates/{}", crate_name);
169    let client = reqwest::Client::new();
170    let res = client.get(&url)
171        .header(reqwest::header::USER_AGENT, "version_check (adguardian.as93.net)")
172        .send()
173        .await?;
174
175    if res.status().is_success() {
176        let response: CratesIoResponse = res.json().await?;
177        Ok(response.krate.max_version)
178    } else {
179        let status = res.status();
180        let body = res.text().await?;
181        Err(format!("Request failed with status {}: body: {}", status, body).into())
182    }
183}
184
185/// Checks for updates to the crate, and prints a message if an update is available
186async fn check_for_updates() {
187    // Get crate name and version from Cargo.toml
188    let crate_name = env!("CARGO_PKG_NAME");
189    let crate_version = env!("CARGO_PKG_VERSION");
190    println!("{}", "\nChecking for updates...".blue());
191    // Parse the current version, and fetch and parse the latest version
192    let current_version = Version::parse(crate_version).unwrap_or_else(|_| {
193        Version::parse("0.0.0").unwrap()
194    });
195    let latest_version = Version::parse(
196        &get_latest_version(crate_name).await.unwrap_or_else(|_| {
197            "0.0.0".to_string()
198        })
199    ).unwrap();
200
201    // Compare the current and latest versions, and print the appropriate message
202    if current_version == Version::parse("0.0.0").unwrap() || latest_version == Version::parse("0.0.0").unwrap() {
203        println!("{}", "Unable to check for updates".yellow());
204    } else if current_version < latest_version {
205        println!("{}",
206            format!(
207                "A new version of AdGuardian is available.\nUpdate from {} to {} for the best experience",
208                current_version.to_string().bold(),
209                latest_version.to_string().bold()
210            ).yellow()
211        );
212    } else if current_version == latest_version {
213        println!(
214            "{}",
215            format!("AdGuardian is up-to-date, running version {}", current_version.to_string().bold()).green()
216        );
217    } else if current_version > latest_version {
218        println!(
219            "{}",
220            format!("Running a pre-released edition of AdGuardian, version {}", current_version.to_string().bold()).green()
221        );
222    } else {
223        println!("{}", "Unable to check for updates".yellow());
224    }
225}
226
227
228/// Initiate the welcome script
229/// This function will:
230/// - Print the AdGuardian ASCII art
231/// - Check if there's an update available
232/// - Check for the required environmental variables
233/// - Prompt the user to enter any missing variables
234/// - Verify the connection to the AdGuard instance
235/// - Verify authentication is successful
236/// - Verify the AdGuard Home version is supported
237/// - Then either print a success message, or show instructions to fix and exit
238pub async fn welcome() -> Result<(), Box<dyn std::error::Error>> {
239    print_ascii_art();
240
241    // Check for updates
242    check_for_updates().await;
243
244    println!("{}", "\nStarting initialization checks...".blue());
245
246    let client = Client::new();
247
248    // List of available flags, ant their associated env vars
249    let flags = [
250        ("--adguard-ip", "ADGUARD_IP"),
251        ("--adguard-port", "ADGUARD_PORT"),
252        ("--adguard-username", "ADGUARD_USERNAME"),
253        ("--adguard-password", "ADGUARD_PASSWORD"),
254    ];
255
256    let protocol: String = env::var("ADGUARD_PROTOCOL").unwrap_or_else(|_| "http".into()).parse()?;
257    env::set_var("ADGUARD_PROTOCOL", protocol);
258
259    // Parse command line arguments
260    let mut args = std::env::args().peekable();
261    while let Some(arg) = args.next() {
262        for &(flag, var) in &flags {
263            if arg == flag {
264                if let Some(value) = args.peek() {
265                    env::set_var(var, value);
266                    args.next();
267                }
268            }
269        }
270    }
271
272    // If any of the env variables or flags are not yet set, prompt the user to enter them
273    for &key in &["ADGUARD_IP", "ADGUARD_PORT", "ADGUARD_USERNAME", "ADGUARD_PASSWORD"] {
274        if env::var(key).is_err() {
275            println!(
276                "{}",
277                format!("The {} environmental variable is not yet set", key.bold()).yellow()
278            );
279            print!("{}", format!("› Enter a value for {}: ", key).blue().bold());
280            io::stdout().flush()?;
281
282            let mut value = String::new();
283            io::stdin().read_line(&mut value)?;
284            env::set_var(key, value.trim());
285        }
286    }
287
288    // Grab the values of the (now set) environmental variables
289    let ip = get_env("ADGUARD_IP")?;
290    let port = get_env("ADGUARD_PORT")?;
291    let protocol = get_env("ADGUARD_PROTOCOL")?;
292    let username = get_env("ADGUARD_USERNAME")?;
293    let password = get_env("ADGUARD_PASSWORD")?;
294    
295    // Verify that we can connect, authenticate, and that version is supported (exit on failure)
296    verify_connection(&client, ip, port, protocol, username, password).await?;
297
298    Ok(())
299}