Tauri 2 DoH-Schutz für Windows: PowerShell-DoH-Takeover, Tamper-Service (SYSTEM, windows-service), Browser-Policies (Chromium built-in-DNS + eigenes DoH aus → OS-Resolver), 24h-Cooldown via bestehende magic/*-Endpoints. GitHub-Actions baut den x64-NSIS-Installer auf windows-latest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
199 lines
6.4 KiB
Rust
199 lines
6.4 KiB
Rust
//! ReBreak Magic für Windows — Tauri-Core.
|
|
//!
|
|
//! Alle Backend-Calls + Token-Handling laufen hier in Rust; die WebView
|
|
//! sieht nur den aggregierten UI-State (nie Tokens). Analog zur Mac-App
|
|
//! (rebreak-magic-mac), gleiche /api/magic/*-Endpoints.
|
|
|
|
mod api;
|
|
mod auth;
|
|
mod device;
|
|
mod setup;
|
|
|
|
use api::Api;
|
|
use protection_core::ProtectionState;
|
|
use serde::Serialize;
|
|
use tauri::Manager;
|
|
|
|
const DEFAULT_BACKEND_URL: &str = "https://staging.rebreak.org";
|
|
|
|
#[derive(Serialize, Clone, Default)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct UiState {
|
|
backend_url: String,
|
|
logged_in: bool,
|
|
nickname: Option<String>,
|
|
plan: Option<String>,
|
|
device_id: String,
|
|
hostname: String,
|
|
/// Magic-Binding existiert (DNS-Token provisioniert)
|
|
registered: bool,
|
|
/// Windows-DoH zeigt aktuell auf ReBreak (lokaler Check)
|
|
protection_applied: bool,
|
|
release_requested_at: Option<String>,
|
|
release_available_at: Option<String>,
|
|
}
|
|
|
|
/// Backend-URL: config.json im App-Config-Dir > Default (Staging).
|
|
/// Gleiche Semantik wie config.example.json der Mac-App.
|
|
fn backend_url(app: &tauri::AppHandle) -> String {
|
|
if let Ok(dir) = app.path().app_config_dir() {
|
|
if let Ok(raw) = std::fs::read_to_string(dir.join("config.json")) {
|
|
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) {
|
|
if let Some(url) = v.get("backendBaseUrl").and_then(|u| u.as_str()) {
|
|
return url.trim_end_matches('/').to_string();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
DEFAULT_BACKEND_URL.into()
|
|
}
|
|
|
|
/// Aggregiert den kompletten UI-State (ein Roundtrip fürs Frontend).
|
|
async fn build_state(app: &tauri::AppHandle) -> UiState {
|
|
let mut state = UiState {
|
|
backend_url: backend_url(app),
|
|
device_id: device::device_id(),
|
|
hostname: device::hostname(),
|
|
..Default::default()
|
|
};
|
|
|
|
// Lokaler DoH-Check ist auth-unabhängig
|
|
let server_ip = protection_core::load_state()
|
|
.map(|s| s.server_ip)
|
|
.unwrap_or_else(setup::resolve_doh_ip);
|
|
state.protection_applied = protection_core::doh_is_applied(&server_ip);
|
|
|
|
let Some(token) = auth::load_session_token() else {
|
|
return state;
|
|
};
|
|
|
|
let api = Api::new(&state.backend_url);
|
|
match api.me(&token).await {
|
|
Ok(me) => {
|
|
state.logged_in = true;
|
|
state.nickname = me.nickname;
|
|
state.plan = me.plan;
|
|
}
|
|
Err(_) => {
|
|
// Token abgelaufen/revoked → wie ausgeloggt behandeln
|
|
return state;
|
|
}
|
|
}
|
|
|
|
if let Ok(devices) = api.devices(&token).await {
|
|
if let Some(mine) = devices.iter().find(|d| d.device_id == state.device_id) {
|
|
state.registered = true;
|
|
state.release_requested_at = mine.release_requested_at.clone();
|
|
state.release_available_at = mine.release_available_at.clone();
|
|
}
|
|
}
|
|
|
|
state
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tauri-Commands — jedes gibt den frischen UiState zurück
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[tauri::command]
|
|
async fn get_state(app: tauri::AppHandle) -> Result<UiState, String> {
|
|
Ok(build_state(&app).await)
|
|
}
|
|
|
|
/// Pairing-Code einlösen + Device registrieren (ein Schritt fürs UI).
|
|
#[tauri::command]
|
|
async fn pair_and_register(app: tauri::AppHandle, code: String) -> Result<UiState, String> {
|
|
let code = code.trim().to_string();
|
|
if !code.chars().all(|c| c.is_ascii_digit()) || code.len() != 6 {
|
|
return Err("Der Code besteht aus 6 Ziffern.".into());
|
|
}
|
|
|
|
let base = backend_url(&app);
|
|
let api = Api::new(&base);
|
|
let hostname = device::hostname();
|
|
|
|
let pair = api.redeem_pair(&code, &hostname).await?;
|
|
auth::save_session_token(&pair.token)?;
|
|
|
|
let reg = api
|
|
.register(
|
|
&pair.token,
|
|
&device::device_id(),
|
|
&hostname,
|
|
"Windows-PC",
|
|
&device::os_version(),
|
|
)
|
|
.await?;
|
|
auth::save_dns_token(®.dns_token)?;
|
|
|
|
Ok(build_state(&app).await)
|
|
}
|
|
|
|
/// Schutz aktivieren: EIN UAC-Prompt → DoH + State + Tamper-Service.
|
|
#[tauri::command]
|
|
async fn activate_protection(app: tauri::AppHandle) -> Result<UiState, String> {
|
|
let dns_token = auth::load_dns_token()
|
|
.ok_or_else(|| "Kein DNS-Token — bitte zuerst koppeln.".to_string())?;
|
|
|
|
let server_ip = setup::resolve_doh_ip();
|
|
let state = ProtectionState {
|
|
doh_template: ProtectionState::doh_template_for(&dns_token),
|
|
dns_token,
|
|
server_ip,
|
|
backend_url: backend_url(&app),
|
|
device_id: device::device_id(),
|
|
};
|
|
|
|
let service_exe = setup::service_exe_path()?;
|
|
let script = setup::build_setup_script(&state, &service_exe.display().to_string());
|
|
setup::run_elevated(&script)?;
|
|
|
|
let ui = build_state(&app).await;
|
|
if !ui.protection_applied {
|
|
return Err("Setup lief durch, aber der DoH-Schutz ist nicht aktiv. Bitte erneut versuchen.".into());
|
|
}
|
|
Ok(ui)
|
|
}
|
|
|
|
/// 24h-Cooldown starten. Teardown macht NICHT die App — der SYSTEM-Service
|
|
/// pollt /api/magic/status und baut erst ab wenn das Backend revoked.
|
|
#[tauri::command]
|
|
async fn request_release(app: tauri::AppHandle) -> Result<UiState, String> {
|
|
let token = auth::load_session_token().ok_or("Nicht eingeloggt")?;
|
|
let api = Api::new(&backend_url(&app));
|
|
api.request_release(&token, &device::device_id()).await?;
|
|
Ok(build_state(&app).await)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn cancel_release(app: tauri::AppHandle) -> Result<UiState, String> {
|
|
let token = auth::load_session_token().ok_or("Nicht eingeloggt")?;
|
|
let api = Api::new(&backend_url(&app));
|
|
api.cancel_release(&token, &device::device_id()).await?;
|
|
Ok(build_state(&app).await)
|
|
}
|
|
|
|
/// Logout entfernt nur die Session — Schutz + Service bleiben aktiv
|
|
/// (Deaktivierung geht ausschließlich über den 24h-Release-Flow).
|
|
#[tauri::command]
|
|
async fn logout(app: tauri::AppHandle) -> Result<UiState, String> {
|
|
auth::clear_all();
|
|
Ok(build_state(&app).await)
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.invoke_handler(tauri::generate_handler![
|
|
get_state,
|
|
pair_and_register,
|
|
activate_protection,
|
|
request_release,
|
|
cancel_release,
|
|
logout,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|
|
|