use std::sync::Arc;
use base64::{engine::general_purpose::STANDARD, Engine};
use chrono::Utc;
use reqwest::Client;
use crate::error::{Error, Result};
use crate::keys::generate_keypair;
use crate::types::*;
use crate::{RegistrationOptions, WarpCredentials};
use wireguard_netstack::WireGuardConfig;
const API_URL: &str = "https://api.cloudflareclient.com";
const API_VERSION: &str = "v0a2483";
const CF_CLIENT_VERSION: &str = "a-6.81-2410012252.0";
fn create_client(auth_token: Option<&str>, teams_jwt: Option<&str>) -> Result<Client> {
let tls_config = rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_protocol_versions(&[&rustls::version::TLS12])
.map_err(|e| Error::Tls(e.to_string()))?
.with_root_certificates(Arc::new(rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
}))
.with_no_client_auth();
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("CF-Client-Version", CF_CLIENT_VERSION.parse().unwrap());
headers.insert(
reqwest::header::ACCEPT,
"application/json; charset=UTF-8".parse().unwrap(),
);
if let Some(token) = auth_token {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", token).parse().unwrap(),
);
}
if let Some(jwt) = teams_jwt {
headers.insert(
"CF-Access-Jwt-Assertion",
jwt.parse()
.map_err(|_| Error::InvalidResponse("Invalid JWT token format".to_string()))?,
);
}
let builder = Client::builder()
.use_preconfigured_tls(tls_config)
.user_agent("1.1.1.1/6.81")
.default_headers(headers)
.http1_only();
builder.build().map_err(Error::from)
}
pub async fn register(options: RegistrationOptions) -> Result<(WireGuardConfig, WarpCredentials)> {
let (private_key, public_key) = generate_keypair();
let public_key_b64 = STANDARD.encode(public_key);
let is_teams = options.teams.is_some();
let teams_jwt = options.teams.as_ref().map(|t| t.jwt_token.as_str());
let client = create_client(None, teams_jwt)?;
let register_req = if let Some(ref teams) = options.teams {
log::info!("Registering device with Cloudflare for Teams (Zero Trust)...");
RegisterRequest {
fcm_token: String::new(),
install_id: String::new(),
key: public_key_b64,
locale: "en_US".to_string(),
model: options.device_model,
tos: None,
device_type: None,
name: Some(teams.device_name.clone().unwrap_or_default()),
serial_number: Some(teams.serial_number.clone().unwrap_or_default()),
}
} else {
let timestamp = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Nanos, true);
log::info!("Registering new device with Cloudflare WARP...");
RegisterRequest {
fcm_token: String::new(),
install_id: String::new(),
key: public_key_b64,
locale: "en_US".to_string(),
model: options.device_model,
tos: Some(timestamp),
device_type: Some("Android".to_string()),
name: None,
serial_number: None,
}
};
let resp: RegisterResponse = client
.post(format!("{}/{}/reg", API_URL, API_VERSION))
.json(®ister_req)
.send()
.await?
.error_for_status()?
.json()
.await?;
log::info!("Device registered successfully with ID: {}", resp.id);
let client_id = parse_client_id(resp.config.client_id.as_deref())?;
let mut credentials = WarpCredentials {
device_id: resp.id,
access_token: resp.token,
private_key,
license_key: resp.account.license,
client_id,
is_teams,
};
if !is_teams {
if let Some(ref license) = options.license_key {
log::info!("Applying Warp+ license key...");
update_license(&credentials, license).await?;
credentials.license_key = license.clone();
}
}
let (config, updated_client_id) = get_config_with_client_id(&credentials).await?;
if updated_client_id.is_some() {
credentials.client_id = updated_client_id;
}
Ok((config, credentials))
}
fn parse_client_id(client_id_b64: Option<&str>) -> Result<Option<[u8; 3]>> {
match client_id_b64 {
Some(s) if !s.is_empty() => {
let bytes = STANDARD
.decode(s)
.map_err(|e| Error::InvalidResponse(format!("Invalid client_id base64: {}", e)))?;
if bytes.len() >= 3 {
Ok(Some([bytes[0], bytes[1], bytes[2]]))
} else {
log::warn!(
"client_id has unexpected length {}, expected at least 3 bytes",
bytes.len()
);
Ok(None)
}
}
_ => Ok(None),
}
}
pub async fn get_config(credentials: &WarpCredentials) -> Result<WireGuardConfig> {
let (config, _) = get_config_with_client_id(credentials).await?;
Ok(config)
}
async fn get_config_with_client_id(
credentials: &WarpCredentials,
) -> Result<(WireGuardConfig, Option<[u8; 3]>)> {
let client = create_client(Some(&credentials.access_token), None)?;
log::info!(
"Fetching WARP configuration for device {}...",
credentials.device_id
);
let resp: GetSourceDeviceResponse = client
.get(format!(
"{}/{}/reg/{}",
API_URL, API_VERSION, credentials.device_id
))
.send()
.await?
.error_for_status()?
.json()
.await?;
let peer = resp
.config
.peers
.first()
.ok_or_else(|| Error::InvalidResponse("No peers in config".to_string()))?;
let peer_public_key: [u8; 32] = STANDARD
.decode(&peer.public_key)
.map_err(|e| Error::InvalidKey(e.to_string()))?
.try_into()
.map_err(|_| Error::InvalidKey("Invalid key length".to_string()))?;
let tunnel_ip = resp
.config
.interface
.addresses
.v4
.split('/')
.next()
.unwrap_or(&resp.config.interface.addresses.v4)
.parse()
.map_err(|_| Error::InvalidAddress(resp.config.interface.addresses.v4.clone()))?;
let endpoint_ip = peer.endpoint.v4
.rsplit_once(':')
.map(|(ip, _)| ip)
.unwrap_or(&peer.endpoint.v4);
let port = peer.endpoint.host
.rsplit_once(':')
.and_then(|(_, p)| p.parse::<u16>().ok())
.unwrap_or(2408);
let peer_endpoint = format!("{}:{}", endpoint_ip, port)
.parse()
.map_err(|_| Error::InvalidEndpoint(peer.endpoint.v4.clone()))?;
let client_id = parse_client_id(resp.config.client_id.as_deref())?;
log::info!(
"Configuration retrieved: tunnel_ip={}, endpoint={}, client_id={:?}",
tunnel_ip,
peer_endpoint,
client_id.map(|id| format!("0x{:02x}{:02x}{:02x}", id[0], id[1], id[2]))
);
let config = WireGuardConfig {
private_key: credentials.private_key,
peer_public_key,
peer_endpoint,
tunnel_ip,
preshared_key: None,
keepalive_seconds: Some(25),
mtu: None, };
Ok((config, client_id))
}
pub async fn update_license(credentials: &WarpCredentials, license_key: &str) -> Result<()> {
let client = create_client(Some(&credentials.access_token), None)?;
let req = UpdateAccountRequest {
license: license_key.to_string(),
};
client
.put(format!(
"{}/{}/reg/{}/account",
API_URL, API_VERSION, credentials.device_id
))
.json(&req)
.send()
.await?
.error_for_status()?;
log::info!("License key updated successfully");
Ok(())
}