mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	Move new code
This commit is contained in:
		
							
								
								
									
										19
									
								
								apps/gpservice/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/gpservice/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "gpservice"
 | 
			
		||||
version.workspace = true
 | 
			
		||||
edition.workspace = true
 | 
			
		||||
license.workspace = true
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
gpapi = { path = "../../crates/gpapi" }
 | 
			
		||||
openconnect = { path = "../../crates/openconnect" }
 | 
			
		||||
clap.workspace = true
 | 
			
		||||
anyhow.workspace = true
 | 
			
		||||
tokio.workspace = true
 | 
			
		||||
tokio-util.workspace = true
 | 
			
		||||
axum = { workspace = true, features = ["ws"] }
 | 
			
		||||
futures.workspace = true
 | 
			
		||||
serde_json.workspace = true
 | 
			
		||||
env_logger.workspace = true
 | 
			
		||||
log.workspace = true
 | 
			
		||||
compile-time.workspace = true
 | 
			
		||||
							
								
								
									
										19
									
								
								apps/gpservice/com.yuezk.gpservice.policy
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/gpservice/com.yuezk.gpservice.policy
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
 | 
			
		||||
<policyconfig>
 | 
			
		||||
  <vendor>GlobalProtect-openconnect</vendor>
 | 
			
		||||
  <vendor_url>https://github.com/yuezk/GlobalProtect-openconnect</vendor_url>
 | 
			
		||||
  <icon_name>gpgui</icon_name>
 | 
			
		||||
  <action id="com.yuezk.gpservice">
 | 
			
		||||
    <description>Run GPService as root</description>
 | 
			
		||||
    <message>Authentication is required to run the GPService as root</message>
 | 
			
		||||
    <defaults>
 | 
			
		||||
      <allow_any>yes</allow_any>
 | 
			
		||||
      <allow_inactive>yes</allow_inactive>
 | 
			
		||||
      <allow_active>yes</allow_active>
 | 
			
		||||
    </defaults>
 | 
			
		||||
    <annotate key="org.freedesktop.policykit.exec.path">/home/kevin/Documents/repos/gp/target/debug/gpservice</annotate>
 | 
			
		||||
    <annotate key="org.freedesktop.policykit.exec.argv1">--with-gui</annotate>
 | 
			
		||||
    <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
 | 
			
		||||
  </action>
 | 
			
		||||
</policyconfig>
 | 
			
		||||
							
								
								
									
										182
									
								
								apps/gpservice/src/cli.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								apps/gpservice/src/cli.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,182 @@
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use std::{collections::HashMap, io::Write};
 | 
			
		||||
 | 
			
		||||
use anyhow::bail;
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use gpapi::{
 | 
			
		||||
  process::gui_launcher::GuiLauncher,
 | 
			
		||||
  service::{request::WsRequest, vpn_state::VpnState},
 | 
			
		||||
  utils::{
 | 
			
		||||
    crypto::generate_key, env_file, lock_file::LockFile, redact::Redaction, shutdown_signal,
 | 
			
		||||
  },
 | 
			
		||||
  GP_SERVICE_LOCK_FILE,
 | 
			
		||||
};
 | 
			
		||||
use log::{info, warn, LevelFilter};
 | 
			
		||||
use tokio::sync::{mpsc, watch};
 | 
			
		||||
 | 
			
		||||
use crate::{vpn_task::VpnTask, ws_server::WsServer};
 | 
			
		||||
 | 
			
		||||
