Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ There's also some additional optional environment variables that you may set:

- `ADGUARD_PROTOCOL` - The protocol to use when connecting to AdGuard (defaults to `http`)
- `ADGUARD_UPDATE_INTERVAL` - The rate at which to refresh the UI in seconds (defaults to `2`)
- `ADGUARD_TIMEOUT` - The per-request timeout when contacting AdGuard, in seconds (defaults to `5`)
- `ADGUARD_QUERYLOG_LIMIT` - The number of query log entries to fetch per update (defaults to `100`)

<details>
<summary>Examples</summary>
Expand Down
57 changes: 37 additions & 20 deletions quick-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,43 +41,60 @@ function print_info {
print_heading "Checking system type"
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
print_info "System type: Linux"
bin_target="adguardian-linux"
case "$(uname -m)" in
x86_64|amd64)
bin_target="adguardian-linux"
;;
aarch64|arm64)
bin_target="adguardian-linux-arm64"
;;
armv7l|armv7*)
bin_target="adguardian-linux-armv7"
;;
*)
exit_script "Unsupported Linux architecture: $(uname -m)"
;;
esac
elif [[ "$OSTYPE" == "darwin"* ]]; then
print_info "System type: Apple OS X"
bin_target="adguardian-macos"
case "$(uname -m)" in
arm64)
bin_target="adguardian-macos"
;;
x86_64)
bin_target="adguardian-macos-x86_64"
;;
*)
exit_script "Unsupported macOS architecture: $(uname -m)"
;;
esac
elif [[ "$OSTYPE" == "cygwin" ]]; then
print_info "System type: Windows/Cygwin"
bin_target="adguardian-windows.exe"
else
exit_script "Unsupported System"
fi

# Make the download link to latest binary for users system type
download_link="$upstream_repo/releases/$adguardian_version/download/$bin_target"

# Check if the binary already exists
print_heading "Preparing to download"
download_link="$upstream_repo/releases/$adguardian_version/download/$bin_target"
if [ -f "$download_location" ]; then
print_info "File already exists, skipping download."
elif hash "curl" 2> /dev/null; then
print_info "Downloading to $download_location (with curl) from $download_link"
curl --fail --location --output "$download_location" "$download_link" \
|| { rm -f "$download_location"; exit_script "Unable to download a binary for your system"; }
elif hash "wget" 2> /dev/null; then
print_info "Downloading to $download_location (with wget) from $download_link"
wget --no-verbose --show-progress --progress=dot:mega -q -S -O "$download_location" "$download_link" \
|| { rm -f "$download_location"; exit_script "Unable to download a binary for your system"; }
else
# Download with either curl or wget, depending on what is installed
if hash "curl" 2> /dev/null; then
print_info "Downloading to $download_location (with curl)"
curl -L -o $download_location $download_link
elif hash "wget" 2> /dev/null; then
print_info "Downloading to $download_location (with wget)"
wget \
--no-verbose --show-progress \
--progress=dot:mega -q -S \
-O $download_location $download_link
else
exit_script "Neither curl nor wget were found on your system"
fi
exit_script "Neither curl nor wget were found on your system"
fi

