From c6892b1a73f1c154b61dd3e5277d3a28f2576cea Mon Sep 17 00:00:00 2001 From: "AB from home.homenet" Date: Sun, 19 Oct 2025 15:23:17 +0300 Subject: [PATCH] Made subs works --- Cargo.lock | 7 +++ Cargo.toml | 2 +- src/config/args.rs | 3 + src/config/mod.rs | 8 +++ src/main.rs | 4 +- src/services/telegram/bot.rs | 4 +- src/services/telegram/handlers/mod.rs | 6 +- src/services/telegram/handlers/user.rs | 4 +- src/services/telegram/mod.rs | 8 ++- src/web/handlers/subscription.rs | 81 +++++++++++++++----------- src/web/mod.rs | 9 ++- 11 files changed, 87 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b3d1a9..1af6c34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3397,6 +3397,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -4500,6 +4506,7 @@ dependencies = [ "getrandom 0.3.3", "js-sys", "serde", + "sha1_smol", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 1f2476b..d858c85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ sea-orm = { version = "1.0", features = ["sqlx-postgres", "runtime-tokio-rustls" sea-orm-migration = "1.0" # Additional utilities -uuid = { version = "1.0", features = ["v4", "serde"] } +uuid = { version = "1.0", features = ["v4", "v5", "serde"] } chrono = { version = "0.4", features = ["serde"] } async-trait = "0.1" log = "0.4" diff --git a/src/config/args.rs b/src/config/args.rs index 9f8e31b..997f819 100644 --- a/src/config/args.rs +++ b/src/config/args.rs @@ -26,6 +26,9 @@ pub struct Args { #[arg(long, default_value = "info")] pub log_level: Option, + /// Base URL for the application (used in subscription links and Telegram messages) + #[arg(long, env = "BASE_URL")] + pub base_url: Option, /// Validate configuration and exit #[arg(long)] diff --git a/src/config/mod.rs b/src/config/mod.rs index 7e3d8a0..523c084 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -37,6 +37,10 @@ pub struct WebConfig { pub jwt_secret: String, #[validate(range(min = 3600))] pub jwt_expiry: u64, + /// Base URL for the application (used in subscription links and Telegram messages) + /// Example: "https://vpn.hexor.cy" + #[validate(url)] + pub base_url: String, } #[derive(Debug, Clone, Serialize, Deserialize, Validate)] @@ -84,6 +88,7 @@ impl Default for WebConfig { cors_origins: vec!["http://localhost:3000".to_string()], jwt_secret: "your-secret-key-change-in-production".to_string(), jwt_expiry: 86400, // 24 hours + base_url: "http://localhost:8080".to_string(), } } } @@ -174,6 +179,9 @@ impl AppConfig { if let Some(log_level) = &args.log_level { builder = builder.set_override("logging.level", log_level.as_str())?; } + if let Some(base_url) = &args.base_url { + builder = builder.set_override("web.base_url", base_url.as_str())?; + } let config: AppConfig = builder.build()?.try_deserialize()?; diff --git a/src/main.rs b/src/main.rs index 61bfd17..9720ece 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,7 +91,7 @@ async fn main() -> Result<()> { TaskScheduler::start_event_handler(db.clone(), event_receiver).await; // Initialize Telegram service if needed - let telegram_service = Arc::new(TelegramService::new(db.clone())); + let telegram_service = Arc::new(TelegramService::new(db.clone(), config.clone())); if let Err(e) = telegram_service.initialize().await { tracing::warn!("Failed to initialize Telegram service: {}", e); } @@ -99,7 +99,7 @@ async fn main() -> Result<()> { // Start web server with task scheduler tokio::select! { - result = web::start_server(db, config.web.clone(), Some(telegram_service.clone())) => { + result = web::start_server(db, config.clone(), Some(telegram_service.clone())) => { match result { Err(e) => tracing::error!("Web server error: {}", e), _ => {} diff --git a/src/services/telegram/bot.rs b/src/services/telegram/bot.rs index 1d9c90a..6e07b58 100644 --- a/src/services/telegram/bot.rs +++ b/src/services/telegram/bot.rs @@ -2,12 +2,14 @@ use teloxide::{Bot, prelude::*}; use tokio::sync::oneshot; use crate::database::DatabaseManager; +use crate::config::AppConfig; use super::handlers::{self, Command}; /// Run the bot polling loop pub async fn run_polling( bot: Bot, db: DatabaseManager, + app_config: AppConfig, mut shutdown_rx: oneshot::Receiver<()>, ) { tracing::info!("Starting Telegram bot polling..."); @@ -30,7 +32,7 @@ pub async fn run_polling( ); let mut dispatcher = Dispatcher::builder(bot.clone(), handler) - .dependencies(dptree::deps![db]) + .dependencies(dptree::deps![db, app_config]) .enable_ctrlc_handler() .build(); diff --git a/src/services/telegram/handlers/mod.rs b/src/services/telegram/handlers/mod.rs index a558e07..a14a93b 100644 --- a/src/services/telegram/handlers/mod.rs +++ b/src/services/telegram/handlers/mod.rs @@ -9,6 +9,7 @@ pub use types::*; use teloxide::{prelude::*, types::CallbackQuery}; use crate::database::DatabaseManager; +use crate::config::AppConfig; /// Handle bot commands pub async fn handle_command( @@ -16,6 +17,7 @@ pub async fn handle_command( msg: Message, cmd: Command, db: DatabaseManager, + app_config: AppConfig, ) -> Result<(), Box> { let chat_id = msg.chat.id; let from = &msg.from.ok_or("No user info")?; @@ -78,6 +80,7 @@ pub async fn handle_message( bot: Bot, msg: Message, db: DatabaseManager, + _app_config: AppConfig, ) -> Result<(), Box> { let chat_id = msg.chat.id; let from = msg.from.as_ref().ok_or("No user info")?; @@ -95,6 +98,7 @@ pub async fn handle_callback_query( bot: Bot, q: CallbackQuery, db: DatabaseManager, + app_config: AppConfig, ) -> Result<(), Box> { if let Some(data) = &q.data { if let Some(callback_data) = CallbackData::parse(data) { @@ -106,7 +110,7 @@ pub async fn handle_callback_query( handle_my_configs_edit(bot, &q, &db).await?; } CallbackData::SubscriptionLink => { - handle_subscription_link(bot, &q, &db).await?; + handle_subscription_link(bot, &q, &db, &app_config).await?; } CallbackData::Support => { handle_support(bot, &q).await?; diff --git a/src/services/telegram/handlers/user.rs b/src/services/telegram/handlers/user.rs index 540e9aa..024b73d 100644 --- a/src/services/telegram/handlers/user.rs +++ b/src/services/telegram/handlers/user.rs @@ -673,6 +673,7 @@ pub async fn handle_subscription_link( bot: Bot, q: &CallbackQuery, db: &DatabaseManager, + app_config: &crate::config::AppConfig, ) -> Result<(), Box> { let from = q.from.clone(); let telegram_id = from.id.0 as i64; @@ -683,8 +684,7 @@ pub async fn handle_subscription_link( let user_repo = UserRepository::new(db.connection()); if let Ok(Some(user)) = user_repo.get_by_telegram_id(telegram_id).await { // Generate subscription URL - let base_url = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); - let subscription_url = format!("{}/sub/{}", base_url, user.id); + let subscription_url = format!("{}/sub/{}", app_config.web.base_url, user.id); let message = match lang { Language::Russian => { diff --git a/src/services/telegram/mod.rs b/src/services/telegram/mod.rs index 5f941a5..579d28d 100644 --- a/src/services/telegram/mod.rs +++ b/src/services/telegram/mod.rs @@ -7,6 +7,7 @@ use uuid::Uuid; use crate::database::DatabaseManager; use crate::database::repository::TelegramConfigRepository; use crate::database::entities::telegram_config::Model as TelegramConfig; +use crate::config::AppConfig; pub mod bot; pub mod handlers; @@ -18,6 +19,7 @@ pub use error::TelegramError; /// Main Telegram service that manages the bot lifecycle pub struct TelegramService { db: DatabaseManager, + app_config: AppConfig, bot: Arc>>, config: Arc>>, shutdown_signal: Arc>>>, @@ -25,9 +27,10 @@ pub struct TelegramService { impl TelegramService { /// Create a new Telegram service - pub fn new(db: DatabaseManager) -> Self { + pub fn new(db: DatabaseManager, app_config: AppConfig) -> Self { Self { db, + app_config, bot: Arc::new(RwLock::new(None)), config: Arc::new(RwLock::new(None)), shutdown_signal: Arc::new(RwLock::new(None)), @@ -83,10 +86,11 @@ impl TelegramService { *self.shutdown_signal.write().await = Some(tx); let db = self.db.clone(); + let app_config = self.app_config.clone(); // Spawn polling task tokio::spawn(async move { - bot::run_polling(bot, db, rx).await; + bot::run_polling(bot, db, app_config, rx).await; }); Ok(()) diff --git a/src/web/handlers/subscription.rs b/src/web/handlers/subscription.rs index bff2186..606ea14 100644 --- a/src/web/handlers/subscription.rs +++ b/src/web/handlers/subscription.rs @@ -1,6 +1,6 @@ use axum::{ extract::{Path, State}, - http::StatusCode, + http::{StatusCode, HeaderMap, HeaderValue}, response::{IntoResponse, Response}, }; use base64::{Engine, engine::general_purpose}; @@ -14,13 +14,13 @@ use crate::{ /// Get subscription links for a user by their ID /// Returns all configuration links for the user, one per line +/// Based on Django implementation for compatibility pub async fn get_user_subscription( State(state): State, Path(user_id): Path, ) -> Result { let user_repo = UserRepository::new(state.db.connection()); let inbound_users_repo = InboundUsersRepository::new(state.db.connection().clone()); - let uri_generator = UriGeneratorService::new(); // Check if user exists let user = match user_repo.get_by_id(user_id).await { @@ -29,13 +29,16 @@ pub async fn get_user_subscription( Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), }; - // Get all inbound accesses for this user - let user_inbounds = match inbound_users_repo.find_by_user_id(user_id).await { - Ok(inbounds) => inbounds, - Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + // Get all client config data for the user (this gets all active inbound accesses) + let all_configs = match inbound_users_repo.get_all_client_configs_for_user(user_id).await { + Ok(configs) => configs, + Err(e) => { + tracing::error!("Failed to get client configs for user {}: {}", user_id, e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } }; - if user_inbounds.is_empty() { + if all_configs.is_empty() { let response_text = "# No configurations available\n".to_string(); let response_base64 = general_purpose::STANDARD.encode(response_text); return Ok(( @@ -47,28 +50,19 @@ pub async fn get_user_subscription( let mut config_lines = Vec::new(); - // Generate URI for each inbound access - for user_inbound in user_inbounds { - // Get client configuration data using the existing repository method - match inbound_users_repo.get_client_config_data(user_id, user_inbound.server_inbound_id).await { - Ok(Some(config_data)) => { - // Generate URI - match uri_generator.generate_client_config(user_id, &config_data) { - Ok(client_config) => { - config_lines.push(client_config.uri); - } - Err(e) => { - tracing::warn!("Failed to generate URI for user {} inbound {}: {}", user_id, user_inbound.server_inbound_id, e); - continue; - } - } - } - Ok(None) => { - tracing::debug!("No config data found for user {} inbound {}", user_id, user_inbound.server_inbound_id); - continue; + // Generate connection strings for each config using existing UriGeneratorService + let uri_generator = UriGeneratorService::new(); + + for config_data in all_configs { + match uri_generator.generate_client_config(user_id, &config_data) { + Ok(client_config) => { + config_lines.push(client_config.uri); + tracing::debug!("Generated {} config for user {}: {}", + config_data.protocol.to_uppercase(), user.name, config_data.template_name); } Err(e) => { - tracing::warn!("Failed to get config data for user {} inbound {}: {}", user_id, user_inbound.server_inbound_id, e); + tracing::warn!("Failed to generate connection string for user {} template {}: {}", + user.name, config_data.template_name, e); continue; } } @@ -84,15 +78,32 @@ pub async fn get_user_subscription( ).into_response()); } - // Join all URIs with newlines + // Join all URIs with newlines (like Django implementation) let response_text = config_lines.join("\n") + "\n"; - // Encode the entire response in base64 + // Encode the entire response in base64 (like Django implementation) let response_base64 = general_purpose::STANDARD.encode(response_text); - Ok(( - StatusCode::OK, - [("content-type", "text/plain; charset=utf-8")], - response_base64, - ).into_response()) -} \ No newline at end of file + // Build response with subscription headers (like Django) + let mut headers = HeaderMap::new(); + + // Add headers required by VPN clients + headers.insert("content-type", HeaderValue::from_static("text/plain; charset=utf-8")); + headers.insert("content-disposition", HeaderValue::from_str(&format!("attachment; filename=\"{}\"", user.name)).unwrap()); + headers.insert("cache-control", HeaderValue::from_static("no-cache")); + + // Profile information + let profile_title = general_purpose::STANDARD.encode("OutFleet VPN"); + headers.insert("profile-title", HeaderValue::from_str(&format!("base64:{}", profile_title)).unwrap()); + headers.insert("profile-update-interval", HeaderValue::from_static("24")); + headers.insert("profile-web-page-url", HeaderValue::from_str(&format!("{}/u/{}", state.config.web.base_url, user_id)).unwrap()); + headers.insert("support-url", HeaderValue::from_str(&format!("{}/admin/", state.config.web.base_url)).unwrap()); + + // Subscription info (unlimited service) + let expire_timestamp = chrono::Utc::now().timestamp() + (365 * 24 * 60 * 60); // 1 year from now + headers.insert("subscription-userinfo", + HeaderValue::from_str(&format!("upload=0; download=0; total=1099511627776; expire={}", expire_timestamp)).unwrap()); + + Ok((StatusCode::OK, headers, response_base64).into_response()) +} + diff --git a/src/web/mod.rs b/src/web/mod.rs index 8ff0ae2..f153e5a 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -14,7 +14,7 @@ use tower_http::services::ServeDir; use tracing::info; use std::sync::Arc; -use crate::config::WebConfig; +use crate::config::{WebConfig, AppConfig}; use crate::database::DatabaseManager; use crate::services::{XrayService, TelegramService}; @@ -27,14 +27,13 @@ use routes::api_routes; #[derive(Clone)] pub struct AppState { pub db: DatabaseManager, - #[allow(dead_code)] - pub config: WebConfig, + pub config: AppConfig, pub xray_service: XrayService, pub telegram_service: Option>, } /// Start the web server -pub async fn start_server(db: DatabaseManager, config: WebConfig, telegram_service: Option>) -> Result<()> { +pub async fn start_server(db: DatabaseManager, config: AppConfig, telegram_service: Option>) -> Result<()> { let xray_service = XrayService::new(); let app_state = AppState { @@ -55,7 +54,7 @@ pub async fn start_server(db: DatabaseManager, config: WebConfig, telegram_servi .layer(CorsLayer::permissive()) .with_state(app_state); - let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?; + let addr: SocketAddr = format!("{}:{}", config.web.host, config.web.port).parse()?; info!("Starting web server on {}", addr); let listener = TcpListener::bind(&addr).await?;