mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-23 16:59:08 +00:00
URI works on android. Shadowsocks doesn't work on iPhone. it's ok - will be fixed.
This commit is contained in:
58
API.md
58
API.md
@@ -77,9 +77,12 @@ All API endpoints are prefixed with `/api`.
|
||||
- `DELETE /users/{id}` - Delete user by ID
|
||||
|
||||
#### Get User Access
|
||||
- `GET /users/{id}/access` - Get user access to inbounds
|
||||
- `GET /users/{id}/access?include_uris=true` - Get user access to inbounds (optionally with client URIs)
|
||||
|
||||
**Response:**
|
||||
**Query Parameters:**
|
||||
- `include_uris`: boolean (optional) - Include client configuration URIs in response
|
||||
|
||||
**Response (without URIs):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
@@ -93,6 +96,40 @@ All API endpoints are prefixed with `/api`.
|
||||
]
|
||||
```
|
||||
|
||||
**Response (with URIs):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"server_inbound_id": "uuid",
|
||||
"xray_user_id": "string",
|
||||
"level": 0,
|
||||
"is_active": true,
|
||||
"uri": "vless://uuid@hostname:port?parameters#alias",
|
||||
"protocol": "vless",
|
||||
"server_name": "Server Name",
|
||||
"inbound_tag": "inbound-tag"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Generate Client Configurations
|
||||
- `GET /users/{user_id}/configs` - Get all client configuration URIs for a user
|
||||
- `GET /users/{user_id}/access/{inbound_id}/config` - Get specific client configuration URI
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"user_id": "uuid",
|
||||
"server_name": "string",
|
||||
"inbound_tag": "string",
|
||||
"protocol": "vmess|vless|trojan|shadowsocks",
|
||||
"uri": "protocol://uri_string",
|
||||
"qr_code": null
|
||||
}
|
||||
```
|
||||
|
||||
### Servers
|
||||
|
||||
#### List Servers
|
||||
@@ -243,6 +280,23 @@ All API endpoints are prefixed with `/api`.
|
||||
#### Remove User from Inbound
|
||||
- `DELETE /servers/{server_id}/inbounds/{inbound_id}/users/{email}` - Remove user access
|
||||
|
||||
#### Get Inbound Client Configurations
|
||||
- `GET /servers/{server_id}/inbounds/{inbound_id}/configs` - Get all client configuration URIs for an inbound
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"user_id": "uuid",
|
||||
"server_name": "string",
|
||||
"inbound_tag": "string",
|
||||
"protocol": "vmess|vless|trojan|shadowsocks",
|
||||
"uri": "protocol://uri_string",
|
||||
"qr_code": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Certificates
|
||||
|
||||
#### List Certificates
|
||||
|
@@ -5,6 +5,7 @@ use uuid::Uuid;
|
||||
use crate::database::entities::inbound_users::{
|
||||
Entity, Model, ActiveModel, CreateInboundUserDto, UpdateInboundUserDto, Column
|
||||
};
|
||||
use crate::services::uri_generator::ClientConfigData;
|
||||
|
||||
pub struct InboundUsersRepository {
|
||||
db: DatabaseConnection,
|
||||
@@ -129,4 +130,97 @@ impl InboundUsersRepository {
|
||||
let exists = self.find_by_user_and_inbound(user_id, inbound_id).await?;
|
||||
Ok(exists.is_some())
|
||||
}
|
||||
|
||||
/// Get complete client configuration data for URI generation
|
||||
pub async fn get_client_config_data(&self, user_id: Uuid, server_inbound_id: Uuid) -> Result<Option<ClientConfigData>> {
|
||||
use crate::database::entities::{
|
||||
user, server, server_inbound, inbound_template, certificate
|
||||
};
|
||||
|
||||
// Get the inbound_user record first
|
||||
let inbound_user = Entity::find()
|
||||
.filter(Column::UserId.eq(user_id))
|
||||
.filter(Column::ServerInboundId.eq(server_inbound_id))
|
||||
.filter(Column::IsActive.eq(true))
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
if let Some(inbound_user) = inbound_user {
|
||||
// Get user info
|
||||
let user_entity = user::Entity::find_by_id(inbound_user.user_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("User not found"))?;
|
||||
|
||||
// Get server inbound info
|
||||
let server_inbound_entity = server_inbound::Entity::find_by_id(inbound_user.server_inbound_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server inbound not found"))?;
|
||||
|
||||
// Get server info
|
||||
let server_entity = server::Entity::find_by_id(server_inbound_entity.server_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Server not found"))?;
|
||||
|
||||
// Get template info
|
||||
let template_entity = inbound_template::Entity::find_by_id(server_inbound_entity.template_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Template not found"))?;
|
||||
|
||||
// Get certificate info (optional)
|
||||
let certificate_domain = if let Some(cert_id) = server_inbound_entity.certificate_id {
|
||||
certificate::Entity::find_by_id(cert_id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.map(|cert| cert.domain)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let config = ClientConfigData {
|
||||
user_name: user_entity.name,
|
||||
xray_user_id: inbound_user.xray_user_id,
|
||||
password: inbound_user.password,
|
||||
level: inbound_user.level,
|
||||
hostname: server_entity.hostname,
|
||||
port: server_inbound_entity.port_override.unwrap_or(template_entity.default_port),
|
||||
protocol: template_entity.protocol,
|
||||
stream_settings: template_entity.stream_settings,
|
||||
base_settings: template_entity.base_settings,
|
||||
certificate_domain,
|
||||
requires_tls: template_entity.requires_tls,
|
||||
variable_values: server_inbound_entity.variable_values,
|
||||
server_name: server_entity.name,
|
||||
inbound_tag: server_inbound_entity.tag,
|
||||
};
|
||||
|
||||
Ok(Some(config))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all client configuration data for a user
|
||||
pub async fn get_all_client_configs_for_user(&self, user_id: Uuid) -> Result<Vec<ClientConfigData>> {
|
||||
// Get all active inbound users for this user
|
||||
let inbound_users = Entity::find()
|
||||
.filter(Column::UserId.eq(user_id))
|
||||
.filter(Column::IsActive.eq(true))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
let mut configs = Vec::new();
|
||||
|
||||
for inbound_user in inbound_users {
|
||||
// Get the client config data for each inbound
|
||||
if let Ok(Some(config)) = self.get_client_config_data(user_id, inbound_user.server_inbound_id).await {
|
||||
configs.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(configs)
|
||||
}
|
||||
}
|
@@ -2,6 +2,8 @@ pub mod xray;
|
||||
pub mod certificates;
|
||||
pub mod events;
|
||||
pub mod tasks;
|
||||
pub mod uri_generator;
|
||||
|
||||
pub use xray::XrayService;
|
||||
pub use tasks::TaskScheduler;
|
||||
pub use tasks::TaskScheduler;
|
||||
pub use uri_generator::UriGeneratorService;
|
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()))
|
||||
}
|
51
src/services/uri_generator/error.rs
Normal file
51
src/services/uri_generator/error.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UriGeneratorError {
|
||||
UnsupportedProtocol(String),
|
||||
MissingRequiredField(String),
|
||||
InvalidConfiguration(String),
|
||||
VariableSubstitution(String),
|
||||
JsonParsing(String),
|
||||
UriEncoding(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for UriGeneratorError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
UriGeneratorError::UnsupportedProtocol(protocol) => {
|
||||
write!(f, "Unsupported protocol: {}", protocol)
|
||||
}
|
||||
UriGeneratorError::MissingRequiredField(field) => {
|
||||
write!(f, "Missing required field: {}", field)
|
||||
}
|
||||
UriGeneratorError::InvalidConfiguration(msg) => {
|
||||
write!(f, "Invalid configuration: {}", msg)
|
||||
}
|
||||
UriGeneratorError::VariableSubstitution(msg) => {
|
||||
write!(f, "Variable substitution error: {}", msg)
|
||||
}
|
||||
UriGeneratorError::JsonParsing(msg) => {
|
||||
write!(f, "JSON parsing error: {}", msg)
|
||||
}
|
||||
UriGeneratorError::UriEncoding(msg) => {
|
||||
write!(f, "URI encoding error: {}", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for UriGeneratorError {}
|
||||
|
||||
impl From<serde_json::Error> for UriGeneratorError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
UriGeneratorError::JsonParsing(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Note: urlencoding crate doesn't have EncodingError in current version
|
||||
// impl From<urlencoding::EncodingError> for UriGeneratorError {
|
||||
// fn from(err: urlencoding::EncodingError) -> Self {
|
||||
// UriGeneratorError::UriEncoding(err.to_string())
|
||||
// }
|
||||
// }
|
128
src/services/uri_generator/mod.rs
Normal file
128
src/services/uri_generator/mod.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod builders;
|
||||
pub mod error;
|
||||
|
||||
use builders::{UriBuilder, VlessUriBuilder, VmessUriBuilder, TrojanUriBuilder, ShadowsocksUriBuilder};
|
||||
use error::UriGeneratorError;
|
||||
|
||||
/// Complete client configuration data aggregated from database
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientConfigData {
|
||||
// User credentials
|
||||
pub user_name: String,
|
||||
pub xray_user_id: String,
|
||||
pub password: Option<String>,
|
||||
pub level: i32,
|
||||
|
||||
// Server connection
|
||||
pub hostname: String,
|
||||
pub port: i32,
|
||||
|
||||
// Protocol & transport
|
||||
pub protocol: String,
|
||||
pub stream_settings: Value,
|
||||
pub base_settings: Value,
|
||||
|
||||
// Security
|
||||
pub certificate_domain: Option<String>,
|
||||
pub requires_tls: bool,
|
||||
|
||||
// Variable substitution
|
||||
pub variable_values: Value,
|
||||
|
||||
// Metadata
|
||||
pub server_name: String,
|
||||
pub inbound_tag: String,
|
||||
}
|
||||
|
||||
/// Generated client configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientConfig {
|
||||
pub user_id: Uuid,
|
||||
pub server_name: String,
|
||||
pub inbound_tag: String,
|
||||
pub protocol: String,
|
||||
pub uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub qr_code: Option<String>,
|
||||
}
|
||||
|
||||
/// URI Generator Service
|
||||
pub struct UriGeneratorService;
|
||||
|
||||
impl UriGeneratorService {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Generate URI for specific protocol and configuration
|
||||
pub fn generate_uri(&self, config: &ClientConfigData) -> Result<String, UriGeneratorError> {
|
||||
let protocol = config.protocol.as_str();
|
||||
|
||||
match protocol {
|
||||
"vless" => {
|
||||
let builder = VlessUriBuilder::new();
|
||||
builder.build_uri(config)
|
||||
},
|
||||
"vmess" => {
|
||||
let builder = VmessUriBuilder::new();
|
||||
builder.build_uri(config)
|
||||
},
|
||||
"trojan" => {
|
||||
let builder = TrojanUriBuilder::new();
|
||||
builder.build_uri(config)
|
||||
},
|
||||
"shadowsocks" => {
|
||||
let builder = ShadowsocksUriBuilder::new();
|
||||
builder.build_uri(config)
|
||||
},
|
||||
_ => Err(UriGeneratorError::UnsupportedProtocol(protocol.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate complete client configuration
|
||||
pub fn generate_client_config(&self, user_id: Uuid, config: &ClientConfigData) -> Result<ClientConfig, UriGeneratorError> {
|
||||
let uri = self.generate_uri(config)?;
|
||||
|
||||
Ok(ClientConfig {
|
||||
user_id,
|
||||
server_name: config.server_name.clone(),
|
||||
inbound_tag: config.inbound_tag.clone(),
|
||||
protocol: config.protocol.clone(),
|
||||
uri,
|
||||
qr_code: None, // TODO: Implement QR code generation if needed
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply variable substitution to JSON values
|
||||
pub fn apply_variable_substitution(&self, 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()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UriGeneratorService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
136
src/web/handlers/client_configs.rs
Normal file
136
src/web/handlers/client_configs.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::repository::InboundUsersRepository;
|
||||
use crate::services::UriGeneratorService;
|
||||
use crate::web::AppState;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IncludeUrisQuery {
|
||||
#[serde(default)]
|
||||
pub include_uris: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ClientConfigResponse {
|
||||
pub user_id: Uuid,
|
||||
pub server_name: String,
|
||||
pub inbound_tag: String,
|
||||
pub protocol: String,
|
||||
pub uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub qr_code: Option<String>,
|
||||
}
|
||||
|
||||
/// Generate URI for specific user and inbound
|
||||
pub async fn get_user_inbound_config(
|
||||
State(app_state): State<AppState>,
|
||||
Path((user_id, inbound_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<Json<ClientConfigResponse>, StatusCode> {
|
||||
let repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||
let uri_service = UriGeneratorService::new();
|
||||
|
||||
// Get client configuration data
|
||||
let config_data = repo.get_client_config_data(user_id, inbound_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let config_data = config_data.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
// Generate URI
|
||||
let client_config = uri_service.generate_client_config(user_id, &config_data)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let response = ClientConfigResponse {
|
||||
user_id: client_config.user_id,
|
||||
server_name: client_config.server_name,
|
||||
inbound_tag: client_config.inbound_tag,
|
||||
protocol: client_config.protocol,
|
||||
uri: client_config.uri,
|
||||
qr_code: client_config.qr_code,
|
||||
};
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
/// Generate all URIs for a user
|
||||
pub async fn get_user_configs(
|
||||
State(app_state): State<AppState>,
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<ClientConfigResponse>>, StatusCode> {
|
||||
let repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||
let uri_service = UriGeneratorService::new();
|
||||
|
||||
// Get all client configuration data for user
|
||||
let configs_data = repo.get_all_client_configs_for_user(user_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let mut responses = Vec::new();
|
||||
|
||||
for config_data in configs_data {
|
||||
match uri_service.generate_client_config(user_id, &config_data) {
|
||||
Ok(client_config) => {
|
||||
responses.push(ClientConfigResponse {
|
||||
user_id: client_config.user_id,
|
||||
server_name: client_config.server_name,
|
||||
inbound_tag: client_config.inbound_tag,
|
||||
protocol: client_config.protocol,
|
||||
uri: client_config.uri,
|
||||
qr_code: client_config.qr_code,
|
||||
});
|
||||
},
|
||||
Err(_) => {
|
||||
// Log error but continue with other configs
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(responses))
|
||||
}
|
||||
|
||||
/// Get all URIs for all users of a specific inbound
|
||||
pub async fn get_inbound_configs(
|
||||
State(app_state): State<AppState>,
|
||||
Path((server_id, inbound_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<Json<Vec<ClientConfigResponse>>, StatusCode> {
|
||||
let repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||
let uri_service = UriGeneratorService::new();
|
||||
|
||||
// Get all users for this inbound
|
||||
let inbound_users = repo.find_active_by_inbound_id(inbound_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let mut responses = Vec::new();
|
||||
|
||||
for inbound_user in inbound_users {
|
||||
// Get client configuration data for each user
|
||||
if let Ok(Some(config_data)) = repo.get_client_config_data(inbound_user.user_id, inbound_id).await {
|
||||
match uri_service.generate_client_config(inbound_user.user_id, &config_data) {
|
||||
Ok(client_config) => {
|
||||
responses.push(ClientConfigResponse {
|
||||
user_id: client_config.user_id,
|
||||
server_name: client_config.server_name,
|
||||
inbound_tag: client_config.inbound_tag,
|
||||
protocol: client_config.protocol,
|
||||
uri: client_config.uri,
|
||||
qr_code: client_config.qr_code,
|
||||
});
|
||||
},
|
||||
Err(_) => {
|
||||
// Log error but continue with other configs
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(responses))
|
||||
}
|
@@ -2,8 +2,10 @@ pub mod users;
|
||||
pub mod servers;
|
||||
pub mod certificates;
|
||||
pub mod templates;
|
||||
pub mod client_configs;
|
||||
|
||||
pub use users::*;
|
||||
pub use servers::*;
|
||||
pub use certificates::*;
|
||||
pub use templates::*;
|
||||
pub use templates::*;
|
||||
pub use client_configs::*;
|
@@ -12,6 +12,8 @@ use crate::database::entities::user::{CreateUserDto, UpdateUserDto, Model as Use
|
||||
use crate::database::repository::UserRepository;
|
||||
use crate::web::AppState;
|
||||
|
||||
use super::client_configs::IncludeUrisQuery;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PaginationQuery {
|
||||
#[serde(default = "default_page")]
|
||||
@@ -197,8 +199,10 @@ pub async fn delete_user(
|
||||
pub async fn get_user_access(
|
||||
State(app_state): State<AppState>,
|
||||
Path(user_id): Path<Uuid>,
|
||||
Query(query): Query<IncludeUrisQuery>,
|
||||
) -> Result<Json<Vec<serde_json::Value>>, StatusCode> {
|
||||
use crate::database::repository::InboundUsersRepository;
|
||||
use crate::services::UriGeneratorService;
|
||||
|
||||
let inbound_users_repo = InboundUsersRepository::new(app_state.db.connection().clone());
|
||||
|
||||
@@ -207,17 +211,51 @@ pub async fn get_user_access(
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let response: Vec<serde_json::Value> = access_list
|
||||
.into_iter()
|
||||
.map(|access| serde_json::json!({
|
||||
"id": access.id,
|
||||
"user_id": access.user_id,
|
||||
"server_inbound_id": access.server_inbound_id,
|
||||
"xray_user_id": access.xray_user_id,
|
||||
"level": access.level,
|
||||
"is_active": access.is_active,
|
||||
}))
|
||||
.collect();
|
||||
let mut response: Vec<serde_json::Value> = Vec::new();
|
||||
|
||||
if query.include_uris {
|
||||
let uri_service = UriGeneratorService::new();
|
||||
|
||||
for access in access_list {
|
||||
let mut access_json = serde_json::json!({
|
||||
"id": access.id,
|
||||
"user_id": access.user_id,
|
||||
"server_inbound_id": access.server_inbound_id,
|
||||
"xray_user_id": access.xray_user_id,
|
||||
"level": access.level,
|
||||
"is_active": access.is_active,
|
||||
});
|
||||
|
||||
// Try to get client config and generate URI
|
||||
if access.is_active {
|
||||
if let Ok(Some(config_data)) = inbound_users_repo
|
||||
.get_client_config_data(user_id, access.server_inbound_id)
|
||||
.await {
|
||||
|
||||
if let Ok(client_config) = uri_service.generate_client_config(user_id, &config_data) {
|
||||
access_json["uri"] = serde_json::Value::String(client_config.uri);
|
||||
access_json["protocol"] = serde_json::Value::String(client_config.protocol);
|
||||
access_json["server_name"] = serde_json::Value::String(client_config.server_name);
|
||||
access_json["inbound_tag"] = serde_json::Value::String(client_config.inbound_tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.push(access_json);
|
||||
}
|
||||
} else {
|
||||
response = access_list
|
||||
.into_iter()
|
||||
.map(|access| serde_json::json!({
|
||||
"id": access.id,
|
||||
"user_id": access.user_id,
|
||||
"server_inbound_id": access.server_inbound_id,
|
||||
"xray_user_id": access.xray_user_id,
|
||||
"level": access.level,
|
||||
"is_active": access.is_active,
|
||||
}))
|
||||
.collect();
|
||||
}
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
@@ -25,4 +25,6 @@ fn user_routes() -> Router<AppState> {
|
||||
.put(handlers::update_user)
|
||||
.delete(handlers::delete_user))
|
||||
.route("/:id/access", get(handlers::get_user_access))
|
||||
.route("/:user_id/configs", get(handlers::get_user_configs))
|
||||
.route("/:user_id/access/:inbound_id/config", get(handlers::get_user_inbound_config))
|
||||
}
|
@@ -21,6 +21,9 @@ pub fn server_routes() -> Router<AppState> {
|
||||
// User management for inbounds
|
||||
.route("/:server_id/inbounds/:inbound_id/users", post(handlers::add_user_to_inbound))
|
||||
.route("/:server_id/inbounds/:inbound_id/users/:email", axum::routing::delete(handlers::remove_user_from_inbound))
|
||||
|
||||
// Client configurations for inbounds
|
||||
.route("/:server_id/inbounds/:inbound_id/configs", get(handlers::get_inbound_configs))
|
||||
}
|
||||
|
||||
pub fn certificate_routes() -> Router<AppState> {
|
||||
|
Reference in New Issue
Block a user