# Make the binary executable, then run the application
print_heading "Preparing to run"
print_info "Updating permissions for $download_location"
chmod +x $download_location
chmod +x "$download_location"
print_info "Starting AdGuardian....\n\n"
$download_location
"$download_location"
6 changes: 6 additions & 0 deletions src/fetch/fetch_filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ pub async fn fetch_adguard_filter_list(
headers.insert("Authorization", auth_header_value.parse()?);

let res: Response = client.get(&url).headers(headers).send().await?;
if !res.status().is_success() {
return Err(anyhow::anyhow!(
"Request failed with status code {}",
res.status()
));
}
let status: AdGuardFilteringStatus = res.json().await?;

Ok(status)
Expand Down
30 changes: 27 additions & 3 deletions src/fetch/fetch_query_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ use base64::{engine::general_purpose::STANDARD, Engine as _};
use reqwest::header::{HeaderValue, AUTHORIZATION, CONTENT_LENGTH};
use serde::Deserialize;

#[derive(Deserialize)]
#[derive(Default, Deserialize)]
#[serde(default)]
pub struct QueryResponse {
pub data: Vec<Query>,
}

#[derive(Deserialize)]
#[derive(Default, Deserialize)]
#[serde(default)]
pub struct Query {
pub cached: bool,
pub client: String,
Expand All @@ -19,7 +21,8 @@ pub struct Query {
pub time: String,
}

#[derive(Deserialize)]
#[derive(Default, Deserialize)]
#[serde(default)]
pub struct Question {
pub class: String,
pub name: String,
Expand Down Expand Up @@ -52,3 +55,24 @@ pub async fn fetch_adguard_query_log(
let data = response.json().await?;
Ok(data)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::fetch::fetch_stats::StatsResponse;
use crate::fetch::fetch_status::StatusResponse;

// Missing or partial fields get decoded to defaults, instead of erroring
#[test]
fn empty_and_partial_json_decode_to_defaults() {
serde_json::from_str::<QueryResponse>("{}").unwrap();
serde_json::from_str::<StatsResponse>("{}").unwrap();
serde_json::from_str::<StatusResponse>("{}").unwrap();
serde_json::from_str::<StatsResponse>(r#"{"num_dns_queries":5}"#).unwrap();

// A blocked query has no `upstream`, default to empty
let q = r#"{"cached":false,"client":"1.2.3.4","elapsedMs":"0.1",
"question":{"class":"IN","name":"x.com","type":"A"},"reason":"x","time":"t"}"#;
assert_eq!(serde_json::from_str::<Query>(q).unwrap().upstream, "");
}
}
3 changes: 2 additions & 1 deletion src/fetch/fetch_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ pub struct DomainData {
pub count: i32,
}

#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Default, Deserialize, Clone)]
#[serde(default)]
pub struct StatsResponse {
pub num_dns_queries: u64,
pub num_blocked_filtering: u64,
Expand Down
3 changes: 2 additions & 1 deletion src/fetch/fetch_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ use serde::Deserialize;
/// * `protection_enabled` - Whether or not protection is currently enabled.
/// * `dhcp_available` - Whether or not DHCP is available.
/// * `running` - Whether or not the AdGuard Home instance is currently running.
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Default, Deserialize, Clone)]
#[serde(default)]
pub struct StatusResponse {
pub version: String,
pub dns_port: u16,
Expand Down
33 changes: 25 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tokio::time::{interval, MissedTickBehavior};
use ui::draw_ui;

use fetch::{
fetch_filters::fetch_adguard_filter_list,
fetch_filters::{fetch_adguard_filter_list, AdGuardFilteringStatus},
fetch_query_log::{fetch_adguard_query_log, Query},
fetch_stats::{fetch_adguard_stats, StatsResponse},
fetch_status::{fetch_adguard_status, StatusResponse},
Expand All @@ -33,8 +33,14 @@ async fn fetch_all(
}

async fn run() -> anyhow::Result<()> {
// Create a reqwest client
let client = Client::new();
// Per-request timeout (seconds), clamped to at least 1, so no request can hang
let timeout_secs: u64 = env::var("ADGUARD_TIMEOUT")
.unwrap_or_else(|_| "5".into())
.parse::<u64>()?
.max(1);
let client = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()?;

// AdGuard instance details, from env vars (verified in welcome.rs)
let ip = env::var("ADGUARD_IP")?;
Expand All @@ -44,8 +50,18 @@ async fn run() -> anyhow::Result<()> {
let username = env::var("ADGUARD_USERNAME")?;
let password = env::var("ADGUARD_PASSWORD")?;

// Fetch data that doesn't require updates
let filters = fetch_adguard_filter_list(&client, &hostname, &username, &password).await?;
// Fetch the filter list, use empty list on failures is fine
let filters = welcome::with_retries(
3,
Duration::from_secs(5),
"Fetching AdGuard filters",
|| fetch_adguard_filter_list(&client, &hostname, &username, &password),
)
.await
.unwrap_or_else(|e| {
eprintln!("Could not fetch filter list, starting without it: {}", e);
AdGuardFilteringStatus { filters: None }
});

// Open channels for data fetching where updates are required
let (queries_tx, queries_rx) = tokio::sync::mpsc::channel(1);
Expand All @@ -64,10 +80,11 @@ async fn run() -> anyhow::Result<()> {
shutdown_tx,
));

// Get update interval (in seconds)
// Get update interval (in seconds), clamped to at least 1 (interval() panics on zero)
let interval_secs: u64 = env::var("ADGUARD_UPDATE_INTERVAL")
.unwrap_or_else(|_| "2".into())
.parse()?;
.parse::<u64>()?
.max(1);
let mut interval = interval(Duration::from_secs(interval_secs));
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);

Expand Down Expand Up @@ -106,7 +123,7 @@ async fn run() -> anyhow::Result<()> {
}

fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
let rt = tokio::runtime::Runtime::new().expect("failed to start async runtime");
rt.block_on(async {
welcome::welcome().await.unwrap_or_else(|e| {
eprintln!("Failed to initialize: {}", e);
Expand Down
Loading