convertor 2.6.12

A profile converter for surge/clash.
Documentation
use redis::{ConnectionAddr, ConnectionInfo, IntoConnectionInfo, ProtocolVersion, RedisConnectionInfo, RedisResult};
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub const REDIS_CONVERTOR_CONFIG_KEY: &str = "convertor:config.toml";
pub const REDIS_CONVERTOR_CONFIG_PUBLISH_CHANNEL: &str = "convertor:config:publish";

#[derive(Default, Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct RedisConfig {
    pub host: String,
    pub port: u16,
    #[serde(default)]
    pub username: String,
    #[serde(default)]
    pub password: String,
    #[serde(default)]
    pub db: Option<u32>,
    #[serde(default)]
    pub prefix: Option<String>,
    #[serde(default)]
    pub tls: Option<TlsConfig>,
}

#[derive(Default, Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct TlsConfig {
    pub ca_cert: Option<String>,
    pub client_cert: Option<String>,
    pub client_key: Option<String>,
}

impl RedisConfig {
    pub fn build_redis_client(&self) -> Result<redis::Client, RedisConfigError> {
        let config = self.validate()?;
        let redis_client = match config.tls.clone() {
            None => redis::Client::open(config),
            Some(tls) => redis::Client::build_with_tls(config, tls.into()),
        }?;
        Ok(redis_client)
    }

    pub fn validate(&self) -> Result<RedisConfig, RedisConfigError> {
        let RedisConfig {
            mut host,
            port,
            mut username,
            password,
            db,
            prefix,
            mut tls,
        } = self.clone();
        host = host.trim().to_string();
        if host.is_empty() {
            return Err(RedisConfigError::EmptyHost);
        } else if host.contains(':') {
            return Err(RedisConfigError::InvalidHost(host));
        }
        if self.port == 0 {
            return Err(RedisConfigError::ZeroPort);
        }
        username = username.trim().replace("default", "").to_string();

        if let Some(TlsConfig {
            ca_cert,
            client_cert,
            client_key,
        }) = &mut tls
        {
            if let Some(ca_cert) = ca_cert.as_mut() {
                *ca_cert = ca_cert.trim().to_string();
            }
            if let Some(client_cert) = client_cert.as_mut() {
                *client_cert = client_cert.trim().to_string();
            }
            if let Some(client_key) = client_key.as_mut() {
                *client_key = client_key.trim().to_string();
            }
            match (client_cert, client_key) {
                (Some(_), None) => return Err(RedisConfigError::HasClientCertButNoKey),
                (None, Some(_)) => return Err(RedisConfigError::HasClientKeyButNoCert),
                _ => {}
            }
        }

        Ok(RedisConfig {
            host,
            port,
            username,
            password,
            db,
            prefix,
            tls,
        })
    }
}

impl RedisConfig {
    pub fn template() -> Self {
        Self {
            host: "127.0.0.1".to_string(),
            port: 6379,
            username: "".to_string(),
            password: "yourpassword".to_string(),
            db: Some(0),
            prefix: Some("convertor:".to_string()),
            tls: Some(TlsConfig::template()),
        }
    }
}

impl TlsConfig {
    pub fn template() -> Self {
        Self {
            ca_cert: Some(
                r#"
-----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
-----END CERTIFICATE-----
            "#
                .trim()
                .to_string(),
            ),
            client_cert: Some(
                r#"
-----BEGIN CERTIFICATE-----
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
-----END CERTIFICATE-----
            "#
                .trim()
                .to_string(),
            ),
            client_key: Some(
                r#"
-----BEGIN PRIVATE KEY-----
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
-----END PRIVATE KEY-----
            "#
                .trim()
                .to_string(),
            ),
        }
    }
}

impl IntoConnectionInfo for RedisConfig {
    fn into_connection_info(self) -> RedisResult<ConnectionInfo> {
        let connection_info = ConnectionInfo {
            addr: match self.tls {
                None => ConnectionAddr::Tcp(self.host, self.port),
                Some(_tls) => ConnectionAddr::TcpTls {
                    host: self.host,
                    port: self.port,
                    insecure: false,
                    tls_params: None,
                },
            },
            redis: RedisConnectionInfo {
                db: self.db.unwrap_or(0) as i64,
                username: match self.username {
                    ref u if u.is_empty() => None,
                    ref u => Some(u.clone()),
                },
                password: match self.password {
                    ref p if p.is_empty() => None,
                    ref p => Some(p.clone()),
                },
                protocol: ProtocolVersion::RESP3,
            },
        };

        Ok(connection_info)
    }
}

impl From<TlsConfig> for redis::TlsCertificates {
    fn from(value: TlsConfig) -> Self {
        let TlsConfig {
            ca_cert,
            client_cert,
            client_key,
        } = value;
        redis::TlsCertificates {
            client_tls: match (client_cert, client_key) {
                (Some(client_cert), Some(client_key)) => Some(redis::ClientTlsConfig {
                    client_cert: client_cert.into_bytes(),
                    client_key: client_key.into_bytes(),
                }),
                _ => None,
            },
            root_cert: ca_cert.map(|ca_cert| ca_cert.into_bytes()),
        }
    }
}

#[derive(Debug, Error)]
pub enum RedisConfigError {
    #[error("Redis host is empty")]
    EmptyHost,
    #[error("Redis host is invalid: {0}")]
    InvalidHost(String),
    #[error("Redis port is 0")]
    ZeroPort,
    #[error("Redis password is empty")]
    EmptyPassword,
    #[error("TLS configuration error: both client_cert and client_key must be provided together")]
    HasClientCertButNoKey,
    #[error("TLS configuration error: both client_cert and client_key must be provided together")]
    HasClientKeyButNoCert,
    #[error(transparent)]
    RedisError(#[from] redis::RedisError),
}