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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,17 @@ docker compose up --build
HTTP_PORT=80 docker compose up --build
```

Маршрутизация:

* `/api/v1/*`, `/agency/*`, `/whitelist/*` идут в Rocket
* остальные запросы идут в SvelteKit SSR

Базы проверок кешируются в `database-cache/`. Путь можно изменить через
`DATABASE_CACHE_DIR`, основной интервал обновления — через
`DATABASE_INTERVAL_SECONDS`, а интервал повтора после ошибки скачивания — через
`DATABASE_RETRY_INTERVAL_SECONDS` (по умолчанию 300 секунд).

---
Маршрутизация:
* `/api/v1/*`, `/agency/*`, `/whitelist/*` идут в Rocket
* остальные запросы идут в SvelteKit SSR
Базы проверок кешируются в `database-cache/`. Путь можно изменить через
`DATABASE_CACHE_DIR`, основной интервал обновления — через
`DATABASE_INTERVAL_SECONDS`, а интервал повтора после ошибки скачивания — через
`DATABASE_RETRY_INTERVAL_SECONDS` (по умолчанию 300 секунд).
---

## Вклад

Expand Down
6 changes: 6 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ services:
MQTT_PORT: 11883
PROBE_TRACEROUTE_ENABLED: "${PROBE_TRACEROUTE_ENABLED:-false}"
PROBE_RATE_LIMIT_RPM: "${PROBE_RATE_LIMIT_RPM:-5}"
PROBE_UPDATE_PUBLIC_BASE_URL: "${PROBE_UPDATE_PUBLIC_BASE_URL:-https://cheburcheck.ru/api/v1/probe-updates}"
PROBE_UPDATE_CACHE_SECONDS: "${PROBE_UPDATE_CACHE_SECONDS:-300}"
PROBE_UPDATE_ASSET_CACHE_DIR: /var/cache/cheburcheck/probe-updates
PROBE_UPDATE_ASSET_CACHE_SECONDS: "${PROBE_UPDATE_ASSET_CACHE_SECONDS:-3600}"
PROBE_UPDATE_DOWNLOAD_RATE_LIMIT_RPM: "${PROBE_UPDATE_DOWNLOAD_RATE_LIMIT_RPM:-2}"
GITHUB_TOKEN: "${GITHUB_TOKEN:-}"
volumes:
- database-cache:/var/cache/cheburcheck/databases
expose:
Expand Down
2 changes: 1 addition & 1 deletion probe/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "probe"
version = "0.5.0"
version = "0.6.0"
edition = "2024"
license-file = "../LICENSE"
description = "Dynamic network probe daemon for Cheburcheck"
Expand Down
4 changes: 2 additions & 2 deletions probe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Cheburcheck Probe (`cheburprobe`) — динамический сетевой с

### 3. Автоматическая установка Debian/OpenWrt

Интерактивный мастер определяет ОС, архитектуру и пакетный менеджер, находит версию Probe среди пакетов последнего GitHub Release, показывает текущую и доступную версии, а затем запрашивает подтверждение установки или обновления. Данные авторизации можно указать сразу или настроить позднее.
Интерактивный мастер определяет ОС, архитектуру и пакетный менеджер, получает список пакетов через сервер `cheburcheck.ru`, показывает текущую и доступную версии, а затем запрашивает подтверждение установки или обновления. Данные авторизации можно указать сразу или настроить позднее.

На Debian/Ubuntu выполните:

Expand Down Expand Up @@ -208,7 +208,7 @@ logread -e cheburprobe

## Автоматические обновления

Пакеты Debian и OpenWrt каждые шесть часов проверяют последний опубликованный GitHub Release. При появлении новой версии пакет обновляется, а сервис перезапускается. В OpenWrt пакет LuCI обновляется вместе с основным.
Пакеты Debian и OpenWrt каждые шесть часов проверяют последний опубликованный релиз. Манифест и файлы обновления загружаются через `https://cheburcheck.ru`, поэтому хосту со сканером не требуется доступ к GitHub. При появлении новой версии пакет обновляется, а сервис перезапускается. В OpenWrt пакет LuCI обновляется вместе с основным.

Автообновления включены по умолчанию. Отключить их в Debian можно командой:

Expand Down
10 changes: 4 additions & 6 deletions probe/install.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
#!/bin/sh
set -eu

REPOSITORY=${CHEBURPROBE_REPOSITORY:-LowderPlay/cheburcheck}
GITHUB_URL=${CHEBURPROBE_GITHUB_URL:-https://github.com}
GITHUB_API_URL=${CHEBURPROBE_GITHUB_API_URL:-https://api.github.com}
UPDATE_API_URL=${CHEBURPROBE_UPDATE_API_BASE_URL:-https://cheburcheck.ru/api/v1/probe-updates}
WITH_LUCI=${CHEBURPROBE_WITH_LUCI:-1}
ASSUME_YES=${CHEBURPROBE_ASSUME_YES:-0}
PROBE_ID=${PROBE_ID:-}
Expand Down Expand Up @@ -61,7 +59,7 @@ probe_version() {
;;
esac
fi
[ -n "$version" ] || fail "the latest GitHub release has no Cheburprobe package for $PLATFORM_NAME/$ARCH"
[ -n "$version" ] || fail "the latest release has no Cheburprobe package for $PLATFORM_NAME/$ARCH"
case "$version" in ''|*[!0-9A-Za-z.+~-]*) fail "invalid Probe package version: $version" ;; esac
printf '%s\n' "$version"
}
Expand Down Expand Up @@ -210,7 +208,7 @@ read_credentials() {
}

asset_url() {
printf '%s/%s/releases/latest/download/%s\n' "$GITHUB_URL" "$REPOSITORY" "$1"
printf '%s/assets/%s\n' "${UPDATE_API_URL%/}" "$1"
}

download_asset() {
Expand Down Expand Up @@ -302,7 +300,7 @@ print_configuration_help() {
WORK_DIR=$(mktemp -d /tmp/cheburprobe-install.XXXXXX)
trap 'rm -rf "$WORK_DIR"' EXIT INT TERM
RELEASE_JSON=$WORK_DIR/latest.json
download "$GITHUB_API_URL/repos/$REPOSITORY/releases/latest" "$RELEASE_JSON"
download "${UPDATE_API_URL%/}/releases/latest" "$RELEASE_JSON"
detect_platform
VERSION=$(probe_version)
select_packages
Expand Down
2 changes: 1 addition & 1 deletion probe/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ impl Cli {

#[derive(Subcommand, Debug, Clone)]
enum Command {
/// Update Cheburprobe from the latest GitHub release.
/// Update Cheburprobe from the trusted update server.
Update,
}

Expand Down
33 changes: 5 additions & 28 deletions probe/src/mock_release_server.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{Context, Result, bail};
use anyhow::{Context, Result};
use clap::Parser;
use serde::Serialize;
use std::fs;
Expand All @@ -15,9 +15,6 @@ struct Args {
#[arg(long, default_value = "http://127.0.0.1:8080")]
public_url: String,

#[arg(long, default_value = "LowderPlay/cheburcheck")]
repository: String,

/// Directory containing .deb, .apk, and .ipk release assets.
#[arg(long)]
assets_dir: PathBuf,
Expand All @@ -36,7 +33,6 @@ struct Asset {

fn main() -> Result<()> {
let args = Args::parse();
validate_repository(&args.repository)?;
let assets_dir = args
.assets_dir
.canonicalize()
Expand All @@ -45,18 +41,13 @@ fn main() -> Result<()> {
let listener = TcpListener::bind(&args.bind)
.with_context(|| format!("failed to listen on {}", args.bind))?;

println!(
"mock release API: {public_url}/repos/{}/releases/latest",
args.repository
);
println!("mock release API: {public_url}/releases/latest");
println!("serving assets from {}", assets_dir.display());

for connection in listener.incoming() {
match connection {
Ok(stream) => {
if let Err(error) =
handle_request(stream, &assets_dir, &args.repository, &public_url)
{
if let Err(error) = handle_request(stream, &assets_dir, &public_url) {
eprintln!("request failed: {error:#}");
}
}
Expand All @@ -66,12 +57,7 @@ fn main() -> Result<()> {
Ok(())
}

fn handle_request(
mut stream: TcpStream,
assets_dir: &Path,
repository: &str,
public_url: &str,
) -> Result<()> {
fn handle_request(mut stream: TcpStream, assets_dir: &Path, public_url: &str) -> Result<()> {
let mut reader = BufReader::new(stream.try_clone().context("failed to read request")?);
let mut request_line = String::new();
reader
Expand All @@ -88,8 +74,7 @@ fn handle_request(
}
println!("request: {method} {path}");

let release_path = format!("/repos/{repository}/releases/latest");
if path == release_path {
if path == "/releases/latest" {
let body = serde_json::to_vec(&Release {
assets: list_assets(assets_dir, public_url)?,
})?;
Expand Down Expand Up @@ -151,14 +136,6 @@ fn valid_name(name: &str) -> bool {
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
}

fn validate_repository(repository: &str) -> Result<()> {
let mut parts = repository.split('/');
match (parts.next(), parts.next(), parts.next()) {
(Some(owner), Some(repo), None) if valid_name(owner) && valid_name(repo) => Ok(()),
_ => bail!("invalid repository {repository:?}; expected owner/name"),
}
}

fn respond(stream: &mut TcpStream, status: u16, content_type: &str, body: &[u8]) -> Result<()> {
let reason = match status {
200 => "OK",
Expand Down
43 changes: 7 additions & 36 deletions probe/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ use std::process::{Command, Output, Stdio};
use std::time::Duration;
use tempfile::TempDir;

const DEFAULT_REPOSITORY: &str = "LowderPlay/cheburcheck";
const DEFAULT_API_BASE_URL: &str = "https://api.github.com";
const DEFAULT_API_BASE_URL: &str = "https://cheburcheck.ru/api/v1/probe-updates";

#[derive(Debug, Deserialize)]
struct Release {
Expand Down Expand Up @@ -82,10 +81,6 @@ pub async fn run() -> Result<()> {
}

async fn update() -> Result<()> {
let repository =
env::var("CHEBURPROBE_UPDATE_REPOSITORY").unwrap_or_else(|_| DEFAULT_REPOSITORY.to_owned());
validate_repository(&repository)?;

let current = Version::parse(env!("CARGO_PKG_VERSION"))
.context("the installed cheburprobe version is invalid")?;
let client = Client::builder()
Expand All @@ -95,10 +90,7 @@ async fn update() -> Result<()> {
.context("failed to create HTTP client")?;
let api_base_url = env::var("CHEBURPROBE_UPDATE_API_BASE_URL")
.unwrap_or_else(|_| DEFAULT_API_BASE_URL.to_owned());
let api_url = format!(
"{}/repos/{repository}/releases/latest",
api_base_url.trim_end_matches('/')
);
let api_url = format!("{}/releases/latest", api_base_url.trim_end_matches('/'));
let release = fetch_release(&client, &api_url).await?;
let (kind, architecture, luci_installed) = detect_platform()?;
let (asset, latest) = select_asset(&release.assets, kind, &architecture)?;
Expand Down Expand Up @@ -137,26 +129,12 @@ async fn fetch_release(client: &Client, api_url: &str) -> Result<Release> {
.header("Accept", "application/vnd.github+json")
.send()
.await
.context("failed to query the latest GitHub release")?
.context("failed to query the latest probe release")?
.error_for_status()
.context("GitHub rejected the latest-release request")?
.context("the update server rejected the latest-release request")?
.json::<Release>()
.await
.context("GitHub returned an invalid release document")
}

fn validate_repository(repository: &str) -> Result<()> {
let mut parts = repository.split('/');
let valid_part = |part: &str| {
!part.is_empty()
&& part
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
};
match (parts.next(), parts.next(), parts.next()) {
(Some(owner), Some(repo), None) if valid_part(owner) && valid_part(repo) => Ok(()),
_ => bail!("invalid GitHub repository {repository:?}; expected owner/name"),
}
.context("the update server returned an invalid release document")
}

fn command_exists(command: &str) -> bool {
Expand Down Expand Up @@ -317,7 +295,7 @@ async fn download(client: &Client, url: Url, destination: &Path) -> Result<()> {
.await
.context("failed to download the update package")?
.error_for_status()
.context("GitHub rejected the package download")?
.context("the update server rejected the package download")?
.bytes()
.await
.context("failed to read the update package")?;
Expand All @@ -338,7 +316,7 @@ async fn download_asset(
bail!("invalid release asset name: {:?}", asset.name);
}
let url = Url::parse(&asset.browser_download_url)
.context("GitHub returned an invalid release asset URL")?;
.context("the update server returned an invalid release asset URL")?;
let destination = directory.join(&asset.name);
download(client, url, &destination).await?;
Ok(destination)
Expand Down Expand Up @@ -485,13 +463,6 @@ mod tests {
);
}

#[test]
fn validates_repository_names() {
assert!(validate_repository("LowderPlay/cheburcheck").is_ok());
assert!(validate_repository("owner/repo/extra").is_err());
assert!(validate_repository("owner?x/repo").is_err());
}

#[test]
fn selects_each_package_format() {
let assets = vec![
Expand Down
2 changes: 1 addition & 1 deletion probe/update/cheburprobe-update.service
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[Unit]
Description=Update Cheburprobe from the latest GitHub release
Description=Update Cheburprobe from the trusted update server
After=network-online.target
Wants=network-online.target

Expand Down
3 changes: 2 additions & 1 deletion website/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "website"
version = "1.2.5"
version = "1.3.0"
edition = "2024"

[dependencies]
Expand All @@ -18,6 +18,7 @@ dotenvy = { version = "0.15.7" }
governor = { version = "0.6", features = ["dashmap"] }
rumqttc = "0.24"
toml = "0.8"
reqwest = { workspace = true }

[build-dependencies]
reqwest = { version = "0.12", features = ["blocking", "json"] }
Expand Down
5 changes: 4 additions & 1 deletion website/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ pub use check_endpoint::check;
pub use feedback_endpoint::feedback;
pub use nodes_endpoint::node_stats;
pub use probe::probe_query;
pub use rate_limit::{build_probe_rate_limiter, build_rate_limiter};
pub use rate_limit::{
ProbeUpdateDownloadRateLimiter, build_probe_rate_limiter,
build_probe_update_download_rate_limiter, build_rate_limiter,
};
pub use status::{get_system_status, healthcheck};
12 changes: 12 additions & 0 deletions website/src/api/rate_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub struct ApiRateLimiter(KeyedRateLimiter);

pub struct ProbeRateLimiter(KeyedRateLimiter);

pub struct ProbeUpdateDownloadRateLimiter(KeyedRateLimiter);

impl ApiRateLimiter {
pub fn check(&self, ip: &IpAddr) -> bool {
self.0.check_key(ip).is_ok()
Expand All @@ -22,6 +24,12 @@ impl ProbeRateLimiter {
}
}

impl ProbeUpdateDownloadRateLimiter {
pub fn check(&self, ip: &IpAddr) -> bool {
self.0.check_key(ip).is_ok()
}
}

pub fn build_rate_limiter(per_minute: u32) -> ApiRateLimiter {
ApiRateLimiter(build_limiter(per_minute))
}
Expand All @@ -30,6 +38,10 @@ pub fn build_probe_rate_limiter(per_minute: u32) -> ProbeRateLimiter {
ProbeRateLimiter(build_limiter(per_minute))
}

pub fn build_probe_update_download_rate_limiter(per_minute: u32) -> ProbeUpdateDownloadRateLimiter {
ProbeUpdateDownloadRateLimiter(build_limiter(per_minute))
}

fn build_limiter(per_minute: u32) -> KeyedRateLimiter {
RateLimiter::keyed(Quota::per_minute(
NonZeroU32::new(per_minute).expect("rate limit must be > 0"),
Expand Down
Loading
Loading