mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-26 10:09:08 +00:00
URI works on android. Shadowsocks doesn't work on iPhone. it's ok - will be fixed.
This commit is contained in:
125
src/services/uri_generator/builders/mod.rs
Normal file
125
src/services/uri_generator/builders/mod.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
|
||||
|
||||
pub mod vless;
|
||||
pub mod vmess;
|
||||
pub mod trojan;
|
||||
pub mod shadowsocks;
|
||||
|
||||
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()));
|
||||
}
|
||||
if config.port <= 0 || config.port > 65535 {
|
||||
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()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper functions for URI building
|
||||
pub mod utils {
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
use crate::services::uri_generator::error::UriGeneratorError;
|
||||
|
||||
/// 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
|
||||
.get("network")
|
||||
.and_then(|v| v.as_str())
|
||||
.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 {
|
||||
stream_settings
|
||||
.get("security")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("tls")
|
||||
.to_string()
|
||||
} else {
|
||||
"none".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract WebSocket path from stream settings
|
||||
pub fn extract_ws_path(stream_settings: &Value) -> Option<String> {
|
||||
stream_settings
|
||||
.get("wsSettings")
|
||||
.and_then(|ws| ws.get("path"))
|
||||
.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
|
||||
.get("wsSettings")
|
||||
.and_then(|ws| ws.get("headers"))
|
||||
.and_then(|headers| headers.get("Host"))
|
||||
.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
|
||||
.get("grpcSettings")
|
||||
.and_then(|grpc| grpc.get("serviceName"))
|
||||
.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> {
|
||||
// Try stream settings first
|
||||
if let Some(sni) = stream_settings
|
||||
.get("tlsSettings")
|
||||
.and_then(|tls| tls.get("serverName"))
|
||||
.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(user_name: &str, server_name: &str, inbound_tag: &str) -> String {
|
||||
format!("{}@{}-{}", user_name, server_name, inbound_tag)
|
||||
}
|
||||
}
|
||||
96
src/services/uri_generator/builders/shadowsocks.rs
Normal file
96
src/services/uri_generator/builders/shadowsocks.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
|
||||
use super::{UriBuilder, utils};
|
||||
|
||||
pub struct ShadowsocksUriBuilder;
|
||||
|
||||
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",
|
||||
|
||||
// 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
|
||||
.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.user_name, &config.server_name, &config.inbound_tag);
|
||||
|
||||
// Build simple SIP002 URI (no plugin parameters for standard Shadowsocks)
|
||||
let uri = format!(
|
||||
"ss://{}@{}:{}#{}",
|
||||
encoded_credentials,
|
||||
config.hostname,
|
||||
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()));
|
||||
}
|
||||
if config.port <= 0 || config.port > 65535 {
|
||||
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()));
|
||||
}
|
||||
|
||||
// Shadowsocks uses xray_user_id as password, already validated above
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShadowsocksUriBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
210
src/services/uri_generator/builders/trojan.rs
Normal file
210
src/services/uri_generator/builders/trojan.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
|
||||
use super::{UriBuilder, utils};
|
||||
|
||||
pub struct TrojanUriBuilder;
|
||||
|
||||
impl TrojanUriBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
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" => {
|
||||
if let Some(path) = utils::extract_ws_path(&stream_settings) {
|
||||
params.insert("path".to_string(), path);
|
||||
}
|
||||
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()) {
|
||||
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()) {
|
||||
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()) {
|
||||
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()) {
|
||||
let alpn_str = alpn
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
if !alpn_str.is_empty() {
|
||||
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()) {
|
||||
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()) {
|
||||
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()) {
|
||||
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()) {
|
||||
params.insert("flow".to_string(), flow.to_string());
|
||||
}
|
||||
|
||||
// Build the URI
|
||||
let query_string = utils::build_query_string(¶ms);
|
||||
let alias = utils::generate_alias(&config.user_name, &config.server_name, &config.inbound_tag);
|
||||
|
||||
let uri = if query_string.is_empty() {
|
||||
format!(
|
||||
"trojan://{}@{}:{}#{}",
|
||||
utils::url_encode(password),
|
||||
config.hostname,
|
||||
config.port,
|
||||
utils::url_encode(&alias)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"trojan://{}@{}:{}?{}#{}",
|
||||
utils::url_encode(password),
|
||||
config.hostname,
|
||||
config.port,
|
||||
query_string,
|
||||
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()));
|
||||
}
|
||||
if config.port <= 0 || config.port > 65535 {
|
||||
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()));
|
||||
}
|
||||
|
||||
// Trojan uses xray_user_id as password, already validated above
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TrojanUriBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply variable substitution to JSON value
|
||||
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);
|
||||
let replacement = match value {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
_ => value.to_string().trim_matches('"').to_string(),
|
||||
};
|
||||
result = result.replace(&placeholder, &replacement);
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::from_str(&result)
|
||||
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
|
||||
}
|
||||
167
src/services/uri_generator/builders/vless.rs
Normal file
167
src/services/uri_generator/builders/vless.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::services::uri_generator::{ClientConfigData, error::UriGeneratorError};
|
||||
use super::{UriBuilder, utils};
|
||||
|
||||
pub struct VlessUriBuilder;
|
||||
|
||||
impl VlessUriBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
apply_variables(&config.stream_settings, &config.variable_values)?
|
||||
} 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" => {
|
||||
if let Some(path) = utils::extract_ws_path(&stream_settings) {
|
||||
params.insert("path".to_string(), path);
|
||||
}
|
||||
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()) {
|
||||
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()) {
|
||||
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()) {
|
||||
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()) {
|
||||
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()) {
|
||||
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()) {
|
||||
params.insert("flow".to_string(), flow.to_string());
|
||||
}
|
||||
|
||||
// Build the URI
|
||||
let query_string = utils::build_query_string(¶ms);
|
||||
let alias = utils::generate_alias(&config.user_name, &config.server_name, &config.inbound_tag);
|
||||
|
||||
let uri = if query_string.is_empty() {
|
||||
format!(
|
||||
"vless://{}@{}:{}#{}",
|
||||
config.xray_user_id,
|
||||
config.hostname,
|
||||
config.port,
|
||||
utils::url_encode(&alias)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"vless://{}@{}:{}?{}#{}",
|
||||
config.xray_user_id,
|
||||
config.hostname,
|
||||
config.port,
|
||||
query_string,
|
||||
utils::url_encode(&alias)
|
||||
)
|
||||
};
|
||||
|
||||
Ok(uri)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VlessUriBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply variable substitution to JSON value
|
||||
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);
|
||||
let replacement = match value {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
_ => value.to_string().trim_matches('"').to_string(),
|
||||
};
|
||||
result = result.replace(&placeholder, &replacement);
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::from_str(&result)
|
||||
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
|
||||
}
|
||||
259
src/services/uri_generator/builders/vmess.rs
Normal file
259
src/services/uri_generator/builders/vmess.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
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};
|
||||
|
||||
pub struct VmessUriBuilder;
|
||||
|
||||
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> {
|
||||
// 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,
|
||||
"aid": "0",
|
||||
"host": "",
|
||||
"id": config.xray_user_id,
|
||||
"net": transport_type,
|
||||
"path": "",
|
||||
"port": config.port,
|
||||
"ps": utils::generate_alias(&config.user_name, &config.server_name, &config.inbound_tag),
|
||||
"scy": "auto",
|
||||
"tls": if security == "none" { "none" } else { &security },
|
||||
"type": "none",
|
||||
"v": "2"
|
||||
});
|
||||
|
||||
// Transport-specific settings
|
||||
match transport_type.as_str() {
|
||||
"ws" => {
|
||||
if let Some(path) = utils::extract_ws_path(&stream_settings) {
|
||||
vmess_config["path"] = Value::String(path);
|
||||
}
|
||||
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()) {
|
||||
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
|
||||
.get("tcpSettings")
|
||||
.and_then(|tcp| tcp.get("header"))
|
||||
.and_then(|header| header.get("request"))
|
||||
.and_then(|request| request.get("headers"))
|
||||
.and_then(|headers| headers.get("Host"))
|
||||
.and_then(|host| host.as_array())
|
||||
.and_then(|arr| arr.first())
|
||||
.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"))
|
||||
.and_then(|header| header.get("request"))
|
||||
.and_then(|request| request.get("path"))
|
||||
.and_then(|path| path.as_array())
|
||||
.and_then(|arr| arr.first())
|
||||
.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()) {
|
||||
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()) {
|
||||
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()) {
|
||||
let alpn_str = alpn
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
if !alpn_str.is_empty() {
|
||||
vmess_config["alpn"] = Value::String(alpn_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
// 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" => {
|
||||
if let Some(path) = utils::extract_ws_path(&stream_settings) {
|
||||
params.insert("path".to_string(), path);
|
||||
}
|
||||
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()) {
|
||||
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()) {
|
||||
params.insert("fp".to_string(), fp.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Build the URI
|
||||
let query_string = utils::build_query_string(¶ms);
|
||||
let alias = utils::generate_alias(&config.user_name, &config.server_name, &config.inbound_tag);
|
||||
|
||||
let uri = if query_string.is_empty() {
|
||||
format!(
|
||||
"vmess://{}@{}:{}#{}",
|
||||
config.xray_user_id,
|
||||
config.hostname,
|
||||
config.port,
|
||||
utils::url_encode(&alias)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"vmess://{}@{}:{}?{}#{}",
|
||||
config.xray_user_id,
|
||||
config.hostname,
|
||||
config.port,
|
||||
query_string,
|
||||
utils::url_encode(&alias)
|
||||
)
|
||||
};
|
||||
|
||||
Ok(uri)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VmessUriBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply variable substitution to JSON value
|
||||
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);
|
||||
let replacement = match value {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
_ => value.to_string().trim_matches('"').to_string(),
|
||||
};
|
||||
result = result.replace(&placeholder, &replacement);
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::from_str(&result)
|
||||
.map_err(|e| UriGeneratorError::VariableSubstitution(e.to_string()))
|
||||
}
|
||||
Reference in New Issue
Block a user