#![allow(clippy::struct_excessive_bools)]
use std::{collections::BTreeMap, fmt};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Default, Deserialize, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct WebSettings {
pub web_listen: String,
pub web_domain: String,
pub web_port: u16,
pub web_cert_file: String,
pub web_key_file: String,
pub web_base_path: String,
pub session_max_age: u32,
#[serde(rename = "trustedProxyCIDRs")]
pub trusted_proxy_cidrs: String,
pub panel_outbound: String,
}
impl fmt::Debug for WebSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WebSettings")
.field("web_listen", &self.web_listen)
.field("web_domain", &self.web_domain)
.field("web_port", &self.web_port)
.field("web_cert_file", &self.web_cert_file)
.field("web_key_file", &"[REDACTED]")
.field("web_base_path", &"[REDACTED]")
.field("session_max_age", &self.session_max_age)
.field("trusted_proxy_cidrs", &self.trusted_proxy_cidrs)
.field("panel_outbound", &"[REDACTED]")
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct DisplaySettings {
pub page_size: u32,
pub expire_diff: u32,
pub traffic_diff: u8,
pub remark_template: String,
pub sub_show_identity_on_all_links: bool,
pub datepicker: String,
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct TelegramSettings {
pub tg_bot_enable: bool,
pub tg_bot_token: String,
pub tg_bot_proxy: String,
#[serde(rename = "tgBotAPIServer")]
pub tg_bot_api_server: String,
pub tg_bot_chat_id: String,
pub tg_run_time: String,
pub tg_bot_backup: bool,
pub tg_cpu: u8,
pub tg_memory: u8,
pub tg_lang: String,
pub tg_enabled_events: String,
}
impl fmt::Debug for TelegramSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TelegramSettings")
.field("tg_bot_enable", &self.tg_bot_enable)
.field("tg_bot_token", &"[REDACTED]")
.field("tg_bot_proxy", &"[REDACTED]")
.field("tg_bot_api_server", &self.tg_bot_api_server)
.field("tg_bot_chat_id", &self.tg_bot_chat_id)
.field("tg_run_time", &self.tg_run_time)
.field("tg_bot_backup", &self.tg_bot_backup)
.field("tg_cpu", &self.tg_cpu)
.field("tg_memory", &self.tg_memory)
.field("tg_lang", &self.tg_lang)
.field("tg_enabled_events", &self.tg_enabled_events)
.finish()
}
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SmtpSettings {
pub smtp_enable: bool,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_username: String,
pub smtp_password: String,
pub smtp_from: String,
pub smtp_from_name: String,
pub smtp_to: String,
pub smtp_encryption_type: String,
pub smtp_enabled_events: String,
pub smtp_cpu: u8,
pub smtp_memory: u8,
}
impl fmt::Debug for SmtpSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SmtpSettings")
.field("smtp_enable", &self.smtp_enable)
.field("smtp_host", &self.smtp_host)
.field("smtp_port", &self.smtp_port)
.field("smtp_username", &self.smtp_username)
.field("smtp_password", &"[REDACTED]")
.field("smtp_from", &self.smtp_from)
.field("smtp_from_name", &self.smtp_from_name)
.field("smtp_to", &self.smtp_to)
.field("smtp_encryption_type", &self.smtp_encryption_type)
.field("smtp_enabled_events", &self.smtp_enabled_events)
.field("smtp_cpu", &self.smtp_cpu)
.field("smtp_memory", &self.smtp_memory)
.finish()
}
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SecuritySettings {
pub reality_scan_candidates: String,
pub outbound_down_threshold: u8,
pub ip_limit_allowlist: String,
pub time_location: String,
pub two_factor_enable: bool,
pub two_factor_token: String,
pub warp_update_interval: u32,
}
impl fmt::Debug for SecuritySettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SecuritySettings")
.field("reality_scan_candidates", &self.reality_scan_candidates)
.field("outbound_down_threshold", &self.outbound_down_threshold)
.field("ip_limit_allowlist", &self.ip_limit_allowlist)
.field("time_location", &self.time_location)
.field("two_factor_enable", &self.two_factor_enable)
.field("two_factor_token", &"[REDACTED]")
.field("warp_update_interval", &self.warp_update_interval)
.finish()
}
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SubscriptionSettings {
pub happ_link_enable: bool,
pub sub_info_node_enable: bool,
pub sub_calendar_expire_inclusive: bool,
pub sub_expired_template: String,
pub sub_traffic_depleted_template: String,
#[serde(flatten)]
pub happ: HappSettings,
pub sub_enable: bool,
pub sub_json_enable: bool,
pub sub_json_auto_detect: bool,
pub sub_json_always_array: bool,
pub sub_json_user_agent_regex: String,
pub sub_clash_auto_detect: bool,
pub sub_clash_user_agent_regex: String,
pub sub_title: String,
pub sub_support_url: String,
pub sub_profile_mode: String,
pub sub_profile_url: String,
pub sub_announce: String,
pub sub_enable_routing: bool,
pub sub_routing_rules: String,
pub sub_incy_enable_routing: bool,
pub sub_incy_routing_rules: String,
pub sub_listen: String,
pub sub_port: u16,
pub sub_path: String,
pub sub_domain: String,
pub sub_cert_file: String,
pub sub_key_file: String,
pub sub_updates: u32,
pub external_traffic_inform_enable: bool,
#[serde(rename = "externalTrafficInformURI")]
pub external_traffic_inform_uri: String,
pub restart_xray_on_client_disable: bool,
pub sub_encrypt: bool,
#[serde(rename = "subURI")]
pub sub_uri: String,
pub sub_json_path: String,
#[serde(rename = "subJsonURI")]
pub sub_json_uri: String,
pub sub_clash_enable: bool,
pub sub_clash_path: String,
#[serde(rename = "subClashURI")]
pub sub_clash_uri: String,
pub sub_clash_enable_routing: bool,
pub sub_clash_rules: String,
pub sub_json_mux: String,
pub sub_json_rules: String,
pub sub_json_routing_rules: String,
pub sub_json_dns: String,
pub sub_json_final_mask: String,
pub sub_json_observatory: String,
pub sub_theme_dir: String,
pub sub_hide_settings: bool,
}
impl fmt::Debug for SubscriptionSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SubscriptionSettings")
.field("sub_enable", &self.sub_enable)
.field("sub_json_enable", &self.sub_json_enable)
.field("sub_json_auto_detect", &self.sub_json_auto_detect)
.field("sub_json_always_array", &self.sub_json_always_array)
.field("sub_clash_auto_detect", &self.sub_clash_auto_detect)
.field("sub_title", &self.sub_title)
.field("sub_listen", &self.sub_listen)
.field("sub_port", &self.sub_port)
.field("sub_domain", &self.sub_domain)
.field("sub_updates", &self.sub_updates)
.field(
"external_traffic_inform_enable",
&self.external_traffic_inform_enable,
)
.field(
"restart_xray_on_client_disable",
&self.restart_xray_on_client_disable,
)
.field("sub_encrypt", &self.sub_encrypt)
.field("sub_clash_enable", &self.sub_clash_enable)
.field("sub_clash_enable_routing", &self.sub_clash_enable_routing)
.field("sub_hide_settings", &self.sub_hide_settings)
.field("paths_uris_rules_and_key_material", &"[REDACTED]")
.finish_non_exhaustive()
}
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct LdapSettings {
pub ldap_enable: bool,
pub ldap_host: String,
pub ldap_port: u16,
#[serde(rename = "ldapUseTLS")]
pub ldap_use_tls: bool,
pub ldap_insecure_skip_verify: bool,
#[serde(rename = "ldapBindDN")]
pub ldap_bind_dn: String,
pub ldap_password: String,
#[serde(rename = "ldapBaseDN")]
pub ldap_base_dn: String,
pub ldap_user_filter: String,
pub ldap_user_attr: String,
pub ldap_vless_field: String,
pub ldap_sync_cron: String,
pub ldap_flag_field: String,
pub ldap_truthy_values: String,
pub ldap_invert_flag: bool,
pub ldap_inbound_tags: String,
pub ldap_auto_create: bool,
pub ldap_auto_delete: bool,
#[serde(rename = "ldapDefaultTotalGB")]
pub ldap_default_total_gb: u64,
pub ldap_default_expiry_days: u32,
#[serde(rename = "ldapDefaultLimitIP")]
pub ldap_default_limit_ip: u32,
}
impl fmt::Debug for LdapSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LdapSettings")
.field("ldap_enable", &self.ldap_enable)
.field("ldap_host", &self.ldap_host)
.field("ldap_port", &self.ldap_port)
.field("ldap_use_tls", &self.ldap_use_tls)
.field("ldap_insecure_skip_verify", &self.ldap_insecure_skip_verify)
.field("ldap_bind_dn", &self.ldap_bind_dn)
.field("ldap_password", &"[REDACTED]")
.field("ldap_base_dn", &self.ldap_base_dn)
.field("ldap_user_filter", &self.ldap_user_filter)
.field("ldap_user_attr", &self.ldap_user_attr)
.field("ldap_vless_field", &self.ldap_vless_field)
.field("ldap_sync_cron", &self.ldap_sync_cron)
.field("ldap_flag_field", &self.ldap_flag_field)
.field("ldap_truthy_values", &self.ldap_truthy_values)
.field("ldap_invert_flag", &self.ldap_invert_flag)
.field("ldap_inbound_tags", &self.ldap_inbound_tags)
.field("ldap_auto_create", &self.ldap_auto_create)
.field("ldap_auto_delete", &self.ldap_auto_delete)
.field("ldap_default_total_gb", &self.ldap_default_total_gb)
.field("ldap_default_expiry_days", &self.ldap_default_expiry_days)
.field("ldap_default_limit_ip", &self.ldap_default_limit_ip)
.finish()
}
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct DiscordSettings {
pub discord_admin_ids: String,
pub discord_bot_backup: bool,
pub discord_bot_enable: bool,
pub discord_bot_token: String,
pub discord_channel_id: String,
pub discord_cpu: u8,
pub discord_enabled_events: String,
pub discord_lang: String,
pub discord_memory: u8,
pub discord_run_time: String,
}
impl fmt::Debug for DiscordSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DiscordSettings")
.field("settings", &"[REDACTED]")
.finish_non_exhaustive()
}
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct HappSettings {
pub sub_happ_always_hwid: bool,
pub sub_happ_auto_connect: bool,
pub sub_happ_auto_connect_type: String,
pub sub_happ_auto_detect: bool,
pub sub_happ_color_profile: String,
pub sub_happ_exclude_apns: bool,
pub sub_happ_exclude_routes: String,
pub sub_happ_fallback_url: String,
pub sub_happ_new_url: String,
pub sub_happ_no_limit: bool,
pub sub_happ_notification_expire: bool,
pub sub_happ_per_app_list: String,
pub sub_happ_per_app_mode: String,
pub sub_happ_ping_type: String,
pub sub_happ_provider_id: String,
pub sub_happ_sub_expire: bool,
pub sub_happ_sub_expire_button_link: String,
pub sub_happ_sub_info_button_link: String,
pub sub_happ_sub_info_button_text: String,
pub sub_happ_sub_info_color: String,
pub sub_happ_sub_info_text: String,
pub sub_happ_tun_mode: String,
pub sub_happ_tun_type: String,
}
impl fmt::Debug for HappSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("HappSettings")
.field("settings", &"[REDACTED]")
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(default)]
pub struct PanelSettings {
#[serde(flatten)]
pub web: WebSettings,
#[serde(flatten)]
pub display: DisplaySettings,
#[serde(flatten)]
pub telegram: TelegramSettings,
#[serde(flatten)]
pub smtp: SmtpSettings,
#[serde(flatten)]
pub discord: DiscordSettings,
#[serde(flatten)]
pub security: SecuritySettings,
#[serde(flatten)]
pub subscriptions: SubscriptionSettings,
#[serde(flatten)]
pub ldap: LdapSettings,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PanelSettingsView {
#[serde(flatten)]
pub settings: PanelSettings,
pub has_tg_bot_token: bool,
pub has_two_factor_token: bool,
pub has_ldap_password: bool,
pub has_api_token: bool,
pub has_warp_secret: bool,
pub has_nord_secret: bool,
pub has_discord_bot_token: bool,
pub has_smtp_password: bool,
}
#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PanelSettingsUpdate {
#[serde(flatten)]
pub settings: PanelSettings,
pub two_factor_code: String,
pub clear_discord_bot_token: bool,
pub clear_tg_bot_token: bool,
pub clear_ldap_password: bool,
pub clear_smtp_password: bool,
}
impl PanelSettingsUpdate {
pub fn new(settings: PanelSettings) -> Self {
Self {
settings,
..Self::default()
}
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct EffectiveDefaults {
pub expire_diff: i64,
pub traffic_diff: i64,
pub page_size: i64,
pub default_cert: String,
pub default_key: String,
pub tg_bot_enable: bool,
pub sub_theme_dir: String,
pub sub_enable: bool,
pub sub_json_enable: bool,
pub sub_clash_enable: bool,
pub sub_title: String,
#[serde(rename = "subURI")]
pub sub_uri: String,
#[serde(rename = "subJsonURI")]
pub sub_json_uri: String,
#[serde(rename = "subClashURI")]
pub sub_clash_uri: String,
pub datepicker: String,
pub ip_limit_enable: bool,
pub access_log_enable: bool,
pub web_domain: String,
pub sub_domain: String,
pub dev_channel_enable: bool,
pub is_dev_build: bool,
}
pub type FactoryDefaults = BTreeMap<String, String>;
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UserCredentialsUpdate {
pub old_username: String,
pub old_password: String,
pub new_username: String,
pub new_password: String,
pub two_factor_code: String,
}
impl fmt::Debug for UserCredentialsUpdate {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("UserCredentialsUpdate")
.field("old_username", &self.old_username)
.field("old_password", &"[REDACTED]")
.field("new_username", &self.new_username)
.field("new_password", &"[REDACTED]")
.field("two_factor_code", &"[REDACTED]")
.finish()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiTokenMetadata {
pub id: i64,
pub name: String,
pub enabled: bool,
pub created_at: i64,
#[serde(default)]
pub scope: ApiTokenScope,
#[serde(default)]
pub expires_at: i64,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ApiTokenScope {
#[default]
Admin,
Monitor,
NodeSync,
}
impl ApiTokenScope {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Admin => "admin",
Self::Monitor => "monitor",
Self::NodeSync => "node-sync",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiTokenCreateRequest {
pub name: String,
#[serde(default)]
pub scope: ApiTokenScope,
#[serde(default)]
pub expires_at: i64,
}
impl ApiTokenCreateRequest {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
scope: ApiTokenScope::Admin,
expires_at: 0,
}
}
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatedApiToken {
pub id: i64,
pub name: String,
pub token: String,
pub enabled: bool,
pub created_at: i64,
pub scope: ApiTokenScope,
pub expires_at: i64,
}
impl fmt::Debug for CreatedApiToken {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CreatedApiToken")
.field("id", &self.id)
.field("name", &self.name)
.field("token", &"[REDACTED]")
.field("enabled", &self.enabled)
.field("created_at", &self.created_at)
.field("scope", &self.scope)
.field("expires_at", &self.expires_at)
.finish()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct GeoFile {
pub name: String,
pub kind: String,
pub size: i64,
pub modified_at: i64,
pub categories: i32,
pub error: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct GeoCategory {
pub code: String,
pub entries: i32,
pub attributes: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct GeoCategoryPage {
pub items: Vec<GeoCategory>,
pub total: i32,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct GeoEntry {
pub kind: String,
pub value: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct GeoEntryPage {
pub items: Vec<GeoEntry>,
pub total: i32,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct GeodataTokenIssue {
pub token: String,
pub reason: String,
pub file: String,
pub code: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct PiaCountry {
pub code: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct PiaRegion {
pub id: String,
pub name: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PiaServer {
pub hostname: String,
pub ip: String,
pub region_id: String,
pub region_name: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct PiaServers {
pub regions: Vec<PiaRegion>,
pub servers: Vec<PiaServer>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PiaAccount {
pub username: String,
pub account_hint: String,
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PiaKey {
pub tag: String,
pub hostname: String,
pub secret_key: String,
pub address: String,
pub public_key: String,
pub endpoint: String,
}
impl fmt::Debug for PiaKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PiaKey")
.field("tag", &self.tag)
.field("hostname", &self.hostname)
.field("secret_key", &"[REDACTED]")
.field("address", &self.address)
.field("public_key", &self.public_key)
.field("endpoint", &self.endpoint)
.finish()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SmtpTestResult {
pub success: bool,
#[serde(default)]
pub stage: String,
#[serde(default, rename = "msg")]
pub message: String,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(transparent)]
pub struct SensitivePayload(String);
impl SensitivePayload {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn json(&self) -> serde_json::Result<Value> {
serde_json::from_str(&self.0)
}
}
impl fmt::Debug for SensitivePayload {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("SensitivePayload")
.field(&"[REDACTED]")
.finish()
}
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WarpRegistration {
pub private_key: String,
pub public_key: String,
}
impl fmt::Debug for WarpRegistration {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WarpRegistration")
.field("private_key", &"[REDACTED]")
.field("public_key", &self.public_key)
.finish()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct GeodataSource {
pub url: String,
pub file: String,
}
#[derive(Clone, Deserialize, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct XraySettingsSnapshot {
pub xray_setting: crate::XrayConfig,
pub geodata_sources: Vec<GeodataSource>,
pub inbound_tags: Vec<String>,
pub client_reverse_tags: Vec<String>,
pub outbound_test_url: String,
pub subscription_outbounds: Vec<Value>,
pub subscription_outbound_tags: Vec<String>,
}
impl Default for XraySettingsSnapshot {
fn default() -> Self {
Self {
xray_setting: crate::XrayConfig::from(Value::Object(serde_json::Map::new())),
inbound_tags: Vec::new(),
geodata_sources: Vec::new(),
client_reverse_tags: Vec::new(),
outbound_test_url: String::new(),
subscription_outbounds: Vec::new(),
subscription_outbound_tags: Vec::new(),
}
}
}
impl fmt::Debug for XraySettingsSnapshot {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("XraySettingsSnapshot")
.field("geodata_sources", &self.geodata_sources)
.field("xray_setting", &"[REDACTED]")
.field("inbound_tags", &self.inbound_tags)
.field("client_reverse_tags", &self.client_reverse_tags)
.field("outbound_test_url", &self.outbound_test_url)
.field("subscription_outbounds", &"[REDACTED]")
.field(
"subscription_outbound_tags",
&self.subscription_outbound_tags,
)
.finish()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct OutboundTraffic {
pub id: i64,
pub tag: String,
pub up: i64,
pub down: i64,
pub total: i64,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum OutboundTestMode {
#[default]
Http,
Tcp,
Real,
}
impl OutboundTestMode {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Http => "",
Self::Tcp => "tcp",
Self::Real => "real",
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct TestEndpointResult {
pub address: String,
pub success: bool,
pub delay: i64,
pub error: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct TestEgressResult {
pub ipv4: String,
pub ipv6: String,
pub country: String,
pub warp: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct OutboundTestResult {
pub tag: String,
pub success: bool,
pub delay: i64,
pub error: String,
pub mode: String,
pub http_status: i32,
pub connect_ms: i64,
#[serde(rename = "tlsMs")]
pub tls_ms: i64,
#[serde(rename = "ttfbMs")]
pub ttfb_ms: i64,
pub endpoints: Vec<TestEndpointResult>,
pub egress: Option<TestEgressResult>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct BalancerStatus {
pub tag: String,
pub running: bool,
#[serde(rename = "override")]
pub override_target: String,
pub selected: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RouteTestRequest {
pub inbound_tag: String,
pub domain: String,
pub ip: String,
pub port: u16,
pub network: String,
pub protocol: String,
pub email: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct RouteTestResult {
pub matched: bool,
pub outbound_tag: String,
pub group_tags: Vec<String>,
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct OutboundSubscription {
pub id: i64,
pub remark: String,
pub url: String,
pub enabled: bool,
pub allow_private: bool,
pub allow_insecure: bool,
pub user_agent: String,
pub tag_prefix: String,
pub update_interval: i32,
pub priority: i32,
pub prepend: bool,
pub last_updated: i64,
pub last_error: String,
pub last_fetched_outbounds: String,
pub created_at: i64,
pub updated_at: i64,
pub outbound_count: i32,
}
impl fmt::Debug for OutboundSubscription {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OutboundSubscription")
.field("user_agent", &"[REDACTED]")
.field("id", &self.id)
.field("remark", &self.remark)
.field("url", &"[REDACTED]")
.field("enabled", &self.enabled)
.field("allow_private", &self.allow_private)
.field("allow_insecure", &self.allow_insecure)
.field("tag_prefix", &self.tag_prefix)
.field("update_interval", &self.update_interval)
.field("priority", &self.priority)
.field("prepend", &self.prepend)
.field("last_updated", &self.last_updated)
.field("last_error", &self.last_error)
.field("last_fetched_outbounds", &"[REDACTED]")
.field("created_at", &self.created_at)
.field("updated_at", &self.updated_at)
.field("outbound_count", &self.outbound_count)
.finish()
}
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OutboundSubscriptionInput {
pub remark: String,
pub url: String,
pub user_agent: String,
pub tag_prefix: String,
pub enabled: bool,
pub allow_private: bool,
pub allow_insecure: bool,
pub prepend: bool,
pub update_interval: i32,
}
impl OutboundSubscriptionInput {
pub fn new(url: impl Into<String>) -> Self {
Self {
remark: String::new(),
url: url.into(),
tag_prefix: String::new(),
user_agent: String::new(),
enabled: true,
allow_private: false,
allow_insecure: false,
prepend: false,
update_interval: 600,
}
}
}
impl fmt::Debug for OutboundSubscriptionInput {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OutboundSubscriptionInput")
.field("user_agent", &"[REDACTED]")
.field("remark", &self.remark)
.field("url", &"[REDACTED]")
.field("tag_prefix", &self.tag_prefix)
.field("enabled", &self.enabled)
.field("allow_private", &self.allow_private)
.field("allow_insecure", &self.allow_insecure)
.field("prepend", &self.prepend)
.field("update_interval", &self.update_interval)
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum MoveDirection {
Up,
Down,
}
impl MoveDirection {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Up => "up",
Self::Down => "down",
}
}
}
#[derive(Clone, Default, Deserialize, PartialEq, Serialize)]
#[serde(transparent)]
pub struct OutboundDocuments(Vec<Value>);
impl OutboundDocuments {
pub fn as_slice(&self) -> &[Value] {
&self.0
}
pub fn into_inner(self) -> Vec<Value> {
self.0
}
}
impl fmt::Debug for OutboundDocuments {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("OutboundDocuments")
.field(&"[REDACTED]")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn secret_bearing_models_redact_debug_output() {
let credentials = UserCredentialsUpdate {
old_username: "old-user".into(),
old_password: "old-password-secret".into(),
new_username: "new-user".into(),
new_password: "new-password-secret".into(),
two_factor_code: "123456-secret".into(),
};
let created = CreatedApiToken {
id: 7,
name: "automation".into(),
token: "bearer-token-secret".into(),
enabled: true,
created_at: 10,
scope: ApiTokenScope::Monitor,
expires_at: 20,
};
let pia_key: PiaKey = serde_json::from_value(json!({
"tag": "pia-de",
"hostname": "de.example.com",
"secretKey": "pia-private-secret",
"address": "10.0.0.2/32",
"publicKey": "public-key",
"endpoint": "de.example.com:1337"
}))
.unwrap();
let registration = WarpRegistration {
private_key: "warp-private-secret".into(),
public_key: "warp-public".into(),
};
let outbound: OutboundSubscription = serde_json::from_value(json!({
"id": 9,
"remark": "remote",
"url": "https://user:subscription-secret@example.com/sub",
"enabled": true,
"lastFetchedOutbounds": "outbound-private-secret"
}))
.unwrap();
let input = OutboundSubscriptionInput::new(
"https://user:input-subscription-secret@example.com/sub",
);
let documents: OutboundDocuments =
serde_json::from_value(json!([{"password": "document-secret"}])).unwrap();
let snapshot = XraySettingsSnapshot {
xray_setting: crate::XrayConfig::from(json!({"privateKey": "xray-secret"})),
subscription_outbounds: vec![json!({"password": "snapshot-secret"})],
..XraySettingsSnapshot::default()
};
let outputs = [
format!("{credentials:?}"),
format!("{created:?}"),
format!("{pia_key:?}"),
format!("{registration:?}"),
format!("{outbound:?}"),
format!("{input:?}"),
format!("{documents:?}"),
format!("{snapshot:?}"),
];
for (output, secret) in outputs.iter().zip([
"old-password-secret",
"bearer-token-secret",
"pia-private-secret",
"warp-private-secret",
"subscription-secret",
"input-subscription-secret",
"document-secret",
"snapshot-secret",
]) {
assert!(!output.contains(secret));
assert!(output.contains("REDACTED"));
}
}
#[test]
fn helpers_preserve_wire_values_and_owned_payloads() {
assert_eq!(ApiTokenScope::Admin.as_str(), "admin");
assert_eq!(ApiTokenScope::Monitor.as_str(), "monitor");
assert_eq!(ApiTokenScope::NodeSync.as_str(), "node-sync");
let request = ApiTokenCreateRequest::new("deploy");
assert_eq!(request.name, "deploy");
assert_eq!(request.scope, ApiTokenScope::Admin);
assert_eq!(request.expires_at, 0);
let payload: SensitivePayload = serde_json::from_value(json!("{\"ok\":true}")).unwrap();
assert_eq!(payload.as_str(), "{\"ok\":true}");
assert_eq!(payload.json().unwrap(), json!({"ok": true}));
assert!(
serde_json::from_value::<SensitivePayload>(json!("not-json"))
.unwrap()
.json()
.is_err()
);
assert!(!format!("{payload:?}").contains("ok"));
assert_eq!(OutboundTestMode::Http.as_str(), "");
assert_eq!(OutboundTestMode::Tcp.as_str(), "tcp");
assert_eq!(OutboundTestMode::Real.as_str(), "real");
assert_eq!(MoveDirection::Up.as_str(), "up");
assert_eq!(MoveDirection::Down.as_str(), "down");
let documents: OutboundDocuments =
serde_json::from_value(json!([{"tag": "one"}, {"tag": "two"}])).unwrap();
assert_eq!(documents.as_slice().len(), 2);
assert_eq!(documents.into_inner()[1]["tag"], "two");
}
}