From 11d003aa905fc1cfe870517f2833d51db753d5a1 Mon Sep 17 00:00:00 2001 From: Patrick Lorio Date: Wed, 17 Dec 2025 16:24:40 -0800 Subject: [PATCH 1/8] Work on 0.17.0 release, adds https support --- Cargo.lock | 25 +- Cargo.toml | 5 +- packages/agent_cli/Cargo.toml | 6 +- packages/agent_cli/src/autorun.rs | 53 +- packages/agent_cli/src/main.rs | 71 +- packages/agent_cli/src/playit_secret.rs | 6 +- packages/agent_core/Cargo.toml | 6 +- packages/agent_core/src/agent_control/mod.rs | 11 +- .../agent_core/src/agent_control/platform.rs | 2 +- .../agent_core/src/agent_control/version.rs | 54 +- .../agent_core/src/network/origin_lookup.rs | 131 +- .../agent_core/src/network/udp/udp_clients.rs | 44 +- packages/api_client/Cargo.toml | 5 +- packages/api_client/src/api.rs | 2636 ++++++++++++----- packages/ping_monitor/Cargo.toml | 27 - 15 files changed, 2144 insertions(+), 938 deletions(-) delete mode 100644 packages/ping_monitor/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 977af9e..6eb7e74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1085,7 +1085,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "playit-agent-core" -version = "0.20.1" +version = "0.17.0" dependencies = [ "byteorder", "chrono", @@ -1123,9 +1123,10 @@ dependencies = [ [[package]] name = "playit-api-client" -version = "0.1.2" +version = "0.2.0" dependencies = [ "byteorder", + "chrono", "reqwest", "rustls", "serde", @@ -1138,7 +1139,7 @@ dependencies = [ [[package]] name = "playit-cli" -version = "0.16.5" +version = "0.17.0" dependencies = [ "clap", "crossterm", @@ -1162,24 +1163,6 @@ dependencies = [ "winres", ] -[[package]] -name = "playit-ping-monitor" -version = "0.1.0" -dependencies = [ - "dirs", - "hex", - "message-encoding", - "playit-agent-proto", - "playit-api-client", - "rand", - "serde", - "tokio", - "toml 0.9.8", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - [[package]] name = "portable-atomic" version = "1.11.1" diff --git a/Cargo.toml b/Cargo.toml index 63e45c1..18f1b94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,11 @@ members = [ "packages/agent_core", "packages/agent_proto", "packages/api_client", - "packages/ping_monitor", ] +resolver = "3" [workspace.package] -version = "0.16.5" +version = "0.17.0" [workspace.dependencies] tokio = { version = "1.48", features = ["full"] } @@ -35,4 +35,3 @@ strip = "debuginfo" opt-level = "z" lto = true codegen-units = 1 - diff --git a/packages/agent_cli/Cargo.toml b/packages/agent_cli/Cargo.toml index 242c211..c475dca 100644 --- a/packages/agent_cli/Cargo.toml +++ b/packages/agent_cli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "playit-cli" version.workspace = true -edition = "2021" +edition = "2024" authors = ["Patrick Lorio "] description = "Making it easy to play games with friends. Makes your server public" repository = "https://github.com/playit-cloud/playit-agent" @@ -28,9 +28,9 @@ serde_yaml = "0.9" crossterm = "0.28" dotenv = "0.15.0" -playit-agent-core = { path = "../agent_core", version = "0.20.1" } +playit-agent-core = { path = "../agent_core", version = "0.17.0" } playit-agent-proto = { path = "../agent_proto", version = "1.3.0" } -playit-api-client = { path = "../api_client", version = "0.1.2" } +playit-api-client = { path = "../api_client", version = "0.2.0" } # playit-ping-monitor = { path = "../ping_monitor" } [target.'cfg(windows)'.build-dependencies] diff --git a/packages/agent_cli/src/autorun.rs b/packages/agent_cli/src/autorun.rs index 1e91fcf..099b32d 100644 --- a/packages/agent_cli/src/autorun.rs +++ b/packages/agent_cli/src/autorun.rs @@ -2,7 +2,7 @@ use std::{fmt::Write, net::SocketAddr, sync::Arc, time::Duration}; use playit_agent_core::{ network::{ - origin_lookup::{OriginLookup, OriginResource}, + origin_lookup::{OriginLookup, OriginResource, OriginTarget}, tcp::tcp_settings::TcpSettings, udp::udp_settings::UdpSettings, }, @@ -13,7 +13,7 @@ use playit_agent_proto::PortProto; use playit_api_client::api::*; // use playit_ping_monitor::PingMonitor; -use crate::{playit_secret::PlayitSecret, ui::UI, CliError, API_BASE}; +use crate::{API_BASE, CliError, playit_secret::PlayitSecret, ui::UI}; pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliError> { let secret_code = secret.ensure_valid(ui).await?.get_or_setup(ui).await?; @@ -169,14 +169,15 @@ pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliErr .custom_domain .as_ref() .unwrap_or(&tunnel.assigned_domain); + let src = match tunnel.tunnel_type.as_deref() { Some("minecraft-java") => addr.clone(), + Some("https") => format!("https://{addr}"), _ => format!("{}:{}", addr, tunnel.port.from), }; - let dst = format!("{}:{}", tunnel.local_ip, tunnel.local_port); - if let Some(disabled) = tunnel.disabled { + let dst = format!("{}:{}", tunnel.local_ip, tunnel.local_port); writeln!(msg, "{} => {} (disabled)", src, dst).unwrap(); if disabled == AgentTunnelDisabled::BySystem { writeln!( @@ -186,18 +187,40 @@ pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliErr ) .unwrap(); } - } else if let Some(tunnel_type) = &tunnel.tunnel_type { - writeln!(msg, "{} => {} ({})", src, dst, tunnel_type).unwrap(); } else { - writeln!( - msg, - "{} => {} (proto: {:?}, port count: {})", - src, - dst, - tunnel.proto, - tunnel.port.to - tunnel.port.from - ) - .unwrap(); + let res = OriginResource::from_agent_tunnel(&tunnel); + + match res.target { + OriginTarget::Https { + ip, + http_port, + https_port, + } => { + writeln!( + msg, + "{} => {} (http: {}, https: {})", + src, ip, http_port, https_port + ) + .unwrap(); + } + OriginTarget::Port { ip, port } => { + if let Some(tunnel_type) = &tunnel.tunnel_type { + writeln!(msg, "{} => {}:{} ({})", src, ip, port, tunnel_type) + .unwrap(); + } else { + writeln!( + msg, + "{} => {}:{} (proto: {:?}, port count: {})", + src, + ip, + port, + tunnel.proto, + tunnel.port.to - tunnel.port.from + ) + .unwrap(); + } + } + } } } diff --git a/packages/agent_cli/src/main.rs b/packages/agent_cli/src/main.rs index 8cb33b9..429a595 100644 --- a/packages/agent_cli/src/main.rs +++ b/packages/agent_cli/src/main.rs @@ -3,10 +3,9 @@ use std::fmt::{Display, Formatter}; use std::sync::LazyLock; use std::time::Duration; -use clap::{arg, Command}; -use playit_agent_core::agent_control::platform::get_platform; -use playit_agent_core::agent_control::version::register_version; -use playit_agent_core::PROTOCOL_VERSION; +use clap::{Command, arg}; +use playit_agent_core::agent_control::platform::current_platform; +use playit_agent_core::agent_control::version::{help_register_version, register_platform}; use rand::Rng; use uuid::Uuid; @@ -14,11 +13,11 @@ use autorun::autorun; use playit_agent_core::agent_control::errors::SetupError; use playit_agent_core::utils::now_milli; use playit_api_client::http_client::HttpClientError; -use playit_api_client::{api::*, PlayitApi}; +use playit_api_client::{PlayitApi, api::*}; use playit_secret::PlayitSecret; use crate::signal_handle::get_signal_handle; -use crate::ui::{UISettings, UI}; +use crate::ui::{UI, UISettings}; pub static API_BASE: LazyLock = LazyLock::new(|| dotenv::var("API_BASE").unwrap_or("https://api.playit.gg".to_string())); @@ -38,19 +37,15 @@ async fn main() -> Result { let platform = if matches.get_flag("platform_docker") { Platform::Docker } else { - get_platform() + current_platform() }; - register_version(PlayitAgentVersion { - version: AgentVersion { - platform, - version: env!("CARGO_PKG_VERSION").to_string(), - has_expired: false, - }, - official: true, - details_website: None, - proto_version: PROTOCOL_VERSION, - }); + register_platform(platform); + + help_register_version( + env!("CARGO_PKG_VERSION"), + "308943e8-faef-4835-a2ba-270351f72aa3", + ); } let mut secret = PlayitSecret::from_args(&matches).await; @@ -78,7 +73,7 @@ async fn main() -> Result { (true, None) => { let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stdout()); tracing_subscriber::fmt() - .with_ansi(get_platform() == Platform::Linux) + .with_ansi(current_platform() == Platform::Linux) .with_writer(non_blocking) .init(); Some(guard) @@ -169,45 +164,11 @@ async fn main() -> Result { .expect("invalid wait value"); let secret_key = - claim_exchange(&mut ui, claim_code, AgentType::SelfManaged, wait).await?; + claim_exchange(&mut ui, claim_code, ClaimAgentType::SelfManaged, wait).await?; ui.write_screen(secret_key).await; } _ => return Err(CliError::NotImplemented), }, - Some(("tunnels", m)) => match m.subcommand() { - Some(("prepare", m)) => { - unimplemented!() - // let api = secret.create_api().await?; - - // let name = m.get_one::("NAME").cloned(); - // let tunnel_type: Option = m.get_one::("TUNNEL_TYPE") - // .and_then(|v| serde_json::from_str(&format!("{:?}", v)).ok()); - // let port_type = serde_json::from_str::(&format!("{:?}", m.get_one::("PORT_TYPE").expect("required"))) - // .map_err(|_| CliError::InvalidPortType)?; - // let port_count = m.get_one::("PORT_COUNT").expect("required") - // .parse::().map_err(|_| CliError::InvalidPortCount)?; - // let exact = m.get_flag("exact"); - // let ignore_name = m.get_flag("ignore_name"); - - // let tunnel_id = tunnels_prepare( - // &api, name, tunnel_type, port_type, - // port_count, exact, ignore_name, - // ).await?; - - // println!("{}", tunnel_id); - } - Some(("list", _)) => { - let api = secret.create_api().await?; - let response = api - .tunnels_list_json(ReqTunnelsList { - tunnel_id: None, - agent_id: None, - }) - .await?; - println!("{}", serde_json::to_string_pretty(&response).unwrap()); - } - _ => return Err(CliError::NotImplemented), - }, _ => return Err(CliError::NotImplemented), } @@ -216,7 +177,7 @@ async fn main() -> Result { pub fn claim_generate() -> String { let mut buffer = [0u8; 5]; - rand::thread_rng().fill(&mut buffer); + rand::rng().fill(&mut buffer); hex::encode(&buffer) } @@ -231,7 +192,7 @@ pub fn claim_url(code: &str) -> Result { pub async fn claim_exchange( ui: &mut UI, claim_code: &str, - agent_type: AgentType, + agent_type: ClaimAgentType, wait_sec: u32, ) -> Result { let api = PlayitApi::create(API_BASE.to_string(), None); diff --git a/packages/agent_cli/src/playit_secret.rs b/packages/agent_cli/src/playit_secret.rs index 7c97dde..1de0fdf 100644 --- a/packages/agent_cli/src/playit_secret.rs +++ b/packages/agent_cli/src/playit_secret.rs @@ -1,11 +1,11 @@ use std::time::Duration; use clap::ArgMatches; -use playit_api_client::{api::*, PlayitApi}; +use playit_api_client::{PlayitApi, api::*}; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; -use crate::{claim_exchange, claim_generate, ui::UI, CliError, API_BASE}; +use crate::{API_BASE, CliError, claim_exchange, claim_generate, ui::UI}; pub struct PlayitSecret { secret: RwLock>, @@ -145,7 +145,7 @@ impl PlayitSecret { } let claim_code = claim_generate(); - let secret = claim_exchange(ui, &claim_code, AgentType::Assignable, 0).await?; + let secret = claim_exchange(ui, &claim_code, ClaimAgentType::Assignable, 0).await?; { let mut lock = self.secret.write().await; diff --git a/packages/agent_core/Cargo.toml b/packages/agent_core/Cargo.toml index e5b7ce5..b8f3d2d 100644 --- a/packages/agent_core/Cargo.toml +++ b/packages/agent_core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "playit-agent-core" -version = "0.20.1" -edition = "2021" +version.workspace = true +edition = "2024" description = "Contains the logic to create a playit.gg agent" license = "BSD-2-Clause" repository = "https://github.com/playit-cloud/playit-agent" @@ -24,7 +24,7 @@ futures-util = { workspace = true } message-encoding = { workspace = true } playit-agent-proto = { path = "../agent_proto", version = "1.3.0" } -playit-api-client = { path = "../api_client", version = "0.1.2" } +playit-api-client = { path = "../api_client", version = "0.2.0" } governor = "0.10.0" crossbeam = "0.8.4" diff --git a/packages/agent_core/src/agent_control/mod.rs b/packages/agent_core/src/agent_control/mod.rs index d6c41c3..462ab9b 100644 --- a/packages/agent_core/src/agent_control/mod.rs +++ b/packages/agent_core/src/agent_control/mod.rs @@ -1,7 +1,7 @@ use std::{ future::Future, net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, - sync::{atomic::AtomicUsize, Arc}, + sync::{Arc, atomic::AtomicUsize}, task::Poll, }; @@ -12,11 +12,11 @@ use version::get_version; pub use playit_api_client::api::SignedAgentKey; use playit_api_client::{ - api::{ReqAgentsRoutingGet, ReqProtoRegister}, PlayitApi, + api::{ReqAgentsRoutingGet, ReqProtoRegister}, }; -use crate::utils::error_helper::ErrorHelper; +use crate::{agent_control::platform::current_platform, utils::error_helper::ErrorHelper}; pub mod errors; @@ -243,9 +243,12 @@ impl AuthResource for AuthApi { let res = self .client .proto_register(ReqProtoRegister { - agent_version: get_version(), + agent_version: None, client_addr: pong.client_addr, tunnel_addr: pong.tunnel_addr, + proto_version: 2, + version: get_version(), + platform: current_platform(), }) .await .with_error(|error| tracing::error!(?error, "failed to sign and register"))?; diff --git a/packages/agent_core/src/agent_control/platform.rs b/packages/agent_core/src/agent_control/platform.rs index d5fe956..4b5f3d2 100644 --- a/packages/agent_core/src/agent_control/platform.rs +++ b/packages/agent_core/src/agent_control/platform.rs @@ -1,6 +1,6 @@ use playit_api_client::api::Platform; -pub fn get_platform() -> Platform { +pub fn current_platform() -> Platform { #[cfg(target_os = "windows")] return Platform::Windows; diff --git a/packages/agent_core/src/agent_control/version.rs b/packages/agent_core/src/agent_control/version.rs index bac36d3..472ed36 100644 --- a/packages/agent_core/src/agent_control/version.rs +++ b/packages/agent_core/src/agent_control/version.rs @@ -1,28 +1,50 @@ use std::sync::OnceLock; -use playit_api_client::api::{AgentVersion, PlayitAgentVersion}; +use playit_api_client::api::{AgentVersion, Platform}; +use std::str::FromStr; +use uuid::Uuid; -use crate::PROTOCOL_VERSION; +use crate::agent_control; -use super::platform::get_platform; +pub static AGENT_VERSION: OnceLock = OnceLock::new(); +pub static PLATFORM: OnceLock = OnceLock::new(); -pub static AGENT_VERSION: OnceLock = OnceLock::new(); +pub fn get_platform() -> Platform { + PLATFORM + .get_or_init(|| agent_control::current_platform()) + .clone() +} -pub fn register_version(version: PlayitAgentVersion) { - AGENT_VERSION.get_or_init(|| version); +pub fn register_platform(platform: Platform) { + PLATFORM.get_or_init(|| platform); +} + +pub fn get_version() -> AgentVersion { + help_register_version( + env!("CARGO_PKG_VERSION"), + "308943e8-faef-4835-a2ba-270351f72aa3", + ) } -pub fn get_version() -> PlayitAgentVersion { +pub fn help_register_version(v_str: &str, variant_id: &str) -> AgentVersion { AGENT_VERSION - .get_or_init(|| PlayitAgentVersion { - version: AgentVersion { - platform: get_platform(), - version: env!("CARGO_PKG_VERSION").to_string(), - has_expired: false, - }, - official: true, - details_website: None, - proto_version: PROTOCOL_VERSION, + .get_or_init(|| { + let mut parts = v_str.split("-").next().unwrap().split("."); + + let major = u32::from_str(parts.next().unwrap()).unwrap(); + let minor = u32::from_str(parts.next().unwrap()).unwrap(); + let patch = u32::from_str(parts.next().unwrap()).unwrap(); + + AgentVersion { + variant_id: Uuid::from_str(variant_id).expect("variant id must be UUID"), + version_major: major, + version_minor: minor, + version_patch: patch, + } }) .clone() } + +pub fn register_version(version: AgentVersion) { + AGENT_VERSION.get_or_init(|| version); +} diff --git a/packages/agent_core/src/network/origin_lookup.rs b/packages/agent_core/src/network/origin_lookup.rs index 29a6fac..b6b3cf9 100644 --- a/packages/agent_core/src/network/origin_lookup.rs +++ b/packages/agent_core/src/network/origin_lookup.rs @@ -1,7 +1,11 @@ -use std::{collections::HashMap, net::SocketAddr}; +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, + str::FromStr, +}; use playit_agent_proto::PortProto; -use playit_api_client::api::{AgentRunData, PortType, ProxyProtocol}; +use playit_api_client::api::{AgentRunData, AgentTunnel, PortType, ProxyProtocol, TunnelType}; use tokio::sync::RwLock; #[derive(Default)] @@ -11,17 +15,12 @@ pub struct OriginLookup { impl OriginLookup { pub async fn update_from_run_data(&self, run_data: &AgentRunData) { - self.update(run_data.tunnels.iter().map(|tunn| OriginResource { - tunnel_id: tunn.internal_id, - proto: match tunn.proto { - PortType::Tcp => PortProto::Tcp, - PortType::Udp => PortProto::Udp, - PortType::Both => PortProto::Both, - }, - local_addr: SocketAddr::new(tunn.local_ip, tunn.local_port), - port_count: tunn.port.to - tunn.port.from, - proxy_protocol: tunn.proxy_protocol, - })) + self.update( + run_data + .tunnels + .iter() + .map(OriginResource::from_agent_tunnel), + ) .await; } @@ -88,22 +87,108 @@ struct Key { pub struct OriginResource { pub tunnel_id: u64, pub proto: PortProto, - pub local_addr: SocketAddr, + pub target: OriginTarget, pub port_count: u16, pub proxy_protocol: Option, } +#[derive(Debug, Clone)] +pub enum OriginTarget { + Https { + ip: IpAddr, + http_port: u16, + https_port: u16, + }, + Port { + ip: IpAddr, + port: u16, + }, +} + impl OriginResource { + pub fn from_agent_tunnel(tunn: &AgentTunnel) -> Self { + let tunnel_type = tunn + .tunnel_type + .clone() + .and_then(|v| serde_json::from_value::(serde_json::Value::String(v)).ok()); + + let target = match tunnel_type { + Some(TunnelType::Https) => OriginTarget::Https { + ip: tunn + .agent_config + .fields + .iter() + .find(|f| f.name.eq("local_ip")) + .and_then(|v| IpAddr::from_str(&v.value).ok()) + .unwrap_or_else(|| "127.0.0.1".parse().unwrap()), + http_port: tunn + .agent_config + .fields + .iter() + .find(|f| f.name.eq("http_port")) + .and_then(|v| u16::from_str(&v.value).ok()) + .unwrap_or(80), + https_port: tunn + .agent_config + .fields + .iter() + .find(|f| f.name.eq("https_port")) + .and_then(|v| u16::from_str(&v.value).ok()) + .unwrap_or(443), + }, + _ => OriginTarget::Port { + ip: tunn + .agent_config + .fields + .iter() + .find(|f| f.name.eq("local_ip")) + .and_then(|v| IpAddr::from_str(&v.value).ok()) + .unwrap_or_else(|| "127.0.0.1".parse().unwrap()), + port: tunn + .agent_config + .fields + .iter() + .find(|f| f.name.eq("local_port")) + .and_then(|v| u16::from_str(&v.value).ok()) + .unwrap_or(tunn.local_port), + }, + }; + + OriginResource { + tunnel_id: tunn.internal_id, + proto: match tunn.proto { + PortType::Tcp => PortProto::Tcp, + PortType::Udp => PortProto::Udp, + PortType::Both => PortProto::Both, + }, + target, + port_count: tunn.port.to - tunn.port.from, + proxy_protocol: tunn.proxy_protocol, + } + } + pub fn resolve_local(&self, port_offset: u16) -> Option { - if port_offset == 0 { - Some(self.local_addr) - } else if port_offset < self.port_count { - Some(SocketAddr::new( - self.local_addr.ip(), - self.local_addr.port() + port_offset, - )) - } else { - None + match &self.target { + OriginTarget::Https { + ip, + http_port, + https_port, + } => { + if port_offset == 0 { + Some(SocketAddr::new(*ip, *http_port)) + } else if port_offset == 1 { + Some(SocketAddr::new(*ip, *https_port)) + } else { + None + } + } + OriginTarget::Port { ip, port } => { + if self.port_count <= port_offset { + return None; + } + + Some(SocketAddr::new(*ip, *port + port_offset)) + } } } } diff --git a/packages/agent_core/src/network/udp/udp_clients.rs b/packages/agent_core/src/network/udp/udp_clients.rs index f909d3f..d375c52 100644 --- a/packages/agent_core/src/network/udp/udp_clients.rs +++ b/packages/agent_core/src/network/udp/udp_clients.rs @@ -1,5 +1,5 @@ use std::{ - collections::{hash_map, HashMap}, + collections::{HashMap, hash_map}, net::{IpAddr, SocketAddr, SocketAddrV4}, num::NonZeroU32, sync::Arc, @@ -10,11 +10,13 @@ use playit_api_client::api::ProxyProtocol; use slab::Slab; use tokio::{ net::UdpSocket, - sync::mpsc::{channel, Receiver}, + sync::mpsc::{Receiver, channel}, }; use crate::network::{ - lan_address::LanAddress, origin_lookup::OriginLookup, proxy_protocol::ProxyProtocolHeader, + lan_address::LanAddress, + origin_lookup::{OriginLookup, OriginTarget}, + proxy_protocol::ProxyProtocolHeader, }; use playit_agent_proto::udp_proto::UdpFlow; @@ -140,17 +142,25 @@ impl UdpClients { return None; }; - if tunnel.local_addr.ip() != IpAddr::V4(*source.ip()) { + let OriginTarget::Port { + ip: local_ip, + port: port_start, + } = tunnel.target + else { + return None; + }; + + if local_ip != IpAddr::V4(*source.ip()) { udp_errors().origin_reject_addr_differ.inc(); return None; } - if source.port() < tunnel.local_addr.port() { + if source.port() < port_start { udp_errors().origin_reject_port_too_low.inc(); return None; } - let port_offset = source.port() - tunnel.local_addr.port(); + let port_offset = source.port() - port_start; if tunnel.port_count <= port_offset { udp_errors().origin_reject_port_too_high.inc(); return None; @@ -195,16 +205,19 @@ impl UdpClients { tunnel_id: extension.tunnel_id.get(), }; - let target_addr = if extension.port_offset == 0 { - let SocketAddr::V4(addr) = origin.local_addr else { - return; - }; - addr - } else { - let IpAddr::V4(ip) = origin.local_addr.ip() else { + let OriginTarget::Port { + ip: local_ip, + port: port_start, + } = origin.target + else { + return; + }; + + let target_addr = { + let IpAddr::V4(ip) = local_ip else { return; }; - SocketAddrV4::new(ip, origin.local_addr.port() + extension.port_offset) + SocketAddrV4::new(ip, port_start + extension.port_offset) }; match self.virtual_client_lookup.entry(key) { @@ -229,8 +242,7 @@ impl UdpClients { return; } - let special_lan = - origin.local_addr.ip().is_loopback() && origin.proxy_protocol.is_none(); + let special_lan = local_ip.is_loopback() && origin.proxy_protocol.is_none(); let socket = match v.key().create_socket(special_lan).await { Ok(socket) => Arc::new(socket), diff --git a/packages/api_client/Cargo.toml b/packages/api_client/Cargo.toml index 7db9c15..c7f81bd 100644 --- a/packages/api_client/Cargo.toml +++ b/packages/api_client/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "playit-api-client" -version = "0.1.2" -edition = "2021" +version = "0.2.0" +edition = "2024" description = "Contains the logic to create a playit.gg agent" license = "BSD-2-Clause" repository = "https://github.com/playit-cloud/playit-agent" @@ -15,6 +15,7 @@ serde_json = { workspace = true } serde = { workspace = true } uuid = { workspace = true } byteorder = { workspace = true } +chrono = { workspace = true } # use rustls with "ring" and not "aws-lc-rs", having trouble cross compiling "aws-lc-rs" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "gzip"] } diff --git a/packages/api_client/src/api.rs b/packages/api_client/src/api.rs index 92cc87e..5e16bf5 100644 --- a/packages/api_client/src/api.rs +++ b/packages/api_client/src/api.rs @@ -1,220 +1,329 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] impl PlayitApiClient { - pub fn new(client: C) -> Self { - PlayitApiClient { client } - } - pub fn get_client(&self) -> &C { - &self.client - } - fn unwrap(res: Result, C::Error>) -> Result> { - match res { - Ok(ApiResult::Success(v)) => Ok(v), - Ok(ApiResult::Fail(fail)) => Err(ApiError::Fail(fail)), - Ok(ApiResult::Error(error)) => Err(ApiError::ApiError(error)), - Err(error) => Err(ApiError::ClientError(error)), - } - } - fn unwrap_no_fail( - res: Result, C::Error>, - ) -> Result> { - match res { - Ok(ApiResult::Success(v)) => Ok(v), - Ok(ApiResult::Fail(_)) => panic!(), - Ok(ApiResult::Error(error)) => Err(ApiErrorNoFail::ApiError(error)), - Err(error) => Err(ApiErrorNoFail::ClientError(error)), - } - } - #[track_caller] - pub fn tunnels_create( - &self, - req: ReqTunnelsCreate, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/tunnels/create", req).await) } - } - #[track_caller] - pub fn tunnels_delete( - &self, - req: ReqTunnelsDelete, - ) -> impl std::future::Future>> + '_ { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/tunnels/delete", req).await) } - } - #[track_caller] - pub fn claim_details( - &self, - req: ReqClaimDetails, - ) -> impl std::future::Future< - Output = Result>, - > + '_ { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/claim/details", req).await) } - } - #[track_caller] - pub fn claim_setup( - &self, - req: ReqClaimSetup, - ) -> impl std::future::Future< - Output = Result>, - > + '_ { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/claim/setup", req).await) } - } - #[track_caller] - pub fn claim_exchange( - &self, - req: ReqClaimExchange, - ) -> impl std::future::Future< - Output = Result>, - > + '_ { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/claim/exchange", req).await) } - } - #[track_caller] - pub fn claim_accept( - &self, - req: ReqClaimAccept, - ) -> impl std::future::Future>> - + '_ { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/claim/accept", req).await) } - } - #[track_caller] - pub fn claim_reject( - &self, - req: ReqClaimReject, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/claim/reject", req).await) } - } - #[track_caller] - pub fn proto_register( - &self, - req: ReqProtoRegister, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { Self::unwrap_no_fail(self.client.call(caller, "/proto/register", req).await) } - } - #[track_caller] - pub fn login_guest( - &self, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { - Self::unwrap( - self.client - .call(caller, "/login/guest", ReqLoginGuest {}) - .await, - ) - } - } - #[track_caller] - pub fn agents_routing_get( - &self, - req: ReqAgentsRoutingGet, - ) -> impl std::future::Future< - Output = Result>, - > + '_ { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/agents/routing/get", req).await) } - } - #[track_caller] - pub fn agents_rundata( - &self, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { - Self::unwrap_no_fail( - self.client - .call(caller, "/agents/rundata", ReqAgentsRundata {}) - .await, - ) - } - } - #[track_caller] - pub fn ping_submit( - &self, - req: ReqPingSubmit, - ) -> impl std::future::Future>> + '_ { - let caller = std::panic::Location::caller(); - async { Self::unwrap_no_fail(self.client.call(caller, "/ping/submit", req).await) } - } - #[track_caller] - pub fn ping_get( - &self, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { Self::unwrap_no_fail(self.client.call(caller, "/ping/get", ReqPingGet {}).await) } - } - #[track_caller] - pub fn tunnels_list_json( - &self, - req: ReqTunnelsList, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { Self::unwrap_no_fail(self.client.call(caller, "/tunnels/list", req).await) } - } - #[track_caller] - pub fn agents_list_json( - &self, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { - Self::unwrap_no_fail( - self.client - .call(caller, "/agents/list", ReqAgentsList {}) - .await, - ) - } - } - #[track_caller] - pub fn query_region( - &self, - req: ReqQueryRegion, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/query/region", req).await) } - } - #[track_caller] - pub fn tunnels_update( - &self, - req: ReqTunnelsUpdate, - ) -> impl std::future::Future>> + '_ { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/tunnels/update", req).await) } - } - #[track_caller] - pub fn tunnels_firewall_assign( - &self, - req: ReqTunnelsFirewallAssign, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { - Self::unwrap( - self.client - .call(caller, "/tunnels/firewall/assign", req) - .await, - ) - } - } - #[track_caller] - pub fn tunnels_proxy_set( - &self, - req: ReqTunnelsProxySet, - ) -> impl std::future::Future>> + '_ - { - let caller = std::panic::Location::caller(); - async { Self::unwrap(self.client.call(caller, "/tunnels/proxy/set", req).await) } - } + pub fn new(client: C) -> Self { + PlayitApiClient { client } + } + pub fn get_client(&self) -> &C { + &self.client + } + fn unwrap(res: Result, C::Error>) -> Result> { + match res { + Ok(ApiResult::Success(v)) => Ok(v), + Ok(ApiResult::Fail(fail)) => Err(ApiError::Fail(fail)), + Ok(ApiResult::Error(error)) => Err(ApiError::ApiError(error)), + Err(error) => Err(ApiError::ClientError(error)), + } + } + fn unwrap_no_fail(res: Result, C::Error>) -> Result> { + match res { + Ok(ApiResult::Success(v)) => Ok(v), + Ok(ApiResult::Fail(_)) => panic!(), + Ok(ApiResult::Error(error)) => Err(ApiErrorNoFail::ApiError(error)), + Err(error) => Err(ApiErrorNoFail::ClientError(error)), + } + } + #[track_caller] + pub fn v1_tunnels_list(&self) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/v1/tunnels/list", ReqTunnelsListV1 {}).await) + } + } + #[track_caller] + pub fn v1_tunnels_create(&self, req: ReqTunnelsCreateV1) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/v1/tunnels/create", req).await) + } + } + #[track_caller] + pub fn v1_schemas_get(&self, req: ReqSchemasGetV1) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/v1/schemas/get", req).await) + } + } + #[track_caller] + pub fn v1_tunnels_config(&self, req: ReqTunnelsConfigV1) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/v1/tunnels/config", req).await) + } + } + #[track_caller] + pub fn v1_tunnels_propset(&self, req: ReqTunnelsPropset) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/v1/tunnels/propset", req).await) + } + } + #[track_caller] + pub fn v1_agents_rundata(&self) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/v1/agents/rundata", ReqAgentsRundataV1 {}).await) + } + } + #[track_caller] + pub fn info_pops(&self) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/info/pops", ReqInfoPops {}).await) + } + } + #[track_caller] + pub fn login_signin(&self, req: ReqLoginSignin) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/login/signin", req).await) + } + } + #[track_caller] + pub fn login_clearcookie(&self) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/login/clearcookie", ReqLoginClearcookie {}).await) + } + } + #[track_caller] + pub fn login_create_guest(&self) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/login/create/guest", ReqLoginCreateGuest {}).await) + } + } + #[track_caller] + pub fn login_guest(&self) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/login/guest", ReqLoginGuest {}).await) + } + } + #[track_caller] + pub fn login_reset_password(&self, req: ReqLoginResetPassword) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/login/reset/password", req).await) + } + } + #[track_caller] + pub fn login_reset_send(&self, req: ReqLoginResetSend) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/login/reset/send", req).await) + } + } + #[track_caller] + pub fn tunnels_create(&self, req: ReqTunnelsCreate) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/tunnels/create", req).await) + } + } + #[track_caller] + pub fn tunnels_list(&self, req: ReqTunnelsList) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/tunnels/list", req).await) + } + } + #[track_caller] + pub fn tunnels_update(&self, req: ReqTunnelsUpdate) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/tunnels/update", req).await) + } + } + #[track_caller] + pub fn tunnels_delete(&self, req: ReqTunnelsDelete) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/tunnels/delete", req).await) + } + } + #[track_caller] + pub fn tunnels_rename(&self, req: ReqTunnelsRename) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/tunnels/rename", req).await) + } + } + #[track_caller] + pub fn tunnels_firewall_assign(&self, req: ReqTunnelsFirewallAssign) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/tunnels/firewall/assign", req).await) + } + } + #[track_caller] + pub fn tunnels_ratelimit(&self, req: ReqTunnelsRatelimit) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/tunnels/ratelimit", req).await) + } + } + #[track_caller] + pub fn tunnels_enable(&self, req: ReqTunnelsEnable) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/tunnels/enable", req).await) + } + } + #[track_caller] + pub fn tunnels_proxy_set(&self, req: ReqTunnelsProxySet) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/tunnels/proxy/set", req).await) + } + } + #[track_caller] + pub fn claim_setup(&self, req: ReqClaimSetup) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/claim/setup", req).await) + } + } + #[track_caller] + pub fn claim_exchange(&self, req: ReqClaimExchange) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/claim/exchange", req).await) + } + } + #[track_caller] + pub fn agents_rename(&self, req: ReqAgentsRename) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/agents/rename", req).await) + } + } + #[track_caller] + pub fn agents_routing_set(&self, req: ReqAgentsRoutingSet) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/agents/routing/set", req).await) + } + } + #[track_caller] + pub fn agents_routing_get(&self, req: ReqAgentsRoutingGet) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/agents/routing/get", req).await) + } + } + #[track_caller] + pub fn agents_rundata(&self) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/agents/rundata", ReqAgentsRundata {}).await) + } + } + #[track_caller] + pub fn domains_list(&self) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/domains/list", ReqDomainsList {}).await) + } + } + #[track_caller] + pub fn shop_prices(&self) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/shop/prices", ReqShopPrices {}).await) + } + } + #[track_caller] + pub fn shop_availability_custom_domain(&self, req: ReqShopAvailabilityCustomDomain) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap_no_fail(self.client.call(caller, "/shop/availability/custom_domain", req).await) + } + } + #[track_caller] + pub fn proto_register(&self, req: ReqProtoRegister) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/proto/register", req).await) + } + } + #[track_caller] + pub fn charge_get(&self, req: ReqChargeGet) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/charge/get", req).await) + } + } + #[track_caller] + pub fn charge_refund(&self, req: ReqChargeRefund) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/charge/refund", req).await) + } + } + #[track_caller] + pub fn query_region(&self, req: ReqQueryRegion) -> impl std::future::Future>> + '_ { + let caller = std::panic::Location::caller(); + async { + Self::unwrap(self.client.call(caller, "/query/region", req).await) + } + } +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "message")] +pub enum ApiResponseError { + #[serde(rename = "validation")] + Validation(String), + #[serde(rename = "path-not-found")] + PathNotFound(PathNotFound), + #[serde(rename = "auth")] + Auth(AuthError), + #[serde(rename = "internal")] + Internal(ApiInternalError), +} + + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct PathNotFound { + pub path: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AuthError { + AuthRequired, + InvalidHeader, + InvalidSignature, + InvalidTimestamp, + InvalidApiKey, + InvalidAgentKey, + SessionExpired, + InvalidAuthType, + ScopeNotAllowed, + NoLongerValid, + GuestAccountNotAllowed, + EmailMustBeVerified, + AccountDoesNotExist, + AdminOnly, + InvalidToken, + TotpRequred, + NotAllowedWithReadOnly, + DefaultAgentBlocked, + AgentNotSelfManaged, + SelfManagedAgentCanOnlyAffectSelf, + AccountNotAuthorized, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ApiInternalError { + pub trace_id: String, +} + +impl std::fmt::Display for ApiResponseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for ApiResponseError { } #[derive(serde::Serialize, serde::Deserialize, Debug)] @@ -241,7 +350,9 @@ impl std::fmt::Display for ApiError std::error::Error for ApiError {} +impl std::error::Error for ApiError { +} + #[derive(Debug, serde::Serialize)] pub enum ApiErrorNoFail { @@ -255,21 +366,15 @@ impl std::fmt::Display for ApiErrorNoFail { } } -impl std::error::Error for ApiErrorNoFail {} +impl std::error::Error for ApiErrorNoFail { +} + + pub trait PlayitHttpClient { type Error; - fn call< - Req: serde::Serialize + std::marker::Send, - Res: serde::de::DeserializeOwned, - Err: serde::de::DeserializeOwned, - >( - &self, - caller: &'static std::panic::Location<'static>, - path: &str, - req: Req, - ) -> impl std::future::Future, Self::Error>>; + fn call(&self, caller: &'static std::panic::Location<'static>, path: &str, req: Req) -> impl std::future::Future, Self::Error>>; } #[derive(Clone)] @@ -278,736 +383,1775 @@ pub struct PlayitApiClient { } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -#[serde(tag = "type", content = "message")] -pub enum ApiResponseError { - #[serde(rename = "validation")] - Validation(String), - #[serde(rename = "path-not-found")] - PathNotFound(PathNotFound), - #[serde(rename = "auth")] - Auth(AuthError), - #[serde(rename = "internal")] - Internal(ApiInternalError), +pub struct ReqTunnelsListV1 { } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct PathNotFound { - pub path: String, +pub struct AccountTunnelsV1 { + pub tunnels: Vec, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum AuthError { - AuthRequired, - InvalidHeader, - InvalidSignature, - InvalidTimestamp, - InvalidApiKey, - InvalidAgentKey, - SessionExpired, - InvalidAuthType, - ScopeNotAllowed, - NoLongerValid, - GuestAccountNotAllowed, - EmailMustBeVerified, - AccountDoesNotExist, - AdminOnly, - InvalidToken, - TotpRequred, - NotAllowedWithReadOnly, - AgentNotSelfManaged, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AccountTunnelV1 { + pub id: uuid::Uuid, + pub created_at: chrono::DateTime, + pub name: Option, + pub user_enabled: bool, + pub offline_reasons: Option>, + pub tunnel_type: Option, + pub port_type: PortType, + pub port_count: u16, + pub firewall_id: Option, + pub props: AccountTunnelProps, + pub origin: AccountTunnelOrigin, + pub port_allocation_requests: Vec, + pub public_allocations: Vec, + pub connect_addresses: Vec, +} + + + + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AccountTunnelOfflineReason { + OriginNotSet, + AgentDisabled, + AgentOverLimit, + TunnelDisabled, + PublicAllocationMissing, + PublicAllocationPending, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelType { + #[serde(rename = "minecraft-java")] + MinecraftJava, + #[serde(rename = "minecraft-bedrock")] + MinecraftBedrock, + #[serde(rename = "valheim")] + Valheim, + #[serde(rename = "terraria")] + Terraria, + #[serde(rename = "starbound")] + Starbound, + #[serde(rename = "rust")] + Rust, + #[serde(rename = "7days")] + Num7days, + #[serde(rename = "unturned")] + Unturned, + #[serde(rename = "https")] + Https, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum PortType { + #[serde(rename = "tcp")] + Tcp, + #[serde(rename = "udp")] + Udp, + #[serde(rename = "both")] + Both, +} + + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AccountTunnelProps { + pub hostname_verify_level: HostnameVerifyLevel, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum HostnameVerifyLevel { + None, + NoRawIp, + NoAutoName, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ApiInternalError { - pub trace_id: String, +#[serde(tag = "type", content = "details")] +pub enum AccountTunnelOrigin { + #[serde(rename = "not-set")] + NotSet(TunnelOriginNotSet), + #[serde(rename = "agent")] + Agent(TunnelToAgent), } -impl std::fmt::Display for ApiResponseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct TunnelOriginNotSet { + pub agent_config: Option, } -impl std::error::Error for ApiResponseError {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqTunnelsCreate { - pub name: Option, - pub tunnel_type: Option, - pub port_type: PortType, - pub port_count: u16, - pub origin: TunnelOriginCreate, - pub enabled: bool, - pub alloc: Option, - pub firewall_id: Option, - pub proxy_protocol: Option, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum TunnelType { - #[serde(rename = "minecraft-java")] - MinecraftJava, - #[serde(rename = "minecraft-bedrock")] - MinecraftBedrock, - #[serde(rename = "valheim")] - Valheim, - #[serde(rename = "terraria")] - Terraria, - #[serde(rename = "starbound")] - Starbound, - #[serde(rename = "rust")] - Rust, - #[serde(rename = "7days")] - Num7days, - #[serde(rename = "unturned")] - Unturned, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum PortType { - #[serde(rename = "tcp")] - Tcp, - #[serde(rename = "udp")] - Udp, - #[serde(rename = "both")] - Both, +pub struct HasAgentConfig { + pub config_schema_id: uuid::Uuid, + pub config_data: AgentTunnelConfig, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -#[serde(tag = "type", content = "data")] -pub enum TunnelOriginCreate { - #[serde(rename = "default")] - Default(AssignedDefaultCreate), - #[serde(rename = "agent")] - Agent(AssignedAgentCreate), - #[serde(rename = "managed")] - Managed(AssignedManagedCreate), +pub struct AgentTunnelConfig { + pub fields: Vec, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AssignedDefaultCreate { - pub local_ip: std::net::IpAddr, - pub local_port: Option, +pub struct AgentTunnelAttr { + pub name: String, + pub value: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AssignedAgentCreate { - pub agent_id: uuid::Uuid, - pub local_ip: std::net::IpAddr, - pub local_port: Option, +pub struct TunnelToAgent { + pub agent_id: uuid::Uuid, + pub name: String, + pub config_schema_id: uuid::Uuid, + pub config_data: AgentTunnelConfig, + pub config_invalid: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AssignedManagedCreate { - pub agent_id: Option, +pub struct InvalidTunnelConfig { + pub agent_schema_id: uuid::Uuid, + pub current_schema: AgentTunnelSchema, + pub target_schema: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -#[serde(tag = "type", content = "details")] -pub enum TunnelCreateUseAllocation { - #[serde(rename = "dedicated-ip")] - DedicatedIp(UseAllocDedicatedIp), - #[serde(rename = "port-allocation")] - PortAllocation(UseAllocPortAlloc), - #[serde(rename = "region")] - Region(UseRegion), +pub struct AgentTunnelSchema { + pub fields: std::collections::HashMap, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct UseAllocDedicatedIp { - pub ip_hostname: String, - pub port: Option, +pub struct AgentTunnelSchemaField { + pub label: Option, + pub description: Option, + pub value_type: AgentTunnelAttrType, + pub allow_null: bool, + pub default_value: Option, + pub variants: Option>, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AgentTunnelAttrType { + Ip, + Ip4, + Ip6, + SockAddr, + SockAddr4, + SockAddr6, + Port, + U64, + I64, + Boolean, + String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct UseAllocPortAlloc { - pub alloc_id: uuid::Uuid, +pub struct PortAllocationRequest { + pub id: uuid::Uuid, + pub status: PortAllocationStatus, + pub region: PlayitNetwork, + pub public_port: Option, + pub public_ip: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum PortAllocationStatus { + Pending, + RanOutOfPorts, + PublicPortNotAvailable, + NoPortsAvailableOnIp, + AccountPortLimitReached, + #[serde(rename = "Other#catch_all")] + OtherCatchAll, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum PlayitNetwork { + #[serde(rename = "global")] + Global, + #[serde(rename = "north-america")] + NorthAmerica, + #[serde(rename = "europe")] + Europe, + #[serde(rename = "asia")] + Asia, + #[serde(rename = "india")] + India, + #[serde(rename = "south-america")] + SouthAmerica, + #[serde(rename = "chile")] + Chile, + #[serde(rename = "seattle-washington")] + SeattleWashington, + #[serde(rename = "los-angeles-california")] + LosAngelesCalifornia, + #[serde(rename = "denver-colorado")] + DenverColorado, + #[serde(rename = "dallas-texas")] + DallasTexas, + #[serde(rename = "chicago-illinois")] + ChicagoIllinois, + #[serde(rename = "new-york")] + NewYork, + #[serde(rename = "_NaReserved1")] + NaReserved1, + #[serde(rename = "_NaReserved2")] + NaReserved2, + #[serde(rename = "united-kingdom")] + UnitedKingdom, + #[serde(rename = "germany")] + Germany, + #[serde(rename = "sweden")] + Sweden, + #[serde(rename = "poland")] + Poland, + #[serde(rename = "romania")] + Romania, + #[serde(rename = "_Test")] + Test, + #[serde(rename = "japan")] + Japan, + #[serde(rename = "australia")] + Australia, } + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct UseRegion { - pub region: AllocationRegion, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum AllocationRegion { - #[serde(rename = "smart-global")] - SmartGlobal, - #[serde(rename = "global")] - Global, - #[serde(rename = "north-america")] - NorthAmerica, - #[serde(rename = "europe")] - Europe, - #[serde(rename = "asia")] - Asia, - #[serde(rename = "india")] - India, - #[serde(rename = "south-america")] - SouthAmerica, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum ProxyProtocol { - #[serde(rename = "proxy-protocol-v1")] - ProxyProtocolV1, - #[serde(rename = "proxy-protocol-v2")] - ProxyProtocolV2, +#[serde(tag = "type", content = "details")] +pub enum PublicAllocation { + #[serde(rename = "PortAllocation")] + PortAllocation(PortAllocation), + #[serde(rename = "HostnameRouting")] + HostnameRouting(HostnameRouting), } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ObjectId { - pub id: uuid::Uuid, +pub struct PortAllocation { + pub alloc_id: uuid::Uuid, + pub ip_region: PlayitNetwork, + pub ip_hostname: String, + pub auto_domain: String, + pub ip: std::net::IpAddr, + pub port: u16, + pub port_count: u16, + pub port_type: PortType, + pub expire_notice: Option, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum TunnelCreateError { - AgentIdRequired, - AgentNotFound, - InvalidAgentId, - DedicatedIpNotFound, - DedicatedIpPortNotAvailable, - DedicatedIpNotEnoughSpace, - PortAllocNotFound, - InvalidIpHostname, - ManagedMissingAgentId, - InvalidPortCount, - RequiresVerifiedAccount, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ExpireNotice { + pub disable_at: chrono::DateTime, + pub remove_at: chrono::DateTime, + pub reason: DisabledReason, } -impl std::fmt::Display for TunnelCreateError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum DisabledReason { + #[serde(rename = "requires-premium")] + RequiresPremium, + #[serde(rename = "over-port-limit")] + OverPortLimit, } -impl std::error::Error for TunnelCreateError {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqTunnelsDelete { - pub tunnel_id: uuid::Uuid, +pub struct HostnameRouting { + pub id: Option, + pub hostname: String, + pub routing_type: HostnameRoutingType, + pub region: PlayitNetwork, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum DeleteError { - TunnelNotFound, +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum HostnameRoutingType { + #[serde(rename = "https")] + Https, + #[serde(rename = "minecraft-java")] + MinecraftJava, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqClaimDetails { - pub code: String, +#[serde(tag = "type", content = "value")] +pub enum ConnectAddress { + #[serde(rename = "addr4")] + Addr4(ConnectAddr4), + #[serde(rename = "addr6")] + Addr6(ConnectAddr6), + #[serde(rename = "ip4")] + Ip4(ConnectIp4), + #[serde(rename = "ip6")] + Ip6(ConnectIp6), + #[serde(rename = "auto")] + Auto(ConnectAutoName), + #[serde(rename = "domain")] + Domain(ConnectDomain), } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AgentClaimDetails { - pub name: String, - pub remote_ip: std::net::IpAddr, - pub agent_type: AgentType, - pub version: String, +pub struct ConnectAddr4 { + pub address: std::net::SocketAddrV4, + pub source: ConnectAddressSource, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum AgentType { - #[serde(rename = "default")] - Default, - #[serde(rename = "assignable")] - Assignable, - #[serde(rename = "self-managed")] - SelfManaged, + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "resource", content = "id")] +pub enum ConnectAddressSource { + #[serde(rename = "port-allocation")] + PortAllocation(uuid::Uuid), + #[serde(rename = "hostname-routing")] + HostnameRouting(uuid::Uuid), } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum ClaimDetailsError { - AlreadyClaimed, - AlreadyRejected, - ClaimExpired, - DifferentOwner, - WaitingForAgent, - InvalidCode, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ConnectAddr6 { + pub address: std::net::SocketAddrV6, + pub source: ConnectAddressSource, } -impl std::fmt::Display for ClaimDetailsError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ConnectIp4 { + pub address: std::net::Ipv4Addr, + pub default_port: u16, + pub source: ConnectAddressSource, } -impl std::error::Error for ClaimDetailsError {} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqClaimSetup { - pub code: String, - pub agent_type: AgentType, - pub version: String, +pub struct ConnectIp6 { + pub address: std::net::Ipv6Addr, + pub default_port: u16, + pub source: ConnectAddressSource, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum ClaimSetupResponse { - WaitingForUserVisit, - WaitingForUser, - UserAccepted, - UserRejected, + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ConnectAutoName { + pub address: String, + pub source: ConnectAddressSource, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum ClaimSetupError { - InvalidCode, - CodeExpired, - VersionTextTooLong, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ConnectDomain { + pub id: uuid::Uuid, + pub domain: String, + pub address: String, + pub mode: DomainMode, + pub source: ConnectAddressSource, } -impl std::fmt::Display for ClaimSetupError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum DomainMode { + Ip, + Srv, + SrvAndIp, + Hostname, } -impl std::error::Error for ClaimSetupError {} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqClaimExchange { - pub code: String, +pub struct ReqTunnelsCreateV1 { + pub ports: TunnelPortDetails, + pub origin: AccountTunnelOriginCreate, + pub enabled: bool, + pub alloc: Option, + pub name: Option, + pub firewall_id: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AgentSecretKey { - pub secret_key: String, +#[serde(tag = "type", content = "details")] +pub enum TunnelPortDetails { + #[serde(rename = "tunnel-type")] + TunnelType(TunnelType), + #[serde(rename = "custom-tcp")] + CustomTcp(u16), + #[serde(rename = "custom-udp")] + CustomUdp(u16), + #[serde(rename = "custom-both")] + CustomBoth(u16), } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum ClaimExchangeError { - CodeNotFound, - CodeExpired, - UserRejected, - NotAccepted, - NotSetup, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "data")] +pub enum AccountTunnelOriginCreate { + #[serde(rename = "agent")] + Agent(AgentOrigin), } -impl std::fmt::Display for ClaimExchangeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentOrigin { + pub agent_id: Option, + pub config: AgentTunnelConfig, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "details")] +pub enum CreateTunnelAllocationRequest { + #[serde(rename = "hostname")] + Hostname(UseHostname), + #[serde(rename = "dedicated-ip")] + DedicatedIp(UseAllocDedicatedIp), + #[serde(rename = "shared-ip")] + SharedIp(UseAllocSharedIp), + #[serde(rename = "region")] + Region(UseAllocRegion), + #[serde(rename = "port-allocation")] + PortAllocation(uuid::Uuid), } -impl std::error::Error for ClaimExchangeError {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqClaimAccept { - pub code: String, - pub name: String, - pub agent_type: AgentType, +pub struct UseHostname { + pub hostname_id: uuid::Uuid, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AgentAccepted { - pub agent_id: uuid::Uuid, +pub struct UseAllocDedicatedIp { + pub ip_hostname: String, + pub port: Option, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum ClaimAcceptError { - InvalidCode, - AgentNotReady, - CodeNotFound, - InvalidAgentType, - ClaimAlreadyAccepted, - ClaimRejected, - CodeExpired, - InvalidName, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct UseAllocSharedIp { + pub ip_hostname: String, + pub port: Option, } -impl std::fmt::Display for ClaimAcceptError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct UseAllocRegion { + pub region: PlayitNetwork, + pub port: Option, } -impl std::error::Error for ClaimAcceptError {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqClaimReject { - pub code: String, +pub struct ObjectId { + pub id: uuid::Uuid, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelCreateErrorV1 { + AgentNotFound, + InvalidAgentId, + DedicatedIpNotFound, + PortAllocNotFound, + InvalidIpHostname, + InvalidPortCount, + RequiresVerifiedAccount, + RegionNotSupported, + InvalidTunnelConfig, + FirewallNotFound, + TunnelNameIsNotAscii, + TunnelNameTooLong, + PortAllocDoesNotMatchPortDetails, + RegionRequiresPlayitPremium, + PortAllocCurrentlyAssigned, + PublicPortRequiresPlayitPremium, + AgentVersionTooOld, + RequiresPlayitPremium, + AllocRequestNotSupportedByPorts, + InvalidHostnameId, + HostnameHasTunnelTypeTarget, +} + +impl std::fmt::Display for TunnelCreateErrorV1 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for TunnelCreateErrorV1 { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqSchemasGetV1 { + pub id: uuid::Uuid, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum ClaimRejectError { - InvalidCode, - CodeNotFound, - ClaimAccepted, - ClaimAlreadyRejected, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct SchemaData { + pub id: uuid::Uuid, + pub details: AgentSchema, } -impl std::fmt::Display for ClaimRejectError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentSchema { + pub default_schema: Option, + pub schemas: Vec, + pub only_explicit_schemas: bool, } -impl std::error::Error for ClaimRejectError {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqProtoRegister { - pub agent_version: PlayitAgentVersion, - pub client_addr: std::net::SocketAddr, - pub tunnel_addr: std::net::SocketAddr, +pub struct AgentSchemaForTunnelType { + pub tunnel_type: AgentSchemaTunnelType, + pub schema: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct PlayitAgentVersion { - pub version: AgentVersion, - pub official: bool, - pub details_website: Option, - pub proto_version: u64, +#[serde(tag = "name", content = "details")] +pub enum AgentSchemaTunnelType { + #[serde(rename = "custom-tcp")] + CustomTcp(AgentTunnelTypeSupportedPorts), + #[serde(rename = "custom-udp")] + CustomUdp(AgentTunnelTypeSupportedPorts), + #[serde(rename = "custom-both")] + CustomBoth(AgentTunnelTypeSupportedPorts), + #[serde(rename = "tunnel-type")] + TunnelType(TunnelType), } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AgentVersion { - pub platform: Platform, - pub version: String, - pub has_expired: bool, +pub struct AgentTunnelTypeSupportedPorts { + pub min: u16, + pub max: u16, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum Platform { - #[serde(rename = "linux")] - Linux, - #[serde(rename = "freebsd")] - Freebsd, - #[serde(rename = "windows")] - Windows, - #[serde(rename = "macos")] - Macos, - #[serde(rename = "android")] - Android, - #[serde(rename = "ios")] - Ios, - #[serde(rename = "docker")] - Docker, - #[serde(rename = "minecraft-plugin")] - MinecraftPlugin, - #[serde(rename = "unknown")] - Unknown, +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum SchemaGetError { + SchemaNotFound, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct SignedAgentKey { - pub key: String, +pub struct ReqTunnelsConfigV1 { + pub tunnel_id: uuid::Uuid, + pub new_agent_id: Option, + pub new_config: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqLoginGuest {} +#[serde(tag = "error", content = "details")] +pub enum TunnelConfigError { + #[serde(rename = "TunnelNotFound")] + TunnelNotFound, + #[serde(rename = "AgentNotFound")] + AgentNotFound, + #[serde(rename = "AgentVersionUnknown")] + AgentVersionUnknown, + #[serde(rename = "CannotConfigTunnelWithoutAgent")] + CannotConfigTunnelWithoutAgent, + #[serde(rename = "SelfManagedAgentCannotReassignTunnel")] + SelfManagedAgentCannotReassignTunnel, + #[serde(rename = "InvalidConfig")] + InvalidConfig(AgentSchemaValidationError), + #[serde(rename = "ConfigNotCompatibleWithAgent")] + ConfigNotCompatibleWithAgent, + #[serde(rename = "NothingToUpdate")] + NothingToUpdate, +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct WebSession { - pub session_key: String, - pub auth: WebAuthToken, +#[serde(tag = "error", content = "field")] +pub enum AgentSchemaValidationError { + #[serde(rename = "NoSchemaFound")] + NoSchemaFound, + #[serde(rename = "TooManyFields")] + TooManyFields, + #[serde(rename = "TunnelTypeNotSupported")] + TunnelTypeNotSupported(AgentTunnelPortAllocTypeDetails), + #[serde(rename = "UnknownField")] + UnknownField(String), + #[serde(rename = "MissingRequiredField")] + MissingRequiredField(String), + #[serde(rename = "InvalidValueForType")] + InvalidValueForType(String), + #[serde(rename = "ValueNotInVariants")] + ValueNotInVariants(String), } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct WebAuthToken { - pub update_version: u32, - pub account_id: u64, - pub timestamp: u64, - pub account_status: AccountStatus, - pub totp_status: TotpStatus, - pub admin_id: Option, - pub read_only: bool, +pub struct AgentTunnelPortAllocTypeDetails { + pub tunnel_type: Option, + pub port_type: PortType, + pub port_count: u16, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum AccountStatus { - #[serde(rename = "guest")] - Guest, - #[serde(rename = "email-not-verified")] - EmailNotVerified, - #[serde(rename = "verified")] - Verified, +impl std::fmt::Display for TunnelConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } } +impl std::error::Error for TunnelConfigError { +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -#[serde(tag = "status")] -pub enum TotpStatus { - #[serde(rename = "required")] - Required, - #[serde(rename = "not-setup")] - NotSetup, - #[serde(rename = "signed")] - Signed(SignedEpoch), +pub struct ReqTunnelsPropset { + pub tunnel_id: uuid::Uuid, + pub details: PropsetDetails, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct SignedEpoch { - pub epoch_sec: u32, +#[serde(tag = "type", content = "value")] +pub enum PropsetDetails { + #[serde(rename = "hostname_verify_level")] + HostnameVerifyLevel(HostnameVerifyLevel), } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum GuestLoginError { - AccountIsNotGuest, +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelProxyPropSetError { + RequiresPermium, + TunnelNotFound, + PropertyValueNotSupportedForTunnelType, +} + +impl std::fmt::Display for TunnelProxyPropSetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } } +impl std::error::Error for TunnelProxyPropSetError { +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqAgentsRoutingGet { - pub agent_id: Option, +pub struct ReqAgentsRundataV1 { } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AgentRouting { - pub agent_id: uuid::Uuid, - pub targets4: Vec, - pub targets6: Vec, - pub disable_ip6: bool, +pub struct AgentRunDataV1 { + pub agent_id: uuid::Uuid, + pub tunnels: Vec, + pub pending: Vec, + pub notices: Vec, + pub permissions: AgentPermissions, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum AgentRoutingGetError { - MissingAgentId, - InvalidAgentId, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentTunnelV1 { + pub id: uuid::Uuid, + pub internal_id: u64, + pub name: String, + pub display_address: String, + pub port_type: PortType, + pub port_count: u16, + pub tunnel_type: Option, + pub tunnel_type_display: String, + pub agent_config: AgentTunnelConfig, + pub disabled_reason: Option>, } -impl std::fmt::Display for AgentRoutingGetError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentPendingTunnelV1 { + pub id: uuid::Uuid, + pub name: String, + pub tunnel_type: Option, + pub tunnel_type_display: String, + pub port_type: PortType, + pub port_count: u16, + pub status_msg: String, } -impl std::error::Error for AgentRoutingGetError {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqAgentsRundata {} +pub struct AgentNotice { + pub priority: AgentNoticePriority, + pub message: std::borrow::Cow<'static,str>, + pub resolve_link: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AgentNoticePriority { + Critical, + High, + Low, +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AgentRunData { - pub agent_id: uuid::Uuid, - pub agent_type: AgentType, - pub account_status: AgentAccountStatus, - pub tunnels: Vec, - pub pending: Vec, - pub account_features: AccountFeatures, +pub struct AgentPermissions { + pub is_self_managed: bool, + pub has_premium: bool, + pub account_status: AccountStatus, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum AgentAccountStatus { - #[serde(rename = "account-delete-scheduled")] - AccountDeleteScheduled, - #[serde(rename = "banned")] - Banned, - #[serde(rename = "has-message")] - HasMessage, - #[serde(rename = "email-not-verified")] - EmailNotVerified, - #[serde(rename = "guest")] - Guest, - #[serde(rename = "ready")] - Ready, - #[serde(rename = "agent-over-limit")] - AgentOverLimit, - #[serde(rename = "agent-disabled")] - AgentDisabled, +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AccountStatus { + #[serde(rename = "guest")] + Guest, + #[serde(rename = "email-not-verified")] + EmailNotVerified, + #[serde(rename = "verified")] + Verified, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AgentTunnel { - pub id: uuid::Uuid, - pub internal_id: u64, - pub name: Option, - pub ip_num: u64, - pub region_num: u16, - pub port: PortRange, - pub proto: PortType, - pub local_ip: std::net::IpAddr, - pub local_port: u16, - pub tunnel_type: Option, - pub assigned_domain: String, - pub custom_domain: Option, - pub disabled: Option, - pub proxy_protocol: Option, +pub struct ReqInfoPops { } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct PortRange { - pub from: u16, - pub to: u16, +pub struct PlayitPops { + pub pops: Vec, + pub regions: Vec, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum AgentTunnelDisabled { - ByUser, - BySystem, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct Pop { + pub pop: PlayitPop, + pub name: String, + pub region: PlayitNetwork, + pub online: bool, + pub ip4_premium: bool, } +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum PlayitPop { + Any, + #[serde(rename = "USLosAngeles")] + UsLosAngeles, + #[serde(rename = "USSeattle")] + UsSeattle, + #[serde(rename = "USDallas")] + UsDallas, + #[serde(rename = "USMiami")] + UsMiami, + #[serde(rename = "USChicago")] + UsChicago, + #[serde(rename = "USNewJersey")] + UsNewJersey, + CanadaToronto, + Mexico, + BrazilSaoPaulo, + Spain, + London, + Germany, + Poland, + Sweden, + IndiaDelhi, + IndiaMumbai, + IndiaBangalore, + Singapore, + Tokyo, + Sydney, + SantiagoChile, + Israel, + Romania, + #[serde(rename = "USNewYork")] + UsNewYork, + #[serde(rename = "USDenver")] + UsDenver, + Staging, +} + +pub type ReqLoginSignin = LoginCredentials; + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AgentPendingTunnel { - pub id: uuid::Uuid, - pub name: Option, - pub proto: PortType, - pub port_count: u16, - pub tunnel_type: Option, - pub is_disabled: bool, - pub region_num: u16, +pub struct LoginCredentials { + pub email: String, + pub password: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct AccountFeatures { - pub regional_tunnels: bool, +pub struct WebSession { + pub session_key: String, + pub auth: WebAuthToken, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqPingSubmit { - pub results: Vec, +pub struct WebAuthToken { + pub update_version: u32, + pub account_id: u64, + pub timestamp: u64, + pub account_status: AccountStatus, + pub totp_status: TotpStatus, + pub admin_id: Option, + pub admin_review_id: Option, + pub read_only: bool, + pub show_admin: bool, } + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct PingExperimentResult { - pub id: u64, - pub target: PingTarget, - pub samples: Vec, +#[serde(tag = "status")] +pub enum TotpStatus { + #[serde(rename = "required")] + Required, + #[serde(rename = "not-setup")] + NotSetup, + #[serde(rename = "signed")] + Signed(SignedEpoch), } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct PingTarget { - pub ip: std::net::IpAddr, - pub port: u16, +pub struct SignedEpoch { + pub epoch_sec: u32, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum SigninFail { + IncorrectCredentials, + AccountBanned, } +impl std::fmt::Display for SigninFail { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for SigninFail { +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct PingSample { - pub tunnel_server_id: u64, - pub dc_id: u64, - pub server_ts: u64, - pub latency: u64, - pub count: u16, - pub num: u16, +pub struct ReqLoginClearcookie { } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqPingGet {} +pub struct ClearWebSession { +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct PingExperiments { - pub experiments: Vec, +pub struct ReqLoginCreateGuest { +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum LoginCreateGuestError { + Blocked, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct PingExperimentDetails { - pub id: u64, - pub test_interval: u64, - pub ping_interval: u64, - pub samples: u64, - pub targets: std::borrow::Cow<'static, [PingTarget]>, +pub struct ReqLoginGuest { +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum GuestLoginError { + AccountIsNotGuest, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqTunnelsList { - pub tunnel_id: Option, - pub agent_id: Option, +pub struct ReqLoginResetPassword { + pub email: String, + pub reset_code: String, + pub new_password: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum PasswordResetError { + ResetCodeExpired, + InvalidResetCode, + InvalidNewPassword, } +impl std::fmt::Display for PasswordResetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for PasswordResetError { +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqAgentsList {} +pub struct ReqLoginResetSend { + pub email: String, +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqQueryRegion { - pub limit_region: Option, +pub struct ReqTunnelsCreate { + pub name: Option, + pub tunnel_type: Option, + pub port_type: PortType, + pub port_count: u16, + pub origin: TunnelOriginCreate, + pub enabled: bool, + pub alloc: Option, + pub firewall_id: Option, + pub proxy_protocol: Option, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum PlayitRegion { - GlobalAnycast, - NorthAmerica, - Europe, - Asia, - India, - SouthAmerica, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "data")] +pub enum TunnelOriginCreate { + #[serde(rename = "default")] + Default(AssignedDefaultCreate), + #[serde(rename = "agent")] + Agent(AssignedAgentCreate), + #[serde(rename = "managed")] + Managed(AssignedManagedCreate), } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct QueryRegion { - pub region: PlayitRegion, - pub pop: PlayitPop, +pub struct AssignedDefaultCreate { + pub local_ip: std::net::IpAddr, + pub local_port: Option, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum PlayitPop { - Any, - #[serde(rename = "USLosAngeles")] - UsLosAngeles, - #[serde(rename = "USSeattle")] - UsSeattle, - #[serde(rename = "USDallas")] - UsDallas, - #[serde(rename = "USMiami")] - UsMiami, - #[serde(rename = "USChicago")] - UsChicago, - #[serde(rename = "USNewJersey")] - UsNewJersey, - CanadaToronto, - Mexico, - BrazilSaoPaulo, - Spain, - London, - Germany, - Poland, - Sweden, - IndiaDelhi, - IndiaMumbai, - IndiaBangalore, - Singapore, - Tokyo, - Sydney, - SantiagoChile, - Israel, - Romania, - #[serde(rename = "USNewYork")] - UsNewYork, - #[serde(rename = "USDenver")] - UsDenver, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum QueryRegionError { - FailedToDetermineLocation, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AssignedAgentCreate { + pub agent_id: uuid::Uuid, + pub local_ip: std::net::IpAddr, + pub local_port: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqTunnelsUpdate { - pub tunnel_id: uuid::Uuid, - pub local_ip: std::net::IpAddr, - pub local_port: Option, - pub agent_id: Option, - pub enabled: bool, +pub struct AssignedManagedCreate { + pub agent_id: Option, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum UpdateError { - ChangingAgentIdNotAllowed, - TunnelNotFound, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "details")] +pub enum TunnelCreateUseAllocation { + #[serde(rename = "dedicated-ip")] + DedicatedIp(UseAllocDedicatedIp), + #[serde(rename = "port-allocation")] + PortAllocation(UseAllocPortAlloc), + #[serde(rename = "region")] + Region(UseRegion), } -impl std::fmt::Display for UpdateError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct UseAllocPortAlloc { + pub alloc_id: uuid::Uuid, } -impl std::error::Error for UpdateError {} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqTunnelsFirewallAssign { - pub tunnel_id: uuid::Uuid, - pub firewall_id: Option, +pub struct UseRegion { + pub region: PlayitNetwork, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum TunnelsFirewallAssignError { - TunnelNotFound, - InvalidFirewallId, +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum ProxyProtocol { + #[serde(rename = "proxy-protocol-v1")] + ProxyProtocolV1, + #[serde(rename = "proxy-protocol-v2")] + ProxyProtocolV2, } -impl std::fmt::Display for TunnelsFirewallAssignError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelCreateError { + DefaultAgentNotSupported, + AgentNotFound, + InvalidAgentId, + AgentVersionTooOld, + DedicatedIpNotFound, + DedicatedIpPortNotAvailable, + DedicatedIpNotEnoughSpace, + PortAllocNotFound, + InvalidIpHostname, + ManagedMissingAgentId, + InvalidPortCount, + RequiresVerifiedAccount, + InvalidTunnelName, + FirewallNotFound, + AllocInvalid, + InvalidOrigin, + RequiresPlayitPremium, + Other, } -impl std::error::Error for TunnelsFirewallAssignError {} +impl std::fmt::Display for TunnelCreateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for TunnelCreateError { +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct ReqTunnelsProxySet { - pub tunnel_id: uuid::Uuid, - pub proxy_protocol: Option, +pub struct ReqTunnelsList { + pub tunnel_id: Option, + pub agent_id: Option, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub enum TunnelProxySetError { - TunnelNotFound, +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AccountTunnels { + pub tunnels: Vec, + pub tcp_alloc: AllocatedPorts, + pub udp_alloc: AllocatedPorts, } + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AccountTunnel { + pub id: uuid::Uuid, + pub tunnel_type: Option, + pub created_at: chrono::DateTime, + pub name: Option, + pub port_type: PortType, + pub port_count: u16, + pub alloc: AccountTunnelAllocation, + pub origin: Option, + pub domain: Option, + pub firewall_id: Option, + pub ratelimit: Ratelimit, + pub active: bool, + pub disabled_reason: Option, + pub region: Option, + pub expire_notice: Option, + pub proxy_protocol: Option, + pub hostname_verify_level: HostnameVerifyLevel, + pub agent_over_limit: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "status", content = "data")] +pub enum AccountTunnelAllocation { + #[serde(rename = "pending")] + Pending, + #[serde(rename = "disabled")] + Disabled(TunnelDisabled), + #[serde(rename = "allocated")] + Allocated(TunnelAllocated), +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct TunnelDisabled { + pub reason: TunnelOfflineReason, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelOfflineReason { + #[serde(rename = "requires-premium")] + RequiresPremium, + #[serde(rename = "over-port-limit")] + OverPortLimit, + #[serde(rename = "ip-used-in-gre")] + IpUsedInGre, + #[serde(rename = "public-port-not-available")] + PublicPortNotAvailable, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct TunnelAllocated { + pub id: uuid::Uuid, + pub ip_hostname: String, + pub static_ip4: Option, + pub static_ip6: std::net::Ipv6Addr, + pub assigned_domain: String, + pub assigned_srv: Option, + pub tunnel_ip: std::net::IpAddr, + pub port_start: u16, + pub port_end: u16, + pub assignment: TunnelAssignment, + pub ip_type: IpType, + pub region: PlayitNetwork, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "subscription")] +pub enum TunnelAssignment { + #[serde(rename = "dedicated-ip")] + DedicatedIp(TunnelDedicatedIp), + #[serde(rename = "shared-ip")] + SharedIp, + #[serde(rename = "dedicated-port")] + DedicatedPort(SubscriptionId), +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct TunnelDedicatedIp { + pub sub_id: uuid::Uuid, + pub region: PlayitNetwork, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct SubscriptionId { + pub sub_id: uuid::Uuid, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum IpType { + #[serde(rename = "both")] + Both, + #[serde(rename = "ip4")] + Ip4, + #[serde(rename = "ip6")] + Ip6, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "data")] +pub enum TunnelOrigin { + #[serde(rename = "agent")] + Agent(AssignedAgent), + #[serde(rename = "managed")] + Managed(AssignedManaged), +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AssignedAgent { + pub agent_id: uuid::Uuid, + pub agent_name: String, + pub local_ip: std::net::IpAddr, + pub local_port: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AssignedManaged { + pub agent_id: uuid::Uuid, + pub agent_name: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct TunnelDomain { + pub id: uuid::Uuid, + pub name: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct Ratelimit { + pub bytes_per_second: Option, + pub packets_per_second: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AllocatedPorts { + pub allowed: u32, + pub claimed: u32, + pub desired: u32, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqTunnelsUpdate { + pub tunnel_id: uuid::Uuid, + pub local_ip: std::net::IpAddr, + pub local_port: Option, + pub agent_id: Option, + pub enabled: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum UpdateError { + ChangingAgentIdNotAllowed, + TunnelNotFound, + CannotUpdateLocalAddressForUnassignedTunnel, + InvalidAgentId, + AddressOrProxyProtoNotSupportedByAgent, +} + +impl std::fmt::Display for UpdateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for UpdateError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqTunnelsDelete { + pub tunnel_id: uuid::Uuid, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum DeleteError { + TunnelNotFound, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqTunnelsRename { + pub tunnel_id: uuid::Uuid, + pub name: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelRenameError { + TunnelNotFound, + NameTooLong, +} + +impl std::fmt::Display for TunnelRenameError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for TunnelRenameError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqTunnelsFirewallAssign { + pub tunnel_id: uuid::Uuid, + pub firewall_id: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelsFirewallAssignError { + TunnelNotFound, + InvalidFirewallId, +} + +impl std::fmt::Display for TunnelsFirewallAssignError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for TunnelsFirewallAssignError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqTunnelsRatelimit { + pub tunnel_id: uuid::Uuid, + pub bytes_per_second: Option, + pub packets_per_second: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelRatelimitError { + TunnelNotFound, + InvalidRatelimit, + PlayitPremiumRequired, +} + +impl std::fmt::Display for TunnelRatelimitError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for TunnelRatelimitError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqTunnelsEnable { + pub tunnel_id: uuid::Uuid, + pub enabled: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelEnableError { + TunnelNotFound, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqTunnelsProxySet { + pub tunnel_id: uuid::Uuid, + pub proxy_protocol: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum TunnelProxySetError { + TunnelNotFound, + ProxyProtocolNotSupportedByAgent, +} + +impl std::fmt::Display for TunnelProxySetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for TunnelProxySetError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqClaimSetup { + pub code: String, + pub agent_type: ClaimAgentType, + pub version: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum ClaimAgentType { + #[serde(rename = "assignable")] + Assignable, + #[serde(rename = "self-managed")] + SelfManaged, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum ClaimSetupResponse { + WaitingForUserVisit, + WaitingForUser, + UserAccepted, + UserRejected, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum ClaimSetupError { + InvalidCode, + CodeExpired, + VersionTextTooLong, +} + +impl std::fmt::Display for ClaimSetupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for ClaimSetupError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqClaimExchange { + pub code: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentSecretKey { + pub secret_key: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum ClaimExchangeError { + CodeNotFound, + CodeExpired, + UserRejected, + NotAccepted, + NotSetup, +} + +impl std::fmt::Display for ClaimExchangeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for ClaimExchangeError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqAgentsRename { + pub agent_id: uuid::Uuid, + pub name: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AgentRenameError { + AgentNotFound, + InvalidName, + InvalidAgentId, +} + +impl std::fmt::Display for AgentRenameError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for AgentRenameError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqAgentsRoutingSet { + pub agent_id: uuid::Uuid, + pub routing: AgentRoutingTarget, + pub disable_ip6: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "details")] +pub enum AgentRoutingTarget { + #[serde(rename = "Automatic")] + Automatic, + #[serde(rename = "Pop")] + Pop(PlayitPop), + #[serde(rename = "Region")] + Region(PlayitNetwork), +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AgentRoutingSetError { + RequiresPremium, + AgentNotFound, + InvalidAgentId, +} + +impl std::fmt::Display for AgentRoutingSetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for AgentRoutingSetError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqAgentsRoutingGet { + pub agent_id: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentRouting { + pub agent_id: uuid::Uuid, + pub targets4: Vec, + pub targets6: Vec, + pub disable_ip6: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AgentRoutingGetError { + MissingAgentId, + InvalidAgentId, +} + +impl std::fmt::Display for AgentRoutingGetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for AgentRoutingGetError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqAgentsRundata { +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentRunData { + pub agent_id: uuid::Uuid, + pub agent_type: AgentType, + pub account_status: AgentAccountStatus, + pub tunnels: Vec, + pub pending: Vec, + pub account_features: AccountFeatures, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AgentType { + #[serde(rename = "default")] + Default, + #[serde(rename = "assignable")] + Assignable, + #[serde(rename = "self-managed")] + SelfManaged, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AgentAccountStatus { + #[serde(rename = "account-delete-scheduled")] + AccountDeleteScheduled, + #[serde(rename = "banned")] + Banned, + #[serde(rename = "has-message")] + HasMessage, + #[serde(rename = "email-not-verified")] + EmailNotVerified, + #[serde(rename = "guest")] + Guest, + #[serde(rename = "ready")] + Ready, + #[serde(rename = "agent-over-limit")] + AgentOverLimit, + #[serde(rename = "agent-disabled")] + AgentDisabled, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentTunnel { + pub id: uuid::Uuid, + pub internal_id: u64, + pub name: Option, + pub ip_num: u64, + pub region_num: u16, + pub port: PortRange, + pub proto: PortType, + pub local_ip: std::net::IpAddr, + pub local_port: u16, + pub tunnel_type: Option, + pub assigned_domain: String, + pub custom_domain: Option, + pub disabled: Option, + pub proxy_protocol: Option, + pub agent_config: AgentTunnelConfig, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct PortRange { + pub from: u16, + pub to: u16, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum AgentTunnelDisabled { + ByUser, + BySystem, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentPendingTunnel { + pub id: uuid::Uuid, + pub name: Option, + pub proto: PortType, + pub port_count: u16, + pub tunnel_type: Option, + pub is_disabled: bool, + pub region_num: u16, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AccountFeatures { + pub regional_tunnels: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqDomainsList { +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct Domains { + pub domains: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct Domain { + pub id: uuid::Uuid, + pub name: String, + pub is_external: bool, + pub parent: Option, + pub sub_id: uuid::Uuid, + pub target: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "details")] +pub enum DomainTarget { + #[serde(rename = "ip-address")] + IpAddress(DomainTargetIp), + #[serde(rename = "tunnel")] + Tunnel(DomainTargetTunnel), + #[serde(rename = "external-cname")] + ExternalCname(DomainTargetExternalCName), +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct DomainTargetIp { + pub ip_address: std::net::IpAddr, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct DomainTargetTunnel { + pub tunnel_id: uuid::Uuid, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct DomainTargetExternalCName { + pub cname: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqShopPrices { +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ShopPrices { + pub custom_domain: ShopPrice, + pub dedicated_ip: std::collections::HashMap, + pub playit_premium: ShopPrice, + pub ports_both: ShopPrice, + pub ports_tcp: ShopPrice, + pub ports_udp: ShopPrice, + pub dedicated_port_global: ShopPrice, + pub dedicated_port_regional: ShopPrice, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ShopPrice { + pub monthly: Option, + pub yearly: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqShopAvailabilityCustomDomain { + pub name: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct IsAvailable { + pub is_available: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqProtoRegister { + pub agent_version: Option, + pub proto_version: u64, + pub version: AgentVersion, + pub platform: Platform, + pub client_addr: std::net::SocketAddr, + pub tunnel_addr: std::net::SocketAddr, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct PlayitAgentVersion { + pub version: AgentVersionOld, + pub proto_version: u64, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentVersionOld { + pub platform: Platform, + pub version: String, + pub has_expired: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum Platform { + #[serde(rename = "linux")] + Linux, + #[serde(rename = "freebsd")] + Freebsd, + #[serde(rename = "windows")] + Windows, + #[serde(rename = "macos")] + Macos, + #[serde(rename = "android")] + Android, + #[serde(rename = "ios")] + Ios, + #[serde(rename = "docker")] + Docker, + #[serde(rename = "minecraft-plugin")] + MinecraftPlugin, + #[serde(rename = "unknown")] + Unknown, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct AgentVersion { + pub variant_id: uuid::Uuid, + pub version_major: u32, + pub version_minor: u32, + pub version_patch: u32, +} + + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct SignedAgentKey { + pub key: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum ProtoRegisterError { + UnknownPlayitVersion, + DisabledByUser, + AgentDisabledOverLimit, + AccountBanned, +} + +impl std::fmt::Display for ProtoRegisterError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for ProtoRegisterError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqChargeGet { + pub reference_code: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ChargeDetails { + pub reference_code: String, + pub created_at: chrono::DateTime, + pub invoice_type: InvoiceType, + pub invoice_status: InvoiceStatus, + pub total_cost: String, + pub items: Vec, + pub refund: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum InvoiceType { + Subscription, + StartSubscription, + StripeSubscription, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum InvoiceStatus { + #[serde(rename = "draft")] + Draft, + #[serde(rename = "open")] + Open, + #[serde(rename = "paid")] + Paid, + #[serde(rename = "void")] + Void, + #[serde(rename = "uncollectible")] + Uncollectible, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ChargeDetailsItem { + pub product: SubProductType, + pub months: u32, + pub total_cost: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum SubProductType { + #[serde(rename = "playit-premium")] + PlayitPremium, + #[serde(rename = "playit-premium-trial")] + PlayitPremiumTrial, + #[serde(rename = "dedicated-ip")] + DedicatedIp, + #[serde(rename = "udp-ports")] + UdpPorts, + #[serde(rename = "tcp-ports")] + TcpPorts, + #[serde(rename = "both-ports")] + BothPorts, + #[serde(rename = "custom-domain")] + CustomDomain, + #[serde(rename = "dedicated-port-alloc")] + DedicatedPortAlloc, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "details")] +pub enum RefundStatus { + #[serde(rename = "Pending")] + Pending(PendingRefundRequest), + #[serde(rename = "Applied")] + Applied(RefundApplied), + #[serde(rename = "DisputeCreated")] + DisputeCreated(DisputeCreated), +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct PendingRefundRequest { + pub created_at: chrono::DateTime, + pub reason: RefundRequestReason, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum RefundRequestReason { + #[serde(rename = "fraud")] + Fraud, + #[serde(rename = "not-satisfied")] + NotSatisfied, + #[serde(rename = "issuer-fraud-warning")] + IssuerFraudWarning, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct RefundApplied { + pub created_at: chrono::DateTime, + pub refund_amount: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct DisputeCreated { + pub created_at: chrono::DateTime, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum ChargeGetError { + ChargeNotFound, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqChargeRefund { + pub reference_code: String, + pub reason: RefundRequestReason, + pub email: Option, + pub refund_message: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum ChargeRefundError { + ChargeNotFound, + MessageTooLarge, + UnauthorizedReason, +} + +impl std::fmt::Display for ChargeRefundError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for ChargeRefundError { +} +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct ReqQueryRegion { + pub limit_region: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct QueryRegion { + pub region: PlayitNetwork, + pub pop: PlayitPop, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum QueryRegionError { + FailedToDetermineLocation, +} + diff --git a/packages/ping_monitor/Cargo.toml b/packages/ping_monitor/Cargo.toml deleted file mode 100644 index 80fa225..0000000 --- a/packages/ping_monitor/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "playit-ping-monitor" -version = "0.1.0" -edition = "2021" -description = "Runs ping experiments to the playit network to help us tune routing" -license = "BSD-2-Clause" -repository = "https://github.com/playit-cloud/playit-agent" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -tokio = { workspace = true } - -tracing = { workspace = true } -tracing-subscriber = { workspace = true } -tracing-appender = { workspace = true } - -message-encoding = { workspace = true } -rand = { workspace = true } - -serde = { workspace = true} -toml = { workspace = true } -hex = { workspace = true } -dirs = { workspace = true } - -playit-agent-proto = { path = "../agent_proto", version = "1.0.0" } -playit-api-client = { path = "../api_client", version = "0.1.0" } \ No newline at end of file From e6247370bc71b5182501b372c2c3f3e3b724e84f Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 18 Dec 2025 11:50:23 -0800 Subject: [PATCH 2/8] Use v1_rundata and support https --- packages/agent_cli/src/autorun.rs | 140 +++++------------- .../agent_core/src/network/origin_lookup.rs | 31 ++-- 2 files changed, 58 insertions(+), 113 deletions(-) diff --git a/packages/agent_cli/src/autorun.rs b/packages/agent_cli/src/autorun.rs index 099b32d..21a6b19 100644 --- a/packages/agent_cli/src/autorun.rs +++ b/packages/agent_cli/src/autorun.rs @@ -35,7 +35,7 @@ pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliErr let lookup = Arc::new(OriginLookup::default()); lookup - .update_from_run_data(&api.agents_rundata().await?) + .update_from_run_data(&api.v1_agents_rundata().await?) .await; let mut error_count = 0; @@ -75,8 +75,8 @@ pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliErr loop { tokio::time::sleep(Duration::from_secs(3)).await; - let account_tunnels_res = api.agents_rundata().await; - let agent_data = match account_tunnels_res { + let account_tunnels_res = api.v1_agents_rundata().await; + let mut agent_data = match account_tunnels_res { Ok(v) => v, Err(error) => { ui.write_error("Failed to load latest tunnels", error).await; @@ -94,8 +94,8 @@ pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliErr agent_data.tunnels.len() ); - match agent_data.account_status { - AgentAccountStatus::Guest => 'login_link: { + match agent_data.permissions.account_status { + AccountStatus::Guest => 'login_link: { let now = now_milli(); match &guest_login_link { @@ -118,128 +118,64 @@ pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliErr } } } - AgentAccountStatus::EmailNotVerified => { + AccountStatus::EmailNotVerified => { writeln!( msg, "Email not verified https://playit.gg/account/settings/account/verify-email" ) .unwrap(); } - AgentAccountStatus::AccountDeleteScheduled => { - writeln!(msg, "Account scheduled for delete: https://playit.gg/account/settings/account/delete-account").unwrap(); - } - AgentAccountStatus::Banned => { - writeln!(msg, "Account banned: https://playit.gg/account").unwrap(); - } - AgentAccountStatus::HasMessage => { - writeln!(msg, "You have a message: https://playit.gg/account").unwrap(); - } - AgentAccountStatus::AgentOverLimit => { - writeln!(msg, "Too many agents: https://playit.gg/account/agents").unwrap(); - } - AgentAccountStatus::AgentDisabled => { - writeln!( - msg, - "Account disabled: https://playit.gg/account/agents/{}", - agent_data.agent_id - ) - .unwrap(); + AccountStatus::Verified => {} + } + + agent_data.notices.sort_by_key(|n| n.priority); + + for notice in &agent_data.notices { + writeln!(msg, "[{:?}] {}", notice.priority, notice.message).unwrap(); + if let Some(link) = ¬ice.resolve_link { + writeln!(msg, "{link}").unwrap(); } - AgentAccountStatus::Ready => {} } writeln!(msg, "\nTUNNELS").unwrap(); if agent_data.tunnels.is_empty() && agent_data.pending.is_empty() { - let agent_id = match agent_data.agent_type { - AgentType::Default => "default".to_string(), - AgentType::Assignable => agent_data.agent_id.to_string(), - AgentType::SelfManaged => agent_data.agent_id.to_string(), - }; - writeln!( msg, "Add tunnels here: https://playit.gg/account/agents/{}", - agent_id + agent_data.agent_id ) .unwrap(); } else { for tunnel in &agent_data.tunnels { - let addr = tunnel - .custom_domain - .as_ref() - .unwrap_or(&tunnel.assigned_domain); - - let src = match tunnel.tunnel_type.as_deref() { - Some("minecraft-java") => addr.clone(), - Some("https") => format!("https://{addr}"), - _ => format!("{}:{}", addr, tunnel.port.from), + let Some(origin) = OriginResource::from_agent_tunnel(&tunnel) else { + continue; }; - if let Some(disabled) = tunnel.disabled { - let dst = format!("{}:{}", tunnel.local_ip, tunnel.local_port); - writeln!(msg, "{} => {} (disabled)", src, dst).unwrap(); - if disabled == AgentTunnelDisabled::BySystem { - writeln!( - msg, - "\tsee: https://playit.gg/account/tunnels/{}", - tunnel.id - ) - .unwrap(); - } - } else { - let res = OriginResource::from_agent_tunnel(&tunnel); - - match res.target { - OriginTarget::Https { - ip, - http_port, - https_port, - } => { - writeln!( - msg, - "{} => {} (http: {}, https: {})", - src, ip, http_port, https_port - ) - .unwrap(); - } - OriginTarget::Port { ip, port } => { - if let Some(tunnel_type) = &tunnel.tunnel_type { - writeln!(msg, "{} => {}:{} ({})", src, ip, port, tunnel_type) - .unwrap(); - } else { - writeln!( - msg, - "{} => {}:{} (proto: {:?}, port count: {})", - src, - ip, - port, - tunnel.proto, - tunnel.port.to - tunnel.port.from - ) - .unwrap(); - } - } - } + if let Some(reason) = &tunnel.disabled_reason { + writeln!(msg, "{} => (disabled {reason})", tunnel.display_address).unwrap(); + continue; } + + let dst = match origin.target { + OriginTarget::Https { + ip, + http_port, + https_port, + } => format!("{ip} (http: {http_port}, https: {https_port})"), + OriginTarget::Port { ip, port } => SocketAddr::new(ip, port).to_string(), + }; + + writeln!(msg, "{} => {}", tunnel.display_address, dst).unwrap(); } for tunnel in &agent_data.pending { - if tunnel.is_disabled { - writeln!( - msg, - "tunnel pending (disabled): https://playit.gg/account/tunnels/{}", - tunnel.id - ) - .unwrap(); - } else { - writeln!( - msg, - "tunnel pending: https://playit.gg/account/tunnels/{}", - tunnel.id - ) - .unwrap(); - } + writeln!( + msg, + "tunnel ({}): https://playit.gg/account/tunnels/{}", + tunnel.status_msg, tunnel.id + ) + .unwrap(); } } diff --git a/packages/agent_core/src/network/origin_lookup.rs b/packages/agent_core/src/network/origin_lookup.rs index b6b3cf9..cf255e5 100644 --- a/packages/agent_core/src/network/origin_lookup.rs +++ b/packages/agent_core/src/network/origin_lookup.rs @@ -5,7 +5,7 @@ use std::{ }; use playit_agent_proto::PortProto; -use playit_api_client::api::{AgentRunData, AgentTunnel, PortType, ProxyProtocol, TunnelType}; +use playit_api_client::api::{AgentRunDataV1, AgentTunnelV1, PortType, ProxyProtocol, TunnelType}; use tokio::sync::RwLock; #[derive(Default)] @@ -14,12 +14,12 @@ pub struct OriginLookup { } impl OriginLookup { - pub async fn update_from_run_data(&self, run_data: &AgentRunData) { + pub async fn update_from_run_data(&self, run_data: &AgentRunDataV1) { self.update( run_data .tunnels .iter() - .map(OriginResource::from_agent_tunnel), + .filter_map(OriginResource::from_agent_tunnel), ) .await; } @@ -106,12 +106,22 @@ pub enum OriginTarget { } impl OriginResource { - pub fn from_agent_tunnel(tunn: &AgentTunnel) -> Self { + pub fn from_agent_tunnel(tunn: &AgentTunnelV1) -> Option { let tunnel_type = tunn .tunnel_type .clone() .and_then(|v| serde_json::from_value::(serde_json::Value::String(v)).ok()); + let proxy_protocol = tunn + .agent_config + .fields + .iter() + .find(|f| f.name.eq("proxy_protocol")) + .and_then(|v| { + serde_json::from_value::(serde_json::Value::String(v.value.clone())) + .ok() + }); + let target = match tunnel_type { Some(TunnelType::Https) => OriginTarget::Https { ip: tunn @@ -149,22 +159,21 @@ impl OriginResource { .fields .iter() .find(|f| f.name.eq("local_port")) - .and_then(|v| u16::from_str(&v.value).ok()) - .unwrap_or(tunn.local_port), + .and_then(|v| u16::from_str(&v.value).ok())?, }, }; - OriginResource { + Some(OriginResource { tunnel_id: tunn.internal_id, - proto: match tunn.proto { + proto: match tunn.port_type { PortType::Tcp => PortProto::Tcp, PortType::Udp => PortProto::Udp, PortType::Both => PortProto::Both, }, target, - port_count: tunn.port.to - tunn.port.from, - proxy_protocol: tunn.proxy_protocol, - } + port_count: tunn.port_count, + proxy_protocol, + }) } pub fn resolve_local(&self, port_offset: u16) -> Option { From ff77f023cfc7fff1c0b56a7833c5f5ef395d210e Mon Sep 17 00:00:00 2001 From: Patrick Lorio Date: Fri, 19 Dec 2025 17:19:57 -0800 Subject: [PATCH 3/8] Fix handling of gateway tunnels --- packages/agent_core/src/network/origin_lookup.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/agent_core/src/network/origin_lookup.rs b/packages/agent_core/src/network/origin_lookup.rs index cf255e5..649a625 100644 --- a/packages/agent_core/src/network/origin_lookup.rs +++ b/packages/agent_core/src/network/origin_lookup.rs @@ -192,6 +192,10 @@ impl OriginResource { } } OriginTarget::Port { ip, port } => { + if self.port_count == 0 { + return Some(SocketAddr::new(*ip, *port)); + } + if self.port_count <= port_offset { return None; } From 5679f00469c78904f8e08becdfdc8b6a5f568ba3 Mon Sep 17 00:00:00 2001 From: Patrick Lorio Date: Tue, 13 Jan 2026 16:54:06 -0800 Subject: [PATCH 4/8] Fix UDP handling error --- .../agent_core/src/network/udp/packets.rs | 7 ++++--- .../agent_core/src/network/udp/udp_channel.rs | 6 +++--- .../src/network/udp/udp_receiver.rs | 20 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/agent_core/src/network/udp/packets.rs b/packages/agent_core/src/network/udp/packets.rs index 36a2473..720b1eb 100644 --- a/packages/agent_core/src/network/udp/packets.rs +++ b/packages/agent_core/src/network/udp/packets.rs @@ -34,7 +34,7 @@ unsafe impl Sync for Packet {} impl Packets { pub fn new(mut packet_count: usize) -> Self { packet_count = packet_count.next_power_of_two(); - let bytes = packet_count.next_power_of_two() * PACKET_LEN; + let bytes = packet_count * PACKET_LEN; let mut buffer = vec![0u8; bytes]; @@ -82,8 +82,9 @@ impl Packets { }); } - if let Err(error) = self.inner.waiting.push(cx.waker().clone()) { - error.wake(); + if let Err(waker) = self.inner.waiting.push(cx.waker().clone()) { + // Queue full - schedule a retry after yielding + waker.wake(); } Poll::Pending diff --git a/packages/agent_core/src/network/udp/udp_channel.rs b/packages/agent_core/src/network/udp/udp_channel.rs index 8127b8e..f520085 100644 --- a/packages/agent_core/src/network/udp/udp_channel.rs +++ b/packages/agent_core/src/network/udp/udp_channel.rs @@ -167,12 +167,12 @@ impl Task { let Some(session) = self.session.as_ref() else { udp_errors().recv_with_no_session.inc(); - return; + continue; }; if session.tunnel_addr != source { udp_errors().recv_source_no_match.inc(); - return; + continue; } packet.set_len(bytes).expect("failed to update packet len"); @@ -212,7 +212,7 @@ impl Task { if old != details { true } else { - 5_000 < now_milli() - self.shared.establish_rx_epoch.load(Ordering::Relaxed) + 5_000 < now_milli().saturating_sub(self.shared.establish_rx_epoch.load(Ordering::Relaxed)) } } }; diff --git a/packages/agent_core/src/network/udp/udp_receiver.rs b/packages/agent_core/src/network/udp/udp_receiver.rs index ed23b40..4f76ce5 100644 --- a/packages/agent_core/src/network/udp/udp_receiver.rs +++ b/packages/agent_core/src/network/udp/udp_receiver.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, time::Duration}; +use std::net::SocketAddr; use tokio::sync::mpsc::Sender; use tokio_util::sync::CancellationToken; @@ -52,7 +52,11 @@ impl UdpReceiver { pub fn is_closed(&mut self) -> bool { if !self.closed { - self.closed = self.end.as_mut().unwrap().try_recv().is_ok(); + self.closed = match self.end.as_mut().unwrap().try_recv() { + Ok(_) => true, + Err(tokio::sync::oneshot::error::TryRecvError::Closed) => true, + Err(tokio::sync::oneshot::error::TryRecvError::Empty) => false, + }; } self.closed } @@ -88,15 +92,9 @@ pub struct UdpReceivedPacket { impl Task { async fn start(self) { 'rx_loop: loop { - let mut packet = loop { - if let Some(packet) = self.packets.allocate() { - break packet; - } - - tokio::select! { - _ = self.cancel.cancelled() => break 'rx_loop, - _ = tokio::time::sleep(Duration::from_millis(30)) => continue, - } + let mut packet = tokio::select! { + _ = self.cancel.cancelled() => break 'rx_loop, + p = self.packets.allocate_wait() => p, }; let res = tokio::select! { From fd08476e00aadc7920cf65f3b584c01eef50a25b Mon Sep 17 00:00:00 2001 From: Patrick Lorio Date: Tue, 13 Jan 2026 16:58:23 -0800 Subject: [PATCH 5/8] Fix IP6 shuffle bug --- packages/agent_core/src/network/lan_address.rs | 4 ++-- packages/agent_core/src/utils/shuffle.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/agent_core/src/network/lan_address.rs b/packages/agent_core/src/network/lan_address.rs index 9b62214..4d20f7b 100644 --- a/packages/agent_core/src/network/lan_address.rs +++ b/packages/agent_core/src/network/lan_address.rs @@ -114,8 +114,8 @@ fn shuffle_ip_to_u32(ip: IpAddr) -> u32 { shuffle(BigEndian::read_u32(&bytes[..4])) ^ shuffle(BigEndian::read_u32(&bytes[4..8])) - ^ shuffle(BigEndian::read_u32(&bytes[4..8])) - ^ shuffle(BigEndian::read_u32(&bytes[4..8])) + ^ shuffle(BigEndian::read_u32(&bytes[8..12])) + ^ shuffle(BigEndian::read_u32(&bytes[12..16])) } } } diff --git a/packages/agent_core/src/utils/shuffle.rs b/packages/agent_core/src/utils/shuffle.rs index b340ec4..c0248d0 100644 --- a/packages/agent_core/src/utils/shuffle.rs +++ b/packages/agent_core/src/utils/shuffle.rs @@ -10,7 +10,7 @@ pub fn shuffle(mut v: u32) -> u32 { pub fn shuffle_slice(mut bytes: &[u8]) -> u32 { let mut v = 0; - while bytes.len() > 4 { + while bytes.len() >= 4 { v ^= shuffle(bytes.read_u32::().unwrap()); } for byte in bytes { From 93ab7d3e535384ad595f48974d9b2cf5e3c853b3 Mon Sep 17 00:00:00 2001 From: Patrick Lorio Date: Tue, 13 Jan 2026 17:02:16 -0800 Subject: [PATCH 6/8] clean up warnings --- packages/agent_cli/src/autorun.rs | 1 - packages/agent_cli/src/main.rs | 14 -------------- packages/agent_core/src/utils/id_slab.rs | 10 +++++----- packages/agent_core/src/utils/non_overlapping.rs | 2 +- packages/agent_proto/src/control_messages.rs | 6 +++--- 5 files changed, 9 insertions(+), 24 deletions(-) diff --git a/packages/agent_cli/src/autorun.rs b/packages/agent_cli/src/autorun.rs index 21a6b19..578120a 100644 --- a/packages/agent_cli/src/autorun.rs +++ b/packages/agent_cli/src/autorun.rs @@ -9,7 +9,6 @@ use playit_agent_core::{ playit_agent::{PlayitAgent, PlayitAgentSettings}, utils::now_milli, }; -use playit_agent_proto::PortProto; use playit_api_client::api::*; // use playit_ping_monitor::PingMonitor; diff --git a/packages/agent_cli/src/main.rs b/packages/agent_cli/src/main.rs index 429a595..a21d717 100644 --- a/packages/agent_cli/src/main.rs +++ b/packages/agent_cli/src/main.rs @@ -279,20 +279,6 @@ pub async fn claim_exchange( Ok(secret_key) } -struct TunnelOption { - id: Uuid, - name: Option, - proto: PortType, - port_count: u16, - tunnel_type: Option, - public_address: Option, -} - -struct TunnelAlloc { - address: String, - port: u16, -} - #[derive(Debug)] pub enum CliError { InvalidClaimCode, diff --git a/packages/agent_core/src/utils/id_slab.rs b/packages/agent_core/src/utils/id_slab.rs index 5b59674..204c094 100644 --- a/packages/agent_core/src/utils/id_slab.rs +++ b/packages/agent_core/src/utils/id_slab.rs @@ -117,7 +117,7 @@ impl IdSlab { Ok(entry.id) } - pub fn vacant_entry(&mut self) -> Option> { + pub fn vacant_entry(&mut self) -> Option> { let slot = self.free_slots.pop()?; let id = self.entries[slot].id & EMPTY_BIT_NEG; @@ -128,7 +128,7 @@ impl IdSlab { }) } - pub fn iter(&self) -> IdSlabIter { + pub fn iter(&self) -> IdSlabIter<'_, T> { IdSlabIter { slab: self, slot: 0, @@ -136,7 +136,7 @@ impl IdSlab { } } - pub fn iter_mut(&mut self) -> IdSlabIterMut { + pub fn iter_mut(&mut self) -> IdSlabIterMut<'_, T> { let remaining = self.len(); IdSlabIterMut { slab: self, @@ -230,7 +230,7 @@ impl<'a, T> Iterator for IdSlabIterMut<'a, T> { mod test { use std::collections::HashSet; - use rand::{seq::SliceRandom, thread_rng}; + use rand::{rng, seq::SliceRandom}; use super::IdSlab; @@ -251,7 +251,7 @@ mod test { assert_eq!(slab.len(), ids.len() + 1); } - ids.shuffle(&mut thread_rng()); + ids.shuffle(&mut rng()); while 7 < ids.len() { let id = ids.pop().unwrap(); diff --git a/packages/agent_core/src/utils/non_overlapping.rs b/packages/agent_core/src/utils/non_overlapping.rs index 5208f00..32b0c29 100644 --- a/packages/agent_core/src/utils/non_overlapping.rs +++ b/packages/agent_core/src/utils/non_overlapping.rs @@ -43,7 +43,7 @@ impl NonOverlapping { .is_some() } - pub fn iter(&self) -> std::slice::Iter { + pub fn iter(&self) -> std::slice::Iter<'_, T> { self.elements.iter() } } diff --git a/packages/agent_proto/src/control_messages.rs b/packages/agent_proto/src/control_messages.rs index 053c7fe..8ffb09c 100644 --- a/packages/agent_proto/src/control_messages.rs +++ b/packages/agent_proto/src/control_messages.rs @@ -609,7 +609,7 @@ mod test { use std::fmt::Debug; use std::net::{IpAddr, Ipv4Addr}; - use rand::{thread_rng, Rng, RngCore}; + use rand::{rng, Rng, RngCore}; use crate::rpc::ControlRpcMessage; use crate::PortProto; @@ -677,7 +677,7 @@ mod test { #[test] fn fuzzy_test_control_request() { - let mut rng = thread_rng(); + let mut rng = rng(); let mut buffer = vec![0u8; 2048]; for _ in 0..100000 { @@ -698,7 +698,7 @@ mod test { #[test] fn fuzzy_test_control_response() { - let mut rng = thread_rng(); + let mut rng = rng(); let mut buffer = vec![0u8; 2048]; for _ in 0..100000 { From 0698b45a6c7236239cf295a00d12c53c4da378e0 Mon Sep 17 00:00:00 2001 From: Patrick Lorio Date: Tue, 13 Jan 2026 17:11:43 -0800 Subject: [PATCH 7/8] Use std::env & fix panic edge cases --- packages/agent_cli/src/playit_secret.rs | 8 ++++---- packages/agent_cli/src/util.rs | 2 +- packages/agent_core/src/network/udp/udp_clients.rs | 4 ++-- packages/agent_core/src/playit_agent.rs | 6 +++++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/agent_cli/src/playit_secret.rs b/packages/agent_cli/src/playit_secret.rs index 1de0fdf..f15fa57 100644 --- a/packages/agent_cli/src/playit_secret.rs +++ b/packages/agent_cli/src/playit_secret.rs @@ -226,14 +226,14 @@ impl PlayitSecret { let mut path = matches.get_one::("secret_path").cloned(); if secret.is_none() && path.is_none() { - if let Some(secret_env) = option_env!("PLAYIT_SECRET") { - secret.replace(secret_env.to_string()); + if let Ok(secret_env) = std::env::var("PLAYIT_SECRET") { + secret.replace(secret_env); } } if path.is_none() { - if let Some(path_env) = option_env!("PLAYIT_SECRET_PATH") { - path.replace(path_env.to_string()); + if let Ok(path_env) = std::env::var("PLAYIT_SECRET_PATH") { + path.replace(path_env); } } diff --git a/packages/agent_cli/src/util.rs b/packages/agent_cli/src/util.rs index ff2b778..0463392 100644 --- a/packages/agent_cli/src/util.rs +++ b/packages/agent_cli/src/util.rs @@ -8,7 +8,7 @@ pub async fn load_config(path: &str) -> Option { } if path.ends_with(".toml") { - return Some(toml::from_str(&data).unwrap()); + return toml::from_str(&data).ok(); } if path.ends_with(".yaml") || path.ends_with(".yml") { diff --git a/packages/agent_core/src/network/udp/udp_clients.rs b/packages/agent_core/src/network/udp/udp_clients.rs index d375c52..f0b5ba8 100644 --- a/packages/agent_core/src/network/udp/udp_clients.rs +++ b/packages/agent_core/src/network/udp/udp_clients.rs @@ -88,8 +88,8 @@ impl UdpClients { pub fn clear_old(&mut self, now_ms: u64) { self.virtual_clients.retain(|slot, client| { - let since_origin = now_ms - client.from_origin_ts; - let since_tunnel = now_ms - client.from_tunnel_ts; + let since_origin = now_ms.saturating_sub(client.from_origin_ts); + let since_tunnel = now_ms.saturating_sub(client.from_tunnel_ts); let remove = { /* both haven't seen action in over 1m */ diff --git a/packages/agent_core/src/playit_agent.rs b/packages/agent_core/src/playit_agent.rs index f84a38c..a2df538 100644 --- a/packages/agent_core/src/playit_agent.rs +++ b/packages/agent_core/src/playit_agent.rs @@ -134,7 +134,11 @@ impl PlayitAgent { udp_clients.handle_tunneled_packet(now_milli(), flow, packet).await; } session_opt = udp_session_rx.recv() => { - udp_channel.update_session(session_opt.unwrap()).await; + let Some(session) = session_opt else { + tracing::warn!("udp session channel closed"); + break; + }; + udp_channel.update_session(session).await; } _ = tokio::time::sleep_until(next_clear) => { next_clear = Instant::now() + Duration::from_secs(16); From 14bb036f31ba3df4f49aaa332a70beec12595259 Mon Sep 17 00:00:00 2001 From: Patrick Lorio Date: Tue, 13 Jan 2026 18:13:36 -0800 Subject: [PATCH 8/8] Add term UI (#124) * Add term UI * Integrate stats and show them in tui --- Cargo.lock | 258 ++++++- Cargo.toml | 2 +- packages/agent_cli/Cargo.toml | 1 + packages/agent_cli/src/autorun.rs | 208 ++++- packages/agent_cli/src/main.rs | 33 +- packages/agent_cli/src/ui/log_capture.rs | 158 ++++ packages/agent_cli/src/{ui.rs => ui/mod.rs} | 166 ++-- packages/agent_cli/src/ui/tui_app.rs | 724 ++++++++++++++++++ packages/agent_cli/src/ui/widgets.rs | 192 +++++ packages/agent_core/src/lib.rs | 1 + .../agent_core/src/network/origin_lookup.rs | 34 +- .../agent_core/src/network/tcp/tcp_client.rs | 24 +- .../agent_core/src/network/tcp/tcp_clients.rs | 12 +- .../agent_core/src/network/tcp/tcp_pipe.rs | 36 + .../agent_core/src/network/udp/udp_clients.rs | 19 +- packages/agent_core/src/playit_agent.rs | 13 +- packages/agent_core/src/stats.rs | 107 +++ 17 files changed, 1885 insertions(+), 103 deletions(-) create mode 100644 packages/agent_cli/src/ui/log_capture.rs rename packages/agent_cli/src/{ui.rs => ui/mod.rs} (63%) create mode 100644 packages/agent_cli/src/ui/tui_app.rs create mode 100644 packages/agent_cli/src/ui/widgets.rs create mode 100644 packages/agent_core/src/stats.rs diff --git a/Cargo.lock b/Cargo.lock index 6eb7e74..7cccd9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -137,6 +146,21 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.47" @@ -219,6 +243,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "compression-codecs" version = "0.4.33" @@ -351,6 +389,40 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -423,6 +495,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -455,6 +533,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -595,6 +679,17 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -603,7 +698,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -833,6 +928,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -864,6 +965,28 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -886,6 +1009,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -951,12 +1083,30 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1065,6 +1215,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1150,6 +1306,7 @@ dependencies = [ "playit-agent-proto", "playit-api-client", "rand", + "ratatui", "serde", "serde_json", "serde_yaml", @@ -1316,6 +1473,27 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -1345,6 +1523,23 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "reqwest" version = "0.12.24" @@ -1648,12 +1843,40 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2001,10 +2224,14 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -2027,6 +2254,35 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unsafe-libyaml" version = "0.2.11" diff --git a/Cargo.toml b/Cargo.toml index 18f1b94..fddeadd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ tokio = { version = "1.48", features = ["full"] } tokio-util = "0.7" tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "0.2" serde = { version = "1", features = ["derive"] } diff --git a/packages/agent_cli/Cargo.toml b/packages/agent_cli/Cargo.toml index c475dca..c5b50d9 100644 --- a/packages/agent_cli/Cargo.toml +++ b/packages/agent_cli/Cargo.toml @@ -26,6 +26,7 @@ clap = { version = "4.5", features = ["derive"] } urlencoding = "2.1" serde_yaml = "0.9" crossterm = "0.28" +ratatui = "0.29" dotenv = "0.15.0" playit-agent-core = { path = "../agent_core", version = "0.17.0" } diff --git a/packages/agent_cli/src/autorun.rs b/packages/agent_cli/src/autorun.rs index 578120a..97257f4 100644 --- a/packages/agent_cli/src/autorun.rs +++ b/packages/agent_cli/src/autorun.rs @@ -7,28 +7,25 @@ use playit_agent_core::{ udp::udp_settings::UdpSettings, }, playit_agent::{PlayitAgent, PlayitAgentSettings}, + stats::AgentStats, utils::now_milli, }; use playit_api_client::api::*; -// use playit_ping_monitor::PingMonitor; +use tokio::sync::mpsc; -use crate::{API_BASE, CliError, playit_secret::PlayitSecret, ui::UI}; +use crate::{ + API_BASE, CliError, + playit_secret::PlayitSecret, + ui::{ + tui_app::{AccountStatusInfo, AgentData, ConnectionStats, NoticeInfo, PendingTunnelInfo, TunnelInfo}, + UI, + }, +}; pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliError> { let secret_code = secret.ensure_valid(ui).await?.get_or_setup(ui).await?; let api = secret.create_api().await?; - // let mut ping_monitor = PingMonitor::new(api.clone()).await.unwrap(); - - // /* start ping monitor */ - // tokio::spawn(async move { - // loop { - // if let Err(error) = ping_monitor.refresh().await { - // tracing::error!(?error, "error running ping monitor"); - // } - // tokio::time::sleep(Duration::from_millis(3_000 + (random::() % 5_000))).await; - // } - // }); tokio::time::sleep(Duration::from_secs(2)).await; @@ -47,9 +44,12 @@ pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliErr secret_key: secret_code.clone(), }; - let runner = loop { + let (runner, stats) = loop { match PlayitAgent::new(settings.clone(), lookup.clone()).await { - Ok(res) => break res, + Ok(res) => { + let stats = res.stats(); + break (res, stats); + } Err(error) => { error_count += 1; if error_count > 5 { @@ -67,8 +67,184 @@ pub async fn autorun(ui: &mut UI, mut secret: PlayitSecret) -> Result<(), CliErr tokio::spawn(runner.run()); - ui.write_screen("tunnel running").await; + tracing::info!("tunnel running"); + + // Run the appropriate UI loop + if ui.is_tui() { + run_tui_loop(ui, api, lookup, stats).await + } else { + run_log_only_loop(ui, api, lookup).await + } +} + +/// Run the TUI event loop with data updates +async fn run_tui_loop( + ui: &mut UI, + api: playit_api_client::PlayitApi, + lookup: Arc, + stats: AgentStats, +) -> Result<(), CliError> { + let (data_tx, mut data_rx) = mpsc::channel::(4); + + // Spawn the data fetcher task + let api_clone = api.clone(); + let lookup_clone = lookup.clone(); + tokio::spawn(async move { + let mut guest_login_link: Option<(String, u64)> = None; + let mut first_run = true; + + loop { + // Don't delay on first run + if first_run { + first_run = false; + } else { + tokio::time::sleep(Duration::from_secs(3)).await; + } + + let account_tunnels_res = api_clone.v1_agents_rundata().await; + let mut api_data = match account_tunnels_res { + Ok(v) => v, + Err(error) => { + tracing::error!(?error, "Failed to load latest tunnels"); + continue; + } + }; + + lookup_clone.update_from_run_data(&api_data).await; + + // Convert API data to TUI AgentData + let account_status = match api_data.permissions.account_status { + AccountStatus::Guest => AccountStatusInfo::Guest, + AccountStatus::EmailNotVerified => AccountStatusInfo::EmailNotVerified, + AccountStatus::Verified => AccountStatusInfo::Verified, + }; + + // Get login link for guest accounts + let login_link = match api_data.permissions.account_status { + AccountStatus::Guest => { + let now = now_milli(); + match &guest_login_link { + Some((link, ts)) if now - *ts < 15_000 => Some(link.clone()), + _ => { + if let Ok(session) = api_clone.login_guest().await { + let link = format!( + "https://playit.gg/login/guest-account/{}", + session.session_key + ); + guest_login_link = Some((link.clone(), now_milli())); + Some(link) + } else { + None + } + } + } + } + _ => None, + }; + + api_data.notices.sort_by_key(|n| n.priority); + + let notices: Vec = api_data + .notices + .iter() + .map(|n| NoticeInfo { + priority: format!("{:?}", n.priority), + message: n.message.to_string(), + resolve_link: n.resolve_link.as_ref().map(|s| s.to_string()), + }) + .collect(); + + let tunnels: Vec = api_data + .tunnels + .iter() + .filter_map(|tunnel| { + let origin = OriginResource::from_agent_tunnel(tunnel)?; + + let destination = match origin.target { + OriginTarget::Https { + ip, + http_port, + https_port, + } => format!("{ip} (http: {http_port}, https: {https_port})"), + OriginTarget::Port { ip, port } => SocketAddr::new(ip, port).to_string(), + }; + + Some(TunnelInfo { + display_address: tunnel.display_address.clone(), + destination, + is_disabled: tunnel.disabled_reason.is_some(), + disabled_reason: tunnel.disabled_reason.as_ref().map(|s| s.to_string()), + }) + }) + .collect(); + + let pending_tunnels: Vec = api_data + .pending + .iter() + .map(|p| PendingTunnelInfo { + id: p.id.to_string(), + status_msg: p.status_msg.clone(), + }) + .collect(); + + let agent_data = AgentData { + version: env!("CARGO_PKG_VERSION").to_string(), + tunnels, + pending_tunnels, + notices, + account_status, + agent_id: api_data.agent_id.to_string(), + login_link, + }; + + if data_tx.send(agent_data).await.is_err() { + // UI has closed + break; + } + } + }); + + // Run the TUI with data updates + loop { + // Check for new data + while let Ok(data) = data_rx.try_recv() { + ui.update_agent_data(data); + } + + // Update stats from the agent + let snapshot = stats.snapshot(); + ui.update_stats(ConnectionStats { + bytes_in: snapshot.bytes_in, + bytes_out: snapshot.bytes_out, + active_tcp: snapshot.active_tcp, + active_udp: snapshot.active_udp, + }); + + // Run one iteration of the TUI + match ui.tick_tui() { + Ok(true) => {} // Continue + Ok(false) => { + // Quit requested + ui.shutdown_tui()?; + std::process::exit(0); + } + Err(e) => { + ui.shutdown_tui()?; + return Err(e); + } + } + + // Yield to allow other tasks to run + tokio::task::yield_now().await; + } +} +/// Run the log-only loop (original behavior) +async fn run_log_only_loop( + ui: &mut UI, + api: playit_api_client::PlayitApi, + lookup: Arc, +) -> Result<(), CliError> { let mut guest_login_link: Option<(String, u64)> = None; loop { diff --git a/packages/agent_cli/src/main.rs b/packages/agent_cli/src/main.rs index a21d717..7396afe 100644 --- a/packages/agent_cli/src/main.rs +++ b/packages/agent_cli/src/main.rs @@ -7,6 +7,9 @@ use clap::{Command, arg}; use playit_agent_core::agent_control::platform::current_platform; use playit_agent_core::agent_control::version::{help_register_version, register_platform}; use rand::Rng; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; use uuid::Uuid; use autorun::autorun; @@ -17,6 +20,7 @@ use playit_api_client::{PlayitApi, api::*}; use playit_secret::PlayitSecret; use crate::signal_handle::get_signal_handle; +use crate::ui::log_capture::LogCaptureLayer; use crate::ui::{UI, UISettings}; pub static API_BASE: LazyLock = @@ -54,7 +58,17 @@ async fn main() -> Result { let log_only = matches.get_flag("stdout"); let log_path = matches.get_one::("log_path"); + // Create UI first so we can get its log capture + let mut ui = UI::new(UISettings { + auto_answer: None, + log_only, + }); + /* setup logging */ + // Get log level from PLAYIT_LOG env var, defaulting to "info" + let log_filter = EnvFilter::try_from_env("PLAYIT_LOG") + .unwrap_or_else(|_| EnvFilter::new("info")); + let _guard = match (log_only, log_path) { (true, Some(_)) => panic!("try to use -s and -l at the same time"), (false, Some(path)) => { @@ -67,6 +81,7 @@ async fn main() -> Result { tracing_subscriber::fmt() .with_ansi(false) .with_writer(non_blocking) + .with_env_filter(log_filter) .init(); Some(guard) } @@ -75,17 +90,23 @@ async fn main() -> Result { tracing_subscriber::fmt() .with_ansi(current_platform() == Platform::Linux) .with_writer(non_blocking) + .with_env_filter(log_filter) .init(); Some(guard) } - _ => None, + (false, None) => { + // TUI mode - set up log capture layer with filter + if let Some(log_capture) = ui.log_capture() { + let capture_layer = LogCaptureLayer::new(log_capture); + tracing_subscriber::registry() + .with(log_filter) + .with(capture_layer) + .init(); + } + None + } }; - let mut ui = UI::new(UISettings { - auto_answer: None, - log_only, - }); - match matches.subcommand() { None => { ui.write_screen("no command provided, doing auto run").await; diff --git a/packages/agent_cli/src/ui/log_capture.rs b/packages/agent_cli/src/ui/log_capture.rs new file mode 100644 index 0000000..e6737ac --- /dev/null +++ b/packages/agent_cli/src/ui/log_capture.rs @@ -0,0 +1,158 @@ +use std::collections::VecDeque; +use std::sync::{Arc, Mutex}; +use tracing::{Event, Subscriber}; +use tracing_subscriber::layer::Context; +use tracing_subscriber::Layer; + +/// A log entry captured from tracing +#[derive(Clone, Debug)] +pub struct LogEntry { + pub timestamp: u64, + pub level: LogLevel, + pub target: String, + pub message: String, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +impl LogLevel { + pub fn as_str(&self) -> &'static str { + match self { + LogLevel::Trace => "TRACE", + LogLevel::Debug => "DEBUG", + LogLevel::Info => "INFO", + LogLevel::Warn => "WARN", + LogLevel::Error => "ERROR", + } + } +} + +impl From<&tracing::Level> for LogLevel { + fn from(level: &tracing::Level) -> Self { + match *level { + tracing::Level::TRACE => LogLevel::Trace, + tracing::Level::DEBUG => LogLevel::Debug, + tracing::Level::INFO => LogLevel::Info, + tracing::Level::WARN => LogLevel::Warn, + tracing::Level::ERROR => LogLevel::Error, + } + } +} + +/// Ring buffer for captured log entries +pub struct LogCapture { + entries: Mutex>, + capacity: usize, +} + +impl LogCapture { + pub fn new(capacity: usize) -> Arc { + Arc::new(LogCapture { + entries: Mutex::new(VecDeque::with_capacity(capacity)), + capacity, + }) + } + + /// Add a log entry to the buffer + pub fn push(&self, entry: LogEntry) { + let mut entries = self.entries.lock().unwrap(); + if entries.len() >= self.capacity { + entries.pop_front(); + } + entries.push_back(entry); + } + + /// Get a snapshot of all log entries + pub fn get_entries(&self) -> Vec { + let entries = self.entries.lock().unwrap(); + entries.iter().cloned().collect() + } + + /// Get the number of log entries + pub fn len(&self) -> usize { + self.entries.lock().unwrap().len() + } + + /// Check if the log buffer is empty + pub fn is_empty(&self) -> bool { + self.entries.lock().unwrap().is_empty() + } + + /// Clear all log entries + #[allow(dead_code)] + pub fn clear(&self) { + self.entries.lock().unwrap().clear(); + } +} + +/// Tracing layer that captures log events into a LogCapture buffer +pub struct LogCaptureLayer { + capture: Arc, +} + +impl LogCaptureLayer { + pub fn new(capture: Arc) -> Self { + LogCaptureLayer { capture } + } +} + +impl Layer for LogCaptureLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + use playit_agent_core::utils::now_milli; + + let metadata = event.metadata(); + let level = LogLevel::from(metadata.level()); + let target = metadata.target().to_string(); + + // Extract the message from the event + let mut visitor = MessageVisitor::default(); + event.record(&mut visitor); + + let entry = LogEntry { + timestamp: now_milli(), + level, + target, + message: visitor.message, + }; + + self.capture.push(entry); + } +} + +/// Visitor to extract the message field from a tracing event +#[derive(Default)] +struct MessageVisitor { + message: String, +} + +impl tracing::field::Visit for MessageVisitor { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + self.message = format!("{:?}", value); + } else if self.message.is_empty() { + // Fallback: use any debug value if no message field + if !self.message.is_empty() { + self.message.push_str(", "); + } + self.message.push_str(&format!("{}={:?}", field.name(), value)); + } + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "message" { + self.message = value.to_string(); + } else { + if !self.message.is_empty() { + self.message.push_str(", "); + } + self.message.push_str(&format!("{}={}", field.name(), value)); + } + } +} diff --git a/packages/agent_cli/src/ui.rs b/packages/agent_cli/src/ui/mod.rs similarity index 63% rename from packages/agent_cli/src/ui.rs rename to packages/agent_cli/src/ui/mod.rs index 880b2f4..f6e3f57 100644 --- a/packages/agent_cli/src/ui.rs +++ b/packages/agent_cli/src/ui/mod.rs @@ -1,25 +1,25 @@ -use std::io::stdout; - -use crossterm::{ - cursor::RestorePosition, - event::{self, Event, KeyCode, KeyEvent}, - style::{Print, ResetColor}, - terminal::Clear, - ExecutableCommand, -}; +use std::sync::Arc; + +use crossterm::event::{self, Event, KeyCode, KeyEvent}; use playit_agent_core::utils::now_milli; use crate::signal_handle::get_signal_handle; use crate::CliError; -pub struct UI { - auto_answer: Option, - last_display: Option<(u64, String)>, - log_only: bool, - wrote_content: bool, +pub mod log_capture; +pub mod tui_app; +pub mod widgets; + +pub use log_capture::LogCapture; +pub use tui_app::{AgentData, ConnectionStats, TuiApp}; + +/// UI mode - either TUI (interactive) or log-only (stdout) +pub enum UI { + Tui(Box), + LogOnly(LogOnlyUI), } -#[derive(Default)] +#[derive(Default, Clone)] pub struct UISettings { pub auto_answer: Option, pub log_only: bool, @@ -27,11 +27,99 @@ pub struct UISettings { impl UI { pub fn new(settings: UISettings) -> Self { - UI { + if settings.log_only { + UI::LogOnly(LogOnlyUI::new(settings)) + } else { + UI::Tui(Box::new(TuiApp::new(settings))) + } + } + + pub async fn write_screen(&mut self, content: T) { + match self { + UI::Tui(tui) => tui.write_screen(content).await, + UI::LogOnly(log_only) => log_only.write_screen(content).await, + } + } + + pub async fn yn_question( + &mut self, + question: T, + default_yes: Option, + ) -> Result { + match self { + UI::Tui(tui) => tui.yn_question(question, default_yes).await, + UI::LogOnly(log_only) => log_only.yn_question(question, default_yes).await, + } + } + + pub async fn write_error( + &mut self, + msg: M, + error: E, + ) { + self.write_screen(format!("Got Error\nMSG: {}\nError: {:?}\n", msg, error)) + .await + } + + /// Update UI with agent data (for TUI mode) + pub fn update_agent_data(&mut self, data: AgentData) { + if let UI::Tui(tui) = self { + tui.update_agent_data(data); + } + } + + /// Update UI with connection stats (for TUI mode) + pub fn update_stats(&mut self, stats: ConnectionStats) { + if let UI::Tui(tui) = self { + tui.update_stats(stats); + } + } + + /// Get the log capture for TUI mode + pub fn log_capture(&self) -> Option> { + if let UI::Tui(tui) = self { + Some(tui.log_capture()) + } else { + None + } + } + + /// Run one iteration of the TUI event loop + /// Returns Ok(true) if should continue, Ok(false) if should quit + pub fn tick_tui(&mut self) -> Result { + if let UI::Tui(tui) = self { + tui.tick() + } else { + Ok(true) + } + } + + /// Shutdown the TUI + pub fn shutdown_tui(&mut self) -> Result<(), CliError> { + if let UI::Tui(tui) = self { + tui.shutdown() + } else { + Ok(()) + } + } + + /// Check if TUI mode is active + pub fn is_tui(&self) -> bool { + matches!(self, UI::Tui(_)) + } +} + +/// Log-only UI mode (original behavior) +pub struct LogOnlyUI { + auto_answer: Option, + last_display: Option<(u64, String)>, +} + +impl LogOnlyUI { + pub fn new(settings: UISettings) -> Self { + LogOnlyUI { auto_answer: settings.auto_answer, - log_only: settings.log_only, last_display: None, - wrote_content: false, } } @@ -78,39 +166,6 @@ impl UI { tracing::info!("{}", content.lines().next().unwrap()); self.last_display = Some((now_milli(), content)); } - - if self.log_only { - return; - } - - let content_ref = &content; - let res: std::io::Result<()> = (|| { - let cleared = if self.wrote_content { - stdout() - .execute(Clear(crossterm::terminal::ClearType::All)) - .is_ok() - } else { - true - }; - - if !cleared { - stdout().execute(Print(format!("\n{}", content_ref)))?; - } else { - stdout() - .execute(RestorePosition)? - .execute(ResetColor)? - .execute(Print(content_ref))?; - } - - Ok(()) - })(); - - if let Err(error) = res { - tracing::error!(?error, "failed to write to screen"); - println!("{}", content); - } - - self.wrote_content = true; } pub async fn yn_question( @@ -188,13 +243,4 @@ impl UI { Err(CliError::AnswerNotProvided) } - - pub async fn write_error( - &mut self, - msg: M, - error: E, - ) { - self.write_screen(format!("Got Error\nMSG: {}\nError: {:?}\n", msg, error)) - .await - } } diff --git a/packages/agent_cli/src/ui/tui_app.rs b/packages/agent_cli/src/ui/tui_app.rs new file mode 100644 index 0000000..d7ae3b7 --- /dev/null +++ b/packages/agent_cli/src/ui/tui_app.rs @@ -0,0 +1,724 @@ +use std::io::{self, stdout, Stdout}; +use std::sync::Arc; +use std::time::Duration; + +use crossterm::{ + event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use playit_agent_core::utils::now_milli; +use ratatui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, + Frame, Terminal, +}; + +use super::log_capture::{LogCapture, LogEntry, LogLevel}; +use super::widgets::{render_header, render_help_bar, render_stats_bar}; +use super::UISettings; +use crate::signal_handle::get_signal_handle; +use crate::CliError; + +/// Data about the running agent +#[derive(Clone, Default)] +pub struct AgentData { + pub version: String, + pub tunnels: Vec, + pub pending_tunnels: Vec, + pub notices: Vec, + pub account_status: AccountStatusInfo, + pub agent_id: String, + pub login_link: Option, +} + +#[derive(Clone, Debug)] +pub struct TunnelInfo { + pub display_address: String, + pub destination: String, + pub is_disabled: bool, + pub disabled_reason: Option, +} + +#[derive(Clone, Debug)] +pub struct PendingTunnelInfo { + pub id: String, + pub status_msg: String, +} + +#[derive(Clone, Debug)] +pub struct NoticeInfo { + pub priority: String, + pub message: String, + pub resolve_link: Option, +} + +#[derive(Clone, Default, Debug, PartialEq)] +pub enum AccountStatusInfo { + #[default] + Unknown, + Guest, + EmailNotVerified, + Verified, +} + +/// Connection statistics +#[derive(Clone, Default)] +pub struct ConnectionStats { + pub bytes_in: u64, + pub bytes_out: u64, + pub active_tcp: u32, + pub active_udp: u32, +} + +/// Which panel is currently focused +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FocusedPanel { + Tunnels, + Logs, +} + +/// UI mode for TuiApp +#[derive(Clone, Debug, PartialEq)] +pub enum TuiMode { + /// Setup mode - showing a message (e.g., claim URL) + Setup { message: String }, + /// Running mode - showing tunnels and stats + Running, +} + +/// Main TUI application state +pub struct TuiApp { + settings: UISettings, + log_capture: Arc, + agent_data: AgentData, + stats: ConnectionStats, + start_time: u64, + + // UI state + mode: TuiMode, + focused_panel: FocusedPanel, + tunnel_list_state: ListState, + log_scroll: usize, + log_follow: bool, // Auto-scroll logs when at bottom + should_quit: bool, + quit_confirm: bool, + + // Terminal + terminal: Option>>, +} + +impl TuiApp { + pub fn new(settings: UISettings) -> Self { + TuiApp { + settings, + log_capture: LogCapture::new(500), + agent_data: AgentData::default(), + stats: ConnectionStats::default(), + start_time: now_milli(), + mode: TuiMode::Setup { message: "Initializing...".to_string() }, + focused_panel: FocusedPanel::Tunnels, + tunnel_list_state: ListState::default(), + log_scroll: 0, + log_follow: true, // Start with follow mode enabled + should_quit: false, + quit_confirm: false, + terminal: None, + } + } + + pub fn log_capture(&self) -> Arc { + self.log_capture.clone() + } + + pub fn update_agent_data(&mut self, data: AgentData) { + self.agent_data = data; + // Switch to running mode when we get agent data + self.mode = TuiMode::Running; + } + + pub fn update_stats(&mut self, stats: ConnectionStats) { + self.stats = stats; + } + + /// Set the setup message to display + pub fn set_setup_message(&mut self, message: String) { + self.mode = TuiMode::Setup { message }; + } + + /// Switch to running mode + pub fn set_running_mode(&mut self) { + self.mode = TuiMode::Running; + } + + /// Initialize the terminal for TUI mode + fn init_terminal(&mut self) -> io::Result<()> { + enable_raw_mode()?; + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + self.terminal = Some(Terminal::new(backend)?); + Ok(()) + } + + /// Restore the terminal to normal mode + fn restore_terminal(&mut self) -> io::Result<()> { + disable_raw_mode()?; + if let Some(ref mut terminal) = self.terminal { + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + } + Ok(()) + } + + /// Initialize the TUI (call once before tick()) + pub fn init(&mut self) -> Result<(), CliError> { + if self.terminal.is_none() { + self.init_terminal().map_err(CliError::RenderError)?; + } + Ok(()) + } + + /// Shutdown the TUI (call when done) + pub fn shutdown(&mut self) -> Result<(), CliError> { + self.restore_terminal().map_err(CliError::RenderError) + } + + /// Check if the TUI should quit + pub fn should_quit(&self) -> bool { + self.should_quit + } + + /// Run one iteration of the TUI (draw + handle events) + /// Returns Ok(true) if should continue, Ok(false) if should quit + pub fn tick(&mut self) -> Result { + // Initialize if not already + if self.terminal.is_none() { + self.init()?; + } + + // Draw the UI + self.draw().map_err(CliError::RenderError)?; + + // Handle events with a short timeout to allow for async updates + if event::poll(Duration::from_millis(50)).map_err(CliError::RenderError)? { + if let Event::Key(key) = event::read().map_err(CliError::RenderError)? { + self.handle_key_event(key); + } + } + + // Check for signal close request + let signal = get_signal_handle(); + if signal.is_confirming_close() && !self.quit_confirm { + self.quit_confirm = true; + } + + // Return whether to continue + Ok(!self.should_quit) + } + + /// Run the TUI event loop (blocking) + pub async fn run(&mut self) -> Result<(), CliError> { + self.init()?; + + loop { + if !self.tick()? { + break; + } + // Yield to allow other tasks to run + tokio::task::yield_now().await; + } + + self.shutdown()?; + Ok(()) + } + + fn handle_key_event(&mut self, key: KeyEvent) { + // Handle quit confirmation + if self.quit_confirm { + match key.code { + KeyCode::Char('y') | KeyCode::Char('Y') => { + self.should_quit = true; + } + KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { + self.quit_confirm = false; + get_signal_handle().decline_close(); + } + _ => {} + } + return; + } + + match key.code { + // Quit + KeyCode::Char('q') => { + self.quit_confirm = true; + } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + self.quit_confirm = true; + } + + // Navigation + KeyCode::Tab => { + self.focused_panel = match self.focused_panel { + FocusedPanel::Tunnels => FocusedPanel::Logs, + FocusedPanel::Logs => FocusedPanel::Tunnels, + }; + } + + // Scrolling + KeyCode::Char('j') | KeyCode::Down => self.scroll_down(), + KeyCode::Char('k') | KeyCode::Up => self.scroll_up(), + KeyCode::Char('g') => self.scroll_to_top(), + KeyCode::Char('G') => self.scroll_to_bottom(), + KeyCode::PageDown => { + for _ in 0..10 { + self.scroll_down(); + } + } + KeyCode::PageUp => { + for _ in 0..10 { + self.scroll_up(); + } + } + + _ => {} + } + } + + fn scroll_down(&mut self) { + match self.focused_panel { + FocusedPanel::Tunnels => { + let total = self.agent_data.tunnels.len(); + if total > 0 { + let i = match self.tunnel_list_state.selected() { + Some(i) => (i + 1).min(total - 1), + None => 0, + }; + self.tunnel_list_state.select(Some(i)); + } + } + FocusedPanel::Logs => { + let total = self.log_capture.len(); + if self.log_scroll < total.saturating_sub(1) { + self.log_scroll += 1; + // Re-enable follow if we scrolled to the bottom + if self.log_scroll >= total.saturating_sub(1) { + self.log_follow = true; + } + } + } + } + } + + fn scroll_up(&mut self) { + match self.focused_panel { + FocusedPanel::Tunnels => { + let i = match self.tunnel_list_state.selected() { + Some(i) => i.saturating_sub(1), + None => 0, + }; + self.tunnel_list_state.select(Some(i)); + } + FocusedPanel::Logs => { + self.log_scroll = self.log_scroll.saturating_sub(1); + // Disable follow when scrolling up + self.log_follow = false; + } + } + } + + fn scroll_to_top(&mut self) { + match self.focused_panel { + FocusedPanel::Tunnels => { + self.tunnel_list_state.select(Some(0)); + } + FocusedPanel::Logs => { + self.log_scroll = 0; + // Disable follow when going to top + self.log_follow = false; + } + } + } + + fn scroll_to_bottom(&mut self) { + match self.focused_panel { + FocusedPanel::Tunnels => { + let total = self.agent_data.tunnels.len(); + if total > 0 { + self.tunnel_list_state.select(Some(total - 1)); + } + } + FocusedPanel::Logs => { + let total = self.log_capture.len(); + self.log_scroll = total.saturating_sub(1); + // Enable follow when going to bottom + self.log_follow = true; + } + } + } + + fn draw(&mut self) -> io::Result<()> { + let terminal = self.terminal.as_mut().unwrap(); + + let mode = self.mode.clone(); + let agent_data = self.agent_data.clone(); + let stats = self.stats.clone(); + let start_time = self.start_time; + let focused_panel = self.focused_panel; + let quit_confirm = self.quit_confirm; + let log_entries = self.log_capture.get_entries(); + let log_follow = self.log_follow; + + // Auto-scroll to bottom if following logs + let log_scroll = if log_follow { + let total = log_entries.len(); + self.log_scroll = total.saturating_sub(1); + self.log_scroll + } else { + self.log_scroll + }; + + let mut tunnel_list_state = self.tunnel_list_state.clone(); + + terminal.draw(|frame| { + let area = frame.area(); + + match &mode { + TuiMode::Setup { message } => { + // Render setup screen with centered message + Self::render_setup_screen(frame, area, message, quit_confirm); + return; + } + TuiMode::Running => { + // Normal running mode + } + } + + // Main layout: Header, Content, Stats, Logs, Help + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Header + Constraint::Min(8), // Tunnels + Constraint::Length(3), // Stats + Constraint::Length(10), // Logs + Constraint::Length(1), // Help bar + ]) + .split(area); + + // Render header + render_header(frame, chunks[0], &agent_data, start_time); + + // Render tunnel list + Self::render_tunnels( + frame, + chunks[1], + &agent_data, + focused_panel == FocusedPanel::Tunnels, + &mut tunnel_list_state, + ); + + // Render stats bar + render_stats_bar(frame, chunks[2], &stats); + + // Render log panel + Self::render_logs( + frame, + chunks[3], + &log_entries, + log_scroll, + focused_panel == FocusedPanel::Logs, + log_follow, + ); + + // Render help bar + render_help_bar(frame, chunks[4], quit_confirm); + })?; + + self.tunnel_list_state = tunnel_list_state; + + Ok(()) + } + + fn render_tunnels( + frame: &mut Frame, + area: Rect, + agent_data: &AgentData, + focused: bool, + list_state: &mut ListState, + ) { + let border_style = if focused { + Style::default().fg(Color::Cyan) + } else { + Style::default().fg(Color::DarkGray) + }; + + let block = Block::default() + .title(" Tunnels ") + .borders(Borders::ALL) + .border_style(border_style); + + if agent_data.tunnels.is_empty() && agent_data.pending_tunnels.is_empty() { + let msg = if agent_data.agent_id.is_empty() { + "No tunnels configured. Setting up..." + } else { + "No tunnels configured. Add tunnels at playit.gg" + }; + let paragraph = Paragraph::new(msg) + .style(Style::default().fg(Color::Yellow)) + .block(block) + .wrap(Wrap { trim: true }); + frame.render_widget(paragraph, area); + return; + } + + let items: Vec = agent_data + .tunnels + .iter() + .map(|tunnel| { + let (style, prefix) = if tunnel.is_disabled { + (Style::default().fg(Color::Red), "✗ ") + } else { + (Style::default().fg(Color::Green), "● ") + }; + + let content = if let Some(reason) = &tunnel.disabled_reason { + format!( + "{}{} => (disabled: {})", + prefix, tunnel.display_address, reason + ) + } else { + format!("{}{} => {}", prefix, tunnel.display_address, tunnel.destination) + }; + + ListItem::new(content).style(style) + }) + .chain(agent_data.pending_tunnels.iter().map(|pending| { + let content = format!("◐ {} ({})", pending.id, pending.status_msg); + ListItem::new(content).style(Style::default().fg(Color::Yellow)) + })) + .collect(); + + let list = List::new(items) + .block(block) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .bg(Color::DarkGray), + ) + .highlight_symbol("▶ "); + + frame.render_stateful_widget(list, area, list_state); + } + + fn render_logs( + frame: &mut Frame, + area: Rect, + log_entries: &[LogEntry], + scroll: usize, + focused: bool, + following: bool, + ) { + let border_style = if focused { + Style::default().fg(Color::Cyan) + } else { + Style::default().fg(Color::DarkGray) + }; + + let title = if following { + format!(" Logs ({}) [following] ", log_entries.len()) + } else { + format!(" Logs ({}) ", log_entries.len()) + }; + + let block = Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(border_style); + + let inner_height = area.height.saturating_sub(2) as usize; + let start = scroll.min(log_entries.len().saturating_sub(inner_height)); + let visible_entries = log_entries + .iter() + .skip(start) + .take(inner_height); + + let lines: Vec = visible_entries + .map(|entry| { + let level_style = match entry.level { + LogLevel::Error => Style::default().fg(Color::Red).bold(), + LogLevel::Warn => Style::default().fg(Color::Yellow).bold(), + LogLevel::Info => Style::default().fg(Color::Green), + LogLevel::Debug => Style::default().fg(Color::Blue), + LogLevel::Trace => Style::default().fg(Color::DarkGray), + }; + + Line::from(vec![ + Span::styled( + format!("[{}] ", entry.level.as_str()), + level_style, + ), + Span::styled( + format!("{}: ", entry.target.split("::").last().unwrap_or(&entry.target)), + Style::default().fg(Color::DarkGray), + ), + Span::raw(&entry.message), + ]) + }) + .collect(); + + let paragraph = Paragraph::new(lines).block(block); + frame.render_widget(paragraph, area); + } + + fn render_setup_screen(frame: &mut Frame, area: Rect, message: &str, quit_confirm: bool) { + use ratatui::layout::Alignment; + + // Create a centered layout + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(30), + Constraint::Min(10), + Constraint::Length(1), + ]) + .split(area); + + // Title block + let title_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Magenta)) + .title(" playit.gg "); + + // Parse message into lines and style URLs differently + let lines: Vec = message + .lines() + .map(|line| { + if line.starts_with("http://") || line.starts_with("https://") { + Line::from(Span::styled( + line, + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + )) + } else if line.contains("https://") || line.contains("http://") { + // Line contains a URL somewhere + let mut spans = Vec::new(); + let mut remaining = line; + while let Some(pos) = remaining.find("https://").or_else(|| remaining.find("http://")) { + if pos > 0 { + spans.push(Span::styled(&remaining[..pos], Style::default().fg(Color::White))); + } + // Find end of URL (space or end of string) + let url_start = pos; + let url_end = remaining[pos..].find(' ').map(|p| pos + p).unwrap_or(remaining.len()); + spans.push(Span::styled( + &remaining[url_start..url_end], + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + )); + remaining = &remaining[url_end..]; + } + if !remaining.is_empty() { + spans.push(Span::styled(remaining, Style::default().fg(Color::White))); + } + Line::from(spans) + } else { + Line::from(Span::styled(line, Style::default().fg(Color::White))) + } + }) + .collect(); + + let paragraph = Paragraph::new(lines) + .block(title_block) + .alignment(Alignment::Center) + .wrap(Wrap { trim: false }); + + frame.render_widget(paragraph, chunks[1]); + + // Help bar + render_help_bar(frame, chunks[2], quit_confirm); + } + + /// Simple screen write for compatibility (used during setup) + pub async fn write_screen(&mut self, content: T) { + let signal = get_signal_handle(); + let exit_confirm = signal.is_confirming_close(); + + if exit_confirm { + match self + .yn_question( + format!("{}\nClose requested, close program?", content), + Some(true), + ) + .await + { + Ok(close) => { + if close { + std::process::exit(0); + } else { + signal.decline_close(); + } + } + Err(error) => { + tracing::error!(%error, "failed to ask close signal question"); + } + } + return; + } + + // Set the setup message and render + let message = content.to_string(); + tracing::info!("{}", message.lines().next().unwrap_or("")); + self.set_setup_message(message); + + // Initialize terminal if not already done + if self.terminal.is_none() { + if let Err(e) = self.init_terminal() { + tracing::error!(?e, "Failed to init terminal"); + return; + } + } + + // Draw the screen + if let Err(e) = self.draw() { + tracing::error!(?e, "Failed to draw screen"); + } + + // Handle any pending keyboard events (non-blocking) + if let Ok(true) = event::poll(Duration::from_millis(10)) { + if let Ok(Event::Key(key)) = event::read() { + self.handle_key_event(key); + } + } + + // Check if quit was requested + if self.should_quit { + let _ = self.restore_terminal(); + std::process::exit(0); + } + } + + pub async fn yn_question( + &mut self, + _question: T, + default_yes: Option, + ) -> Result { + // For TUI mode, we use the quit confirm mechanism + // For now, return the default if available + if let Some(default) = default_yes { + return Ok(default); + } + if let Some(auto) = self.settings.auto_answer { + return Ok(auto); + } + Err(CliError::AnswerNotProvided) + } +} + +impl Drop for TuiApp { + fn drop(&mut self) { + let _ = self.restore_terminal(); + } +} diff --git a/packages/agent_cli/src/ui/widgets.rs b/packages/agent_cli/src/ui/widgets.rs new file mode 100644 index 0000000..3a42126 --- /dev/null +++ b/packages/agent_cli/src/ui/widgets.rs @@ -0,0 +1,192 @@ +use playit_agent_core::utils::now_milli; +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, +}; + +use super::tui_app::{AccountStatusInfo, AgentData, ConnectionStats}; + +/// Format bytes into a human-readable string +fn format_bytes(bytes: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + + if bytes >= GB { + format!("{:.2} GB", bytes as f64 / GB as f64) + } else if bytes >= MB { + format!("{:.2} MB", bytes as f64 / MB as f64) + } else if bytes >= KB { + format!("{:.2} KB", bytes as f64 / KB as f64) + } else { + format!("{} B", bytes) + } +} + +/// Format duration in seconds to a human-readable string +fn format_uptime(start_time: u64) -> String { + let elapsed_ms = now_milli().saturating_sub(start_time); + let elapsed_secs = elapsed_ms / 1000; + + let hours = elapsed_secs / 3600; + let minutes = (elapsed_secs % 3600) / 60; + let seconds = elapsed_secs % 60; + + if hours > 0 { + format!("{}h {:02}m {:02}s", hours, minutes, seconds) + } else if minutes > 0 { + format!("{}m {:02}s", minutes, seconds) + } else { + format!("{}s", seconds) + } +} + +/// Render the header bar with version, uptime, and connection status +pub fn render_header(frame: &mut Frame, area: Rect, agent_data: &AgentData, start_time: u64) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(30), + Constraint::Percentage(30), + ]) + .split(area); + + // Version and title + let version = if agent_data.version.is_empty() { + env!("CARGO_PKG_VERSION").to_string() + } else { + agent_data.version.clone() + }; + + let title_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Magenta)); + + let title = Paragraph::new(Line::from(vec![ + Span::styled("playit", Style::default().fg(Color::Magenta).bold()), + Span::styled(format!(" v{}", version), Style::default().fg(Color::White)), + ])) + .block(title_block) + .alignment(Alignment::Left); + + frame.render_widget(title, chunks[0]); + + // Uptime + let uptime_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)); + + let uptime = Paragraph::new(Line::from(vec![ + Span::styled("⏱ ", Style::default().fg(Color::Cyan)), + Span::styled(format_uptime(start_time), Style::default().fg(Color::White)), + ])) + .block(uptime_block) + .alignment(Alignment::Center); + + frame.render_widget(uptime, chunks[1]); + + // Account status + let (status_text, status_color) = match &agent_data.account_status { + AccountStatusInfo::Verified => ("● Verified", Color::Green), + AccountStatusInfo::Guest => ("○ Guest", Color::Yellow), + AccountStatusInfo::EmailNotVerified => ("◐ Email Not Verified", Color::Yellow), + AccountStatusInfo::Unknown => ("? Connecting...", Color::DarkGray), + }; + + let status_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)); + + let status = Paragraph::new(Span::styled(status_text, Style::default().fg(status_color))) + .block(status_block) + .alignment(Alignment::Right); + + frame.render_widget(status, chunks[2]); +} + +/// Render the stats bar showing connection statistics +pub fn render_stats_bar(frame: &mut Frame, area: Rect, stats: &ConnectionStats) { + let block = Block::default() + .title(" Statistics ") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)); + + let inner = block.inner(area); + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ]) + .split(inner); + + frame.render_widget(block, area); + + // Bytes In + let bytes_in = Paragraph::new(Line::from(vec![ + Span::styled("↓ In: ", Style::default().fg(Color::Green)), + Span::styled(format_bytes(stats.bytes_in), Style::default().fg(Color::White)), + ])) + .alignment(Alignment::Center); + frame.render_widget(bytes_in, chunks[0]); + + // Bytes Out + let bytes_out = Paragraph::new(Line::from(vec![ + Span::styled("↑ Out: ", Style::default().fg(Color::Blue)), + Span::styled(format_bytes(stats.bytes_out), Style::default().fg(Color::White)), + ])) + .alignment(Alignment::Center); + frame.render_widget(bytes_out, chunks[1]); + + // TCP Connections + let tcp = Paragraph::new(Line::from(vec![ + Span::styled("TCP: ", Style::default().fg(Color::Cyan)), + Span::styled(stats.active_tcp.to_string(), Style::default().fg(Color::White)), + ])) + .alignment(Alignment::Center); + frame.render_widget(tcp, chunks[2]); + + // UDP Flows + let udp = Paragraph::new(Line::from(vec![ + Span::styled("UDP: ", Style::default().fg(Color::Magenta)), + Span::styled(stats.active_udp.to_string(), Style::default().fg(Color::White)), + ])) + .alignment(Alignment::Center); + frame.render_widget(udp, chunks[3]); +} + +/// Render the help bar with keybindings +pub fn render_help_bar(frame: &mut Frame, area: Rect, quit_confirm: bool) { + let help_text = if quit_confirm { + Line::from(vec![ + Span::styled("Quit? ", Style::default().fg(Color::Yellow).bold()), + Span::styled("[y]", Style::default().fg(Color::Green).bold()), + Span::raw(" Yes "), + Span::styled("[n]", Style::default().fg(Color::Red).bold()), + Span::raw(" No"), + ]) + } else { + Line::from(vec![ + Span::styled("j/k", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), + Span::raw(" Scroll "), + Span::styled("Tab", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), + Span::raw(" Switch Panel "), + Span::styled("g/G", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), + Span::raw(" Top/Bottom "), + Span::styled("q", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), + Span::raw(" Quit"), + ]) + }; + + let help = Paragraph::new(help_text) + .style(Style::default().fg(Color::DarkGray)) + .alignment(Alignment::Center); + + frame.render_widget(help, area); +} diff --git a/packages/agent_core/src/lib.rs b/packages/agent_core/src/lib.rs index e97e4cd..d7da071 100644 --- a/packages/agent_core/src/lib.rs +++ b/packages/agent_core/src/lib.rs @@ -3,6 +3,7 @@ extern crate core; pub mod agent_control; pub mod network; pub mod playit_agent; +pub mod stats; pub mod utils; pub const PROTOCOL_VERSION: u64 = 2; diff --git a/packages/agent_core/src/network/origin_lookup.rs b/packages/agent_core/src/network/origin_lookup.rs index 649a625..6e48b41 100644 --- a/packages/agent_core/src/network/origin_lookup.rs +++ b/packages/agent_core/src/network/origin_lookup.rs @@ -146,21 +146,33 @@ impl OriginResource { .and_then(|v| u16::from_str(&v.value).ok()) .unwrap_or(443), }, - _ => OriginTarget::Port { - ip: tunn - .agent_config - .fields - .iter() - .find(|f| f.name.eq("local_ip")) - .and_then(|v| IpAddr::from_str(&v.value).ok()) - .unwrap_or_else(|| "127.0.0.1".parse().unwrap()), - port: tunn + _ => { + // Get local_port from config, or fall back to public port from display_address + let local_port = tunn .agent_config .fields .iter() .find(|f| f.name.eq("local_port")) - .and_then(|v| u16::from_str(&v.value).ok())?, - }, + .and_then(|v| u16::from_str(&v.value).ok()) + .or_else(|| { + // Extract port from display_address (format: "hostname:port" or "ip:port") + tunn.display_address + .rsplit(':') + .next() + .and_then(|p| u16::from_str(p).ok()) + })?; + + OriginTarget::Port { + ip: tunn + .agent_config + .fields + .iter() + .find(|f| f.name.eq("local_ip")) + .and_then(|v| IpAddr::from_str(&v.value).ok()) + .unwrap_or_else(|| "127.0.0.1".parse().unwrap()), + port: local_port, + } + } }; Some(OriginResource { diff --git a/packages/agent_core/src/network/tcp/tcp_client.rs b/packages/agent_core/src/network/tcp/tcp_client.rs index 52a1b69..d954b04 100644 --- a/packages/agent_core/src/network/tcp/tcp_client.rs +++ b/packages/agent_core/src/network/tcp/tcp_client.rs @@ -2,7 +2,9 @@ use serde::Serialize; use tokio::net::TcpStream; use tokio_util::sync::CancellationToken; -use super::tcp_pipe::TcpPipe; +use crate::stats::AgentStats; + +use super::tcp_pipe::{PipeDirection, TcpPipe}; pub struct TcpClient { tunn_to_origin: TcpPipe, @@ -11,14 +13,30 @@ pub struct TcpClient { impl TcpClient { pub async fn create(tunn: TcpStream, origin: TcpStream) -> Self { + Self::create_with_stats(tunn, origin, None).await + } + + pub async fn create_with_stats(tunn: TcpStream, origin: TcpStream, stats: Option) -> Self { let (tunn_read, tunn_write) = tunn.into_split(); let (origin_read, origin_write) = origin.into_split(); let cancel = CancellationToken::new(); TcpClient { - tunn_to_origin: TcpPipe::new_with_cancel(cancel.clone(), tunn_read, origin_write), - origin_to_tunn: TcpPipe::new_with_cancel(cancel, origin_read, tunn_write), + tunn_to_origin: TcpPipe::new_with_stats( + cancel.clone(), + tunn_read, + origin_write, + stats.clone(), + PipeDirection::TunnelToOrigin, + ), + origin_to_tunn: TcpPipe::new_with_stats( + cancel, + origin_read, + tunn_write, + stats, + PipeDirection::OriginToTunnel, + ), } } diff --git a/packages/agent_core/src/network/tcp/tcp_clients.rs b/packages/agent_core/src/network/tcp/tcp_clients.rs index d3e70b5..a2f168c 100644 --- a/packages/agent_core/src/network/tcp/tcp_clients.rs +++ b/packages/agent_core/src/network/tcp/tcp_clients.rs @@ -16,6 +16,7 @@ use crate::{ network::{ lan_address::LanAddress, origin_lookup::OriginLookup, proxy_protocol::ProxyProtocolHeader, }, + stats::AgentStats, utils::now_milli, }; @@ -37,6 +38,7 @@ struct Worker { events_tx: Sender, cancel: CancellationToken, settings: TcpSettings, + stats: AgentStats, clients: Vec, next_client_id: u64, @@ -90,7 +92,7 @@ enum Event { } impl TcpClients { - pub fn new(settings: TcpSettings, lookup: Arc) -> Self { + pub fn new(settings: TcpSettings, lookup: Arc, stats: AgentStats) -> Self { let quota = unsafe { Quota::per_second(NonZeroU32::new_unchecked(settings.new_client_ratelimit)).allow_burst( NonZeroU32::new_unchecked(settings.new_client_ratelimit_burst), @@ -108,6 +110,7 @@ impl TcpClients { events_tx: events_tx.clone(), cancel: cancel.clone(), settings, + stats, clients: Vec::with_capacity(32), } .start(), @@ -218,6 +221,7 @@ impl Worker { let setting_tcp_no_delay = self.settings.tcp_no_delay; let event_tx = self.events_tx.clone(); + let stats = self.stats.clone(); tokio::spawn(async move { /* connect to tunnel server */ @@ -357,7 +361,7 @@ impl Worker { } } - let tcp_client = TcpClient::create(tunn_stream, origin_stream).await; + let tcp_client = TcpClient::create_with_stats(tunn_stream, origin_stream, Some(stats)).await; let _ = event_tx .send(Event::ConnectedClient(Client { id: client_id, @@ -377,6 +381,7 @@ impl Worker { } Event::ConnectedClient(client) => { self.clients.push(client); + self.stats.set_tcp(self.clients.len() as u32); } Event::ClearOld => { let now = now_milli(); @@ -403,6 +408,9 @@ impl Worker { true }); + + // Update active TCP connection count + self.stats.set_tcp(self.clients.len() as u32); } } } diff --git a/packages/agent_core/src/network/tcp/tcp_pipe.rs b/packages/agent_core/src/network/tcp/tcp_pipe.rs index 2b20e1e..59b5b0f 100644 --- a/packages/agent_core/src/network/tcp/tcp_pipe.rs +++ b/packages/agent_core/src/network/tcp/tcp_pipe.rs @@ -6,8 +6,18 @@ use std::sync::{ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio_util::sync::CancellationToken; +use crate::stats::AgentStats; use crate::utils::now_milli; +/// Direction of data flow for stats tracking +#[derive(Clone, Copy)] +pub enum PipeDirection { + /// Data flowing from tunnel to local origin (bytes in) + TunnelToOrigin, + /// Data flowing from local origin to tunnel (bytes out) + OriginToTunnel, +} + pub struct TcpPipe { cancel: CancellationToken, shared: Arc, @@ -33,6 +43,19 @@ impl TcpPipe { cancel: CancellationToken, from: R, to: W, + ) -> Self { + Self::new_with_stats(cancel, from, to, None, PipeDirection::TunnelToOrigin) + } + + pub fn new_with_stats< + R: AsyncRead + Unpin + Send + 'static, + W: AsyncWrite + Unpin + Send + 'static, + >( + cancel: CancellationToken, + from: R, + to: W, + stats: Option, + direction: PipeDirection, ) -> Self { let shared = Arc::new(Shared { last_activity: AtomicU64::new(now_milli()), @@ -47,6 +70,8 @@ impl TcpPipe { shared: this.shared.clone(), from, to, + stats, + direction, } .start(), ); @@ -88,6 +113,8 @@ struct Worker { shared: Arc, from: R, to: W, + stats: Option, + direction: PipeDirection, } impl Worker { @@ -130,6 +157,15 @@ impl Worker { self.shared .bytes_written .fetch_add(byte_count as u64, Ordering::AcqRel); + + // Update global stats if provided + if let Some(ref stats) = self.stats { + let bytes = byte_count as u64; + match self.direction { + PipeDirection::TunnelToOrigin => stats.add_bytes_in(bytes), + PipeDirection::OriginToTunnel => stats.add_bytes_out(bytes), + } + } } self.shared.last_activity.store(u64::MAX, Ordering::Release); diff --git a/packages/agent_core/src/network/udp/udp_clients.rs b/packages/agent_core/src/network/udp/udp_clients.rs index f0b5ba8..40ce942 100644 --- a/packages/agent_core/src/network/udp/udp_clients.rs +++ b/packages/agent_core/src/network/udp/udp_clients.rs @@ -18,6 +18,7 @@ use crate::network::{ origin_lookup::{OriginLookup, OriginTarget}, proxy_protocol::ProxyProtocolHeader, }; +use crate::stats::AgentStats; use playit_agent_proto::udp_proto::UdpFlow; use super::{ @@ -36,6 +37,7 @@ pub struct UdpClients { rx: Receiver, new_client_limiter: DefaultDirectRateLimiter, + stats: AgentStats, } struct Client { @@ -64,7 +66,7 @@ impl UdpClientKey { } impl UdpClients { - pub fn new(settings: UdpSettings, lookup: Arc, packets: Packets) -> Self { + pub fn new(settings: UdpSettings, lookup: Arc, packets: Packets, stats: AgentStats) -> Self { let (origin_tx, origin_rx) = channel(2048); let quota = unsafe { @@ -83,6 +85,7 @@ impl UdpClients { }, rx: origin_rx, new_client_limiter: RateLimiter::direct(quota), + stats, } } @@ -108,6 +111,9 @@ impl UdpClients { true } }); + + // Update active UDP count + self.stats.set_udp(self.virtual_clients.len() as u32); } pub async fn recv_origin_packet(&mut self) -> UdpReceivedPacket { @@ -168,6 +174,10 @@ impl UdpClients { client.from_origin_ts = now_ms; + // Track bytes going out (from origin to tunnel) + let packet_len = packet.packet.len() as u64; + self.stats.add_bytes_out(packet_len); + let mut flow = client.flow; match &mut flow { UdpFlow::V4 { @@ -220,6 +230,10 @@ impl UdpClients { SocketAddrV4::new(ip, port_start + extension.port_offset) }; + // Track bytes coming in (from tunnel to origin) + let packet_len = packet.len() as u64; + self.stats.add_bytes_in(packet_len); + match self.virtual_client_lookup.entry(key) { hash_map::Entry::Occupied(o) => { let slot = *o.get(); @@ -325,6 +339,9 @@ impl UdpClients { v.insert(slot); entry.insert(client); + + // Update active UDP count for new client + self.stats.set_udp(self.virtual_clients.len() as u32); } } } diff --git a/packages/agent_core/src/playit_agent.rs b/packages/agent_core/src/playit_agent.rs index a2df538..25d4ca5 100644 --- a/packages/agent_core/src/playit_agent.rs +++ b/packages/agent_core/src/playit_agent.rs @@ -16,6 +16,7 @@ use crate::network::udp::packets::Packets; use crate::network::udp::udp_channel::UdpChannel; use crate::network::udp::udp_clients::UdpClients; use crate::network::udp::udp_settings::UdpSettings; +use crate::stats::AgentStats; use crate::utils::now_milli; pub struct PlayitAgent { @@ -26,6 +27,7 @@ pub struct PlayitAgent { tcp_clients: TcpClients, keep_running: Arc, + stats: AgentStats, } #[derive(Clone, Debug)] @@ -50,8 +52,9 @@ impl PlayitAgent { .await .map_err(SetupError::IoError)?; - let udp_clients = UdpClients::new(settings.udp_settings, lookup.clone(), packets.clone()); - let tcp_clients = TcpClients::new(settings.tcp_settings, lookup.clone()); + let stats = AgentStats::new(); + let udp_clients = UdpClients::new(settings.udp_settings, lookup.clone(), packets.clone(), stats.clone()); + let tcp_clients = TcpClients::new(settings.tcp_settings, lookup.clone(), stats.clone()); Ok(PlayitAgent { control, @@ -59,6 +62,7 @@ impl PlayitAgent { udp_channel, tcp_clients, keep_running: Arc::new(AtomicBool::new(true)), + stats, }) } @@ -66,6 +70,11 @@ impl PlayitAgent { self.keep_running.clone() } + /// Get a handle to the agent stats + pub fn stats(&self) -> AgentStats { + self.stats.clone() + } + pub async fn run(self) { let mut control = self.control; let tunnel_run = self.keep_running.clone(); diff --git a/packages/agent_core/src/stats.rs b/packages/agent_core/src/stats.rs new file mode 100644 index 0000000..f9bce98 --- /dev/null +++ b/packages/agent_core/src/stats.rs @@ -0,0 +1,107 @@ +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use std::sync::Arc; + +/// Shared statistics for the agent +#[derive(Debug, Default, Clone)] +pub struct AgentStats { + inner: Arc, +} + +#[derive(Debug, Default)] +struct StatsInner { + /// Bytes received from tunnel (incoming to local) + pub bytes_in: AtomicU64, + /// Bytes sent to tunnel (outgoing from local) + pub bytes_out: AtomicU64, + /// Active TCP connections + pub active_tcp: AtomicU32, + /// Active UDP flows + pub active_udp: AtomicU32, +} + +impl AgentStats { + pub fn new() -> Self { + AgentStats { + inner: Arc::new(StatsInner::default()), + } + } + + /// Add bytes received from tunnel + pub fn add_bytes_in(&self, bytes: u64) { + self.inner.bytes_in.fetch_add(bytes, Ordering::Relaxed); + } + + /// Add bytes sent to tunnel + pub fn add_bytes_out(&self, bytes: u64) { + self.inner.bytes_out.fetch_add(bytes, Ordering::Relaxed); + } + + /// Increment active TCP connections + pub fn inc_tcp(&self) { + self.inner.active_tcp.fetch_add(1, Ordering::Relaxed); + } + + /// Decrement active TCP connections + pub fn dec_tcp(&self) { + self.inner.active_tcp.fetch_sub(1, Ordering::Relaxed); + } + + /// Set active TCP connection count + pub fn set_tcp(&self, count: u32) { + self.inner.active_tcp.store(count, Ordering::Relaxed); + } + + /// Increment active UDP flows + pub fn inc_udp(&self) { + self.inner.active_udp.fetch_add(1, Ordering::Relaxed); + } + + /// Decrement active UDP flows + pub fn dec_udp(&self) { + self.inner.active_udp.fetch_sub(1, Ordering::Relaxed); + } + + /// Set active UDP flow count + pub fn set_udp(&self, count: u32) { + self.inner.active_udp.store(count, Ordering::Relaxed); + } + + /// Get current bytes received from tunnel + pub fn bytes_in(&self) -> u64 { + self.inner.bytes_in.load(Ordering::Relaxed) + } + + /// Get current bytes sent to tunnel + pub fn bytes_out(&self) -> u64 { + self.inner.bytes_out.load(Ordering::Relaxed) + } + + /// Get active TCP connection count + pub fn active_tcp(&self) -> u32 { + self.inner.active_tcp.load(Ordering::Relaxed) + } + + /// Get active UDP flow count + pub fn active_udp(&self) -> u32 { + self.inner.active_udp.load(Ordering::Relaxed) + } + + /// Get a snapshot of all stats + pub fn snapshot(&self) -> StatsSnapshot { + StatsSnapshot { + bytes_in: self.bytes_in(), + bytes_out: self.bytes_out(), + active_tcp: self.active_tcp(), + active_udp: self.active_udp(), + } + } +} + +/// A snapshot of stats at a point in time +#[derive(Debug, Clone, Default)] +pub struct StatsSnapshot { + pub bytes_in: u64, + pub bytes_out: u64, + pub active_tcp: u32, + pub active_udp: u32, +}