diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af3f8241..d8bd793e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: uses: dtolnay/rust-toolchain@nightly - uses: Swatinem/rust-cache@v2 - name: Build Auth - run: cargo build --release --bin auth --no-default-features --features email + run: cargo build --release --bin auth --no-default-features - name: Upload Artifact uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8625eeff..a5128ded 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,10 +14,13 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 + - name: Install Rust uses: dtolnay/rust-toolchain@nightly + - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + - name: Test Lib run: cargo test --lib @@ -26,23 +29,39 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - target: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu] + target: [x86_64-unknown-linux-musl, aarch64-unknown-linux-musl] + steps: - uses: actions/checkout@v4 + - name: Install Rust uses: dtolnay/rust-toolchain@nightly with: targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.target }} + - name: Install Build Tools run: | - sudo apt-get update && sudo apt-get install -y protobuf-compiler + sudo apt-get update + sudo apt-get install -y protobuf-compiler musl-tools pip3 install ziglang cargo install cargo-zigbuild - - name: Build Node - run: cargo zigbuild --release --bin node --target ${{ matrix.target }} --features xray,wireguard + + - name: Build Node (static) + run: | + RUSTFLAGS="-C target-feature=+crt-static" \ + cargo zigbuild --release \ + --bin node \ + --target ${{ matrix.target }} \ + --features xray,wireguard + + - name: Verify static binary + run: | + ldd target/${{ matrix.target }}/release/node || true + - name: Upload Artifact uses: actions/upload-artifact@v4 with: @@ -52,36 +71,81 @@ jobs: build-api: needs: [tests] runs-on: ubuntu-22.04 + steps: - uses: actions/checkout@v4 + - name: Install Rust uses: dtolnay/rust-toolchain@nightly - - name: Build API - run: cargo build --release --bin api --no-default-features + with: + targets: x86_64-unknown-linux-musl + + - name: Install Build Tools + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler musl-tools + pip3 install ziglang + cargo install cargo-zigbuild + + - name: Build API (static) + run: | + RUSTFLAGS="-C target-feature=+crt-static" \ + cargo zigbuild --release \ + --bin api \ + --no-default-features \ + --target x86_64-unknown-linux-musl + + - name: Verify static binary + run: | + ldd target/x86_64-unknown-linux-musl/release/api || true + - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: fcore-api - path: target/release/api + path: target/x86_64-unknown-linux-musl/release/api build-auth: needs: [tests] runs-on: ubuntu-22.04 + steps: - uses: actions/checkout@v4 + - name: Install Rust uses: dtolnay/rust-toolchain@nightly - - name: Build Auth - run: cargo build --release --bin auth --no-default-features --features email + with: + targets: x86_64-unknown-linux-musl + + - name: Install Build Tools + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler musl-tools + pip3 install ziglang + cargo install cargo-zigbuild + + - name: Build Auth (static) + run: | + RUSTFLAGS="-C target-feature=+crt-static" \ + cargo zigbuild --release \ + --bin auth \ + --no-default-features \ + --target x86_64-unknown-linux-musl + + - name: Verify static binary + run: | + ldd target/x86_64-unknown-linux-musl/release/auth || true + - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: fcore-auth - path: target/release/auth + path: target/x86_64-unknown-linux-musl/release/auth collect-binaries: runs-on: ubuntu-latest needs: [build-node, build-api, build-auth] + steps: - name: Download all artifacts uses: actions/download-artifact@v4 @@ -89,6 +153,7 @@ jobs: path: collected pattern: fcore-* merge-multiple: false + - name: Upload combined bundle uses: actions/upload-artifact@v4 with: diff --git a/Cargo.lock b/Cargo.lock index d068a9b4..362b4ba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -656,7 +656,7 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fcore" -version = "0.5.0" +version = "0.5.1-dev" dependencies = [ "async-trait", "base32", diff --git a/Cargo.toml b/Cargo.toml index ace0f2bc..7961101d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fcore" -version = "0.5.0" +version = "0.5.1-dev" edition = "2021" build = "build.rs" @@ -23,7 +23,7 @@ console-subscriber = {version = "0.4", optional = true} dashmap = "6.1.0" defguard_wireguard_rs = {version = "0.7.2", features=["serde"], optional = true} futures = "0.3" -hex = { version = "0.4", optional = true} +hex = { version = "0.4"} hmac = "0.12" openssl = { version = "0.10", features = ["vendored"] } prost = { version = "0.13", optional = true } @@ -54,7 +54,7 @@ zmq = "0.10" parking_lot = "0.12.5" sha2 = "0.10" data-encoding = "2.5" -lettre = { version = "0.11", features = ["tokio1", "builder", "smtp-transport", "tokio1-native-tls"], optional = true} +lettre = { version = "0.11", features = ["tokio1", "builder", "smtp-transport", "tokio1-native-tls"]} [build-dependencies] tonic-build = {version = "0.12", optional = true} @@ -71,7 +71,6 @@ default = [] debug = ["console-subscriber"] wireguard = ["defguard_wireguard_rs"] xray = ["prost", "prost-derive", "tonic", "tonic-build"] -email = ["lettre", "hex"] [[bin]] name = "node" diff --git a/src/bin/api/config.rs b/src/bin/api/config.rs index 662f4e9b..5d177257 100644 --- a/src/bin/api/config.rs +++ b/src/bin/api/config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use std::net::Ipv4Addr; -use fcore::{IpAddrMask, Result, Settings}; +use fcore::{Env, IpAddrMask, Result, Settings, Tag}; #[derive(Clone, Debug, Deserialize)] pub struct ServiceSettings { @@ -9,6 +9,7 @@ pub struct ServiceSettings { pub pg: PostgresConfig, pub metrics: MetricsRxConfig, pub tasks: TasksConfig, + pub smtp: SmtpConfig, } impl Settings for ServiceSettings { @@ -49,6 +50,9 @@ pub struct ServiceConfig { #[serde(default = "default_log_level")] pub log_level: String, pub updates_endpoint_zmq: String, + pub enabled_envs: Vec, + pub enabled_tags: Vec, + pub trial_days: i64, } #[derive(Clone, Debug, Deserialize, Default)] @@ -73,3 +77,22 @@ pub struct MetricsRxConfig { pub max_points: usize, pub retention_seconds: i64, } + +fn default_company_website() -> String { + "http://localhost:8080".to_string() +} +#[derive(Clone, Debug, Deserialize, Default)] +pub struct SmtpConfig { + pub server: String, + pub username: String, + pub password: String, + pub port: u16, + pub from: String, + pub title: String, + pub company_name: String, + pub support: String, + pub email_file: String, + pub email_sign_token: Vec, + #[serde(default = "default_company_website")] + pub company_website: String, +} diff --git a/src/bin/auth/email.rs b/src/bin/api/email.rs similarity index 100% rename from src/bin/auth/email.rs rename to src/bin/api/email.rs diff --git a/src/bin/api/http/filters.rs b/src/bin/api/http/filters.rs index 60c2cf80..a0377193 100644 --- a/src/bin/api/http/filters.rs +++ b/src/bin/api/http/filters.rs @@ -1,13 +1,16 @@ use std::sync::Arc; use warp::Filter; +use super::super::email::EmailStore; +use super::super::sync::MemSync; + +use fcore::{Env, Tag}; + use fcore::{ Connection, ConnectionApiOperations, ConnectionBaseOperations, IpAddrMask, MetricStorage, NodeStorageOperations, SubscriptionOperations, }; -use super::super::sync::MemSync; - /// Provides application state filter pub fn with_sync( mem_sync: MemSync, @@ -31,6 +34,16 @@ pub fn with_param_vec( ) -> impl Filter,), Error = std::convert::Infallible> + Clone { warp::any().map(move || param.clone()) } +pub fn with_param_envs( + param: Vec, +) -> impl Filter,), Error = std::convert::Infallible> + Clone { + warp::any().map(move || param.clone()) +} +pub fn with_param_tags( + param: Vec, +) -> impl Filter,), Error = std::convert::Infallible> + Clone { + warp::any().map(move || param.clone()) +} pub fn with_param_ipaddrmask( param: IpAddrMask, @@ -49,3 +62,9 @@ pub fn with_metrics( ) -> impl Filter,), Error = std::convert::Infallible> + Clone { warp::any().map(move || metrics.clone()) } + +pub fn with_email_store( + email_store: EmailStore, +) -> impl Filter + Clone { + warp::any().map(move || email_store.clone()) +} diff --git a/src/bin/api/http/handlers/mod.rs b/src/bin/api/http/handlers/mod.rs index 647bbc85..3032d671 100644 --- a/src/bin/api/http/handlers/mod.rs +++ b/src/bin/api/http/handlers/mod.rs @@ -3,6 +3,7 @@ pub mod key; pub mod metrics; pub mod node; pub mod subscription; +pub mod trial; use warp::http::StatusCode; diff --git a/src/bin/api/http/handlers/trial.rs b/src/bin/api/http/handlers/trial.rs new file mode 100644 index 00000000..0f377c7a --- /dev/null +++ b/src/bin/api/http/handlers/trial.rs @@ -0,0 +1,194 @@ +use chrono::{DateTime, Utc}; +use std::net::{IpAddr, Ipv4Addr}; + +use fcore::{ + http::helpers as http, http::response::Instance, utils, utils::get_uuid_last_octet_simple, + Connection, ConnectionApiOperations, ConnectionBaseOperations, ConnectionStorageApiOperations, + Env, IpAddrMask, NodeStorageOperations, Proto, Status, Subscription, SubscriptionOperations, + SubscriptionStorageOperations, Tag, Topic, WgKeys, WgParam, +}; + +use super::super::super::email::EmailStore; +use super::super::super::sync::{tasks::SyncOp, MemSync}; +use super::super::request; + +pub async fn post_trial_handler( + req: request::Trial, + memory: MemSync, + store: EmailStore, + wireguard_network: IpAddrMask, + system_refer_codes: Vec, + envs: Vec, + protos: Vec, + trial_days: i64, + bonus: i64, +) -> Result +where + N: NodeStorageOperations + Sync + Send + Clone + 'static, + C: ConnectionApiOperations + + ConnectionBaseOperations + + Sync + + Send + + Clone + + 'static + + From + + PartialEq, + Connection: From, + S: SubscriptionOperations + Send + Sync + Clone + 'static + PartialEq + From, +{ + req.validate()?; + + if let Some(ref user) = req.user { + if store.check_email_hmac(user).await { + return Ok(http::bad_request("Trial already requested")); + } + } + + if let Some(ref email) = req.email { + if store.check_email_hmac(email).await { + return Ok(http::bad_request("Trial already requested")); + } + } + + let mut bonus_days = 0; + let ref_by = req.referred_by.clone().unwrap_or_else(|| "WEB".to_string()); + let sub_id = uuid::Uuid::new_v4(); + + let sub_id_to_update = if let Some(ref_by_code) = req.referred_by.clone() { + let mem = memory.memory.read().await; + + let is_system_code = system_refer_codes.iter().any(|c| c == &ref_by_code); + let is_user_referral = !is_system_code; + + if let Some(sub) = mem.subscriptions.find_by_refer_code(&ref_by_code) { + if is_user_referral { + bonus_days = bonus; + } + Some(sub.id()) + } else { + return Ok(http::bad_request("Refer code not found")); + } + } else { + None + }; + + if let Some(id) = sub_id_to_update { + if let Err(e) = SyncOp::add_days(&memory, &id, bonus_days).await { + return Ok(http::internal_error(&format!( + "Couldn't add bonus days: {}", + e + ))); + } + } + + let now = Utc::now(); + + let expires_at: Option> = Some(now + chrono::Duration::days(trial_days.into())); + + let ref_code = get_uuid_last_octet_simple(&sub_id); + let sub = Subscription::new(sub_id, req.referred_by, ref_code, expires_at); + + let new_sub_id = match SyncOp::add_sub(&memory, sub.clone()).await { + Ok(Status::Ok(id)) => id, + _ => return Ok(http::internal_error("Failed to add sub")), + }; + + for env in envs { + for p in &protos { + let proto = match p { + Tag::Wireguard => { + let mem = memory.memory.read().await; + + let last_ip: Option = mem + .connections + .get_last_wg_addr() + .and_then(|mask| mask.as_ipv4()); + + let next = match last_ip { + Some(ip) => IpAddrMask::increment_ipv4(ip), + None => wireguard_network.first_peer_ip(), + }; + + let next = match next { + Some(ip) => ip, + None => return Ok(http::internal_error("Failed to allocate IP")), + }; + + if !wireguard_network.contains_ipv4(next) { + return Ok(http::internal_error("IP out of range")); + } + + Proto::Wireguard { + param: WgParam { + keys: WgKeys::default(), + address: IpAddrMask { + address: IpAddr::V4(next), + cidr: 32, + }, + }, + } + } + Tag::Shadowsocks => { + let password = utils::generate_random_password(15); + Proto::Shadowsocks { password } + } + Tag::VlessTcpReality + | Tag::VlessGrpcReality + | Tag::VlessXhttpReality + | Tag::Vmess => Proto::Xray(p.clone()), + Tag::Hysteria2 => { + let token = uuid::Uuid::new_v4(); + Proto::Hysteria2 { token } + } + Tag::Mtproto => { + let secret = utils::generate_random_password(15); + Proto::Mtproto { secret } + } + }; + + let conn: Connection = Connection::new(&env, Some(new_sub_id), proto, None); + let conn_id = uuid::Uuid::new_v4(); + let msg = conn.as_create_message(&conn_id); + let messages = vec![msg]; + + match SyncOp::add_conn(&memory, &conn_id, conn.clone()).await { + Ok(Status::Ok(_)) => { + let bytes = match rkyv::to_bytes::<_, 1024>(&messages) { + Ok(b) => b, + Err(e) => { + return Ok(http::internal_error(&format!("Serialization error: {}", e))) + } + }; + + let topic = if conn.get_token().is_some() { + Some(Topic::Auth) + } else if conn.get_proto().is_mtproto() { + None + } else { + Some(conn.get_env().into()) + }; + + if let Some(topic) = topic { + let _ = memory.publisher.send_binary(&topic, bytes.as_ref()).await; + } + } + _ => continue, + } + } + } + + if let Some(email) = req.user { + let _ = store.save_trial_hmac(&email, &sub.id, &now, &ref_by).await; + } + + if let Some(email) = req.email { + let _ = store.save_trial_hmac(&email, &sub.id, &now, &ref_by).await; + let _ = store.send_email_background(email, sub.id).await; + } + + Ok(http::success_response( + "Trial activated. Check email".into(), + Some(sub.id), + Instance::None, + )) +} diff --git a/src/bin/api/http/request.rs b/src/bin/api/http/request.rs index 2b9e4e16..23c795f8 100644 --- a/src/bin/api/http/request.rs +++ b/src/bin/api/http/request.rs @@ -173,3 +173,19 @@ impl ConnectionInfoRequest { Ok(()) } } + +#[derive(Debug, Deserialize)] +pub struct Trial { + pub user: Option, + pub email: Option, + pub referred_by: Option, +} + +impl Trial { + pub fn validate(&self) -> Result<(), Error> { + if self.user.is_some() && self.email.is_some() { + return Err(Error::Custom("Only one param is allowed".to_string())); + } + Ok(()) + } +} diff --git a/src/bin/api/http/routes.rs b/src/bin/api/http/routes.rs index c1f5405f..661c2110 100644 --- a/src/bin/api/http/routes.rs +++ b/src/bin/api/http/routes.rs @@ -11,7 +11,9 @@ use fcore::{ use super::{ super::{config::ServiceConfig, service::Service}, filters::*, - handlers::{connection::*, healthcheck_handler, key::*, metrics::*, node::*, subscription::*}, + handlers::{ + connection::*, healthcheck_handler, key::*, metrics::*, node::*, subscription::*, trial::*, + }, param::*, rejection, request::*, @@ -102,7 +104,7 @@ where .and(warp::body::json()) .and(with_sync(self.sync.clone())) .and(with_i64(params.bonus_days)) - .and(with_param_vec_string(params.system_refer_codes)) + .and(with_param_vec_string(params.system_refer_codes.clone())) .and_then(post_subscription_handler); let put_subscription_route = warp::put() @@ -148,7 +150,7 @@ where .and(auth.clone()) .and(warp::body::json()) .and(with_sync(self.sync.clone())) - .and(with_param_ipaddrmask(params.wireguard_network)) + .and(with_param_ipaddrmask(params.wireguard_network.clone())) .and_then(create_connection_handler); let delete_connection_route = warp::delete() @@ -164,7 +166,6 @@ where .and(warp::path("key")) .and(warp::path("validate")) .and(warp::path::end()) - .and(auth.clone()) .and(warp::query::()) .and(with_sync(self.sync.clone())) .and(with_param_vec(params.key_sign_token.clone())) @@ -183,11 +184,25 @@ where .and(warp::path("key")) .and(warp::path("activate")) .and(warp::path::end()) - .and(auth.clone()) .and(warp::body::json()) .and(with_sync(self.sync.clone())) .and_then(post_activate_key_handler); + //Trial + let post_trial_route = warp::post() + .and(warp::path("trial")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(with_sync(self.sync.clone())) + .and(with_email_store(self.email_store.clone())) + .and(with_param_ipaddrmask(params.wireguard_network)) + .and(with_param_vec_string(params.system_refer_codes)) + .and(with_param_envs(params.enabled_envs)) + .and(with_param_tags(params.enabled_tags)) + .and(with_i64(params.trial_days)) + .and(with_i64(params.bonus_days)) + .and_then(post_trial_handler); + use uuid::Uuid; let ws_all_metrics_route = warp::path!("metrics" / "all" / Uuid / u64 / "ws") .and(warp::ws()) @@ -237,6 +252,8 @@ where .or(get_key_validation_route) .or(post_key_route) .or(post_activate_key_route) + //Trial + .or(post_trial_route) // Metrics .or(ws_all_metrics_route) .or(ws_aggregate_route) diff --git a/src/bin/api/main.rs b/src/bin/api/main.rs index 63d6c585..051f5554 100644 --- a/src/bin/api/main.rs +++ b/src/bin/api/main.rs @@ -20,6 +20,7 @@ use crate::{ }; mod config; +mod email; mod http; mod metrics; mod postgres; @@ -64,10 +65,14 @@ async fn main() -> Result<()> { settings.metrics.max_points, settings.metrics.retention_seconds, )); + let email_store = email::EmailStore::new(settings.smtp.clone()); + email_store.load_trials().await?; + let api_service = Arc::new(Service::new( mem_sync.clone(), settings.clone(), metric_storage, + email_store.clone(), )); measure_time(api_service.get_state_from_db(), "Init PostgreSQL DB").await?; diff --git a/src/bin/api/service.rs b/src/bin/api/service.rs index a10c52bb..3dd87f98 100644 --- a/src/bin/api/service.rs +++ b/src/bin/api/service.rs @@ -7,6 +7,8 @@ use fcore::{ Node, NodeStorageOperations, Subscription, SubscriptionOperations, Subscriptions, }; +use super::email::EmailStore; + use super::{config::ServiceSettings, sync::MemSync}; pub type State = Cache>, Connection, Subscription>; @@ -26,6 +28,7 @@ where pub sync: MemSync, pub settings: ServiceSettings, pub metrics: Arc, + pub email_store: EmailStore, } impl Service @@ -44,11 +47,13 @@ where sync: MemSync, settings: ServiceSettings, metrics: Arc, + email_store: EmailStore, ) -> Self { Self { sync, settings, metrics, + email_store: email_store, } } } diff --git a/src/bin/auth/config.rs b/src/bin/auth/config.rs index 35ebca2b..d895123d 100644 --- a/src/bin/auth/config.rs +++ b/src/bin/auth/config.rs @@ -1,15 +1,13 @@ use serde::Deserialize; use std::net::Ipv4Addr; -use fcore::{ApiAccessConfig, Env, MetricsTxConfig, NodeConfigRaw, Result, Settings, Tag}; +use fcore::{ApiAccessConfig, MetricsTxConfig, NodeConfigRaw, Result, Settings}; #[derive(Clone, Debug, Deserialize)] pub struct ServiceSettings { pub service: ServiceConfig, pub node: NodeConfigRaw, pub api: ApiAccessConfig, - #[cfg(feature = "email")] - pub smtp: SmtpConfig, pub metrics: MetricsTxConfig, } @@ -31,11 +29,6 @@ fn default_cors_origin() -> String { "http://localhost:8080".to_string() } -#[cfg(feature = "email")] -fn default_company_website() -> String { - "http://localhost:8080".to_string() -} - #[derive(Clone, Debug, Deserialize)] pub struct ServiceConfig { pub log_level: String, @@ -48,24 +41,4 @@ pub struct ServiceConfig { #[serde(default = "default_cors_origin")] pub origin: String, pub updates_endpoint_zmq: String, - - pub enabled_envs: Vec, - pub enabled_protos: Vec, -} - -#[cfg(feature = "email")] -#[derive(Clone, Debug, Deserialize, Default)] -pub struct SmtpConfig { - pub server: String, - pub username: String, - pub password: String, - pub port: u16, - pub from: String, - pub title: String, - pub company_name: String, - pub support: String, - pub email_file: String, - pub email_sign_token: Vec, - #[serde(default = "default_company_website")] - pub company_website: String, } diff --git a/src/bin/auth/filters.rs b/src/bin/auth/filters.rs index 25d0acf5..d6b2a6aa 100644 --- a/src/bin/auth/filters.rs +++ b/src/bin/auth/filters.rs @@ -1,16 +1,6 @@ use fcore::{ApiAccessConfig, Env, Tag}; use warp::Filter; -#[cfg(feature = "email")] -use super::email::EmailStore; - -#[cfg(feature = "email")] -pub fn with_store( - store: EmailStore, -) -> impl Filter + Clone { - warp::any().map(move || store.clone()) -} - pub fn with_api_settings( api: ApiAccessConfig, ) -> impl Filter + Clone { diff --git a/src/bin/auth/handlers.rs b/src/bin/auth/handlers.rs index 6846e926..ee789bdf 100644 --- a/src/bin/auth/handlers.rs +++ b/src/bin/auth/handlers.rs @@ -1,20 +1,10 @@ use std::sync::Arc; use tokio::sync::RwLock; -use fcore::{ - http::{helpers as http, response::Instance}, - ApiAccessConfig, ConnectionBaseOperations, ConnectionStorageBaseOperations, Connections, Env, - Tag, -}; +use fcore::{ConnectionBaseOperations, ConnectionStorageBaseOperations, Connections}; -#[cfg(feature = "email")] -use super::email::EmailStore; -use super::helpers::{activate_key, validate_key}; -use super::helpers::{create_connection, create_subscription, get_subscription}; -use super::http::HttpClient; use super::request; use super::response; -use super::service::DEFAULT_DAYS; pub async fn auth_handler( req: request::Auth, @@ -37,178 +27,3 @@ where })) } } - -pub async fn activate_key_handler( - req: request::ActivateKey, - http: HttpClient, - api: ApiAccessConfig, - - envs: Vec, - protos: Vec, -) -> Result { - let key = match validate_key(&http, &api.endpoint, &api.token, &req.code).await { - Ok(k) => k, - Err(e) => { - tracing::error!("Key code is not valid: {}", e); - return Ok(http::bad_request(&format!("Failed: {}. ", e))); - } - }; - - if key.activated { - return Ok(http::bad_request("Failed: Key is already activated. ")); - } - - let subscription_id = if let Some(subscription_id) = req.subscription_id { - subscription_id - } else { - let referred_by = "WEB"; - let sub = - match create_subscription(&http, &api.endpoint, &api.token, DEFAULT_DAYS, referred_by) - .await - { - Ok(s) => s, - Err(e) => return Ok(http::internal_error(&format!("Creation failed: {}", e))), - }; - - if let Err(e) = setup_connections(&http, &api, &sub.id, envs, protos).await { - tracing::error!("{}", e); - return Ok(http::internal_error("Failed to establish connections.")); - } - - sub.id - }; - - let sub = match get_subscription(&http, &api.endpoint, &api.token, &subscription_id).await { - Ok(s) => s, - Err(e) => return Ok(http::not_found(&format!("Subscription not found: {}", e))), - }; - - let activated_key = - match activate_key(&http, &api.endpoint, &api.token, &req.code, &sub.id).await { - Ok(k) => k, - Err(e) => { - tracing::error!("Code activation failed: {}", e); - return Ok(http::bad_request(&format!("Activation failed: {}", e))); - } - }; - - let mut updated_sub = sub.clone(); - let now = chrono::Utc::now(); - let base_date = if updated_sub.expires > now { - updated_sub.expires - } else { - now - }; - updated_sub.expires = base_date + chrono::Duration::days(activated_key.days as i64); - - Ok(http::success_response( - "Key code activated.".to_string(), - Some(activated_key.id), - Instance::SubscriptionResponse(updated_sub), - )) -} - -#[cfg(feature = "email")] -pub async fn trial_handler( - req: request::Trial, - store: EmailStore, - http: HttpClient, - api: ApiAccessConfig, - envs: Vec, - protos: Vec, -) -> Result { - if store.check_email_hmac(&req.email).await { - return Ok(http::bad_request("Trial already requested")); - } - - let ref_by = req.referred_by.clone().unwrap_or_else(|| "WEB".to_string()); - - let sub = - match create_subscription(&http, &api.endpoint, &api.token, DEFAULT_DAYS, &ref_by).await { - Ok(s) => s, - Err(e) => { - return Ok(http::internal_error(&format!("Subscription failed: {}", e))); - } - }; - - if let Err(e) = setup_connections(&http, &api, &sub.id, envs, protos).await { - tracing::error!("{}", e); - return Ok(http::internal_error( - "Failed to establish trial connections.", - )); - } - - let now = chrono::Utc::now(); - - if let Err(e) = store - .save_trial_hmac(&req.email, &sub.id, &now, &ref_by) - .await - { - tracing::error!("HMAC save error: {}", e); - return Ok(http::internal_error("Failed to record trial.")); - } - - store.send_email_background(req.email.clone(), sub.id).await; - - Ok(http::success_response( - "Trial activated. Check email".into(), - Some(sub.id), - Instance::None, - )) -} - -pub async fn tg_trial_handler( - req: request::TgTrial, - http: HttpClient, - api: ApiAccessConfig, - envs: Vec, - protos: Vec, -) -> Result { - let referred_by = req.referred_by.unwrap_or_else(|| "TG".to_string()); - - // 1. Create subscription - let sub = - match create_subscription(&http, &api.endpoint, &api.token, DEFAULT_DAYS, &referred_by) - .await - { - Ok(s) => s, - Err(e) => return Ok(http::internal_error(&format!("Subscription failed: {}", e))), - }; - - // 2. Create connections - if let Err(e) = setup_connections(&http, &api, &sub.id, envs, protos).await { - tracing::error!("{}", e); - return Ok(http::internal_error( - "Failed to establish trial connections.", - )); - } - - Ok(http::success_response( - "Trial activated".into(), - Some(sub.id), - Instance::Subscription(sub), - )) -} - -async fn setup_connections( - http: &HttpClient, - api: &ApiAccessConfig, - sub_id: &uuid::Uuid, - envs: Vec, - protos: Vec, -) -> Result<(), String> { - let futures = envs.iter().flat_map(|env| { - protos.iter().map(move |proto| { - create_connection(http, env, proto, sub_id, &api.endpoint, &api.token) - }) - }); - - let results = futures::future::join_all(futures).await; - if results.iter().any(|r| r.is_err()) { - return Err(format!( - "Failed to establish connections for sub {}", - sub_id - )); - } - Ok(()) -} diff --git a/src/bin/auth/helpers.rs b/src/bin/auth/helpers.rs index 9e91e506..6913a905 100644 --- a/src/bin/auth/helpers.rs +++ b/src/bin/auth/helpers.rs @@ -12,199 +12,3 @@ fn auth_headers(req: reqwest::RequestBuilder, api_token: &str) -> reqwest::Reque .header("Accept", "application/json") .header("Content-Type", "application/json") } - -pub async fn validate_key( - http: &HttpClient, - api_address: &str, - api_token: &str, - key: &Code, -) -> Result { - let url = format!("{}/key/validate?key={}", api_address, key); - tracing::debug!("URL = {}", url); - - let res = auth_headers(http.get(url), api_token).send().await?; - let status = res.status(); - let text = res.text().await?; - - if status.is_success() { - let parsed: ResponseMessage> = serde_json::from_str(&text)?; - - tracing::debug!("Response validate_key {} {}", parsed.status, parsed.message); - - match parsed.response.instance { - Instance::Key(key) => Ok(key), - _ => Err(Error::Custom("Unexpected instance type".into())), - } - } else { - #[derive(Deserialize)] - struct ErrResp { - message: Option, - } - let err: ErrResp = serde_json::from_str(&text).unwrap_or(ErrResp { message: None }); - Err(Error::Custom( - err.message.unwrap_or_else(|| "Unknown error".to_string()), - )) - } -} - -pub async fn get_subscription( - http: &HttpClient, - api_address: &str, - api_token: &str, - subscription_id: &uuid::Uuid, -) -> Result { - let url = format!("{}/subscription/{}", api_address, subscription_id); - tracing::debug!("URL = {}", url); - - let res = auth_headers(http.get(url), api_token).send().await?; - let status = res.status(); - let text = res.text().await?; - - if status.is_success() { - let parsed: SubscriptionResponse = serde_json::from_str(&text)?; - - tracing::debug!( - "Response for GET subscription/{} {:?}", - subscription_id, - parsed - ); - - Ok(parsed) - } else { - #[derive(Deserialize)] - struct ErrResp { - message: Option, - } - let err: ErrResp = serde_json::from_str(&text).unwrap_or(ErrResp { message: None }); - Err(Error::Custom( - err.message.unwrap_or_else(|| "Unknown error".to_string()), - )) - } -} - -pub async fn activate_key( - http: &HttpClient, - api_address: &str, - api_token: &str, - key: &Code, - sub_id: &uuid::Uuid, -) -> Result { - let url = format!("{}/key/activate?", api_address); - tracing::debug!("URL = {}", url); - - let res = auth_headers( - http.post(url).json(&serde_json::json!({ - "subscription_id": sub_id, - "code": key, - })), - api_token, - ) - .send() - .await?; - let status = res.status(); - let text = res.text().await?; - - if status.is_success() { - let parsed: ResponseMessage> = serde_json::from_str(&text)?; - - tracing::debug!("Response activate_key {} {}", parsed.status, parsed.message); - - match parsed.response.instance { - Instance::Key(key) => Ok(key), - _ => Err(Error::Custom("Unexpected instance type".into())), - } - } else { - #[derive(Deserialize)] - struct ErrResp { - message: Option, - } - let err: ErrResp = serde_json::from_str(&text).unwrap_or(ErrResp { message: None }); - Err(Error::Custom( - err.message.unwrap_or_else(|| "Unknown error".to_string()), - )) - } -} - -pub async fn create_subscription( - http: &HttpClient, - api_address: &str, - api_token: &str, - days: i64, - referred_by: &str, -) -> Result { - let url = format!("{}/subscription", api_address); - tracing::debug!("URL = {}", url); - - let res = auth_headers( - http.post(url).json(&serde_json::json!({ - "days": days, - "referred_by": referred_by.to_string() - })), - api_token, - ) - .send() - .await?; - - let status = res.status(); - let text = res.text().await?; - - if status.is_success() { - let parsed: ResponseMessage> = serde_json::from_str(&text)?; - - match parsed.response.instance { - Instance::Subscription(sub) => Ok(sub), - _ => Err(Error::Custom("Unexpected instance type".into())), - } - } else { - #[derive(Deserialize)] - struct ErrResp { - message: Option, - } - let err: ErrResp = serde_json::from_str(&text).unwrap_or(ErrResp { message: None }); - Err(Error::Custom( - err.message.unwrap_or_else(|| "Unknown error".to_string()), - )) - } -} - -pub async fn create_connection( - http: &HttpClient, - env: &Env, - proto: &Tag, - sub_id: &uuid::Uuid, - api_address: &str, - api_token: &str, -) -> Result { - tracing::debug!("POST /connection {}", env); - - let res = auth_headers( - http.post(format!("{}/connection", api_address)) - .json(&serde_json::json!({ - "env": env, - "proto": proto, - "subscription_id": sub_id - })), - api_token, - ) - .send() - .await?; - - let status = res.status(); - let text = res.text().await?; - - tracing::debug!("Connection resp: {}", text); - - if text.is_empty() { - Error::Custom(format!("empty connection response, status = {}", status)); - } - - let parsed: ResponseMessage> = serde_json::from_str(&text)?; - - tracing::debug!( - "Response create_connection {} {}", - parsed.status, - parsed.message - ); - - Ok(parsed.response.id) -} diff --git a/src/bin/auth/main.rs b/src/bin/auth/main.rs index b696347b..e6f8b497 100644 --- a/src/bin/auth/main.rs +++ b/src/bin/auth/main.rs @@ -1,9 +1,5 @@ mod config; -#[cfg(feature = "email")] -mod email; -mod filters; mod handlers; -mod helpers; mod http; mod metrics; mod request; diff --git a/src/bin/auth/request.rs b/src/bin/auth/request.rs index 2ce47a3c..7f68808f 100644 --- a/src/bin/auth/request.rs +++ b/src/bin/auth/request.rs @@ -1,27 +1,8 @@ -use serde::{Deserialize, Serialize}; +use serde::Deserialize; -use fcore::Code; - -#[cfg(feature = "email")] -#[derive(Debug, Deserialize)] -pub struct Trial { - pub email: String, - pub referred_by: Option, -} - -#[derive(Debug, Deserialize)] -pub struct TgTrial { - pub referred_by: Option, -} #[derive(Deserialize)] pub struct Auth { pub addr: String, pub auth: uuid::Uuid, pub tx: u64, } - -#[derive(Debug, Serialize, Deserialize)] -pub struct ActivateKey { - pub code: Code, - pub subscription_id: Option, -} diff --git a/src/bin/auth/service.rs b/src/bin/auth/service.rs index 2b7a9ae5..677d3146 100644 --- a/src/bin/auth/service.rs +++ b/src/bin/auth/service.rs @@ -10,24 +10,17 @@ use tokio::{ use warp::Filter; use fcore::{ - http::filters as my_filters, ApiAccessConfig, BaseConnection as Connection, - ConnectionBaseOperations, Connections, Env, MetricBuffer, Node, NodeConfig, Publisher, Result, - SnapshotManager, Subscriber, Tag, Topic, + BaseConnection as Connection, ConnectionBaseOperations, Connections, MetricBuffer, Node, + NodeConfig, Publisher, Result, SnapshotManager, Subscriber, Tag, Topic, }; use super::config::ServiceSettings; -#[cfg(feature = "email")] -use super::email::EmailStore; -use super::filters; -#[cfg(feature = "email")] -use super::handlers::trial_handler; -use super::handlers::{activate_key_handler, auth_handler, tg_trial_handler}; -use super::http::{ApiRequests, HttpClient}; + +use super::handlers::auth_handler; +use super::http::ApiRequests; use super::request; use super::tasks::Tasks; -pub const DEFAULT_DAYS: i64 = 1; - pub struct Service where C: ConnectionBaseOperations + Send + Sync + Clone + 'static, @@ -36,15 +29,9 @@ where pub metrics: Arc, pub node: Node, pub subscriber: Subscriber, - #[cfg(feature = "email")] - pub email_store: EmailStore, - pub http_client: HttpClient, - pub api: ApiAccessConfig, pub listen: Ipv4Addr, pub port: u16, pub origin: String, - pub envs: Vec, - pub protos: Vec, } impl Service @@ -55,13 +42,8 @@ where metrics: Arc, node: Node, subscriber: Subscriber, - #[cfg(feature = "email")] email_store: EmailStore, - http_client: HttpClient, - api: ApiAccessConfig, listen: (Ipv4Addr, u16), origin: String, - envs: Vec, - protos: Vec, ) -> Self { let memory = Arc::new(RwLock::new(Connections::default())); Self { @@ -69,15 +51,9 @@ where metrics, node, subscriber, - #[cfg(feature = "email")] - email_store, - http_client, - api, listen: listen.0, port: listen.1, origin, - envs, - protos, } } @@ -93,32 +69,7 @@ where tracing::debug!("CORS: {:?}", cors.clone()); - #[cfg(feature = "email")] - let email_store = self.email_store.clone(); - let memory = self.memory.clone(); - let http_client = self.http_client.clone(); - let api = self.api.clone(); - - #[cfg(feature = "email")] - let trial_route = warp::post() - .and(warp::path("trial")) - .and(warp::body::json::()) - .and(filters::with_store(email_store.clone())) - .and(my_filters::with_http_client(http_client.clone())) - .and(filters::with_api_settings(api.clone())) - .and(filters::with_envs(self.envs.clone())) - .and(filters::with_protos(self.protos.clone())) - .and_then(trial_handler); - - let tg_trial_route = warp::post() - .and(warp::path("tg-trial")) - .and(warp::body::json::()) - .and(my_filters::with_http_client(http_client.clone())) - .and(filters::with_api_settings(api.clone())) - .and(filters::with_envs(self.envs.clone())) - .and(filters::with_protos(self.protos.clone())) - .and_then(tg_trial_handler); let auth_route = warp::post() .and(warp::path("auth")) @@ -126,22 +77,7 @@ where .and(warp::any().map(move || memory.clone())) .and_then(auth_handler); - let activate_route = warp::post() - .and(warp::path("activate")) - .and(warp::body::json::()) - .and(my_filters::with_http_client(http_client)) - .and(filters::with_api_settings(api)) - .and(filters::with_envs(self.envs.clone())) - .and(filters::with_protos(self.protos.clone())) - .and_then(activate_key_handler); - - let routes = health_check - .or(auth_route) - .or(tg_trial_route) - .or(activate_route); - - #[cfg(feature = "email")] - let routes = routes.or(trial_route); + let routes = health_check.or(auth_route); warp::serve(routes.with(cors)) .run(SocketAddr::new(IpAddr::V4(self.listen), self.port)) @@ -163,13 +99,6 @@ pub async fn run(settings: ServiceSettings) -> Result<()> { vec![topic_init, Topic::Auth], ); - #[cfg(feature = "email")] - let email_store = EmailStore::new(settings.smtp.clone()); - #[cfg(feature = "email")] - email_store.load_trials().await?; - - let http_client = HttpClient::new(); - let metrics = MetricBuffer { batch: parking_lot::Mutex::new(Vec::new()), publisher: Publisher::connect(&settings.metrics.publisher).await?, @@ -179,14 +108,8 @@ pub async fn run(settings: ServiceSettings) -> Result<()> { Arc::new(metrics), node, subscriber?, - #[cfg(feature = "email")] - email_store, - http_client, - settings.api.clone(), (settings.service.listen, settings.service.port), settings.service.origin.clone(), - settings.service.enabled_envs.clone(), - settings.service.enabled_protos.clone(), )); let snapshot_manager = SnapshotManager::new( diff --git a/src/lib.rs b/src/lib.rs index 2e1bb0c1..c0e573ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ pub const BANNER: &str = r#" // |_| |___/ "#; -pub const VERSION: &str = "0.5.0"; +pub const VERSION: &str = "0.5.1-dev"; pub use config::{ clash::InboundClashConfig,