const VERSION: &str = concat!(
 | 
			
		||||
  env!("CARGO_PKG_VERSION"),
 | 
			
		||||
  " (",
 | 
			
		||||
  compile_time::date_str!(),
 | 
			
		||||
  ")"
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
#[derive(Parser)]
 | 
			
		||||
#[command(version = VERSION)]
 | 
			
		||||
struct Cli {
 | 
			
		||||
  #[clap(long)]
 | 
			
		||||
  minimized: bool,
 | 
			
		||||
  #[clap(long)]
 | 
			
		||||
  env_file: Option<String>,
 | 
			
		||||
  #[cfg(debug_assertions)]
 | 
			
		||||
  #[clap(long)]
 | 
			
		||||
  no_gui: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Cli {
 | 
			
		||||
  async fn run(&mut self, redaction: Arc<Redaction>) -> anyhow::Result<()> {
 | 
			
		||||
    let lock_file = Arc::new(LockFile::new(GP_SERVICE_LOCK_FILE));
 | 
			
		||||
 | 
			
		||||
    if lock_file.check_health().await {
 | 
			
		||||
      bail!("Another instance of the service is already running");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let api_key = self.prepare_api_key();
 | 
			
		||||
 | 
			
		||||
    // Channel for sending requests to the VPN task
 | 
			
		||||
    let (ws_req_tx, ws_req_rx) = mpsc::channel::<WsRequest>(32);
 | 
			
		||||
    // Channel for receiving the VPN state from the VPN task
 | 
			
		||||
    let (vpn_state_tx, vpn_state_rx) = watch::channel(VpnState::Disconnected);
 | 
			
		||||
 | 
			
		||||
    let mut vpn_task = VpnTask::new(ws_req_rx, vpn_state_tx);
 | 
			
		||||
    let ws_server = WsServer::new(
 | 
			
		||||
      api_key.clone(),
 | 
			
		||||
      ws_req_tx,
 | 
			
		||||
      vpn_state_rx,
 | 
			
		||||
      lock_file.clone(),
 | 
			
		||||
      redaction,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4);
 | 
			
		||||
    let shutdown_tx_clone = shutdown_tx.clone();
 | 
			
		||||
    let vpn_task_token = vpn_task.cancel_token();
 | 
			
		||||
    let server_token = ws_server.cancel_token();
 | 
			
		||||
 | 
			
		||||
    let vpn_task_handle = tokio::spawn(async move { vpn_task.start(server_token).await });
 | 
			
		||||
    let ws_server_handle = tokio::spawn(async move { ws_server.start(shutdown_tx_clone).await });
 | 
			
		||||
 | 
			
		||||
    #[cfg(debug_assertions)]
 | 
			
		||||
    let no_gui = self.no_gui;
 | 
			
		||||
 | 
			
		||||
    #[cfg(not(debug_assertions))]
 | 
			
		||||
    let no_gui = false;
 | 
			
		||||
 | 
			
		||||
    if no_gui {
 | 
			
		||||
      info!("GUI is disabled");
 | 
			
		||||
    } else {
 | 
			
		||||
      let envs = self
 | 
			
		||||
        .env_file
 | 
			
		||||
        .as_ref()
 | 
			
		||||
        .map(env_file::load_env_vars)
 | 
			
		||||
        .transpose()?;
 | 
			
		||||
 | 
			
		||||
      let minimized = self.minimized;
 | 
			
		||||
 | 
			
		||||
      tokio::spawn(async move {
 | 
			
		||||
        launch_gui(envs, api_key, minimized).await;
 | 
			
		||||
        let _ = shutdown_tx.send(()).await;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tokio::select! {
 | 
			
		||||
        _ = shutdown_signal() => {
 | 
			
		||||
            info!("Shutdown signal received");
 | 
			
		||||
        }
 | 
			
		||||
        _ = shutdown_rx.recv() => {
 | 
			
		||||
            info!("Shutdown request received, shutting down");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    vpn_task_token.cancel();
 | 
			
		||||
    let _ = tokio::join!(vpn_task_handle, ws_server_handle);
 | 
			
		||||
 | 
			
		||||
    lock_file.unlock()?;
 | 
			
		||||
 | 
			
		||||
    info!("gpservice stopped");
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fn prepare_api_key(&self) -> Vec<u8> {
 | 
			
		||||
    #[cfg(debug_assertions)]
 | 
			
		||||
    if self.no_gui {
 | 
			
		||||
      return gpapi::GP_API_KEY.to_vec();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    generate_key().to_vec()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn init_logger() -> Arc<Redaction> {
 | 
			
		||||
  let redaction = Arc::new(Redaction::new());
 | 
			
		||||
  let redaction_clone = Arc::clone(&redaction);
 | 
			
		||||
  // let target = Box::new(File::create("log.txt").expect("Can't create file"));
 | 
			
		||||
  env_logger::builder()
 | 
			
		||||
    .filter_level(LevelFilter::Info)
 | 
			
		||||
    .format(move |buf, record| {
 | 
			
		||||
      let timestamp = buf.timestamp();
 | 
			
		||||
      writeln!(
 | 
			
		||||
        buf,
 | 
			
		||||
        "[{} {} {}] {}",
 | 
			
		||||
        timestamp,
 | 
			
		||||
        record.level(),
 | 
			
		||||
        record.module_path().unwrap_or_default(),
 | 
			
		||||
        redaction_clone.redact_str(&record.args().to_string())
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
    // .target(env_logger::Target::Pipe(target))
 | 
			
		||||
    .init();
 | 
			
		||||
 | 
			
		||||
  redaction
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn launch_gui(envs: Option<HashMap<String, String>>, api_key: Vec<u8>, mut minimized: bool) {
 | 
			
		||||
  loop {
 | 
			
		||||
    let api_key_clone = api_key.clone();
 | 
			
		||||
    let gui_launcher = GuiLauncher::new()
 | 
			
		||||
      .envs(envs.clone())
 | 
			
		||||
      .api_key(api_key_clone)
 | 
			
		||||
      .minimized(minimized);
 | 
			
		||||
 | 
			
		||||
    match gui_launcher.launch().await {
 | 
			
		||||
      Ok(exit_status) => {
 | 
			
		||||
        // Exit code 99 means that the GUI needs to be restarted
 | 
			
		||||
        if exit_status.code() != Some(99) {
 | 
			
		||||
          info!("GUI exited with code {:?}", exit_status.code());
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        info!("GUI exited with code 99, restarting");
 | 
			
		||||
        minimized = false;
 | 
			
		||||
      }
 | 
			
		||||
      Err(err) => {
 | 
			
		||||
        warn!("Failed to launch GUI: {}", err);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn run() {
 | 
			
		||||
  let mut cli = Cli::parse();
 | 
			
		||||
 | 
			
		||||
  let redaction = init_logger();
 | 
			
		||||
  info!("gpservice started: {}", VERSION);
 | 
			
		||||
 | 
			
		||||
  if let Err(e) = cli.run(redaction).await {
 | 
			
		||||
    eprintln!("Error: {}", e);
 | 
			
		||||
    std::process::exit(1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										94
									
								
								apps/gpservice/src/handlers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								apps/gpservice/src/handlers.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
use std::{borrow::Cow, ops::ControlFlow, sync::Arc};
 | 
			
		||||
 | 
			
		||||
use axum::{
 | 
			
		||||
  extract::{
 | 
			
		||||
    ws::{self, CloseFrame, Message, WebSocket},
 | 
			
		||||
    State, WebSocketUpgrade,
 | 
			
		||||
  },
 | 
			
		||||
  response::IntoResponse,
 | 
			
		||||
};
 | 
			
		||||
use futures::{SinkExt, StreamExt};
 | 
			
		||||
use gpapi::service::event::WsEvent;
 | 
			
		||||
use log::{info, warn};
 | 
			
		||||
 | 
			
		||||
use crate::ws_server::WsServerContext;
 | 
			
		||||
 | 
			
		||||
pub(crate) async fn health() -> impl IntoResponse {
 | 
			
		||||
  "OK"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) async fn active_gui(State(ctx): State<Arc<WsServerContext>>) -> impl IntoResponse {
 | 
			
		||||
  ctx.send_event(WsEvent::ActiveGui).await;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) async fn ws_handler(
 | 
			
		||||
  ws: WebSocketUpgrade,
 | 
			
		||||
  State(ctx): State<Arc<WsServerContext>>,
 | 
			
		||||
) -> impl IntoResponse {
 | 
			
		||||
  ws.on_upgrade(move |socket| handle_socket(socket, ctx))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn handle_socket(mut socket: WebSocket, ctx: Arc<WsServerContext>) {
 | 
			
		||||
  // Send ping message
 | 
			
		||||
  if let Err(err) = socket.send(Message::Ping("Hi".into())).await {
 | 
			
		||||
    warn!("Failed to send ping: {}", err);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Wait for pong message
 | 
			
		||||
  if socket.recv().await.is_none() {
 | 
			
		||||
    warn!("Failed to receive pong");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  info!("New client connected");
 | 
			
		||||
 | 
			
		||||
  let (mut sender, mut receiver) = socket.split();
 | 
			
		||||
  let (connection, mut msg_rx) = ctx.add_connection().await;
 | 
			
		||||
 | 
			
		||||
  let send_task = tokio::spawn(async move {
 | 
			
		||||
    while let Some(msg) = msg_rx.recv().await {
 | 
			
		||||
      if let Err(err) = sender.send(msg).await {
 | 
			
		||||
        info!("Failed to send message: {}", err);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let close_msg = Message::Close(Some(CloseFrame {
 | 
			
		||||
      code: ws::close_code::NORMAL,
 | 
			
		||||
      reason: Cow::from("Goodbye"),
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    if let Err(err) = sender.send(close_msg).await {
 | 
			
		||||
      warn!("Failed to close socket: {}", err);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  let conn = Arc::clone(&connection);
 | 
			
		||||
  let ctx_clone = Arc::clone(&ctx);
 | 
			
		||||
  let recv_task = tokio::spawn(async move {
 | 
			
		||||
    while let Some(Ok(msg)) = receiver.next().await {
 | 
			
		||||
      let ControlFlow::Continue(ws_req) = conn.recv_msg(msg) else {
 | 
			
		||||
        break;
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      if let Err(err) = ctx_clone.forward_req(ws_req).await {
 | 
			
		||||
        info!("Failed to forward request: {}", err);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  tokio::select! {
 | 
			
		||||
    _ = send_task => {
 | 
			
		||||
        info!("WS server send task completed");
 | 
			
		||||
    },
 | 
			
		||||
    _ = recv_task => {
 | 
			
		||||
        info!("WS server recv task completed");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  info!("Client disconnected");
 | 
			
		||||
 | 
			
		||||
  ctx.remove_connection(connection).await;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								apps/gpservice/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/gpservice/src/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
mod cli;
 | 
			
		||||
mod handlers;
 | 
			
		||||
mod routes;
 | 
			
		||||
mod vpn_task;
 | 
			
		||||
mod ws_server;
 | 
			
		||||
mod ws_connection;
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
  cli::run().await;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								apps/gpservice/src/routes.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								apps/gpservice/src/routes.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use axum::{routing::{get, post}, Router};
 | 
			
		||||
 | 
			
		||||
use crate::{handlers, ws_server::WsServerContext};
 | 
			
		||||
 | 
			
		||||
pub(crate) fn routes(ctx: Arc<WsServerContext>) -> Router {
 | 
			
		||||
  Router::new()
 | 
			
		||||
    .route("/health", get(handlers::health))
 | 
			
		||||
    .route("/active-gui", post(handlers::active_gui))
 | 
			
		||||
    .route("/ws", get(handlers::ws_handler))
 | 
			
		||||
    .with_state(ctx)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										144
									
								
								apps/gpservice/src/vpn_task.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								apps/gpservice/src/vpn_task.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
use std::{sync::Arc, thread};
 | 
			
		||||
 | 
			
		||||
use gpapi::service::{
 | 
			
		||||
  request::{ConnectRequest, WsRequest},
 | 
			
		||||
  vpn_state::VpnState,
 | 
			
		||||
};
 | 
			
		||||
use log::info;
 | 
			
		||||
use openconnect::Vpn;
 | 
			
		||||
use tokio::sync::{mpsc, oneshot, watch, RwLock};
 | 
			
		||||
use tokio_util::sync::CancellationToken;
 | 
			
		||||
 | 
			
		||||
pub(crate) struct VpnTaskContext {
 | 
			
		||||
  vpn_handle: Arc<RwLock<Option<Vpn>>>,
 | 
			
		||||
  vpn_state_tx: Arc<watch::Sender<VpnState>>,
 | 
			
		||||
  disconnect_rx: RwLock<Option<oneshot::Receiver<()>>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl VpnTaskContext {
 | 
			
		||||
  pub fn new(vpn_state_tx: watch::Sender<VpnState>) -> Self {
 | 
			
		||||
    Self {
 | 
			
		||||
      vpn_handle: Default::default(),
 | 
			
		||||
      vpn_state_tx: Arc::new(vpn_state_tx),
 | 
			
		||||
      disconnect_rx: Default::default(),
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub async fn connect(&self, req: ConnectRequest) {
 | 
			
		||||
    let vpn_state = self.vpn_state_tx.borrow().clone();
 | 
			
		||||
    if !matches!(vpn_state, VpnState::Disconnected) {
 | 
			
		||||
      info!("VPN is not disconnected, ignore the request");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let info = req.info().clone();
 | 
			
		||||
    let vpn_handle = self.vpn_handle.clone();
 | 
			
		||||
    let args = req.args();
 | 
			
		||||
    let vpn = Vpn::builder(req.gateway().server(), args.cookie())
 | 
			
		||||
      .user_agent(args.user_agent())
 | 
			
		||||
      .script(args.vpnc_script())
 | 
			
		||||
      .os(args.openconnect_os())
 | 
			
		||||
      .build();
 | 
			
		||||
 | 
			
		||||
    // Save the VPN handle
 | 
			
		||||
    vpn_handle.write().await.replace(vpn);
 | 
			
		||||
 | 
			
		||||
    let vpn_state_tx = self.vpn_state_tx.clone();
 | 
			
		||||
    let connect_info = Box::new(info.clone());
 | 
			
		||||
    vpn_state_tx.send(VpnState::Connecting(connect_info)).ok();
 | 
			
		||||
 | 
			
		||||
    let (disconnect_tx, disconnect_rx) = oneshot::channel::<()>();
 | 
			
		||||
    self.disconnect_rx.write().await.replace(disconnect_rx);
 | 
			
		||||
 | 
			
		||||
    // Spawn a new thread to process the VPN connection, cannot use tokio::spawn here.
 | 
			
		||||
    // Otherwise, it will block the tokio runtime and cannot send the VPN state to the channel
 | 
			
		||||
    thread::spawn(move || {
 | 
			
		||||
      let vpn_state_tx_clone = vpn_state_tx.clone();
 | 
			
		||||
 | 
			
		||||
      vpn_handle.blocking_read().as_ref().map(|vpn| {
 | 
			
		||||
        vpn.connect(move || {
 | 
			
		||||
          let connect_info = Box::new(info.clone());
 | 
			
		||||
          vpn_state_tx.send(VpnState::Connected(connect_info)).ok();
 | 
			
		||||
        })
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Notify the VPN is disconnected
 | 
			
		||||
      vpn_state_tx_clone.send(VpnState::Disconnected).ok();
 | 
			
		||||
      // Remove the VPN handle
 | 
			
		||||
      vpn_handle.blocking_write().take();
 | 
			
		||||
 | 
			
		||||
      disconnect_tx.send(()).ok();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub async fn disconnect(&self) {
 | 
			
		||||
    if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() {
 | 
			
		||||
      if let Some(vpn) = self.vpn_handle.read().await.as_ref() {
 | 
			
		||||
        self.vpn_state_tx.send(VpnState::Disconnecting).ok();
 | 
			
		||||
        vpn.disconnect()
 | 
			
		||||
      }
 | 
			
		||||
      // Wait for the VPN to be disconnected
 | 
			
		||||
      disconnect_rx.await.ok();
 | 
			
		||||
      info!("VPN disconnected");
 | 
			
		||||
    } else {
 | 
			
		||||
      info!("VPN is not connected, skip disconnect");
 | 
			
		||||
      self.vpn_state_tx.send(VpnState::Disconnected).ok();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) struct VpnTask {
 | 
			
		||||
  ws_req_rx: mpsc::Receiver<WsRequest>,
 | 
			
		||||
  ctx: Arc<VpnTaskContext>,
 | 
			
		||||
  cancel_token: CancellationToken,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl VpnTask {
 | 
			
		||||
  pub fn new(ws_req_rx: mpsc::Receiver<WsRequest>, vpn_state_tx: watch::Sender<VpnState>) -> Self {
 | 
			
		||||
    let ctx = Arc::new(VpnTaskContext::new(vpn_state_tx));
 | 
			
		||||
    let cancel_token = CancellationToken::new();
 | 
			
		||||
 | 
			
		||||
    Self {
 | 
			
		||||
      ws_req_rx,
 | 
			
		||||
      ctx,
 | 
			
		||||
      cancel_token,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub fn cancel_token(&self) -> CancellationToken {
 | 
			
		||||
    self.cancel_token.clone()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub async fn start(&mut self, server_cancel_token: CancellationToken) {
 | 
			
		||||
    let cancel_token = self.cancel_token.clone();
 | 
			
		||||
 | 
			
		||||
    tokio::select! {
 | 
			
		||||
        _ = self.recv() => {
 | 
			
		||||
            info!("VPN task stopped");
 | 
			
		||||
        }
 | 
			
		||||
        _ = cancel_token.cancelled() => {
 | 
			
		||||
            info!("VPN task cancelled");
 | 
			
		||||
            self.ctx.disconnect().await;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    server_cancel_token.cancel();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async fn recv(&mut self) {
 | 
			
		||||
    while let Some(req) = self.ws_req_rx.recv().await {
 | 
			
		||||
      tokio::spawn(process_ws_req(req, self.ctx.clone()));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn process_ws_req(req: WsRequest, ctx: Arc<VpnTaskContext>) {
 | 
			
		||||
  match req {
 | 
			
		||||
    WsRequest::Connect(req) => {
 | 
			
		||||
      ctx.connect(*req).await;
 | 
			
		||||
    }
 | 
			
		||||
    WsRequest::Disconnect(_) => {
 | 
			
		||||
      ctx.disconnect().await;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								apps/gpservice/src/ws_connection.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								apps/gpservice/src/ws_connection.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
use std::{ops::ControlFlow, sync::Arc};
 | 
			
		||||
 | 
			
		||||
use axum::extract::ws::{CloseFrame, Message};
 | 
			
		||||
use gpapi::{
 | 
			
		||||
  service::{event::WsEvent, request::WsRequest},
 | 
			
		||||
  utils::crypto::Crypto,
 | 
			
		||||
};
 | 
			
		||||
use log::{info, warn};
 | 
			
		||||
use tokio::sync::mpsc;
 | 
			
		||||
 | 
			
		||||
pub(crate) struct WsConnection {
 | 
			
		||||
  crypto: Arc<Crypto>,
 | 
			
		||||
  tx: mpsc::Sender<Message>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl WsConnection {
 | 
			
		||||
  pub fn new(crypto: Arc<Crypto>, tx: mpsc::Sender<Message>) -> Self {
 | 
			
		||||
    Self { crypto, tx }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub async fn send_event(&self, event: &WsEvent) -> anyhow::Result<()> {
 | 
			
		||||
    let encrypted = self.crypto.encrypt(event)?;
 | 
			
		||||
    let msg = Message::Binary(encrypted);
 | 
			
		||||
 | 
			
		||||
    self.tx.send(msg).await?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub fn recv_msg(&self, msg: Message) -> ControlFlow<(), WsRequest> {
 | 
			
		||||
    match msg {
 | 
			
		||||
      Message::Binary(data) => match self.crypto.decrypt(data) {
 | 
			
		||||
        Ok(ws_req) => ControlFlow::Continue(ws_req),
 | 
			
		||||
        Err(err) => {
 | 
			
		||||
          info!("Failed to decrypt message: {}", err);
 | 
			
		||||
          ControlFlow::Break(())
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      Message::Close(cf) => {
 | 
			
		||||
        if let Some(CloseFrame { code, reason }) = cf {
 | 
			
		||||
          info!("Client sent close, code {} and reason `{}`", code, reason);
 | 
			
		||||
        } else {
 | 
			
		||||
          info!("Client somehow sent close message without CloseFrame");
 | 
			
		||||
        }
 | 
			
		||||
        ControlFlow::Break(())
 | 
			
		||||
      }
 | 
			
		||||
      _ => {
 | 
			
		||||
        warn!("WS server received unexpected message: {:?}", msg);
 | 
			
		||||
        ControlFlow::Break(())
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										158
									
								
								apps/gpservice/src/ws_server.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								apps/gpservice/src/ws_server.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,158 @@
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use axum::extract::ws::Message;
 | 
			
		||||
use gpapi::{
 | 
			
		||||
  service::{event::WsEvent, request::WsRequest, vpn_state::VpnState},
 | 
			
		||||
  utils::{crypto::Crypto, lock_file::LockFile, redact::Redaction},
 | 
			
		||||
};
 | 
			
		||||
use log::{info, warn};
 | 
			
		||||
use tokio::{
 | 
			
		||||
  net::TcpListener,
 | 
			
		||||
  sync::{mpsc, watch, RwLock},
 | 
			
		||||
};
 | 
			
		||||
use tokio_util::sync::CancellationToken;
 | 
			
		||||
 | 
			
		||||
use crate::{routes, ws_connection::WsConnection};
 | 
			
		||||
 | 
			
		||||
pub(crate) struct WsServerContext {
 | 
			
		||||
  crypto: Arc<Crypto>,
 | 
			
		||||
  ws_req_tx: mpsc::Sender<WsRequest>,
 | 
			
		||||
  vpn_state_rx: watch::Receiver<VpnState>,
 | 
			
		||||
  redaction: Arc<Redaction>,
 | 
			
		||||
  connections: RwLock<Vec<Arc<WsConnection>>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl WsServerContext {
 | 
			
		||||
  pub fn new(
 | 
			
		||||
    api_key: Vec<u8>,
 | 
			
		||||
    ws_req_tx: mpsc::Sender<WsRequest>,
 | 
			
		||||
    vpn_state_rx: watch::Receiver<VpnState>,
 | 
			
		||||
    redaction: Arc<Redaction>,
 | 
			
		||||
  ) -> Self {
 | 
			
		||||
    Self {
 | 
			
		||||
      crypto: Arc::new(Crypto::new(api_key)),
 | 
			
		||||
      ws_req_tx,
 | 
			
		||||
      vpn_state_rx,
 | 
			
		||||
      redaction,
 | 
			
		||||
      connections: Default::default(),
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub async fn send_event(&self, event: WsEvent) {
 | 
			
		||||
    let connections = self.connections.read().await;
 | 
			
		||||
 | 
			
		||||
    for conn in connections.iter() {
 | 
			
		||||
      let _ = conn.send_event(&event).await;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub async fn add_connection(&self) -> (Arc<WsConnection>, mpsc::Receiver<Message>) {
 | 
			
		||||
    let (tx, rx) = mpsc::channel::<Message>(32);
 | 
			
		||||
    let conn = Arc::new(WsConnection::new(Arc::clone(&self.crypto), tx));
 | 
			
		||||
 | 
			
		||||
    // Send current VPN state to new client
 | 
			
		||||
    info!("Sending current VPN state to new client");
 | 
			
		||||
    let vpn_state = self.vpn_state_rx.borrow().clone();
 | 
			
		||||
    if let Err(err) = conn.send_event(&WsEvent::VpnState(vpn_state)).await {
 | 
			
		||||
      warn!("Failed to send VPN state to new client: {}", err);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    self.connections.write().await.push(Arc::clone(&conn));
 | 
			
		||||
 | 
			
		||||
    (conn, rx)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub async fn remove_connection(&self, conn: Arc<WsConnection>) {
 | 
			
		||||
    let mut connections = self.connections.write().await;
 | 
			
		||||
    connections.retain(|c| !Arc::ptr_eq(c, &conn));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fn vpn_state_rx(&self) -> watch::Receiver<VpnState> {
 | 
			
		||||
    self.vpn_state_rx.clone()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub async fn forward_req(&self, req: WsRequest) -> anyhow::Result<()> {
 | 
			
		||||
    if let WsRequest::Connect(ref req) = req {
 | 
			
		||||
      self
 | 
			
		||||
        .redaction
 | 
			
		||||
        .add_values(&[req.gateway().server(), req.args().cookie()])?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    self.ws_req_tx.send(req).await?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) struct WsServer {
 | 
			
		||||
  ctx: Arc<WsServerContext>,
 | 
			
		||||
  cancel_token: CancellationToken,
 | 
			
		||||
  lock_file: Arc<LockFile>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl WsServer {
 | 
			
		||||
  pub fn new(
 | 
			
		||||
    api_key: Vec<u8>,
 | 
			
		||||
    ws_req_tx: mpsc::Sender<WsRequest>,
 | 
			
		||||
    vpn_state_rx: watch::Receiver<VpnState>,
 | 
			
		||||
    lock_file: Arc<LockFile>,
 | 
			
		||||
    redaction: Arc<Redaction>,
 | 
			
		||||
  ) -> Self {
 | 
			
		||||
    let ctx = Arc::new(WsServerContext::new(
 | 
			
		||||
      api_key,
 | 
			
		||||
      ws_req_tx,
 | 
			
		||||
      vpn_state_rx,
 | 
			
		||||
      redaction,
 | 
			
		||||
    ));
 | 
			
		||||
    let cancel_token = CancellationToken::new();
 | 
			
		||||
 | 
			
		||||
    Self {
 | 
			
		||||
      ctx,
 | 
			
		||||
      cancel_token,
 | 
			
		||||
      lock_file,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub fn cancel_token(&self) -> CancellationToken {
 | 
			
		||||
    self.cancel_token.clone()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  pub async fn start(&self, shutdown_tx: mpsc::Sender<()>) {
 | 
			
		||||
    if let Ok(listener) = TcpListener::bind("127.0.0.1:0").await {
 | 
			
		||||
      let local_addr = listener.local_addr().unwrap();
 | 
			
		||||
 | 
			
		||||
      self.lock_file.lock(local_addr.port().to_string()).unwrap();
 | 
			
		||||
 | 
			
		||||
      info!("WS server listening on port: {}", local_addr.port());
 | 
			
		||||
 | 
			
		||||
      tokio::select! {
 | 
			
		||||
        _ = watch_vpn_state(self.ctx.vpn_state_rx(), Arc::clone(&self.ctx)) => {
 | 
			
		||||
          info!("VPN state watch task completed");
 | 
			
		||||
        }
 | 
			
		||||
        _ = start_server(listener, self.ctx.clone()) => {
 | 
			
		||||
            info!("WS server stopped");
 | 
			
		||||
        }
 | 
			
		||||
        _ = self.cancel_token.cancelled() => {
 | 
			
		||||
          info!("WS server cancelled");
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let _ = shutdown_tx.send(()).await;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn watch_vpn_state(mut vpn_state_rx: watch::Receiver<VpnState>, ctx: Arc<WsServerContext>) {
 | 
			
		||||
  while vpn_state_rx.changed().await.is_ok() {
 | 
			
		||||
    let vpn_state = vpn_state_rx.borrow().clone();
 | 
			
		||||
    ctx.send_event(WsEvent::VpnState(vpn_state)).await;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn start_server(listener: TcpListener, ctx: Arc<WsServerContext>) -> anyhow::Result<()> {
 | 
			
		||||
  let routes = routes::routes(ctx);
 | 
			
		||||
 | 
			
		||||
  axum::serve(listener, routes).await?;
 | 
			
		||||
 | 
			
		||||
  Ok(())
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user