Added usermanagement in TG admin

This commit is contained in:
AB from home.homenet
2025-10-24 18:11:34 +03:00
parent c6892b1a73
commit 78bf75b24e
89 changed files with 4389 additions and 2419 deletions

View File

@@ -1,30 +1,36 @@
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
pub mod shadowsocks;
pub mod trojan;
pub mod vless;
pub mod vmess;
pub mod trojan;
pub mod shadowsocks;
pub use shadowsocks::ShadowsocksUriBuilder;
pub use trojan::TrojanUriBuilder;
pub use vless::VlessUriBuilder;
pub use vmess::VmessUriBuilder;
pub use trojan::TrojanUriBuilder;
pub use shadowsocks::ShadowsocksUriBuilder;
/// Common trait for all URI builders
pub trait UriBuilder {
/// Build URI string from client configuration data
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError>;
/// Validate configuration for this protocol
fn validate_config(&self, config: &ClientConfigData) -> Result<(), UriGeneratorError> {
if config.hostname.is_empty() {
return Err(UriGeneratorError::MissingRequiredField("hostname".to_string()));
return Err(UriGeneratorError::MissingRequiredField(
"hostname".to_string(),
));
}
if config.port <= 0 || config.port > 65535 {
return Err(UriGeneratorError::InvalidConfiguration("Invalid port number".to_string()));
return Err(UriGeneratorError::InvalidConfiguration(
"Invalid port number".to_string(),
));
}
if config.xray_user_id.is_empty() {
return Err(UriGeneratorError::MissingRequiredField("xray_user_id".to_string()));
return Err(UriGeneratorError::MissingRequiredField(
"xray_user_id".to_string(),
));
}
Ok(())
}
@@ -32,28 +38,28 @@ pub trait UriBuilder {
/// Helper functions for URI building
pub mod utils {
use std::collections::HashMap;
use serde_json::Value;
use crate::services::uri_generator::error::UriGeneratorError;
use serde_json::Value;
use std::collections::HashMap;
/// URL encode a string safely
pub fn url_encode(input: &str) -> String {
urlencoding::encode(input).to_string()
}
/// Build query string from parameters
pub fn build_query_string(params: &HashMap<String, String>) -> String {
let mut query_parts: Vec<String> = Vec::new();
for (key, value) in params {
if !value.is_empty() {
query_parts.push(format!("{}={}", url_encode(key), url_encode(value)));
}
}
query_parts.join("&")
}
/// Extract transport type from stream settings
pub fn extract_transport_type(stream_settings: &Value) -> String {
stream_settings
@@ -62,7 +68,7 @@ pub mod utils {
.unwrap_or("tcp")
.to_string()
}
/// Extract security type from stream settings
pub fn extract_security_type(stream_settings: &Value, has_certificate: bool) -> String {
if has_certificate {
@@ -75,7 +81,7 @@ pub mod utils {
"none".to_string()
}
}
/// Extract WebSocket path from stream settings
pub fn extract_ws_path(stream_settings: &Value) -> Option<String> {
stream_settings
@@ -84,7 +90,7 @@ pub mod utils {
.and_then(|p| p.as_str())
.map(|s| s.to_string())
}
/// Extract WebSocket host from stream settings
pub fn extract_ws_host(stream_settings: &Value) -> Option<String> {
stream_settings
@@ -94,7 +100,7 @@ pub mod utils {
.and_then(|host| host.as_str())
.map(|s| s.to_string())
}
/// Extract gRPC service name from stream settings
pub fn extract_grpc_service_name(stream_settings: &Value) -> Option<String> {
stream_settings
@@ -103,23 +109,27 @@ pub mod utils {
.and_then(|name| name.as_str())
.map(|s| s.to_string())
}
/// Extract TLS SNI from stream settings
pub fn extract_tls_sni(stream_settings: &Value, certificate_domain: Option<&str>) -> Option<String> {
pub fn extract_tls_sni(
stream_settings: &Value,
certificate_domain: Option<&str>,
) -> Option<String> {
// Try stream settings first
if let Some(sni) = stream_settings
.get("tlsSettings")
.and_then(|tls| tls.get("serverName"))
.and_then(|sni| sni.as_str()) {
.and_then(|sni| sni.as_str())
{
return Some(sni.to_string());
}
// Fall back to certificate domain
certificate_domain.map(|s| s.to_string())
}
/// Determine alias for the URI
pub fn generate_alias(server_name: &str, template_name: &str) -> String {
format!("{} - {}", server_name, template_name)
}
}
}

View File

@@ -1,8 +1,8 @@
use base64::{Engine as _, engine::general_purpose};
use base64::{engine::general_purpose, Engine as _};
use serde_json::Value;
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
use super::{UriBuilder, utils};
use super::{utils, UriBuilder};
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
pub struct ShadowsocksUriBuilder;
@@ -10,54 +10,56 @@ impl ShadowsocksUriBuilder {
pub fn new() -> Self {
Self
}
/// Map Xray cipher type to Shadowsocks method name
fn map_xray_cipher_to_shadowsocks_method(&self, cipher: &str) -> &str {
match cipher {
// AES GCM variants
"AES_256_GCM" | "aes-256-gcm" => "aes-256-gcm",
"AES_128_GCM" | "aes-128-gcm" => "aes-128-gcm",
// ChaCha20 variants
"CHACHA20_POLY1305" | "chacha20-ietf-poly1305" | "chacha20-poly1305" => "chacha20-ietf-poly1305",
// ChaCha20 variants
"CHACHA20_POLY1305" | "chacha20-ietf-poly1305" | "chacha20-poly1305" => {
"chacha20-ietf-poly1305"
}
// AES CFB variants
"AES_256_CFB" | "aes-256-cfb" => "aes-256-cfb",
"AES_128_CFB" | "aes-128-cfb" => "aes-128-cfb",
// Legacy ciphers
"RC4_MD5" | "rc4-md5" => "rc4-md5",
"AES_256_CTR" | "aes-256-ctr" => "aes-256-ctr",
"AES_128_CTR" | "aes-128-ctr" => "aes-128-ctr",
// Default to most secure and widely supported
_ => "aes-256-gcm",
}
}
}
impl UriBuilder for ShadowsocksUriBuilder {
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
self.validate_config(config)?;
// Get cipher type from base_settings and map to Shadowsocks method
let cipher = config.base_settings
let cipher = config
.base_settings
.get("cipherType")
.and_then(|c| c.as_str())
.or_else(|| config.base_settings.get("method").and_then(|m| m.as_str()))
.unwrap_or("AES_256_GCM");
let method = self.map_xray_cipher_to_shadowsocks_method(cipher);
// Shadowsocks SIP002 format: ss://base64(method:password)@hostname:port#remark
// Use xray_user_id as password (following Marzban approach)
let credentials = format!("{}:{}", method, config.xray_user_id);
let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes());
// Generate alias for the URI
let alias = utils::generate_alias(&config.server_name, &config.template_name);
// Build simple SIP002 URI (no plugin parameters for standard Shadowsocks)
let uri = format!(
"ss://{}@{}:{}#{}",
@@ -66,24 +68,30 @@ impl UriBuilder for ShadowsocksUriBuilder {
config.port,
utils::url_encode(&alias)
);
Ok(uri)
}
fn validate_config(&self, config: &ClientConfigData) -> Result<(), UriGeneratorError> {
// Basic validation
if config.hostname.is_empty() {
return Err(UriGeneratorError::MissingRequiredField("hostname".to_string()));
return Err(UriGeneratorError::MissingRequiredField(
"hostname".to_string(),
));
}
if config.port <= 0 || config.port > 65535 {
return Err(UriGeneratorError::InvalidConfiguration("Invalid port number".to_string()));
return Err(UriGeneratorError::InvalidConfiguration(
"Invalid port number".to_string(),
));
}
if config.xray_user_id.is_empty() {
return Err(UriGeneratorError::MissingRequiredField("xray_user_id".to_string()));
return Err(UriGeneratorError::MissingRequiredField(
"xray_user_id".to_string(),
));
}
// Shadowsocks uses xray_user_id as password, already validated above
Ok(())
}
}
@@ -93,4 +101,3 @@ impl Default for ShadowsocksUriBuilder {
Self::new()
}
}

View File

@@ -1,8 +1,8 @@
use std::collections::HashMap;
use serde_json::Value;
use std::collections::HashMap;
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
use super::{UriBuilder, utils};
use super::{utils, UriBuilder};
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
pub struct TrojanUriBuilder;
@@ -15,32 +15,32 @@ impl TrojanUriBuilder {
impl UriBuilder for TrojanUriBuilder {
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
self.validate_config(config)?;
// Trojan uses xray_user_id as password
let password = &config.xray_user_id;
// Apply variable substitution to stream settings
let stream_settings = if !config.variable_values.is_null() {
apply_variables(&config.stream_settings, &config.variable_values)?
} else {
config.stream_settings.clone()
};
let mut params = HashMap::new();
// Determine security layer (Trojan typically uses TLS)
let has_certificate = config.certificate_domain.is_some();
let security = utils::extract_security_type(&stream_settings, has_certificate);
// Trojan usually requires TLS, but allow other security types
if security != "none" {
params.insert("security".to_string(), security.clone());
}
// Transport type - always specify explicitly
let transport_type = utils::extract_transport_type(&stream_settings);
params.insert("type".to_string(), transport_type.clone());
// Transport-specific parameters
match transport_type.as_str() {
"ws" => {
@@ -50,48 +50,53 @@ impl UriBuilder for TrojanUriBuilder {
if let Some(host) = utils::extract_ws_host(&stream_settings) {
params.insert("host".to_string(), host);
}
},
}
"grpc" => {
if let Some(service_name) = utils::extract_grpc_service_name(&stream_settings) {
params.insert("serviceName".to_string(), service_name);
}
// gRPC mode for Trojan
params.insert("mode".to_string(), "gun".to_string());
},
}
"tcp" => {
// Check for HTTP header type
if let Some(header_type) = stream_settings
.get("tcpSettings")
.and_then(|tcp| tcp.get("header"))
.and_then(|header| header.get("type"))
.and_then(|t| t.as_str()) {
.and_then(|t| t.as_str())
{
if header_type != "none" {
params.insert("headerType".to_string(), header_type.to_string());
}
}
},
}
_ => {} // Other transport types
}
// TLS/Security specific parameters
if security == "tls" || security == "reality" {
if let Some(sni) = utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref()) {
if let Some(sni) =
utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref())
{
params.insert("sni".to_string(), sni);
}
// TLS fingerprint
if let Some(fp) = stream_settings
.get("tlsSettings")
.and_then(|tls| tls.get("fingerprint"))
.and_then(|fp| fp.as_str()) {
.and_then(|fp| fp.as_str())
{
params.insert("fp".to_string(), fp.to_string());
}
// ALPN
if let Some(alpn) = stream_settings
.get("tlsSettings")
.and_then(|tls| tls.get("alpn"))
.and_then(|alpn| alpn.as_array()) {
.and_then(|alpn| alpn.as_array())
{
let alpn_str = alpn
.iter()
.filter_map(|v| v.as_str())
@@ -101,46 +106,47 @@ impl UriBuilder for TrojanUriBuilder {
params.insert("alpn".to_string(), alpn_str);
}
}
// Allow insecure connections (optional)
if let Some(allow_insecure) = stream_settings
.get("tlsSettings")
.and_then(|tls| tls.get("allowInsecure"))
.and_then(|ai| ai.as_bool()) {
.and_then(|ai| ai.as_bool())
{
if allow_insecure {
params.insert("allowInsecure".to_string(), "1".to_string());
}
}
// REALITY specific parameters
if security == "reality" {
if let Some(pbk) = stream_settings
.get("realitySettings")
.and_then(|reality| reality.get("publicKey"))
.and_then(|pbk| pbk.as_str()) {
.and_then(|pbk| pbk.as_str())
{
params.insert("pbk".to_string(), pbk.to_string());
}
if let Some(sid) = stream_settings
.get("realitySettings")
.and_then(|reality| reality.get("shortId"))
.and_then(|sid| sid.as_str()) {
.and_then(|sid| sid.as_str())
{
params.insert("sid".to_string(), sid.to_string());
}
}
}
// Flow control for XTLS (if supported)
if let Some(flow) = stream_settings
.get("flow")
.and_then(|f| f.as_str()) {
if let Some(flow) = stream_settings.get("flow").and_then(|f| f.as_str()) {
params.insert("flow".to_string(), flow.to_string());
}
// Build the URI
let query_string = utils::build_query_string(&params);
let alias = utils::generate_alias(&config.server_name, &config.template_name);
let uri = if query_string.is_empty() {
format!(
"trojan://{}@{}:{}#{}",
@@ -159,24 +165,30 @@ impl UriBuilder for TrojanUriBuilder {
utils::url_encode(&alias)
)
};
Ok(uri)
}
fn validate_config(&self, config: &ClientConfigData) -> Result<(), UriGeneratorError> {
// Basic validation
if config.hostname.is_empty() {
return Err(UriGeneratorError::MissingRequiredField("hostname".to_string()));
return Err(UriGeneratorError::MissingRequiredField(
"hostname".to_string(),
));
}
if config.port <= 0 || config.port > 65535 {
return Err(UriGeneratorError::InvalidConfiguration("Invalid port number".to_string()));
return Err(UriGeneratorError::InvalidConfiguration(
"Invalid port number".to_string(),
));
}
if config.xray_user_id.is_empty() {
return Err(UriGeneratorError::MissingRequiredField("xray_user_id".to_string()));
return Err(UriGeneratorError::MissingRequiredField(
"xray_user_id".to_string(),
));
}
// Trojan uses xray_user_id as password, already validated above
Ok(())
}
}
@@ -191,7 +203,7 @@ impl Default for TrojanUriBuilder {
fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGeneratorError> {
let template_str = template.to_string();
let mut result = template_str;
if let Value::Object(var_map) = variables {
for (key, value) in var_map {
let placeholder = format!("${{{}}}", key);
@@ -204,7 +216,7 @@ fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGene
result = result.replace(&placeholder, &replacement);
}
}
serde_json::from_str(&result)
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
}
}

View File

@@ -1,8 +1,8 @@
use std::collections::HashMap;
use serde_json::Value;
use std::collections::HashMap;
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
use super::{UriBuilder, utils};
use super::{utils, UriBuilder};
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
pub struct VlessUriBuilder;
@@ -15,7 +15,7 @@ impl VlessUriBuilder {
impl UriBuilder for VlessUriBuilder {
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
self.validate_config(config)?;
// Apply variable substitution to stream settings
let stream_settings = if !config.variable_values.is_null() {
// Simple variable substitution for stream settings
@@ -23,23 +23,23 @@ impl UriBuilder for VlessUriBuilder {
} else {
config.stream_settings.clone()
};
let mut params = HashMap::new();
// VLESS always uses no encryption
params.insert("encryption".to_string(), "none".to_string());
// Determine security layer
let has_certificate = config.certificate_domain.is_some();
let security = utils::extract_security_type(&stream_settings, has_certificate);
if security != "none" {
params.insert("security".to_string(), security.clone());
}
// Transport type - always specify explicitly
let transport_type = utils::extract_transport_type(&stream_settings);
params.insert("type".to_string(), transport_type.clone());
// Transport-specific parameters
match transport_type.as_str() {
"ws" => {
@@ -49,72 +49,76 @@ impl UriBuilder for VlessUriBuilder {
if let Some(host) = utils::extract_ws_host(&stream_settings) {
params.insert("host".to_string(), host);
}
},
}
"grpc" => {
if let Some(service_name) = utils::extract_grpc_service_name(&stream_settings) {
params.insert("serviceName".to_string(), service_name);
}
// Default gRPC mode
params.insert("mode".to_string(), "gun".to_string());
},
}
"tcp" => {
// Check for HTTP header type
if let Some(header_type) = stream_settings
.get("tcpSettings")
.and_then(|tcp| tcp.get("header"))
.and_then(|header| header.get("type"))
.and_then(|t| t.as_str()) {
.and_then(|t| t.as_str())
{
if header_type != "none" {
params.insert("headerType".to_string(), header_type.to_string());
}
}
},
}
_ => {} // Other transport types can be added as needed
}
// TLS/Security specific parameters
if security == "tls" || security == "reality" {
if let Some(sni) = utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref()) {
if let Some(sni) =
utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref())
{
params.insert("sni".to_string(), sni);
}
// TLS fingerprint
if let Some(fp) = stream_settings
.get("tlsSettings")
.and_then(|tls| tls.get("fingerprint"))
.and_then(|fp| fp.as_str()) {
.and_then(|fp| fp.as_str())
{
params.insert("fp".to_string(), fp.to_string());
}
// REALITY specific parameters
if security == "reality" {
if let Some(pbk) = stream_settings
.get("realitySettings")
.and_then(|reality| reality.get("publicKey"))
.and_then(|pbk| pbk.as_str()) {
.and_then(|pbk| pbk.as_str())
{
params.insert("pbk".to_string(), pbk.to_string());
}
if let Some(sid) = stream_settings
.get("realitySettings")
.and_then(|reality| reality.get("shortId"))
.and_then(|sid| sid.as_str()) {
.and_then(|sid| sid.as_str())
{
params.insert("sid".to_string(), sid.to_string());
}
}
}
// Flow control for XTLS
if let Some(flow) = stream_settings
.get("flow")
.and_then(|f| f.as_str()) {
if let Some(flow) = stream_settings.get("flow").and_then(|f| f.as_str()) {
params.insert("flow".to_string(), flow.to_string());
}
// Build the URI
let query_string = utils::build_query_string(&params);
let alias = utils::generate_alias(&config.server_name, &config.template_name);
let uri = if query_string.is_empty() {
format!(
"vless://{}@{}:{}#{}",
@@ -133,7 +137,7 @@ impl UriBuilder for VlessUriBuilder {
utils::url_encode(&alias)
)
};
Ok(uri)
}
}
@@ -148,7 +152,7 @@ impl Default for VlessUriBuilder {
fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGeneratorError> {
let template_str = template.to_string();
let mut result = template_str;
if let Value::Object(var_map) = variables {
for (key, value) in var_map {
let placeholder = format!("${{{}}}", key);
@@ -161,7 +165,7 @@ fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGene
result = result.replace(&placeholder, &replacement);
}
}
serde_json::from_str(&result)
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
}
}

View File

@@ -1,9 +1,9 @@
use base64::{engine::general_purpose, Engine as _};
use serde_json::{json, Value};
use std::collections::HashMap;
use serde_json::{Value, json};
use base64::{Engine as _, engine::general_purpose};
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
use super::{UriBuilder, utils};
use super::{utils, UriBuilder};
use crate::services::uri_generator::{error::UriGeneratorError, ClientConfigData};
pub struct VmessUriBuilder;
@@ -11,20 +11,23 @@ impl VmessUriBuilder {
pub fn new() -> Self {
Self
}
/// Build VMess URI in Base64 JSON format (following Marzban approach)
fn build_base64_json_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
fn build_base64_json_uri(
&self,
config: &ClientConfigData,
) -> Result<String, UriGeneratorError> {
// Apply variable substitution to stream settings
let stream_settings = if !config.variable_values.is_null() {
apply_variables(&config.stream_settings, &config.variable_values)?
} else {
config.stream_settings.clone()
};
let transport_type = utils::extract_transport_type(&stream_settings);
let has_certificate = config.certificate_domain.is_some();
let security = utils::extract_security_type(&stream_settings, has_certificate);
// Build VMess JSON configuration following Marzban structure
let mut vmess_config = json!({
"add": config.hostname,
@@ -40,7 +43,7 @@ impl VmessUriBuilder {
"type": "none",
"v": "2"
});
// Transport-specific settings
match transport_type.as_str() {
"ws" => {
@@ -50,23 +53,24 @@ impl VmessUriBuilder {
if let Some(host) = utils::extract_ws_host(&stream_settings) {
vmess_config["host"] = Value::String(host);
}
},
}
"grpc" => {
if let Some(service_name) = utils::extract_grpc_service_name(&stream_settings) {
vmess_config["path"] = Value::String(service_name);
}
// For gRPC in VMess, use "gun" type
vmess_config["type"] = Value::String("gun".to_string());
},
}
"tcp" => {
// Check for HTTP header type
if let Some(header_type) = stream_settings
.get("tcpSettings")
.and_then(|tcp| tcp.get("header"))
.and_then(|header| header.get("type"))
.and_then(|t| t.as_str()) {
.and_then(|t| t.as_str())
{
vmess_config["type"] = Value::String(header_type.to_string());
// If HTTP headers, get host and path
if header_type == "http" {
if let Some(host) = stream_settings
@@ -77,10 +81,11 @@ impl VmessUriBuilder {
.and_then(|headers| headers.get("Host"))
.and_then(|host| host.as_array())
.and_then(|arr| arr.first())
.and_then(|h| h.as_str()) {
.and_then(|h| h.as_str())
{
vmess_config["host"] = Value::String(host.to_string());
}
if let Some(path) = stream_settings
.get("tcpSettings")
.and_then(|tcp| tcp.get("header"))
@@ -88,34 +93,39 @@ impl VmessUriBuilder {
.and_then(|request| request.get("path"))
.and_then(|path| path.as_array())
.and_then(|arr| arr.first())
.and_then(|p| p.as_str()) {
.and_then(|p| p.as_str())
{
vmess_config["path"] = Value::String(path.to_string());
}
}
}
},
}
_ => {} // Other transport types
}
// TLS settings
if security != "none" {
if let Some(sni) = utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref()) {
if let Some(sni) =
utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref())
{
vmess_config["sni"] = Value::String(sni);
}
// TLS fingerprint
if let Some(fp) = stream_settings
.get("tlsSettings")
.and_then(|tls| tls.get("fingerprint"))
.and_then(|fp| fp.as_str()) {
.and_then(|fp| fp.as_str())
{
vmess_config["fp"] = Value::String(fp.to_string());
}
// ALPN
if let Some(alpn) = stream_settings
.get("tlsSettings")
.and_then(|tls| tls.get("alpn"))
.and_then(|alpn| alpn.as_array()) {
.and_then(|alpn| alpn.as_array())
{
let alpn_str = alpn
.iter()
.filter_map(|v| v.as_str())
@@ -126,41 +136,44 @@ impl VmessUriBuilder {
}
}
}
// Convert to JSON string and encode in Base64
let json_string = vmess_config.to_string();
let encoded = general_purpose::STANDARD.encode(json_string.as_bytes());
Ok(format!("vmess://{}", encoded))
}
/// Build VMess URI in query parameter format (alternative)
fn build_query_param_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
fn build_query_param_uri(
&self,
config: &ClientConfigData,
) -> Result<String, UriGeneratorError> {
// Apply variable substitution to stream settings
let stream_settings = if !config.variable_values.is_null() {
apply_variables(&config.stream_settings, &config.variable_values)?
} else {
config.stream_settings.clone()
};
let mut params = HashMap::new();
// VMess uses auto encryption
params.insert("encryption".to_string(), "auto".to_string());
// Determine security layer
let has_certificate = config.certificate_domain.is_some();
let security = utils::extract_security_type(&stream_settings, has_certificate);
if security != "none" {
params.insert("security".to_string(), security.clone());
}
// Transport type
let transport_type = utils::extract_transport_type(&stream_settings);
if transport_type != "tcp" {
params.insert("type".to_string(), transport_type.clone());
}
// Transport-specific parameters
match transport_type.as_str() {
"ws" => {
@@ -170,34 +183,37 @@ impl VmessUriBuilder {
if let Some(host) = utils::extract_ws_host(&stream_settings) {
params.insert("host".to_string(), host);
}
},
}
"grpc" => {
if let Some(service_name) = utils::extract_grpc_service_name(&stream_settings) {
params.insert("serviceName".to_string(), service_name);
}
params.insert("mode".to_string(), "gun".to_string());
},
}
_ => {}
}
// TLS specific parameters
if security != "none" {
if let Some(sni) = utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref()) {
if let Some(sni) =
utils::extract_tls_sni(&stream_settings, config.certificate_domain.as_deref())
{
params.insert("sni".to_string(), sni);
}
if let Some(fp) = stream_settings
.get("tlsSettings")
.and_then(|tls| tls.get("fingerprint"))
.and_then(|fp| fp.as_str()) {
.and_then(|fp| fp.as_str())
{
params.insert("fp".to_string(), fp.to_string());
}
}
// Build the URI
let query_string = utils::build_query_string(&params);
let alias = utils::generate_alias(&config.server_name, &config.template_name);
let uri = if query_string.is_empty() {
format!(
"vmess://{}@{}:{}#{}",
@@ -216,7 +232,7 @@ impl VmessUriBuilder {
utils::url_encode(&alias)
)
};
Ok(uri)
}
}
@@ -224,7 +240,7 @@ impl VmessUriBuilder {
impl UriBuilder for VmessUriBuilder {
fn build_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
self.validate_config(config)?;
// Prefer Base64 JSON format as it's more widely supported
self.build_base64_json_uri(config)
}
@@ -240,7 +256,7 @@ impl Default for VmessUriBuilder {
fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGeneratorError> {
let template_str = template.to_string();
let mut result = template_str;
if let Value::Object(var_map) = variables {
for (key, value) in var_map {
let placeholder = format!("${{{}}}", key);
@@ -253,7 +269,7 @@ fn apply_variables(template: &Value, variables: &Value) -> Result<Value, UriGene
result = result.replace(&placeholder, &replacement);
}
}
serde_json::from_str(&result)
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
}
}