rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
//! Proxy configuration types for [`McClient`](crate::McClient).
//!
//! Supports **SOCKS5** and **HTTP CONNECT** proxies for Java Edition (TCP).
//!
//! # Bedrock / UDP note
//!
//! Both proxy kinds tunnel TCP only. Bedrock pings (UDP) through a proxy
//! configured without [`UdpSupport::Yes`] return
//! [`McError::Proxy(ProxyError::UdpUnsupported)`](crate::error::ProxyError)
//! immediately. Use [`ProxyConfig::socks5_with_udp`] to opt-in when your
//! SOCKS5 proxy genuinely supports UDP ASSOCIATE.

use std::fmt;

// ─── ProxyAddr ────────────────────────────────────────────────────────────────

/// Proxy server address, e.g. `"127.0.0.1:1080"` or `"proxy.example.com:3128"`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProxyAddr {
    pub host: String,
    pub port: u16,
}

impl ProxyAddr {
    pub fn new(host: impl Into<String>, port: u16) -> Self {
        Self { host: host.into(), port }
    }

    /// Parse `"host:port"`.
    pub fn parse(s: &str) -> Result<Self, crate::McError> {
        match s.rsplit_once(':') {
            Some((h, p)) => {
                let port = p.parse::<u16>()
                    .map_err(|e| crate::McError::invalid_port(e.to_string()))?;
                Ok(Self::new(h, port))
            }
            None => Err(crate::McError::invalid_address(
                format!("proxy address must be 'host:port', got '{s}'")
            )),
        }
    }
}

impl fmt::Display for ProxyAddr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}:{}", self.host, self.port)
    }
}

// ─── ProxyAuth ────────────────────────────────────────────────────────────────

/// Optional username/password credentials.
///
/// The password is never included in `Debug` output or error messages.
#[derive(Clone)]
pub struct ProxyAuth {
    pub username: String,
    pub password: String,
}

impl fmt::Debug for ProxyAuth {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ProxyAuth")
            .field("username", &self.username)
            .field("password", &"***")
            .finish()
    }
}

// ─── UdpSupport ───────────────────────────────────────────────────────────────

/// Whether the proxy supports UDP ASSOCIATE (SOCKS5 only, needed for Bedrock).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UdpSupport {
    /// No UDP support (default). Bedrock pings return an error immediately.
    #[default]
    No,
    /// Proxy supports UDP ASSOCIATE. Bedrock pings are routed through it.
    Yes,
}

// ─── ProxyKind ────────────────────────────────────────────────────────────────

/// Which proxy protocol to use.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProxyKind {
    /// SOCKS5 proxy — supports both TCP and optionally UDP (via UDP ASSOCIATE).
    Socks5,
    /// HTTP CONNECT proxy — TCP tunnel only, no UDP.
    ///
    /// Widely supported by corporate firewalls, Squid, Nginx, HAProxy, etc.
    Http,
}

// ─── ProxyConfig ─────────────────────────────────────────────────────────────

/// Proxy configuration attached to an [`McClient`](crate::McClient).
///
/// # Quick reference
///
/// | Constructor | Protocol | Auth | UDP (Bedrock) |
/// |---|---|---|---|
/// | [`socks5`](Self::socks5) | SOCKS5 | optional | No |
/// | [`socks5_with_udp`](Self::socks5_with_udp) | SOCKS5 | optional | Yes |
/// | [`http`](Self::http) | HTTP CONNECT | optional | No |
///
/// # Examples
///
/// ```rust
/// use rust_mc_status::proxy::ProxyConfig;
///
/// // SOCKS5 — anonymous
/// let cfg = ProxyConfig::socks5("127.0.0.1:1080");
///
/// // SOCKS5 — authenticated
/// let cfg = ProxyConfig::socks5("proxy.example.com:1080")
///     .with_auth("alice", "s3cr3t");
///
/// // SOCKS5 — with UDP (Bedrock support)
/// let cfg = ProxyConfig::socks5_with_udp("10.0.0.1:1080");
///
/// // HTTP CONNECT — anonymous
/// let cfg = ProxyConfig::http("squid.example.com:3128");
///
/// // HTTP CONNECT — authenticated
/// let cfg = ProxyConfig::http("squid.example.com:3128")
///     .with_auth("user", "pass");
/// ```
#[derive(Debug, Clone)]
pub struct ProxyConfig {
    pub(crate) addr:        ProxyAddr,
    pub(crate) auth:        Option<ProxyAuth>,
    pub(crate) kind:        ProxyKind,
    pub(crate) udp_support: UdpSupport,
}

impl ProxyConfig {
    // ── SOCKS5 ────────────────────────────────────────────────────────────────

    /// SOCKS5 proxy for Java Edition (TCP) only.
    ///
    /// Bedrock pings will fail with `ProxyError::UdpUnsupported`.
    /// Use [`socks5_with_udp`](Self::socks5_with_udp) if your proxy
    /// supports UDP ASSOCIATE.
    pub fn socks5(addr: impl AsRef<str>) -> Self {
        Self {
            addr:        ProxyAddr::parse(addr.as_ref())
                             .expect("invalid proxy address — use 'host:port'"),
            auth:        None,
            kind:        ProxyKind::Socks5,
            udp_support: UdpSupport::No,
        }
    }

    /// SOCKS5 proxy with UDP ASSOCIATE support (usable for Bedrock too).
    pub fn socks5_with_udp(addr: impl AsRef<str>) -> Self {
        Self {
            addr:        ProxyAddr::parse(addr.as_ref())
                             .expect("invalid proxy address — use 'host:port'"),
            auth:        None,
            kind:        ProxyKind::Socks5,
            udp_support: UdpSupport::Yes,
        }
    }

    // ── HTTP CONNECT ──────────────────────────────────────────────────────────

    /// HTTP CONNECT proxy (TCP tunnel only).
    ///
    /// Compatible with Squid, Nginx, HAProxy, corporate HTTP proxies, and any
    /// server that speaks the `CONNECT` method. UDP is not supported — Bedrock
    /// pings will fail with `ProxyError::UdpUnsupported`.
    ///
    /// Authentication is sent as a `Proxy-Authorization: Basic ...` header.
    ///
    /// # Example
    ///
    /// ```rust
    /// use rust_mc_status::proxy::ProxyConfig;
    ///
    /// let cfg = ProxyConfig::http("squid.corp.example.com:3128");
    /// let cfg = ProxyConfig::http("proxy.example.com:8080")
    ///     .with_auth("user", "pass");
    /// ```
    pub fn http(addr: impl AsRef<str>) -> Self {
        Self {
            addr:        ProxyAddr::parse(addr.as_ref())
                             .expect("invalid proxy address — use 'host:port'"),
            auth:        None,
            kind:        ProxyKind::Http,
            udp_support: UdpSupport::No, // HTTP CONNECT is TCP-only
        }
    }

    // ── Shared ────────────────────────────────────────────────────────────────

    /// Add username/password authentication.
    ///
    /// For SOCKS5 this uses username/password sub-negotiation (RFC 1929).
    /// For HTTP CONNECT this sends a `Proxy-Authorization: Basic` header.
    pub fn with_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
        self.auth = Some(ProxyAuth {
            username: username.into(),
            password: password.into(),
        });
        self
    }

    // ── Accessors ─────────────────────────────────────────────────────────────

    pub fn addr(&self)        -> &ProxyAddr         { &self.addr }
    pub fn auth(&self)        -> Option<&ProxyAuth>  { self.auth.as_ref() }
    pub fn kind(&self)        -> ProxyKind           { self.kind }
    pub fn udp_support(&self) -> UdpSupport          { self.udp_support }
}